`
modabobo
  • 浏览: 508254 次
文章分类
社区版块
存档分类
最新评论

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

 
阅读更多

转自:http://www.cnblogs.com/javaexam2/archive/2011/07/24/2632543.html

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

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核心类源码解读

package com.crazycoder2010.email;

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.mail.Authenticator;
import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;

/**
 * 郵件服務器
 * 
 * @author Kevin
 * 
 */
public class EmailServer {
	private static final int POOL_SIZE = 5;
	private Session session;
	private ExecutorService theadPool;
	/**
	 * 郵件監聽器
	 */
	private List<EmailSendListener> emailSendListeners = new ArrayList<EmailSendListener>();

	public void init() {
		final Properties properties = SysConfig.getConfiguration();
		this.theadPool = Executors.newFixedThreadPool(POOL_SIZE);
		this.session = Session.getDefaultInstance(properties,
				new Authenticator() {
					@Override
					protected PasswordAuthentication getPasswordAuthentication() {
						return new PasswordAuthentication(properties
								.getProperty("mail.smtp.username"), properties
								.getProperty("mail.smtp.password"));
					}
				});
		this.session.setDebug(true);//生产环境把其设置为false
	}

	/**
	 * 發送單條email
	 * 
	 * @param emailInfo
	 */
	public void send(final EmailInfo emailInfo) {
		this.theadPool.execute(new Runnable() {
			public void run() {
				EmailContext emailContext = new EmailContext();
				emailContext.setEmailInfo(emailInfo);
				doBefore(emailContext);
				try {
					Message msg = buildEmailMessage(emailInfo);
					Transport.send(msg);
					doAfter(emailContext);
				} catch (Exception e) {
					emailContext.setThrowable(e);
					doAfterThrowable(emailContext);
				}
			}
		});
	}

	private Message buildEmailMessage(EmailInfo emailInfo)
			throws AddressException, MessagingException {
		MimeMessage message = new MimeMessage(this.session);
		message.setFrom(convertString2InternetAddress(emailInfo.getFrom()));
		message.setRecipients(Message.RecipientType.TO,
				converStrings2InternetAddresses(emailInfo.getTo()));
		message.setRecipients(Message.RecipientType.CC,
				converStrings2InternetAddresses(emailInfo.getCc()));

		Multipart multipart = new MimeMultipart();
		BodyPart messageBodyPart = new MimeBodyPart();
		messageBodyPart.setContent(emailInfo.getContent(), "text/html;charset=UTF-8");
		multipart.addBodyPart(messageBodyPart);
		message.setContent(multipart);
		message.setSubject(emailInfo.getTitle());
		message.saveChanges();
		return message;
	}

	private InternetAddress convertString2InternetAddress(String address)
			throws AddressException {
		return new InternetAddress(address);
	}

	private InternetAddress[] converStrings2InternetAddresses(String[] addresses)
			throws AddressException {
		final int len = addresses.length;
		InternetAddress[] internetAddresses = new InternetAddress[len];
		for (int i = 0; i < len; i++) {
			internetAddresses[i] = convertString2InternetAddress(addresses[i]);
		}
		return internetAddresses;
	}

	public void addEmailListener(EmailSendListener emailSendListener) {
		this.emailSendListeners.add(emailSendListener);
	}

	/**
	 * 發送多條email
	 * 
	 * @param emailInfos
	 */
	public void send(List<EmailInfo> emailInfos) {
		for (EmailInfo emailInfo : emailInfos) {
			send(emailInfo);
		}
	}

	private void doBefore(EmailContext emailContext) {
		for (EmailSendListener emailSendListener : this.emailSendListeners) {
			emailSendListener.before(emailContext);
		}
	}

	private void doAfter(EmailContext emailContext) {
		for (EmailSendListener emailSendListener : this.emailSendListeners) {
			emailSendListener.after(emailContext);
		}
	}

	private void doAfterThrowable(EmailContext emailContext) {
		for (EmailSendListener emailSendListener : this.emailSendListeners) {
			emailSendListener.afterThrowable(emailContext);
		}
	}
}

邮件服务器的配置参数

mail.transport.protocol=smtp
mail.smtp.port=25
mail.smtp.host=smtp.163.com
mail.smtp.username=chongzi1266
mail.smtp.password=*********
mail.smtp.connectiontimeout=10000
mail.smtp.timeout=10000
mail.smtp.auth=true

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

FreemarkerEmalTemplateService

package com.crazycoder2010.email;

import java.io.StringWriter;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import freemarker.cache.ClassTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;

/**
 * 基于Freemarker模板技术的邮件模板服务
 * @author Administrator
 *
 */
public class FreemarkerEmailTemplateService implements EmailTemplateService {
	/**
	 * 邮件模板的存放位置
	 */
	private static final String TEMPLATE_PATH = "/email/";
	/**
	 * 启动模板缓存
	 */
	private static final Map<String, Template> TEMPLATE_CACHE = new HashMap<String, Template>();
	/**
	 * 模板文件后缀
	 */
	private static final String SUFFIX = ".ftl";
	/**
	 * 模板引擎配置
	 */
	private Configuration configuration;
	public void init(){
		configuration = new Configuration();
		configuration.setTemplateLoader(new ClassTemplateLoader(FreemarkerEmailTemplateService.class, TEMPLATE_PATH));
		configuration.setEncoding(Locale.getDefault(), "UTF-8");
		configuration.setDateFormat("yyyy-MM-dd HH:mm:ss");
	}

	public String getText(String templateId, Map<Object, Object> parameters) {
		String templateFile = templateId + SUFFIX;
		try {
			Template template = TEMPLATE_CACHE.get(templateFile);
			if(template == null){
				template = configuration.getTemplate(templateFile);
				TEMPLATE_CACHE.put(templateFile, template);
			}
			StringWriter stringWriter = new StringWriter();
			template.process(parameters, stringWriter);
			return stringWriter.toString();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
}
默认的模板技术实现,这里对模板采用了缓存技术,第一次用到模板的时候会去读取文件,以后都共享内存中的实例了

EmailSendFacade

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

package com.crazycoder2010.email;

/**
 * 邮件发送门面类,用于客户端直接调用
 * @author Administrator
 *
 */
public class EmailSendFacade {
	private EmailTemplateService emailTemplateService;
	private EmailServer emailServer;
	public void setEmailTemplateService(EmailTemplateService emailTemplateService) {
		this.emailTemplateService = emailTemplateService;
	}
	public void setEmailServer(EmailServer emailServer) {
		this.emailServer = emailServer;
	}
	/**
	 * 发送邮件
	 * @param emailInfo 邮件参数封装,emailInfo的title和content字段的值将被重置为实际的值
	 */
	public void send(EmailInfo emailInfo){
		String title = emailTemplateService.getText(emailInfo.getTemplateId()+"-title", emailInfo.getParameters());
		String content = emailTemplateService.getText(emailInfo.getTemplateId()+"-body", emailInfo.getParameters());
		emailInfo.setContent(content);
		emailInfo.setTitle(title);
		
		emailServer.send(emailInfo);
	}
}
注意这里对邮件模板做了约定,因为邮件模板包括两部分标题和内容,所以对于一个指定的邮件模板templateId=reset_password,其模板分别为reset_password-title.ftl和reset_password-body.ftl,通过这个约定,调用者只需要传递一个template就可以了而程序内部会去分别读取body和title的值

客户端调用(junit)

package com.crazycoder2010.email;

import org.junit.Test;

public class EmailSendFacadeTest {
	@Test
	public void testSend() throws InterruptedException {
		//启动邮件服务器
		EmailServer emailServer = new EmailServer();
		emailServer.init();
		emailServer.addEmailListener(new ConsoleEmailSendListener());
		emailServer.addEmailListener(new DatabaseEmailSendListener());
		
		//启动模板服务
		EmailTemplateService emailTemplateService = new FreemarkerEmailTemplateService();
		emailTemplateService.init();//模板引擎初始化
		
		//组装邮件发送门面类
		EmailSendFacade emailSendFacade = new EmailSendFacade();
		emailSendFacade.setEmailServer(emailServer);//注册邮件服务器
		emailSendFacade.setEmailTemplateService(emailTemplateService);//注册模板
		
		//测试数据
		EmailInfo emailInfo = new EmailInfo();
		emailInfo.setFrom("chongzi1266@163.com");
		//emailInfo.setTo(new String[]{"to_01@localhost","to_02@localhost"});
		//emailInfo.setCc(new String[]{"cc_01@localhost","cc_02@localhost"});
		emailInfo.setTo(new String[]{"wangxuzheng@gmail.com","12708826@qq.com"});
		emailInfo.setCc(new String[]{"kwang2003@msn.com","wangxuzheng1983@hotmail.com"});
		emailInfo.setTemplateId("reset_password");
		emailInfo.addParameter("name", "Kevin");
		emailInfo.addParameter("newPassword", "123456");
		
		//发送
		emailSendFacade.send(emailInfo);
		Thread.sleep(10000);
	}
}
这个测试程序写了很长的代码,其实大部分都在做一些核心对象的创建和set操作,在真实的生产环境中这些代码都由DI容器(spring,guiice)自动完成
总结:

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

其他:

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


分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics