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=trueEmailServer是一個典型的模板模式和觀察者模式的應用,模板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,包括工程(文件編碼),模板編碼,郵件內容編碼,否則會出現糾結的中文亂碼問題
工程源碼下載鏈接
