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

Spring Boot 整合 FreeMarker 發送郵件與附件完整實戰

     編輯:Java 綜合教學

Spring Boot 整合 FreeMarker 發送郵件與附件完整實戰

Spring 對 JavaMail 的封裝極大簡化了郵件發送開發。結合 FreeMarker 模板引擎,能優雅地生成富文本 HTML 郵件。本文基於 Spring Boot 3.x / Spring 6.x,展示從配置到發送的完整流程,含附件、內嵌圖片、異步發送等進階場景。

一、依賴配置

<dependencies>
    <!-- Spring Boot Mail Starter (含 JavaMail) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>
    <!-- FreeMarker 模板引擎 -->
    <dependency>
        <groupId>org.freemarker</groupId>
        <artifactId>freemarker</artifactId>
    </dependency>
    <!-- Lombok 簡化代碼 (可選) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

二、應用配置

# application.yml
spring:
  mail:
    host: smtp.qq.com
    port: 465                    # SSL 端口
    username: your_email@qq.com
    password: your_auth_code     # 授權碼,非登錄密碼
    protocol: smtp
    default-encoding: UTF-8
    properties:
      mail:
        smtp:
          auth: true
          ssl:
            enable: true         # 啟用 SSL
            # required: true     # 強制 SSL
          socketFactory:
            class: javax.net.ssl.SSLSocketFactory
            fallback: false
        debug: false             # 生產關閉
  freemarker:
    template-loader-path: classpath:/templates/mail/
    suffix: .ftl
    charset: UTF-8
    settings:
      number_format: '#'         # 避免數字顯示科學計數法

三、郵件模板設計

