Velocity 是一个基于Java的模板引擎。它容许任何人使用一种简单但强大的模板语言去引用Java代码中定义的对象。css
Velocity的基本经常使用语法:https://www.cnblogs.com/xiohao/p/5788932.htmlhtml
最近在作ESL的邮件报警功能,邮件内容包含两个表格,分别填充两种报警内容,须要根据系统的语言设置显示不同的表头。前端
核心作法:java
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)如:spring
<!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>
发送邮件是在一个定时任务中,定时任务的代码如:apache
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找不到模板文件session
在我本地运行的时候并无这种问题,试了不少种方法,最后只能使用绝对路径,修改MailUtils中velocityEngine的Velocity.FILE_RESOURCE_LOADER_PATH的值:app
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); }
问题解决。dom
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,问题获得解决。