基于Freemarker模板技术的邮件发送模块设计

来源:互联网 发布:孤岛惊魂4优化太垃圾 编辑:程序博客网 时间:2024/05/22 00:51
1.项目背景    设计一个通用的邮件发送模块,为上层应用提供服务,对上层屏蔽掉发送邮件的细节,上层只需要简单的调用即可,要求可以实时发送但又不能影响效率,对发送失败的邮件系统可以记录下来,以便后期重发
2.需求分析
   关键点有
   2.1邮件内容的存放
      a)直接把邮件内容写死在代码里,然后拼接成一个很长的字符串,缺点也很明显,要改邮件的内容必修修改代码,重新编译打包
 b)邮件内容与代码相分离.将邮件的内容文件化,java代码中只是引用模板的位置,然后解析模块中的内容输出,这种方案有着更高的可维护性,扩展起来也更方便
   2.2发送邮件的效率
      发邮件是一件很耗费性能的操作,如果系统中会频繁用到邮件发送,邮件发送不要影响正常的业务操作
   2.3自动记录错误和重发
      邮件发送失败时,出错的邮件要保存起来,以便日后重发
3.关键技术点
   3.1.email发送可以通过javamail api实现
   3.2邮件内容模板采用的是freemarker技术来实现
   3.3异步发送邮件,采用的是java的多线程机制
4.设计细节

   4.1整体类图


4.2类描述

EmailServer:邮件服务器,用来进行邮件服务器的配置和实际的邮件发送,这里调用底层的javamail实现,核心方法

      send(EmailInfo emailInfo)这个是个邮件发送的模板方法

EmailSendListener:邮件发送器监听程序,一个observer模式的实现,当有邮件要发送时触发,可以为邮件服务器配置一个或多个监听程序,定义了三个核心接口方法

      before(EmailContext emailContext)邮件发送前做的操作

      after(EmailContext emailContext)邮件发送结束后做的操作

      afterThrowable(EmailContext emailContext)邮件发送出现异常时做的处理

EmailTemplateService:邮件的内容采用了模板技术来实现, 定义一个统一的顶层接口getText,对于不同的模板技术实现Freemarker或Velocity分别实现该方法

EmailSendFacade:邮件发送模块对外暴露的外部接口,用来封装各个底层实现细节

EmailContext:邮件监听器用到的邮件发送上线文信息,主要有EmailInfo邮件基本信息和Throwable两个字段

4.3系统时序图


4.4项目整体目录结构


4.5核心类源码解读

