SpringMail 邮件服务 - 第四章. Simple Java Mail:更优雅的 API

SpringMail 邮件服务 - 第四章. Simple Java Mail:更优雅的 API

本章将介绍 Simple Java Mail 这个第三方库,它提供了比 Spring 原生 API 更简洁的邮件构建方式,但也会增加项目依赖。我们将深入分析它的设计哲学,并给出明确的选型建议。

本章学习路径

  • 对比阶段:理解原生 API 的痛点
  • 认知阶段:掌握 Fluent API 的设计思想
  • 实践阶段:使用 Simple Java Mail 构建邮件
  • 决策阶段:学会在不同场景下做技术选型

4.1. 原生 JavaMailSender 的痛点

在第二章中,我们已经掌握了 Spring 原生的 JavaMailSenderMimeMessageHelper。它们功能完整、稳定可靠,但在实际使用中会遇到一些体验问题。

4.1.1. MimeMessageHelper 的繁琐配置

回顾一下第二章中发送 HTML 邮件的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void sendHtmlMail(String to, String subject, String htmlContent) {
try {
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");

helper.setFrom("你的邮箱@qq.com");
helper.setTo(to);
helper.setSubject(subject);
helper.setText(htmlContent, true);

mailSender.send(mimeMessage);

} catch (MessagingException e) {
throw new RuntimeException("HTML 邮件发送失败", e);
}
}

这段代码有几个问题:

问题一:样板代码过多

每次发送邮件都需要:

  1. 创建 MimeMessage
  2. 创建 MimeMessageHelper
  3. 设置各种属性
  4. 手动处理 MessagingException

如果你的项目中有 10 个不同的邮件发送场景(验证码、订单通知、密码重置等),就需要写 10 遍类似的代码。

问题二:参数含义不直观

1
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");

这行代码中的 true 是什么意思?如果不看文档,你很难知道它表示 “支持 multipart(附件)”。

再看这行:

1
helper.setText(htmlContent, true);

第二个 true 又是什么意思?它表示 “内容是 HTML 格式”。

这种 “布尔陷阱”(Boolean Trap)让代码的可读性大打折扣。

问题三:异常处理繁琐

MimeMessageHelper 的几乎所有方法都会抛出 MessagingException,这是一个受检异常(Checked Exception),必须显式处理。

在实际项目中,我们通常会将其包装成运行时异常:

1
2
3
4
5
try {
// 邮件发送逻辑
} catch (MessagingException e) {
throw new RuntimeException("邮件发送失败", e);
}

每个方法都要写这样的 try-catch,代码变得冗长。

4.1.2. 代码可读性问题

当邮件逻辑变复杂时(比如同时包含 HTML、附件、内嵌图片),代码会变得难以阅读。

看一个真实的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void sendComplexMail(String to, String subject, String htmlContent,
String attachmentPath, String logoPath) {
try {
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");

helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(htmlContent, true);

// 添加附件
File attachment = new File(attachmentPath);
helper.addAttachment(attachment.getName(), new FileSystemResource(attachment));

// 添加内嵌图片
File logo = new File(logoPath);
helper.addInline("logo", new FileSystemResource(logo));

mailSender.send(mimeMessage);

} catch (MessagingException e) {
throw new RuntimeException("复杂邮件发送失败", e);
}
}

这段代码的问题:

  1. 顺序性强:必须先创建 MimeMessage,再创建 Helper,再设置属性,顺序不能乱
  2. 层次不清晰:所有操作都在同一层级,看不出哪些是核心逻辑,哪些是辅助操作
  3. 难以扩展:如果要添加抄送、密送、回复地址,代码会越来越长

4.2. Simple Java Mail 的设计哲学

Simple Java Mail 是一个开源的 Java 邮件库,它的核心目标是 “让邮件发送变得简单”。

4.2.1. Fluent API 链式调用

Fluent API | 流式接口 | 一种 API 设计风格,通过方法链式调用来构建对象,提高代码可读性

Simple Java Mail 采用了 Builder 模式Fluent API,让邮件构建过程像 “说话” 一样自然。

对比一下同样的功能,用 Simple Java Mail 怎么写:

1
2
3
4
5
6
7
EmailBuilder.startingBlank()
.from("发件人", "sender@example.com")
.to("收件人", "recipient@example.com")
.withSubject("邮件主题")
.withHTMLText("<h1>这是 HTML 内容</h1>")
.withAttachment("报表.pdf", new FileDataSource("report.pdf"))
.buildEmail();

