首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

打造可維護的 Java 郵件系統:XML 模板 + Commons Digester + JavaMail

     編輯:Java 相關知識

痛點:硬編碼郵件內容的維護噩夢

Web 系統中「發送郵件」看似簡單,實則暗藏維護陷阱:

  • 郵件類型多樣(驗證碼、訂單確認、密碼重設、行銷推播),每種主題、內容、收件人規則皆不同。
  • 傳統做法將文字寫死在 Java 程式碼或 .properties 檔,導致:
    1. 屬性檔鍵值結構單一,難以表達「多收件人、多抄送、HTML/純文字雙版本」等複雜結構。
    2. 長文本在單行屬性值中極難閱讀、編輯(換行需轉義、特殊字元需跳脫)。
    3. 動態參數插值(如 {0}、{1})只能靠 MessageFormat 硬拼,缺乏型別安全。

XML 以樹狀結構天然適合描述郵件模板,配合 Commons Digester 將 XML 直接映射為 Java Bean,再由 JavaMail 發送,即可構建「模板外部化、參數化、型別安全」的郵件引擎。

一、郵件模板 XML 結構設計

<?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>

優勢:

  • 結構化表達多值欄位(to/cc/bcc 可重複)。
  • CDATA 區塊完整保留 HTML 格式、換行、特殊字元,無需轉義。
  • 參數佔位符 {0}、{1} 統一由 MessageFormat 處理,主題與內容共用同一參數陣列。

二、領域模型:EmailTemplate

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); }
    }
}

三、Commons Digester 解析規則

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 用於提取屬性值。

四、統一發送入口:EmailSender

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);
        }
    }
}

五、Servlet / Service 層調用範例

@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("註冊成功,確認信已寄出。");
    }
}

六、進階擴展方向

  1. 雙版本郵件:模板新增 <bodyPlain>、<bodyHtml>,發送時組裝 multipart/alternative。
  2. 附件動態生成:<attachment generator="com.example.InvoicePdfGenerator" param="orderId"/>,發送時反射實例化產生器。
  3. 模板熱加載:搭配 WatchService 監控模板目錄,變更時自動重新解析,無需重啟應用。
  4. 發送狀態追蹤:引入資料表記錄 message-id、狀態、重試次數,配合排程重發失敗郵件。

結語

「XML 模板 + Digester 映射 + JavaMail 發送」三步走,將郵件內容從業務代碼中徹底剝離,實現:

  • 前端/產品人員可直接編輯 XML 調整版面、文字,無需開發介入。
  • 結構化資料支援多收件人、抄送、附件、HTML 等豐富功能。
  • 統一參數化機制,型別安全、易測試、易國際化(同一模板多語言版本)。

這是從「能跑」邁向「好維護」的關鍵架構升級。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved