發(fā)送郵件是web應(yīng)用系統(tǒng)的一個基本功能。一般來說,郵件都有特定的類型,比如說密碼提醒,歡迎信息,訂單確認(rèn)或者收信確認(rèn)。盡管不同應(yīng)用郵件的內(nèi)容各不相同,但是發(fā)送郵件的過程基本上是一樣的。 構(gòu)建消息,發(fā)送給郵件服務(wù)器,發(fā)送。
當(dāng)使用java開發(fā)的時候,我們常常使用JavaMail API 來連接郵件服務(wù)器發(fā)送郵件。但是這種方式過于笨重(主要由郵件的靈活性造成的),所以當(dāng)你需要多次使用這種方式發(fā)送郵件的時候,最好寫一個wrapper.根據(jù)使用的方式不同,wrapper可以是發(fā)送某一特定的郵件,比如說密碼提醒,或者作為一種通用的模式,接受主題,接收人,郵件內(nèi)容作為參數(shù)。
一旦使用wrapper發(fā)送郵件,你需要一個自主構(gòu)建消息的系統(tǒng)。讓我們使用密碼提醒作為例子。基本上所有的郵件都包含主題,內(nèi)容和接收人。當(dāng)我們發(fā)送密碼提醒郵件的時候,用戶地址和密碼是從某個記錄登陸信息的知識庫里提取的。主題和內(nèi)容需要和數(shù)據(jù)庫提取的數(shù)據(jù)合并,并且被保存在某個地方。系統(tǒng)設(shè)計(jì)最大的問題就是在什么地方保存這種類型的字符串。在很多情形下,字串被保存在屬性文件里,這種方式分離了數(shù)據(jù)和源代碼,并且使本地化更加容易。我在很多web應(yīng)用系統(tǒng)中使用了這種存儲機(jī)制,但很不幸的是,這種方式有很多缺陷。
以下是利用屬性文件存儲郵件字串不合適的原因:
·屬性文件使用一種非常簡單的數(shù)據(jù)結(jié)構(gòu)-名稱和值組合。當(dāng)你需要很多值對應(yīng)一個名稱的時候這種結(jié)構(gòu)就不合適了。比如,一個郵件有4個接收人,3個抄送人,使用屬性文件很難解決這個問題。
·屬性文件的格式非常嚴(yán)格。名稱和值必須在同一行上,所以當(dāng)你編輯文件的時候長字符串是很難處理的。比如,把一個郵件的所有內(nèi)容放進(jìn)屬性文件是一件多么痛苦的事情。如果你希望值的內(nèi)容包括換行,你必須使用
另一種選擇是使用XML作為郵件模板,這也是本篇文章所要討論的內(nèi)容。XML為你構(gòu)建模板提供了極大的靈活性,并且它不會有屬性文件所有的格式限制,因此這種方式很容易處理長字符串。XML主要弱勢就是它處理起來比屬性文件復(fù)雜。使用屬性文件的時候,裝載文件和裝載后訪問文件非常容易。而裝載XML文件和使用java提供的多個XML處理庫之一處理XML文件就需要更多的工作了。
這篇文章和所附的代碼提供了一個通用的模板使你能夠使用XML文件創(chuàng)建模板并且發(fā)送郵件,希望由此能夠減輕這個過程的痛苦。在這個模板里,我將使用Jakarta 項(xiàng)目里的Commons Digester 包來處理XML,使用JavaMail API發(fā)送郵件。
郵件模板
讓我們來看看郵件模板的格式。模板是XML文件,它包含一個根元素和一系列根的子元素。根元素是<email>。必要的子元素是<subject>, <body>, 和 <from>。可選的子元素是 <to>, <cc>, 和 <bcc>。如果你使用過郵件系統(tǒng),那么你可以推導(dǎo)出這些元素實(shí)際包含的內(nèi)容。可選的元素有多個實(shí)例,所以你可以為每種類型的接收者指定多個地址。我待會會在描述消息處理的時候來解釋運(yùn)行機(jī)制。以下是一個模板文件的例子。
<email>
<from>rafe@rafe.us</from>
<to>someone@example.com</to>
<cc>someoneelse@example.com</cc>
<bcc>rafe@rafe.us</bcc>
<subject>This is the subject</subject>
<body>This is the body of an email message.</body>
</email>
可定制的模板
屬性文件的一個有用的特性是你可以使用MessageFormat 類用動態(tài)傳入的值替代屬性文件里的被指定參數(shù)。比如說,如果你需要在屬性文件里指定errors,其中一個errors是file not found, 你可以這樣寫:
file.not.found.error=Error, could not find file {0}.
然后,在運(yùn)行時刻,你這樣使用MessageFormat:
ResourceBundle bundle = ResourceBundle.getBundle(
"MyProperties", currentLocale);
Object[] arguments = { "some_file.txt" };
String newString = MessageFormat.format(
bundle.getString("file.not.found.error"), arguments);
最后,newString 將包含Error, could not find file some_file.txt.我在這個系統(tǒng)里加入了類似的靈活性。 可以格式化所有的字符串,所以你可以在郵件模版的subject 和body元素里內(nèi)嵌在屬性文件使用的同樣的令牌。
在某種情形下,你希望在發(fā)送郵件的時候插入個人化的信息。比如,你希望在郵件內(nèi)容里或者訂單的內(nèi)容里包含收件人的姓。本系統(tǒng)使用MessageFormat 來處理郵件模版的內(nèi)容和主題,從而解決這個問題。處理內(nèi)容和主題的時候只使用一個參數(shù)數(shù)組。這樣主題里可以包含令牌{0}, {2}, {3}, 內(nèi)容可以包含令牌{0}, {1}, {4} 。我之所以采用這種方式是因?yàn)樵诤芏嗲樾蜗轮黝}和內(nèi)容使用相同的參數(shù),同時這種方式也簡化了傳遞給EmailSender所需要的參數(shù)。
處理模版
創(chuàng)建完模版,下一步所要做的就是處理它。我們知道,現(xiàn)在有很多的XML處理包可供選擇。Commons Digester是Jakarta的公共項(xiàng)目,最初是為了在Struts項(xiàng)目中快速方便的解析Struts的的配置文件而產(chǎn)生的。它提供了從XML文件里的元素到使用類似于XPath 語法的數(shù)據(jù)結(jié)構(gòu)的映射。 好處在于為了從 XML文件里得到某個元素你不必用SAX一個節(jié)點(diǎn)一個節(jié)點(diǎn)的解析,也不必使用DOM處理樹狀數(shù)據(jù)結(jié)構(gòu)。
下面這個方法從XML文件里讀取數(shù)據(jù),然后把數(shù)據(jù)拷貝到EmailTemplate對象中。
public static EmailTemplate getEmailTemplate(InputStream aStream)
{
Digester digester = new Digester();
digester.setValidating(false);
digester.addObjectCreate("email", EmailTemplate.class);
digester.addBeanPropertySetter("email/subject", "subject");
digester.addBeanPropertySetter("email/body", "body");
digester.addBeanPropertySetter("email/from", "from");
digester.addCallMethod("email/to", "addTo", 0);
digester.addCallMethod("email/cc", "addCc", 0);
digester.addCallMethod("email/bcc", "addBcc", 0);
try
{
return (EmailTemplate)digester.parse(aStream);
}
catch (IOException e)
{
logger.error("Error: ", e);
return null;
}
catch (SAXException e)
{
logger.error("Error: ", e);
return null;
}
}
讓我們來逐行研究這段代碼。Commons Digester工作的原理是由你來指定解析文件的一些規(guī)則。因?yàn)闆]有規(guī)范郵件模版的DTD文件,所以在指定處理規(guī)則之前,我將validating flag設(shè)定為false。開始處理文件的時候,我實(shí)例化Digester對象然后調(diào)用方法建立數(shù)據(jù)映射規(guī)則。首先,我調(diào)用addObjectCreate()方法來建立創(chuàng)建EmailTemplate對象的規(guī)則。email是XML模版文件的根元素。因此模版文件和EmailTemplate 對象一一對應(yīng)。
我使用addBeanPropertySetter()來處理在模版文件中只出現(xiàn)一次的元素。這個方法有兩個參數(shù),元素的路徑和要調(diào)用的賦值方法。在第一次調(diào)用的時候,我指定在文件中符合email/subject 模式的元素應(yīng)該賦值給EmailTemplate 類的subject 。我們用 “/”來描速XML文件的內(nèi)嵌關(guān)系。在這個例子中,符合subject模式的元素是email 子元素。為了提供更多的靈活性我們可以使用Wildcards。參考Commons Digester的JavaDoc 你可以了解詳細(xì)的模式的構(gòu)成方式。
使用賦值方法處理在模版文件中出現(xiàn)多次的元素是不可行的。我們使用addCallMethod()來處理這種情形,這個方法從元素中取值并且調(diào)用指定的方法。我使用這個方法有三個參數(shù)的版本,它們是:匹配的模式,調(diào)用的方法,調(diào)用方法所使用的參數(shù)數(shù)量。在例子的三種情形中第三個參數(shù)都是0,說明符合模式的元素是調(diào)用方法的唯一參數(shù)。在EmailTemplate類中我定義了三個方法:addTo(), addCc(), addBcc(),這三個方法將模版文件中的收件人列表加入到模版類的收件人集合中。
郵件元素的六種類型的子元素的規(guī)則都被指定好之后,我開始解析這個文件。在這個例子中, 我傳入getEmailTemplate 方法的輸入?yún)?shù)InputStream 。parse方法可以解析File,SAX InputSource, InputStream, Reader, 目標(biāo)文件的URI。我使用InputStream。 由調(diào)用這個方法的代碼取得XML文件并且把它轉(zhuǎn)化為InputStream 。為了讓這個方法更加通用,我可以用Object作為參數(shù),并且在方法內(nèi)部使用instanceof 來確定參數(shù)的類型,再用相應(yīng)的方式來處理。
方法parse 拋出IOException 或者SAXException。把這些異常傳給Log4J,由它來處理,返回null. 如果沒有異常拋出, 將返回由Digester創(chuàng)建的EmailTemplate對象。
EmailTemplate類剩下的部分
getEmailTemplate()方法是類EmailTemplate的核心。其他的部分是一些屬性值和一些輔助性的方法。有3個String 類型的屬性值:內(nèi)容,主題,寄件人地址,3個ArrayList屬性值:to, CC, BCC 列表,這3個值都以String作為基本元素。還有相應(yīng)的get,set和加入集合的方法。還有3個附加的方便的方法:getToAddresses(), getCcAddresses(), 和 getBccAddresses()。JavaMail接口需要InternetAddress 數(shù)組作為地址集合的參數(shù),這些方法可以把對象的String數(shù)組轉(zhuǎn)化為JavaMail接口需要的數(shù)組形式。
類EmailSender
當(dāng)模版文件被解析成EmailTemplate對象,下一步就是發(fā)送郵件信息。EmailSender 類包含一個靜態(tài)的,重載的方法-sendEmail()。 這個方法可以通過很多種方式調(diào)用,所有的方式都是對下面這個完全參數(shù)方法的一個引用:
public static void sendEmail(
String aTo,
EmailTemplate aTemplate,
String[] aArgs)
參數(shù)不需要過多的解釋。第一個是郵件的發(fā)送地址。你可以在郵件模版里指定很多接收人地址,但是在運(yùn)行時刻,大多數(shù)情況下,系統(tǒng)只需要一個接收人。比如說,你發(fā)送一封密碼提醒的郵件,只需要指定申請密碼的用戶的郵件地址。在郵件模版里指定的收件人列表在某種情況下適用:作為測試,系統(tǒng)需要發(fā)送郵件到特定收件人列表或者發(fā)送時需要包含特定收件人列表。比如說,假設(shè)一個系統(tǒng)每當(dāng)訂單提交的時候需要通過一封郵件觸發(fā)一個workflow,在這種情形下郵件模版種特定的接收人地址是有意義的。
第二個參數(shù)是EmailTemplate自身。第三個參數(shù)是MessageFormat解析郵件主題和內(nèi)容所需要的參數(shù)集。由調(diào)用這個方法的代碼來創(chuàng)建個性化郵件模版所需要的信息數(shù)組。也有其他申明的方法簡化了這個方法的調(diào)用(所以你可以在不指定收件人,或者在沒有參數(shù)的情況下調(diào)用這個方法)。
方法內(nèi)部由使用JavaMail發(fā)送郵件所需要的一系列調(diào)用組成。我覺得使用JavaMail會造成許多冗余,我們來具體看一下。首先,我要通過檢測來確定EmailTemplate是否為空。如果為空,什么都不能做。設(shè)定的第一步是使用SMTP server的設(shè)置創(chuàng)建一個Properties對象(Hashtable)。我把SMTP server的設(shè)置設(shè)定在 文件里,所以我把這個值從屬性文件里讀出來然后放到我創(chuàng)建的properties對象里去。
接著我創(chuàng)建了一個JavaMail Session 對象傳入Properties 對象。Session對象在創(chuàng)建MimeMessage對象的時候需要。這個是我待會要做的。然后我將From:的值指定到傳入?yún)?shù)EmailTemplate對象的相應(yīng)欄位。下一步我把To:的值設(shè)定到我構(gòu)建的消息中。這里會有一些技巧,因?yàn)橛脩艨梢詡魅隩o: 地址,同時郵件模版里也包含一些To:地址。問題在于JavaMail 喜歡使用數(shù)組描速地址列表,所以由我來決定接收人列表的有多大,然后構(gòu)建傳入的參數(shù)。
因?yàn)镃C: BCC:的地址必須在模版里指定,我們可以直接來處理它們。我使用EmailTemplate類里的方法把其他的收件人加入到消息里。就像我開始提到的,我使用MessageFormat解析處理郵件主題和內(nèi)容的方法所需要的參數(shù)集。做完之后,我把新的主題拷貝到消息主體里。如此處理消息的內(nèi)容。剩下的就是調(diào)用Transport.send()并且傳入MimeMessage 對象。
使用這個系統(tǒng)
我剛才已經(jīng)解釋了系統(tǒng)的運(yùn)作原理,現(xiàn)在我來解釋如何通過 servlet來使用它,在其他程序里調(diào)用的方式是類似的。以下是代碼:
// Grab the email template.
InputStream template =
getServlet()
.getServletConfig()
.getServletContext()
.getResourceAsStream(
"/WEB-INF/email/registrationNotification.xml");
EmailTemplate notification = EmailTemplate.getEmailTemplate(template);
// Create the section of the email containing the actual user data.
String[] args = { "Rafe" };
EmailSender.sendEmail("rafe@rafe.us", notification, args);
使用這個系統(tǒng)的第一步是把你的XML模版文件轉(zhuǎn)化成InputStream。 因?yàn)槲沂褂玫氖莝ervlet,我從ServletContext取得這個文件。當(dāng)然還有其他的方式取得這個文件,但是在servlet環(huán)境里,這種方式很好用。我只用把InputStream 傳給剛才所描述的EmailTemplate.getEmailTemplate()方法就可以了。下一步,建立個性化郵件所需要的參數(shù)數(shù)組,然后調(diào)用方法EmailSender.sendEmail()。
更多
這個系統(tǒng)還可以更多的優(yōu)化,有兩個比較明顯的需要改善的地方:系統(tǒng)應(yīng)該同時支持純文本和HTML;支持附件。創(chuàng)建這種類型的信息需要使用類型javax.mail.MimeMultipart。還有在何處存儲附件和如何指定附件的問題。在我的系統(tǒng)里,我沒有在模版文件里處理附件,因?yàn)槲业母郊窃卩]件發(fā)送的時候創(chuàng)建的。
Rafe Colburn 是一個Java開發(fā)工程師,同時也是一名計(jì)算機(jī)圖書的作者,他使用過Perl,CGI, HTML, JAVA