[java] view plaincopyprint?
  1. package com.crazycoder2010.email;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.List;  
  5. import java.util.Properties;  
  6. import java.util.concurrent.ExecutorService;  
  7. import java.util.concurrent.Executors;  
  8.   
  9. import javax.mail.Authenticator;  
  10. import javax.mail.BodyPart;  
  11. import javax.mail.Message;  
  12. import javax.mail.MessagingException;  
  13. import javax.mail.Multipart;  
  14. import javax.mail.PasswordAuthentication;  
  15. import javax.mail.Session;  
  16. import javax.mail.Transport;  
  17. import javax.mail.internet.AddressException;  
  18. import javax.mail.internet.InternetAddress;  
  19. import javax.mail.internet.MimeBodyPart;  
  20. import javax.mail.internet.MimeMessage;  
  21. import javax.mail.internet.MimeMultipart;  
  22.   
  23. /** 
  24.  * 郵件服務器 
  25.  *  
  26.  * @author Kevin 
  27.  *  
  28.  */  
  29. public class EmailServer {  
  30.     private static final int POOL_SIZE = 5;  
  31.     private Session session;  
  32.     private ExecutorService theadPool;  
  33.     /** 
  34.      * 郵件監聽器 
  35.      */  
  36.     private List<EmailSendListener> emailSendListeners = new ArrayList<EmailSendListener>();  
  37.   
  38.     public void init() {  
  39.         final Properties properties = SysConfig.getConfiguration();  
  40.         this.theadPool = Executors.newFixedThreadPool(POOL_SIZE);  
  41.         this.session = Session.getDefaultInstance(properties,  
  42.                 new Authenticator() {  
  43.                     @Override  
  44.                     protected PasswordAuthentication getPasswordAuthentication() {  
  45.                         return new PasswordAuthentication(properties  
  46.                                 .getProperty("mail.smtp.username"), properties  
  47.                                 .getProperty("mail.smtp.password"));  
  48.                     }  
  49.                 });  
  50.         this.session.setDebug(true);//生产环境把其设置为false  
  51.     }  
  52.   
  53.     /** 
  54.      * 發送單條email 
  55.      *  
  56.      * @param emailInfo 
  57.      */  
  58.     public void send(final EmailInfo emailInfo) {  
  59.         this.theadPool.execute(new Runnable() {  
  60.             public void run() {  
  61.                 EmailContext emailContext = new EmailContext();  
  62.                 emailContext.setEmailInfo(emailInfo);  
  63.                 doBefore(emailContext);  
  64.                 try {  
  65.                     Message msg = buildEmailMessage(emailInfo);  
  66.                     Transport.send(msg);  
  67.                     doAfter(emailContext);  
  68.                 } catch (Exception e) {  
  69.                     emailContext.setThrowable(e);  
  70.                     doAfterThrowable(emailContext);  
  71.                 }  
  72.             }  
  73.         });  
  74.     }  
  75.   
  76.     private Message buildEmailMessage(EmailInfo emailInfo)  
  77.             throws AddressException, MessagingException {  
  78.         MimeMessage message = new MimeMessage(this.session);  
  79.         message.setFrom(convertString2InternetAddress(emailInfo.getFrom()));  
  80.         message.setRecipients(Message.RecipientType.TO,  
  81.                 converStrings2InternetAddresses(emailInfo.getTo()));  
  82.         message.setRecipients(Message.RecipientType.CC,  
  83.                 converStrings2InternetAddresses(emailInfo.getCc()));  
  84.   
  85.         Multipart multipart = new MimeMultipart();  
  86.         BodyPart messageBodyPart = new MimeBodyPart();  
  87.         messageBodyPart.setContent(emailInfo.getContent(), "text/html;charset=UTF-8");  
  88.         multipart.addBodyPart(messageBodyPart);  
  89.         message.setContent(multipart);  
  90.         message.setSubject(emailInfo.getTitle());  
  91.         message.saveChanges();  
  92.         return message;  
  93.     }  
  94.   
  95.     private InternetAddress convertString2InternetAddress(String address)  
  96.             throws AddressException {  
  97.         return new InternetAddress(address);  
  98.     }  
  99.   
  100.     private InternetAddress[] converStrings2InternetAddresses(String[] addresses)  
  101.             throws AddressException {  
  102.         final int len = addresses.length;  
  103.         InternetAddress[] internetAddresses = new InternetAddress[len];  
  104.         for (int i = 0; i < len; i++) {  
  105.             internetAddresses[i] = convertString2InternetAddress(addresses[i]);  
  106.         }  
  107.         return internetAddresses;  
  108.     }  
  109.   
  110.     public void addEmailListener(EmailSendListener emailSendListener) {  
  111.         this.emailSendListeners.add(emailSendListener);  
  112.     }  
  113.   
  114.     /** 
  115.      * 發送多條email 
  116.      *  
  117.      * @param emailInfos 
  118.      */  
  119.     public void send(List<EmailInfo> emailInfos) {  
  120.         for (EmailInfo emailInfo : emailInfos) {  
  121.             send(emailInfo);  
  122.         }  
  123.     }  
  124.   
  125.     private void doBefore(EmailContext emailContext) {  
  126.         for (EmailSendListener emailSendListener : this.emailSendListeners) {  
  127.             emailSendListener.before(emailContext);  
  128.         }  
  129.     }  
  130.   
  131.     private void doAfter(EmailContext emailContext) {  
  132.         for (EmailSendListener emailSendListener : this.emailSendListeners) {  
  133.             emailSendListener.after(emailContext);  
  134.         }  
  135.     }  
  136.   
  137.     private void doAfterThrowable(EmailContext emailContext) {  
  138.         for (EmailSendListener emailSendListener : this.emailSendListeners) {  
  139.             emailSendListener.afterThrowable(emailContext);  
  140.         }  
  141.     }  
  142. }  

