Velocity 是一个基于Java的模板引擎。它允许任何人使用一种简单但强大的模板语言去引用Java代码中定义的对象。
Velocity的基本常用语法:https://www.cnblogs.com/xiohao/p/5788932.html
最近在做ESL的邮件报警功能,邮件内容包含两个表格,分别填充两种报警内容,需要根据系统的语言设置显示不一样的表头。
核心做法:
package com.zk.mail;
import lombok.extern.slf4j.Slf4j;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.VelocityEngine;
import org.springframework.stereotype.Component;
import org.springframework.ui.velocity.VelocityEngineUtils;
import org.springframework.util.StringUtils;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.mail.*;
import javax.mail.internet.*;
import javax.mail.util.ByteArrayDataSource;
import java.io.*;
import java.util.*;
@Slf4j
@Component(value = "mailUtils")
public class MailUtils {
public static final String HTML_CONTENT = "text/html;charset=UTF-8";
public static final String ATTACHMENT_CONTENT = "text/plain;charset=gb2312";
private static VelocityEngine velocityEngine = new VelocityEngine();
static {
Properties properties = new Properties();
String basePath = "src/main/resources/mailTemplate/";
// 设置模板的路径
properties.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, basePath);
// 初始花velocity 让设置的路径生效
velocityEngine.init(properties);
}
public <T extends List> void sendEmail(T t1, T t2, String title, String[] to, String[] bcc, String templateName, EmailServerConfig config, Map<String, String> content) {
Map map = new HashMap();
map.put("priceTagDatas", t1);
map.put("ApDatas", t2);
map.put("merchantName", content.get("merchantName"));
map.put("storeName", content.get("storeName"));
map.put("alarmStartTime", content.get("alarmStartTime"));
map.put("alarmEndTime", content.get("alarmEndTime"));
Email email = new Email.Builder(title, to, null).model(map).templateName(templateName).bcc(bcc).build();
sendEmail(email, config);
}
private void sendEmail(Email email, EmailServerConfig config) {
Long startTime = System.currentTimeMillis();
// 发件人
try {
MimeMessage message = this.getMessage(email, config);
// 新建一个存放信件内容的BodyPart对象
Multipart multiPart = new MimeMultipart();
MimeBodyPart mdp = new MimeBodyPart();
// 给BodyPart对象设置内容和格式/编码方式
setContent(email);
mdp.setContent(email.getContent(), HTML_CONTENT);
multiPart.addBodyPart(mdp);
// 新建一个MimeMultipart对象用来存放BodyPart对象(事实上可以存放多个)
if (null != email.getData()) {
MimeBodyPart attchment = new MimeBodyPart();
ByteArrayInputStream in = new ByteArrayInputStream(email.getData());
DataSource fds = new ByteArrayDataSource(in, email.getFileType());
attchment.setDataHandler(new DataHandler(fds));
attchment.setFileName(MimeUtility.encodeText(email.getFileName()));
multiPart.addBodyPart(attchment);
if (in != null) {
in.close();
}
}
message.setContent(multiPart);
message.saveChanges();
Transport.send(message);
Long endTime = System.currentTimeMillis();
log.info("Email sent successfully, consume time:" + (endTime - startTime) / 1000 + "s");
} catch (Exception e) {
log.error("Error while sending mail.", e);
}
}
private Email setContent(Email email) {
if (StringUtils.isEmpty(email.getContent())) {
email.setContent("");
}
if (!StringUtils.isEmpty(email.getTemplateName()) && null != email.getModel()) {
String content = VelocityEngineUtils.mergeTemplateIntoString(velocityEngine, email.getTemplateName(), "UTF-8", email.getModel());
email.setContent(content);
}
return email;
}
private MimeMessage getMessage(Email email, EmailServerConfig config) {
MimeMessage message = null;
try {
if (email.getTo() == null || email.getTo().length == 0 || StringUtils.isEmpty(email.getSubject())) {
throw new Exception("Recipient or subject is empty.");
}
Properties props = new Properties();
props.setProperty("mail.smtp.host", config.getMailSmtpHost());
props.setProperty("mail.smtp.socketFactory.class", config.getMailSmtpSocketFatoryClass());
props.setProperty("mail.smtp.socketFactory.fallback", config.getMailSmtpSocketFatoryFallback());
props.setProperty("mail.smtp.port", config.getMailSmtpPort());
props.setProperty("mail.smtp.socketFactory.port", config.getMailSmtpSocketFatoryPort());
props.setProperty("mail.smtp.auth", config.getMailSmtpAuth());
//解决553的问题,用Session.getInstance取代Session.getDefaultInstance
// Session mailSession = Session.getDefaultInstance(props, new Authenticator() {
// protected PasswordAuthentication getPasswordAuthentication() {
// return new PasswordAuthentication(config.getMailSmtpFromAddress(), //config.getMailSmtpAuthPass());
// }
// });
Session mailSession = Session.getInstance(props, new Authenticator(){
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(config.getMailSmtpFromAddress(), config.getMailSmtpAuthPass());
}});
message = new MimeMessage(mailSession);
message.setFrom(new InternetAddress(config.getMailSmtpFromAddress()));
for (String mailTo : email.getTo()) {
message.addRecipient(Message.RecipientType.TO, new InternetAddress(mailTo));
}
List<InternetAddress> ccAddress = new ArrayList<>();
if (null != email.getBcc()) {
for (String mailCC : email.getBcc()) {
ccAddress.add(new InternetAddress(mailCC));
}
message.addRecipients(Message.RecipientType.CC,
ccAddress.toArray(new InternetAddress[email.getBcc().length]));
}
message.setSentDate(new Date());
message.setSubject(email.getSubject());
} catch (Exception e) {
log.error("Error while sending mail." + e.getMessage(), e);
}
return message;
}
}
Velocity的模板, 提供不同语言的模板,模板名称带上语言后缀(中文模板:mail_cn.vm)如:
<!DOCTYPE html>
<html lang="zh">
<head>
<META http-equiv=Content-Type content='text/html; charset=UTF-8'>
<title>Title</title>
<style type="text/css">
table.reference, table.tecspec {
border-collapse: collapse;
width: 100%;
margin-bottom: 4px;
margin-top: 4px;
}
table.reference tr:nth-child(even) {
background-color: #fff;
}
table.reference tr:nth-child(odd) {
background-color: #f6f4f0;
}
table.reference th {
color: #fff;
background-color: #555;
border: 1px solid #555;
font-size: 12px;
padding: 3px;
vertical-align: top;
}
table.reference td {
line-height: 2em;
min-width: 24px;
border: 1px solid #d4d4d4;
padding: 5px;
padding-top: 7px;
padding-bottom: 7px;
vertical-align: top;
}
.article-body h3 {
font-size: 1.8em;
margin: 2px 0;
line-height: 1.8em;
}
</style>
</head>
<body>
<h3 style=";">ESL系统报警信息</h3>
<div>
<div>时间: $alarmStartTime 至 $alarmEndTime</div>
<div>商家名称: $merchantName</div>
<div>门店名称: $storeName</div>
<div>报警内容:</div>
#if ($priceTagDatas.size() > 0)
<table class="reference">
<tbody>
<tr>价签报警</tr>
<tr>
<th>价签条码</th>
<th>商品条码</th>
<th>商品名称</th>
<th>报警类型</th>
<th>报警时间</th>
</tr>
#foreach($element in $priceTagDatas)
<tr>
<td>
#if($element.getDeviceMac())
$element.getDeviceMac()
#end
</td>
<td>
#if($element.getItemBarCode())
$element.getItemBarCode()
#end
</td>
<td>
#if($element.getItemName())
$element.getItemName()
#end
</td>
<td>
#if($element.getFaultType())
$element.getFaultType()
#end
</td>
<td>
#if($element.getCreatedTime())
$element.getCreatedTime()
#end
</td>
</tr>
#end
</tbody>
</table>
#end
#if ($ApDatas.size() > 0)
<table class="reference">
<tbody>
<tr>基站报警</tr>
<tr>
<th>基站名称</th>
<th>基站MAC</th>
<th>报警类型</th>
<th>报警时间</th>
<th>状态</th>
</tr>
#foreach($element in $ApDatas)
<tr>
<td>
#if($element.getDeviceMac())
$element.getDeviceMac()
#end
</td>
<td>
#if($element.getDeviceMac())
$element.getDeviceMac()
#end
</td>
<td>
#if($element.getFaultType())
$element.getFaultType()
#end
</td>
<td>
#if($element.getCreatedTime())
$element.getCreatedTime()
#end
</td>
<td>
#if($element.getProcessStatus())
$element.getProcessStatus()
#end
</td>
</tr>
#end
</tbody>
</table>
#end
<div style="float: left; margin-top: 300px;;">
<p>系统邮件(请勿回复) | ESL 报警中心</p>
</div>
</div>
</body>
</html>
发送邮件是在一个定时任务中,定时任务的代码如:
package com.zk.quartz;
import com.zk.dao.*;
import com.zk.mail.AlarmEmailTitle;
import com.zk.mail.EmailServerConfig;
import com.zk.mail.MailUtils;
import com.zk.model.*;
import com.zk.service.MailSenderService;
import com.zk.util.DateUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.data.jpa.domain.Specification;
import javax.annotation.Resource;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.*;
import java.util.stream.Collectors;
/**
* Created by zk on 2019/3/28.
*/
@Slf4j
public class AlarmJob implements Job {
@Resource
private StoreRepository storeRepository;
@Resource
private MerchantRepository merchantRepository;
@Resource
private AgencyAlarmConfigRepository agencyAlarmConfigRepository;
@Resource
private AlarmRepository alarmRepository;
@Resource
private MailUtils mailUtils;
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
List faultTypeList = (List) jobDataMap.get("faultTypeList");
String merchantId = (String) jobDataMap.get("merchantId");
String storeId = (String) jobDataMap.get("storeId");
String sendTO = (String) jobDataMap.get("sendTo");
String language = (String) jobDataMap.get("language");
String templateName = "mail_" + language + ".vm";
List<Alarm> alarmList = alarmRepository.findAll(getSpecification(merchantId, storeId, faultTypeList));
if (alarmList.size() == 0) {
log.info("Alarm job run without alarms for storeId: " + storeId);
return;
}
String merchantName = merchantRepository.findByMerchantIdAndFlag(merchantId, 1).getMerchantName();
String storeName = storeRepository.findByStoreIdAndFlag(storeId, 1).getStoreName();
List<Alarm> priceTagAlarmList = alarmList.stream().filter(alarm -> "2".equals(alarm.getAlarmType())).collect(Collectors.toList());
List<Alarm> apAlarmList = alarmList.stream().filter(alarm -> "1".equals(alarm.getAlarmType())).collect(Collectors.toList());
Date alarmStartTime = alarmList.stream().map(alarm -> DateUtils.stringToDateTime(alarm.getCreatedTime())).min(Comparator.naturalOrder()).get();
Date alarmEndTime = alarmList.stream().map(alarm -> DateUtils.stringToDateTime(alarm.getCreatedTime())).max(Comparator.naturalOrder()).get();
Map<String, String> content = new HashMap<>(4);
content.put("merchantName", merchantName);
content.put("storeName", storeName);
content.put("alarmStartTime", DateUtils.format(alarmStartTime));
content.put("alarmEndTime", DateUtils.format(alarmEndTime));
AgencyAlarmConfig agencyAlarmConfig = agencyAlarmConfigRepository.findConfigByAgencyId(merchantId);
agencyAlarmConfig.setTestMail(sendTO);
String[] toArr = sendTO.split(",");
EmailServerConfig config = getEmailServerConfig(agencyAlarmConfig);
mailUtils.sendEmail(priceTagAlarmList, apAlarmList, AlarmEmailTitle.getTitleFromLanguage(language), toArr, null, templateName, config, content);
for(Alarm alarm : alarmList) {
alarm.setHasSent(true);
alarmRepository.save(alarm);
}
}
private EmailServerConfig getEmailServerConfig(AgencyAlarmConfig agencyAlarmConfig) {
EmailServerConfig config = new EmailServerConfig();
config.setMailSmtpHost(agencyAlarmConfig.getSendServer());
config.setMailSmtpSocketFatoryClass("javax.net.ssl.SSLSocketFactory");
config.setMailSmtpSocketFatoryFallback("false");
config.setMailSmtpPort("465");
config.setMailSmtpSocketFatoryPort("465");
config.setMailSmtpAuth("true");
config.setMailSmtpFromAddress(agencyAlarmConfig.getAccount());
config.setMailSmtpAuthPass(agencyAlarmConfig.getPassword());
return config;
}
private Specification<Alarm> getSpecification(String merchantId, String storeId, List<String> typeList) {
return new Specification<Alarm>() {
@Override
public Predicate toPredicate(Root<Alarm> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
List<Predicate> predicates = new ArrayList<Predicate>();
Predicate predicate = null;
if (StringUtils.isNotBlank(merchantId)) {
predicate = criteriaBuilder.equal(root.get("merchantId"), merchantId);
predicates.add(predicate);
}
if (StringUtils.isNotBlank(storeId)) {
predicate = criteriaBuilder.equal(root.get("storeId"), storeId);
predicates.add(predicate);
}
if (typeList != null && typeList.size() > 0) {
CriteriaBuilder.In<String> in = criteriaBuilder.in(root.get("faultType"));
for (String type : typeList) {
in.value(type);
}
predicates.add(in);
}
predicate = criteriaBuilder.isNull(root.get("hasSent"));
predicates.add(predicate);
return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
}
};
}
}
后记:部署后遇到了两个坑
1)Velocity找不到模板文件
在我本地运行的时候并没有这种问题,试了很多种方法,最后只能使用绝对路径,修改MailUtils中velocityEngine的Velocity.FILE_RESOURCE_LOADER_PATH的值:
static {
Properties properties = new Properties();
// 将basePath修改为服务器上的绝对路径, 并将模板文件上传到该路径下。
// String basePath = "src/main/resources/mailTemplate/";
String basePath = "/usr/local/esl/";
properties.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, basePath);
velocityEngine.init(properties);
}
问题解决。
2)使用了163邮箱作为测试服务器,遇到了邮件被认为是垃圾邮件的问题:
解决方法:将邮件抄送一份给发送账号,在MailUtils的getMessage方法中,添加以下代码:
List<InternetAddress> ccAddress = new ArrayList<>();
// if (null != email.getBcc()) {
// for (String mailCC : email.getBcc()) {
// ccAddress.add(new InternetAddress(mailCC));
// }
// message.addRecipients(Message.RecipientType.CC,
// ccAddress.toArray(new InternetAddress[email.getBcc().length]));
// }
ccAddress.add(new InternetAddress(config.getMailSmtpFromAddress()));
message.addRecipients(Message.RecipientType.CC, ccAddress.toArray(new InternetAddress[1]));
成功解决554 DT:SPM问题!
后记2:解决邮件发送中出现553问题
在本地用单测进行邮件发送,都没有问题。但是部署之后,通过前端调用接口的方式,经常会出现553的问题,如:
553意味着mail from和登录的邮箱账号存在不一致的情况,考虑到部署后首次发送是成功的,想到会不会是前一次登录的账号信息被保留下来了,观察代码,mail from和account的信息分别设置如:
Session mailSession = Session.getDefaultInstance(props, new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(config.getMailSmtpFromAddress(), config.getMailSmtpAuthPass());
}
});
message = new MimeMessage(mailSession);
message.setFrom(new InternetAddress(config.getMailSmtpFromAddress()));
跟进到Session.getDefaultInstance的代码发现,defaultSession是一个类静态变量,首次登录一个邮箱后这个session就会被保留下来,导致和后续的测试账户不匹配从而报错553。找到原因之后,使用Session.getInstance()方法取代Session.getDefaultInstance()去重新new一个session,问题得到解决。