这段代码的优势:

  1. 自解释:每个方法名都清晰表达了意图(fromtowithSubject
  2. 链式调用:一气呵成,不需要中间变量
  3. 无异常:不需要 try-catch,异常在内部处理

4.2.2. 与 Spring Boot 的无缝集成

Simple Java Mail 提供了 Spring Boot Starter,可以自动读取 application.yml 中的配置,无需手动创建 Mailer 对象。

它的集成策略是:

  1. 复用 Spring 的配置:读取 spring.mail.* 配置
  2. 提供额外配置:通过 simplejavamail.* 前缀添加专属配置
  3. 自动装配:提供 Mailer Bean,可以直接注入使用

这意味着,你可以在不改变现有配置的情况下,逐步迁移到 Simple Java Mail。


4.3. 快速上手

现在我们动手使用 Simple Java Mail,体验它的便利性。

4.3.1. 引入依赖

pom.xml 中添加 Simple Java Mail 的 Spring Boot Starter:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.simplejavamail</groupId>
<artifactId>simple-java-mail</artifactId>
<version>8.5.1</version>
</dependency>

<dependency>
<groupId>org.simplejavamail</groupId>
<artifactId>spring-module</artifactId>
<version>8.5.1</version>
</dependency>

依赖解读:

  • simple-java-mail:核心库,提供 EmailBuilderMailer 等类
  • spring-module:Spring 集成模块,提供自动配置和 Bean 注入

版本选择:8.5.1 是截至 2025 年的最稳定版本,支持 Jakarta Mail(Spring Boot 3 必需)。

包体积:Simple Java Mail 核心库约 500KB,加上依赖总共约 2MB。相比 Thymeleaf(3MB+),它更轻量。

4.3.2. 配置文件与 Spring 集成

Simple Java Mail 可以复用 Spring Boot 的邮件配置,也可以使用自己的配置前缀。

方式一:复用 Spring 配置(推荐)

如果你已经配置了 spring.mail.*,Simple Java Mail 会自动读取:

1
2
3
4
5
6
7
8
9
10
11
12
spring:
mail:
host: smtp.qq.com
port: 587
username: 你的邮箱@qq.com
password: 授权码
properties:
mail:
smtp:
auth: true
starttls:
enable: true

Simple Java Mail 会自动创建一个 Mailer Bean,你可以直接注入使用。

方式二:使用 Simple Java Mail 专属配置

如果你想使用 Simple Java Mail 的高级特性(如连接池、自定义验证),可以使用 simplejavamail.* 前缀:

1
2
3
4
5
6
7
8
9
10
11
simplejavamail:
smtp:
host: smtp.qq.com
port: 587
username: 你的邮箱@qq.com
password: 授权码
transportstrategy: SMTP_TLS
defaults:
from:
name: 系统通知
address: 你的邮箱@qq.com

配置解读:

  • transportstrategy:传输策略,可选值:
    • SMTP:明文传输(不推荐)
    • SMTPS:SSL 加密(465 端口)
    • SMTP_TLS:STARTTLS 加密(587 端口)
  • defaults.from:默认发件人,所有邮件都会使用这个发件人(除非显式覆盖)

两种配置的优先级simplejavamail.* 的优先级高于 spring.mail.*。如果两者都配置了,Simple Java Mail 会使用自己的配置。

创建必要的 config 文件

1
2
3
4
5
6
7
8
9
10
package com.example.demo.config;

import org.simplejavamail.springsupport.SimpleJavaMailSpringSupport;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import(SimpleJavaMailSpringSupport.class)
public class MailConfig {
}

4.3.3. 使用 EmailBuilder 构建邮件

现在我们可以使用 Simple Java Mail 发送邮件了。创建一个新的 Service 类:

📄 文件:src/main/java/com/example/demo/service/SimpleMailService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.example.demo.service;

import lombok.RequiredArgsConstructor;
import org.simplejavamail.api.email.Email;
import org.simplejavamail.api.mailer.Mailer;
import org.simplejavamail.email.EmailBuilder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class SimpleMailService {
// 注入 Simple Java Mail 自动配置的 Mailer
private final Mailer mailer;

/**
* 发送纯文本邮件
*/
public void sendSimpleText(String to, String subject, String Content) {
Email email = EmailBuilder.startingBlank()
// 设置发件人,第一个参数是显示名称,第二个是邮箱地址
.from("系统通知", "3381292732@qq.com")
// 设置收件人
.to("收件人", to)
// 设置主题
.withSubject(subject)
.withPlainText(Content)
.buildEmail();
mailer.sendMail(email);
}
}

代码解读:

  1. EmailBuilder.startingBlank():创建一个空白的邮件构建器
  2. from(name, address):设置发件人,第一个参数是显示名称,第二个是邮箱地址
  3. to(name, address):设置收件人
  4. withSubject():设置主题
  5. withPlainText():设置纯文本内容
  6. buildEmail():构建最终的 Email 对象
  7. mailer.sendMail():发送邮件

与原生 API 的对比

维度原生 JavaMailSenderSimple Java Mail
代码行数10+ 行7 行
异常处理需要 try-catch无需处理
可读性中等
参数含义不直观(布尔陷阱)自解释

发送 HTML 邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 发送 HTML 邮件
*/
public void sendHtmlMail(String to, String subject, String htmlContent) {
Email email = EmailBuilder.startingBlank()
.from("系统通知", "你的邮箱@qq.com")
.to(to)
.withSubject(subject)
.withHTMLText(htmlContent)
.buildEmail();

mailer.sendMail(email);
}

注意 to(to) 的简写形式:如果不需要显示名称,可以只传邮箱地址。

发送带附件的邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.simplejavamail.api.email.AttachmentResource;
import javax.activation.FileDataSource;

/**
* 发送带附件的邮件
*/
public void sendMailWithAttachment(String to, String subject, String content, String filePath) {
Email email = EmailBuilder.startingBlank()
.from("系统通知", "你的邮箱@qq.com")
.to(to)
.withSubject(subject)
.withPlainText(content)
.withAttachment("附件.pdf", new FileDataSource(filePath))
.buildEmail();

mailer.sendMail(email);
}

withAttachment 方法的两个参数:

  1. 显示名称:附件在邮件中显示的名称(可以与实际文件名不同)
  2. 数据源FileDataSource 从文件系统加载,也可以用 ByteArrayDataSource 从内存加载

发送带内嵌图片的邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 发送带内嵌图片的 HTML 邮件
*/
public void sendHtmlMailWithInlineImage(String to, String subject) {
String htmlContent = "<html><body>" +
"<h2>欢迎使用我们的服务</h2>" +
"<img src='cid:logo' style='width:200px;' />" +
"</body></html>";

Email email = EmailBuilder.startingBlank()
.from("系统通知", "你的邮箱@qq.com")
.to(to)
.withSubject(subject)
.withHTMLText(htmlContent)
.withEmbeddedImage("logo", new FileDataSource("src/main/resources/static/images/logo.png"))
.buildEmail();

mailer.sendMail(email);
}

withEmbeddedImage 的第一个参数是 CID(Content-ID),必须与 HTML 中的 cid: 后面的名称一致。


4.4. 高级特性

Simple Java Mail 不仅简化了基础操作,还提供了一些原生 API 难以实现的高级特性。

4.4.1. 理解邮件发送的两种模式

在讲连接池之前,我们先理解邮件发送的本质:每次发送邮件都需要与 SMTP 服务器建立连接。

模式一:每封邮件独立连接(原生 JavaMailSender 的默认行为)

1
2
3
4
5
6
7
8
// 伪代码演示连接过程
for (int i = 0; i < 10; i++) {
// 1. 建立 TCP 连接(三次握手)
// 2. SMTP 握手(EHLO/HELO)
// 3. 认证(AUTH LOGIN)
// 4. 发送邮件(MAIL FROM, RCPT TO, DATA)
// 5. 关闭连接(QUIT)
}

这种模式的问题:每次都要重复步骤 1-3,浪费时间。

模式二:连接复用(Simple Java Mail 的 batch-module)

1
2
3
4
5
6
7
8
// 伪代码演示连接池
// 1. 预先建立 4 个连接(默认)
// 2. 发送邮件时从池中获取连接
for (int i = 0; i < 10; i++) {
// 直接使用已建立的连接发送邮件
// 跳过 TCP 握手和 SMTP 认证
}
// 3. 连接空闲 5 秒后自动关闭

这种模式的优势:省去了重复的握手和认证过程。

4.4.2. batch-module 的真实价值

Simple Java Mail 的 batch-module 提供了连接池功能。根据官方文档,它能将性能提升约 2 倍

适用场景

连接池并不是所有场景都需要。以下是典型的业务场景分类:

场景一:单封邮件(不需要连接池)

  • 用户注册时发送验证码
  • 用户点击 “忘记密码” 后发送重置链接
  • 用户提交工单后发送确认邮件

这些场景的特点是:触发频率低,单次只发一封。使用连接池反而会浪费资源(连接一直保持但很少使用)。

场景二:短时间内连续发送(需要连接池)

  • 电商平台:每天晚上 8 点批量发送 “订单发货通知”(可能几百封)
  • SaaS 系统:每月 1 号批量发送账单邮件(可能几千封)
  • 营销系统:活动开始时批量发送促销邮件

这些场景的特点是:短时间内集中发送多封邮件。连接池能显著提升效率。

4.4.3. 引入 batch-module

pom.xml 中添加依赖:

1
2
3
4
5
<dependency>
<groupId>org.simplejavamail</groupId>
<artifactId>batch-module</artifactId>
<version>8.5.1</version>
</dependency>

重要提示:引入 batch-module 后,JVM 不会自动退出(因为连接池线程一直运行)。在应用关闭时,需要手动调用:

1
mailer.shutdownConnectionPool();

4.4.4. 配置连接池

batch-module 提供了 4 个核心配置参数。

参数一:coresize(核心连接数)

1
2
3
4
simplejavamail:
defaults:
connectionpool:
coresize: 2

含义:连接池中始终保持 2 个活跃连接,即使没有邮件发送。

何时使用:如果你的应用需要持续发送邮件(如每分钟都有订单通知),设置 coresize > 0 可以避免频繁创建连接。

何时不用:如果邮件发送是偶发的(如每小时才几封),设置 coresize = 0(默认值),让连接在空闲时自动关闭。

参数二:maxsize(最大连接数)

1
2
3
4
simplejavamail:
defaults:
connectionpool:
maxsize: 10

含义:连接池最多同时存在 10 个连接。如果 10 个连接都在使用中,第 11 封邮件会等待。

如何选择:取决于 SMTP 服务器的并发限制。大多数邮件服务商限制单个账号最多 10-20 个并发连接。

参数三:claimtimeout(获取连接超时)

1
2
3
4
5
simplejavamail:
defaults:
connectionpool:
claimtimeout:
millis: 10000

含义:如果连接池满了,等待最多 10 秒获取连接。超时则抛出异常。

参数四:expireafter(连接空闲多久后关闭)

1
2
3
4
5
simplejavamail:
defaults:
connectionpool:
expireafter:
millis: 5000

含义:连接空闲 5 秒后自动关闭(默认值)。

如何选择

  • 如果邮件发送是 “突发式” 的(如每天晚上 8 点集中发送),保持默认 5 秒即可
  • 如果邮件发送是 “持续式” 的(如每分钟都有),可以延长到 30 秒或更久

4.4.5. 真实场景:批量发送邮件

假设你需要批量发送邮件,例如每天晚上 8 点批量发送通知,平均 500 封邮件。

步骤 1:配置连接池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
simplejavamail:
smtp:
host: smtp.qq.com
port: 587
username: 你的邮箱@qq.com
password: 授权码
transportstrategy: SMTP_TLS
defaults:
connectionpool:
coresize: 0 # 不需要常驻连接
maxsize: 10 # 最多 10 个并发连接
claimtimeout:
millis: 30000 # 等待 30 秒
expireafter:
millis: 10000 # 空闲 10 秒后关闭

步骤 2:编写批量发送代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 批量发送邮件通知
*/
public void sendShippingNotifications(List<String> recipients) {
for (String recipient : recipients) {
Email email = EmailBuilder.startingBlank()
.from("商城客服", "service@shop.com")
.to("尊敬的客户", recipient)
.withSubject("您的订单已发货")
.withHTMLText("您好,您的订单已发货,感谢您的购买!")
.buildEmail();

// 异步发送(不阻塞主线程)
CompletableFuture<Void> future = mailer.sendMail(email, true);
future.whenComplete((result, exception) -> {
if (exception != null) {
log.error("邮件发送失败:{}", recipient, exception);
// 记录失败收件人,稍后重试
} else {
log.info("邮件发送成功:{}", recipient);
}
});
}

System.out.println("已提交 " + recipients.size() + " 封邮件到发送队列");
}

sendMail 的第二个参数 true 表示异步发送。邮件会被放入队列,由后台线程池处理。

步骤 3:监控发送结果

Simple Java Mail 的异步发送返回 CompletableFuture,可以监控每封邮件的发送状态。上面的代码示例中已经包含了监控逻辑,通过 whenComplete 回调可以:

  • 记录发送成功的邮件
  • 捕获发送失败的异常,便于后续重试
  • 实时查看发送进度(通过日志)

步骤 4:使用 Controller 接口测试批量发送

由于批量发送是异步的,JUnit 测试会在主线程结束后立即关闭,导致异步任务可能未完成就被中断。建议使用 Controller 接口进行测试,这样可以保持应用运行,让异步任务有足够时间完成。

创建测试接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@RestController
@RequestMapping("/api/mail")
@RequiredArgsConstructor
public class MailController {

private final SimpleMailService simpleMailService;

/**
* 测试批量发送邮件接口
* 访问示例: http://localhost: 8080/api/mail/test-batch?count = 100
*/
@GetMapping("/test-batch")
public Map<String, Object> testBatchSend(@RequestParam(defaultValue = "100") int count) {
log.info("开始测试批量发送邮件,数量: {}", count);

List<String> recipients = new ArrayList<>();
for (int i = 1; i <= count; i++) {
recipients.add("test-user-" + i + "@example.com");
}

long startTime = System.currentTimeMillis();
simpleMailService.sendShippingNotifications(recipients);
long endTime = System.currentTimeMillis();

Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "已提交 " + count + " 封邮件到发送队列");
response.put("count", count);
response.put("submitTime", endTime - startTime + "ms");
response.put("note", "邮件正在后台异步发送,请查看日志了解发送进度");

return response;
}
}

