基於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,包括工程(文件編碼),模板編碼,郵件內容編碼,否則會出現糾結的中文亂碼問題
工程源碼下載鏈接





免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM