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,問題獲得解決。