测试方法:

  1. 启动应用后,在浏览器或使用 curl 访问:

    1
    http://localhost:8080/api/mail/test-batch?count=500
  2. 接口会立即返回提交结果,邮件在后台异步发送。

  3. 查看应用日志,监控每封邮件的发送状态(通过步骤 2 中的 whenComplete 回调)。

优势:

  • 应用持续运行,异步任务不会被中断
  • 可以随时调整发送数量进行测试
  • 通过日志实时查看发送进度和结果
  • 更接近生产环境的真实场景

4.4.6. 自定义邮件验证规则

Simple Java Mail 内置了 RFC 5322 标准的邮箱地址验证。如果你的业务有特殊需求,可以自定义验证规则。

场景:只允许公司内部邮箱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class MailConfig {

@Bean
public Mailer customMailer() {
return MailerBuilder
.withSMTPServer("smtp.company.com", 587, "user", "pass")
.withTransportStrategy(TransportStrategy.SMTP_TLS)
.withEmailValidator(email -> {
if (!email.endsWith("@company.com")) {
throw new IllegalArgumentException("只允许公司邮箱");
}
})
.buildMailer();
}
}

这样,如果尝试发送邮件到 user@gmail.com,会在发送前抛出异常。


4.5. 本章小结与配置速查

在这一章中,我们深入学习了 Simple Java Mail 这个第三方库。

