Web 系統中「發送郵件」看似簡單,實則暗藏維護陷阱:
.properties 檔,導致:
{0}、{1})只能靠 MessageFormat 硬拼,缺乏型別安全。XML 以樹狀結構天然適合描述郵件模板,配合 Commons Digester 將 XML 直接映射為 Java Bean,再由 JavaMail 發送,即可構建「模板外部化、參數化、型別安全」的郵件引擎。
<?xml version="1.0" encoding="UTF-8"?>
<email>
<from>system@example.com</from>
<to>user@example.com</to>
<to>admin@example.com</to> <!-- 多收件人 -->
<cc>backup@example.com</cc>
<subject>Welcome, {0}! Your order {1} is confirmed.</subject>
<body contentType="text/html"><![CDATA[
<h1>Hello {0},</h1>
<p>Order <strong>{1}</strong> has been confirmed.</p>
<p>Estimated delivery: {2}</p>
]]></body>
<attachment path="/reports/invoice_{1}.pdf"/> <!-- 可選附件 -->
</email>
優勢:
{0}、{1} 統一由 MessageFormat 處理,主題與內容共用同一參數陣列。public class EmailTemplate {
private String from;
private final List to = new ArrayList<>();
private final List cc = new ArrayList<>();
private final List bcc = new ArrayList<>();
private String subject;
private String body;
private String contentType = "text/plain"; // 或 text/html
private final List attachments = new ArrayList<>();
// Getter/Setter 省略
public InternetAddress[] getToAddresses() { return toAddresses(to); }
public InternetAddress[] getCcAddresses() { return toAddresses(cc); }
public InternetAddress[] getBccAddresses() { return toAddresses(bcc); }
private InternetAddress[] toAddresses(List list) {
return list.stream().map(this::toAddr).toArray(InternetAddress[]::new);
}
private InternetAddress toAddr(String s) {
try { return new InternetAddress(s); }
catch (Exception e) { throw new IllegalArgumentException("Invalid email: " + s, e); }
}
}
public static EmailTemplate parse(InputStream xml) {
Digester digester = new Digester();
digester.setValidating(false); // 無 DTD/XSD 時關閉驗證
digester.addObjectCreate("email", EmailTemplate.class);
// 單值屬性
digester.addBeanPropertySetter("email/from", "from");
digester.addBeanPropertySetter("email/subject", "subject");
digester.addBeanPropertySetter("email/body", "body");
digester.addBeanPropertySetter("email/body/@contentType", "contentType");
// 多值屬性:呼叫 addTo/addCc/addBcc
digester.addCallMethod("email/to", "addTo", 0);
digester.addCallMethod("email/cc", "addCc", 0);
digester.addCallMethod("email/bcc", "addBcc", 0);
// 附件(示範單屬性)
digester.addCallMethod("email/attachment", "addAttachment", 1);
digester.addCallParam("email/attachment/@path", 0);
try {
return (EmailTemplate) digester.parse(xml);
} catch (IOException | SAXException e) {
log.error("Parse email template failed", e);
return null;
}
}
關鍵點:addCallMethod(..., 0)表示元素體文本作為唯一參數傳給同名方法;addCallParam用於提取屬性值。
public class EmailSender {
private static final Session SESSION;
static {
Properties props = new Properties();
props.put("mail.smtp.host", Config.get("smtp.host"));
props.put("mail.smtp.port", Config.get("smtp.port"));
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
SESSION = Session.getInstance(props, new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(Config.get("smtp.user"), Config.get("smtp.pass"));
}
});
}
/** 發送郵件
* @param toOverride 若非 null,覆蓋模板內 to 清單(如密碼重設只寄給申請人)
* @param template 解析後的模板
* @param args MessageFormat 參數陣列,{0} {1}... 對應 subject/body 佔位符
*/
public static void send(String toOverride, EmailTemplate template, Object... args) {
if (template == null) return;
try {
MimeMessage msg = new MimeMessage(SESSION);
msg.setFrom(new InternetAddress(template.getFrom()));
msg.setRecipients(Message.RecipientType.TO,
toOverride != null ? new InternetAddress[]{new InternetAddress(toOverride)}
: template.getToAddresses());
if (!template.getCc().isEmpty()) msg.setRecipients(Message.RecipientType.CC, template.getCcAddresses());
if (!template.getBcc().isEmpty()) msg.setRecipients(Message.RecipientType.BCC, template.getBccAddresses());
// 參數化主題與內容
String subject = MessageFormat.format(template.getSubject(), args);
String body = MessageFormat.format(template.getBody(), args);
msg.setSubject(subject, "UTF-8");
// 支援純文字 / HTML / 附件 的 MimeMultipart 組裝(此處略,參考 JavaMail 文件)
msg.setContent(body, template.getContentType() + "; charset=UTF-8");
Transport.send(msg);
} catch (MessagingException e) {
log.error("Send mail failed", e);
throw new RuntimeException(e);
}
}
}
@WebServlet("/register")
public class RegisterServlet extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String username = req.getParameter("username");
String email = req.getParameter("email");
String orderId = UUID.randomUUID().toString().substring(0, 8);
// 1. 載入模板(放在 WEB-INF/email/ 目錄,不對外公開)
InputStream is = getServletContext().getResourceAsStream("/WEB-INF/email/orderConfirm.xml");
EmailTemplate tmpl = EmailTemplate.parse(is);
// 2. 發送(覆蓋 to 為用戶信箱,參數依序對應 {0} {1} {2})
EmailSender.send(email, tmpl, username, orderId, LocalDate.now().plusDays(3).toString());
resp.getWriter().write("註冊成功,確認信已寄出。");
}
}
<bodyPlain>、<bodyHtml>,發送時組裝 multipart/alternative。<attachment generator="com.example.InvoicePdfGenerator" param="orderId"/>,發送時反射實例化產生器。WatchService 監控模板目錄,變更時自動重新解析,無需重啟應用。「XML 模板 + Digester 映射 + JavaMail 發送」三步走,將郵件內容從業務代碼中徹底剝離,實現:
這是從「能跑」邁向「好維護」的關鍵架構升級。