邮件服务器的配置参数

[plain] view plaincopyprint?
  1. mail.transport.protocol=smtp  
  2. mail.smtp.port=25  
  3. mail.smtp.host=smtp.163.com  
  4. mail.smtp.username=chongzi1266  
  5. mail.smtp.password=*********  
  6. mail.smtp.connectiontimeout=10000  
  7. mail.smtp.timeout=10000  
  8. mail.smtp.auth=true  
EmailServer是一个典型的模板模式和观察者模式的应用,模板send方法中采用java线程池技术ExcecuteService,在初始化时初始大小为5的线程池,以后每次发送邮件都开启一个新的任务来执行,每发送一个邮件都依次执行EmailSendListener的before,after,afterThrowable方法,从来可以灵活扩展邮件发送的处理逻辑,如默认情况下我们可能只是想要跟踪一下邮件的发送过程,在邮件的发送开始,结束和异常出现时打印出一些基本信息(ConsoleEmailSendListener),实际生产环境时,我们希望把发送失败的邮件和失败的原因记录到数据库,以存后期重发用,这个时候我们就可以提供另一个实现类(DatabaseEmailSendListener)来达到这个效果了,而对于我们整个EmailSever不需要做任何改动,从而达到开闭的原则

FreemarkerEmalTemplateService

[java] view plaincopyprint?
  1. package com.crazycoder2010.email;  
  2.   
  3. import java.io.StringWriter;  
  4. import java.util.HashMap;  
  5. import java.util.Locale;  
  6. import java.util.Map;  
  7.   
  8. import freemarker.cache.ClassTemplateLoader;  
  9. import freemarker.template.Configuration;  
  10. import freemarker.template.Template;  
  11.   
  12. /** 
  13.  * 基于Freemarker模板技术的邮件模板服务 
  14.  * @author Administrator 
  15.  * 
  16.  */  
  17. public class FreemarkerEmailTemplateService implements EmailTemplateService {  
  18.     /** 
  19.      * 邮件模板的存放位置 
  20.      */  
  21.     private static final String TEMPLATE_PATH = "/email/";  
  22.     /** 
  23.      * 启动模板缓存 
  24.      */  
  25.     private static final Map<String, Template> TEMPLATE_CACHE = new HashMap<String, Template>();  
  26.     /** 
  27.      * 模板文件后缀 
  28.      */  
  29.     private static final String SUFFIX = ".ftl";  
  30.     /** 
  31.      * 模板引擎配置 
  32.      */  
  33.     private Configuration configuration;  
  34.     public void init(){  
  35.         configuration = new Configuration();  
  36.         configuration.setTemplateLoader(new ClassTemplateLoader(FreemarkerEmailTemplateService.class, TEMPLATE_PATH));  
  37.         configuration.setEncoding(Locale.getDefault(), "UTF-8");  
  38.         configuration.setDateFormat("yyyy-MM-dd HH:mm:ss");  
  39.     }  
  40.   
  41.     public String getText(String templateId, Map<Object, Object> parameters) {  
  42.         String templateFile = templateId + SUFFIX;  
  43.         try {  
  44.             Template template = TEMPLATE_CACHE.get(templateFile);  
  45.             if(template == null){  
  46.                 template = configuration.getTemplate(templateFile);  
  47.                 TEMPLATE_CACHE.put(templateFile, template);  
  48.             }  
  49.             StringWriter stringWriter = new StringWriter();  
  50.             template.process(parameters, stringWriter);  
  51.             return stringWriter.toString();  
  52.         } catch (Exception e) {  
  53.             throw new RuntimeException(e);  
  54.         }  
  55.     }  
  56. }  
默认的模板技术实现,这里对模板采用了缓存技术,第一次用到模板的时候会去读取文件,以后都共享内存中的实例了

EmailSendFacade

门面模式的应用,封装了EmailServer和EmailTemplateService,对外部封装内部实现细节