核心知识回顾

我们解决了以下问题:

  • 原生 API 的三大痛点(样板代码、布尔陷阱、异常处理)
  • Fluent API 的设计思想(链式调用、自解释)
  • batch-module 的真实价值(连接复用,性能提升约 2 倍)
  • 如何根据业务场景做技术选型(量化标准)

两种 API 对比速查表

维度JavaMailSenderSimple Java Mail
依赖大小0(Spring 内置)核心库 500KB,总计约 2MB
代码量基准减少 33%
可读性中等
连接池不支持支持(需 batch-module)
性能提升基准批量发送约快 2 倍
学习成本

核心配置模板

基础配置(不使用连接池)

1
2
3
4
5
6
7
8
9
10
11
simplejavamail:
smtp:
host: smtp.qq.com
port: 587
username: 你的邮箱@qq.com
password: 授权码
transportstrategy: SMTP_TLS
defaults:
from:
name: 系统通知
address: 你的邮箱@qq.com

高级配置(使用连接池)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
simplejavamail:
smtp:
host: smtp.qq.com
port: 587
username: 你的邮箱@qq.com
password: 授权码
transportstrategy: SMTP_TLS
defaults:
from:
name: 系统通知
address: 你的邮箱@qq.com
connectionpool:
coresize: 0
maxsize: 10
claimtimeout:
millis: 30000
expireafter:
millis: 10000

核心代码模板

场景一:单封邮件(同步发送)

1
2
3
4
5
6
7
8
Email email = EmailBuilder.startingBlank()
.from("发件人", "sender@example.com")
.to("recipient@example.com")
.withSubject("主题")
.withPlainText("内容")
.buildEmail();

mailer.sendMail(email); // 阻塞直到发送完成

场景二:批量邮件(异步发送)

1
2
3
4
5
6
7
8
9
10
for (String recipient : recipients) {
Email email = EmailBuilder.startingBlank()
.from("sender@example.com")
.to(recipient)
.withSubject("批量通知")
.withPlainText("内容")
.buildEmail();

mailer.sendMail(email, true); // 异步发送,立即返回
}

场景三:监控发送结果

1
2
3
4
5
6
7
8
9
CompletableFuture<Void> future = mailer.sendMail(email, true);

future.whenComplete((result, exception) -> {
if (exception != null) {
log.error("发送失败", exception);
} else {
log.info("发送成功");
}
});