1. 用戶刪除通知模板 templates/mail/user-deleted.ftl

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <style>
        body { font-family: 'Microsoft YaHei', sans-serif; line-height: 1.6; color: #333; }
        .container { max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 8px; }
        .header { background: #f8f9fa; padding: 15px; border-radius: 6px 6px 0 0; border-bottom: 1px solid #e0e0e0; }
        .content { padding: 20px 15px; }
        .detail { background: #f8f9fa; padding: 15px; border-radius: 6px; margin-top: 15px; font-size: 14px; }
        .footer { margin-top: 20px; padding-top: 15px; border-top: 1px solid #e0e0e0; font-size: 12px; color: #999; text-align: center; }
        .badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; background: #ff4d4f; color: white; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h2 style="margin: 0; color: #1890ff;">用戶刪除通知</h2>
        </div>
        <div class="content">
            <p>您好,系統檢測到用戶 ${user.name} <span class="badge">已刪除</span>。</p>
            <div class="detail">
                <p><strong>用戶 ID</strong>: ${user.id}</p>
                <p><strong>郵箱</strong>: ${user.email}</p>
                <p><strong>註冊時間</strong>: ${user.createdTime?string("yyyy-MM-dd HH:mm:ss")}</p>
                <p><strong>操作人</strong>: ${operator}</p>
            </div>
        </div>
        <div class="footer">
            <p>此郵件由系統自動發送,請勿直接回覆。</p>
            <p>${systemName} 運營團隊 <#if currentTime??>- ${currentTime?string("yyyy-MM-dd HH:mm:ss")}</#if></p>
        </div>
    </div>
</body>
</html>

四、郵件發送服務層

package com.example.mail;

import freemarker.template.Configuration;
import freemarker.template.Template;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;

import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

@Service
@RequiredArgsConstructor
@Slf4j
public class MailService {

    private final JavaMailSender mailSender;
    private final Configuration freemarkerConfig;

    /**
     * 發送用戶刪除通知郵件 (含附件與內嵌圖片)
     */
    @Async("mailExecutor") // 異步發送,需配置 ThreadPoolTaskExecutor Bean 名為 mailExecutor
    public void sendUserDeletedNotification(User user, String operator) {
        MimeMessagePreparator preparator = mimeMessage -> {
            MimeMessageHelper helper = new MimeMessageHelper(
                    mimeMessage, 
                    MimeMessageHelper.MULTIPART_MODE_MIXED_RELATED, // 支援附件+內嵌資源
                    StandardCharsets.UTF_8.name()
            );

            helper.setFrom("your_email@qq.com");
            helper.setTo(user.getEmail()); // 或管理員郵箱
            helper.setSubject("【系統通知】用戶刪除:" + user.getName());

            // 1. 準備模型數據
            Map model = new HashMap<>();
            model.put("user", user);
            model.put("operator", operator);
            model.put("systemName", "MyApp 管理後台");
            model.put("currentTime", java.time.LocalDateTime.now());

            // 2. 渲染 FreeMarker 模板
            String htmlContent = FreeMarkerTemplateUtils.processTemplateIntoString(
                    freemarkerConfig.getTemplate("user-deleted.ftl"), model);

            // 3. 設置 HTML 內容 (true 表示是 HTML)
            helper.setText(htmlContent, true);

            // 4. 添加附件 (如用戶導出數據 CSV)
            helper.addAttachment("user-data-export.csv", 
                    new ClassPathResource("static/exports/user-" + user.getId() + ".csv"));

            // 5. 添加內嵌圖片 (HTML 中用 <img src="cid:logo"> 引用)
            helper.addInline("logo", new ClassPathResource("static/images/logo.png"));
        };

        try {
            mailSender.send(preparator);
            log.info("用戶刪除通知郵件發送成功: userId={}, to={}", user.getId(), user.getEmail());
        } catch (MailException e) {
            log.error("郵件發送失敗: userId={}, error={}", user.getId(), e.getMessage(), e);
            // 可拋出業務異常觸發重試/告警
        }
    }

    /**
     * 發送簡單文本郵件
     */
    public void sendSimpleMail(String to, String subject, String text) {
        try {
            MimeMessage message = mailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message, true, StandardCharsets.UTF_8.name());
            helper.setFrom("your_email@qq.com");
            helper.setTo(to);
            helper.setSubject(subject);
            helper.setText(text, false); // 純文本
            mailSender.send(message);
        } catch (MessagingException e) {
            throw new RuntimeException("簡單郵件發送失敗", e);
        }
    }
}

五、異步執行緒池配置 (避免阻塞主線程)

package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean("mailExecutor")
    public Executor mailExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("mail-async-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 拒絕策略:調用者執行
        executor.initialize();
        return executor;
    }
}

六、控制器調用示例

package com.example.controller;

import com.example.mail.MailService;
import com.example.entity.User;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {

    private final MailService mailService;

    @DeleteMapping("/{id}")
    public ResponseEntity deleteUser(@PathVariable Long id) {
        // 1. 查找用戶 (模擬)
        User user = userService.findById(id);
        if (user == null) {
            return ResponseEntity.notFound().build();
        }

        // 2. 執行刪除業務邏輯
        userService.deleteById(id);

        // 3. 異步發送通知郵件 (非阻塞)
        mailService.sendUserDeletedNotification(user, "admin");

        return ResponseEntity.noContent().build();
    }
}

七、進階技巧與避坑指南

1. 內嵌圖片 vs 附件

  • 內嵌圖片:`helper.addInline("cidId", resource)`,HTML 中 `` 顯示在郵件正文中。
  • 附件:`helper.addAttachment("filename", resource)`,顯示為郵件附件下載項。
  • MULTIPART_MODE_MIXED_RELATED:同時支援附件與內嵌資源的混合模式。

2. 郵件發送失敗重試策略

@Retryable(retryFor = {MailException.class}, maxAttempts = 3, backoff = @Backoff(delay = 2000))
public void sendWithRetry(MimeMessagePreparator preparator) {
    mailSender.send(preparator);
}

需啟用 `@EnableRetry` 並引入 `spring-retry` 依賴。

3. 大量郵件發送優化

  • 使用 `MimeMessagePreparator` 批量準備,單次 `send` 發送多個。
  • 複用 SMTP 連接:`spring.mail.properties.mail.smtp.connectionpool=true` (JavaMail 內建池化)。
  • 考慮專業郵件服務商 API (SendGrid, Amazon SES, 阿里雲郵件推送) 替代自建 SMTP,提升送達率。

4. 模板安全:防 XSS

FreeMarker 默認不轉義 `${var}`。若模型數據來自用戶輸入,務必手動轉義或配置全域轉義:

// 模板中手動轉義
${user.name?html}

// 或配置 Configuration
configuration.setOutputFormat(OutputFormat.HTML); // FreeMarker 2.3.24+
// 則 ${var} 自動 HTML 轉義,需原始輸出用 ${var?no_esc}

5. 常見問題排查

現象可能原因解決方案
535 Error: Authentication failed授權碼錯誤 / 未開啟 SMTP 服務郵箱設定中生成專用授權碼,開啟 SMTP/IMAP 服務
Could not connect to SMTP host端口/協議錯誤 / 防火牆攔截QQ 郵箱 SSL 用 465/587,確認 `ssl.enable=true`
附件中文檔名亂碼編碼問題使用 `MimeUtility.encodeText(filename)` 或 Spring Boot 2.2+ 自動處理
模板找不到路徑前綴/後綴配置錯誤檢查 `spring.freemarker.template-loader-path` 對應 `classpath:/templates/mail/`

八、總結

Spring Boot + JavaMail + FreeMarker 組合提供了企業級郵件發送能力:

  1. 配置簡單:Starter 自動裝配,YAML 外部化配置。
  2. 模板強大:FreeMarker 支援條件、循環、格式化、宏、佈局繼承。
  3. 功能完整:HTML、附件、內嵌圖片、異步、重試一應俱全。
  4. 生產就緒:結合線程池、連接池、專業郵件服務商,滿足高併發、高送達率需求。

掌握此棧,讓系統通知、營銷郵件、報表訂閱等場景開發事半功倍。

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