[java] view plaincopyprint?
  1. package com.crazycoder2010.email;  
  2.   
  3. /** 
  4.  * 邮件发送门面类,用于客户端直接调用 
  5.  * @author Administrator 
  6.  * 
  7.  */  
  8. public class EmailSendFacade {  
  9.     private EmailTemplateService emailTemplateService;  
  10.     private EmailServer emailServer;  
  11.     public void setEmailTemplateService(EmailTemplateService emailTemplateService) {  
  12.         this.emailTemplateService = emailTemplateService;  
  13.     }  
  14.     public void setEmailServer(EmailServer emailServer) {  
  15.         this.emailServer = emailServer;  
  16.     }  
  17.     /** 
  18.      * 发送邮件 
  19.      * @param emailInfo 邮件参数封装,emailInfo的title和content字段的值将被重置为实际的值 
  20.      */  
  21.     public void send(EmailInfo emailInfo){  
  22.         String title = emailTemplateService.getText(emailInfo.getTemplateId()+"-title", emailInfo.getParameters());  
  23.         String content = emailTemplateService.getText(emailInfo.getTemplateId()+"-body", emailInfo.getParameters());  
  24.         emailInfo.setContent(content);  
  25.         emailInfo.setTitle(title);  
  26.           
  27.         emailServer.send(emailInfo);  
  28.     }  
  29. }  
注意这里对邮件模板做了约定,因为邮件模板包括两部分标题和内容,所以对于一个指定的邮件模板templateId=reset_password,其模板分别为reset_password-title.ftl和reset_password-body.ftl,通过这个约定,调用者只需要传递一个template就可以了而程序内部会去分别读取body和title的值

客户端调用(junit)

[java] view plaincopyprint?
  1. package com.crazycoder2010.email;  
  2.   
  3. import org.junit.Test;  
  4.   
  5. public class EmailSendFacadeTest {  
  6.     @Test  
  7.     public void testSend() throws InterruptedException {  
  8.         //启动邮件服务器  
  9.         EmailServer emailServer = new EmailServer();  
  10.         emailServer.init();  
  11.         emailServer.addEmailListener(new ConsoleEmailSendListener());  
  12.         emailServer.addEmailListener(new DatabaseEmailSendListener());  
  13.           
  14.         //启动模板服务  
  15.         EmailTemplateService emailTemplateService = new FreemarkerEmailTemplateService();  
  16.         emailTemplateService.init();//模板引擎初始化  
  17.           
  18.         //组装邮件发送门面类  
  19.         EmailSendFacade emailSendFacade = new EmailSendFacade();  
  20.         emailSendFacade.setEmailServer(emailServer);//注册邮件服务器  
  21.         emailSendFacade.setEmailTemplateService(emailTemplateService);//注册模板  
  22.           
  23.         //测试数据  
  24.         EmailInfo emailInfo = new EmailInfo();  
  25.         emailInfo.setFrom("chongzi1266@163.com");  
  26.         //emailInfo.setTo(new String[]{"to_01@localhost","to_02@localhost"});  
  27.         //emailInfo.setCc(new String[]{"cc_01@localhost","cc_02@localhost"});  
  28.         emailInfo.setTo(new String[]{"wangxuzheng@gmail.com","12708826@qq.com"});  
  29.         emailInfo.setCc(new String[]{"kwang2003@msn.com","wangxuzheng1983@hotmail.com"});  
  30.         emailInfo.setTemplateId("reset_password");  
  31.         emailInfo.addParameter("name""Kevin");  
  32.         emailInfo.addParameter("newPassword""123456");  
  33.           
  34.         //发送  
  35.         emailSendFacade.send(emailInfo);  
  36.         Thread.sleep(10000);  
  37.     }  
  38. }  
这个测试程序写了很长的代码,其实大部分都在做一些核心对象的创建和set操作,在真实的生产环境中这些代码都由DI容器(spring,guiice)自动完成
总结:

这个模块的设计参考了junit3.8优秀的设计思想,采用observer+template来实现灵活扩展邮件功能的方式,采用了邮件模板技术来实现邮件发送内容多样化,配置化,多线程的引入提高了系统的执行效率

其他:

项目中统一编码为UTF-8,包括工程(文件编码),模板编码,邮件内容编码,否则会出现纠结的中文乱码问题
工程源码下载链接

0 0
原创粉丝点击