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