Java Email:八年老开发手把手教你搞定邮件发送(附附件实战)
作为一名写了八年 Java 的开发者,发送邮件这事儿几乎贯穿了我参与的所有项目 —— 从早期的用户注册验证码,到后来的订单通知、系统告警,再到带 Excel 报表的批量推送。看似简单的功能,实则藏着不少 “坑”:要么附件发不出去,要么邮件进垃圾箱,要么高并发下直接阻塞主线程。
这篇文章就从实际业务出发,结合八年踩坑经验,聊聊 Java 发送邮件(尤其是带附件)的核心逻辑、代码实现和最佳实践。内容偏实战,适合有一定基础的开发者,新手也能跟着步骤走通。
一、先聊聊:哪些业务场景必须搞定邮件发送?
八年里,我经手的邮件发送场景大概分这几类,几乎覆盖了 90% 的业务需求:
1. 系统通知类(最基础)
-
用户注册后的验证邮件(带激活链接)
-
密码重置邮件(带临时验证码)
-
账号异常登录告警(IP、设备信息)
-
订单状态变更(支付成功、发货通知)
这类场景的特点:内容简单(文本 / HTML)、实时性要求高、不能失败。比如验证码邮件晚到 1 分钟,用户可能就流失了。
2. 业务数据类(带附件高频)
-
每日 / 月度报表推送(Excel/PDF 格式的销售数据、日志统计)
-
合同 / 单据发送(PDF 格式的电子合同、发货单)
-
批量数据导出结果通知(用户导出大量数据后,邮件发送下载链接 + 附件)
这类场景的核心:附件处理、大文件适配、批量发送。曾遇到过财务系统发月报时,附件太大被邮箱服务器拒收的坑。
3. 营销 / 运营类(易踩垃圾邮件坑)
-
活动通知(新功能上线、优惠活动)
-
会员关怀(生日祝福、积分提醒)
这类场景最容易出问题:内容太 “硬” 会被标记为垃圾邮件,需要严格控制发送频率和内容格式。
二、核心思路:Java 发送邮件的底层逻辑是什么?
1. 本质:基于 SMTP 协议的客户端实现
邮件发送的本质是 “客户端→服务器→接收方服务器” 的通信,Java 只是实现了 SMTP 协议的客户端。流程如下:
你的应用(Java程序)→ 发件人邮箱SMTP服务器(如smtp.qq.com)→ 收件人邮箱服务器 → 收件人客户端
2. 选对工具:别再自己写底层,用 Spring Boot 封装好的 JavaMailSender
早期用原生 JavaMail API(javax.mail),需要手动处理会话、协议、异常,代码冗余且易出错。现在优先用 Spring Boot 的JavaMailSender,封装了所有底层细节,几行代码就能搞定。
核心依赖(Maven):
<!-- Spring Boot邮件 starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>2.7.0</version> <!-- 用项目对应的Spring Boot版本 -->
</dependency>
<!-- 工具类简化(非必需,八年开发习惯用Lombok) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
3. 关键配置:不同邮箱的 SMTP 参数(避坑重点)
发送邮件前必须配置发件人邮箱的 SMTP 信息,不同邮箱的参数不同,八年经验总结最常用的几个:
| 邮箱类型 | SMTP 服务器地址 | 端口(SSL) | 备注 |
|---|---|---|---|
| QQ 邮箱 | smtp.qq.com | 465 | 需要开启 “SMTP 服务”,用授权码登录 |
| 163/126 邮箱 | smtp.163.com | 465 | 授权码 = 登录密码(部分账号) |
| 企业邮箱(阿里) | smtp.qiye.aliyun.com | 465 | 用企业邮箱账号 + 密码登录 |
| Gmail | smtp.gmail.com | 465 | 需科学上网,且开启低安全应用权限 |
坑点提醒:
- QQ 邮箱必须在 “设置→账户” 中开启 “POP3/SMTP 服务”,并获取授权码(不是登录密码),否则会报 “认证失败”。
- 企业邮箱可能需要 IT 部门开放 SMTP 权限,曾遇到过因 “IP 白名单” 限制导致发送失败的情况。
三、核心代码:从基础邮件到带附件,一步到位
1. 配置文件(application.yml)
先在配置文件中填写 SMTP 参数,建议区分环境(开发 / 生产):
spring:
mail:
host: smtp.qq.com # SMTP服务器地址
port: 465 # SSL端口
username: 123456@qq.com # 发件人邮箱
password: xxxxxxxxxx # 授权码(不是登录密码!)
default-encoding: UTF-8
properties:
mail:
smtp:
auth: true # 开启认证
ssl:
enable: true # 启用SSL加密
starttls:
enable: true
required: true
# 可选:配置发件人昵称(显示在收件箱中)
from: 系统通知<123456@qq.com>
2. 封装通用邮件工具类(八年开发:复用 = 效率)
写一个EmailService,封装文本邮件、HTML 邮件、带附件邮件的发送逻辑,避免重复代码:
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.File;
@Service
@Slf4j
@RequiredArgsConstructor // 注入JavaMailSender
public class EmailService {
private final JavaMailSender mailSender;
@Value("${spring.mail.from}") // 从配置文件获取发件人
private String from;
/**
* 发送简单文本邮件
* @param to 收件人邮箱(可多个,用逗号分隔)
* @param subject 邮件主题
* @param content 邮件内容
*/
public void sendSimpleMail(String to, String subject, String content) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from); // 发件人
message.setTo(to.split(","));// 收件人(支持多个)
message.setSubject(subject); // 主题
message.setText(content); // 内容
try {
mailSender.send(message);
log.info("简单邮件发送成功,收件人:{}", to);
} catch (Exception e) {
log.error("简单邮件发送失败,收件人:{},原因:{}", to, e.getMessage(), e);
// 实际业务中可加入重试机制(如用Spring Retry)
throw new RuntimeException("邮件发送失败", e);
}
}
/**
* 发送HTML邮件(支持图片、样式)
* @param to 收件人
* @param subject 主题
* @param htmlContent HTML内容(如"<h1>验证码:1234</h1>")
*/
public void sendHtmlMail(String to, String subject, String htmlContent) {
MimeMessage message = mailSender.createMimeMessage();
try {
// 第二个参数true表示支持多部分内容(附件、图片等)
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to.split(","));
helper.setSubject(subject);
// 第二个参数true表示内容为HTML
helper.setText(htmlContent, true);
mailSender.send(message);
log.info("HTML邮件发送成功,收件人:{}", to);
} catch (MessagingException e) {
log.error("HTML邮件发送失败,收件人:{},原因:{}", to, e.getMessage(), e);
throw new RuntimeException("HTML邮件发送失败", e);
}
}
/**
* 发送带附件的邮件
* @param to 收件人
* @param subject 主题
* @param content 内容(文本或HTML)
* @param filePaths 附件路径数组(如["D:/report.xlsx", "D:/log.pdf"])
*/
public void sendMailWithAttachments(String to, String subject, String content, String... filePaths) {
MimeMessage message = mailSender.createMimeMessage();
try {
// true:支持多部分内容(必须设为true才能加附件)
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to.split(","));
helper.setSubject(subject);
helper.setText(content, false); // 这里用false表示文本,需要HTML则设为true
// 遍历添加附件
for (String filePath : filePaths) {
File file = new File(filePath);
if (!file.exists()) {
log.warn("附件不存在,路径:{}", filePath);
continue; // 跳过不存在的附件,避免发送失败
}
// 添加附件(FileSystemResource:从本地文件系统读取)
FileSystemResource resource = new FileSystemResource(file);
// 附件显示名称(用原文件名)
String fileName = file.getName();
helper.addAttachment(fileName, resource);
}
mailSender.send(message);
log.info("带附件邮件发送成功,收件人:{},附件数:{}", to, filePaths.length);
} catch (MessagingException e) {
log.error("带附件邮件发送失败,收件人:{},原因:{}", to, e.getMessage(), e);
throw new RuntimeException("带附件邮件发送失败", e);
}
}
/**
* 异步发送邮件(关键!避免阻塞主线程)
* 需在启动类加@EnableAsync注解
*/
@Async
public void sendAsyncMail(String to, String subject, String content, String... filePaths) {
if (filePaths == null || filePaths.length == 0) {
sendSimpleMail(to, subject, content);
} else {
sendMailWithAttachments(to, subject, content, filePaths);
}
}
}
3. 调用示例:不同场景下的使用方式
(1)发送验证码邮件(文本 / HTML)
// 控制器层调用(示例)
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private EmailService emailService;
// 发送注册验证码
@PostMapping("/send-verify-code")
public Result sendVerifyCode(@RequestParam String email) {
String code = RandomStringUtils.randomNumeric(6); // 生成6位数字验证码
// 内容用HTML更美观(加粗、换行)
String htmlContent = String.format(
"<h3>您的注册验证码是:</h3><p style='color:red;font-size:20px'>%s</p><p>5分钟内有效,请勿泄露给他人</p>",
code
);
// 调用HTML邮件方法
emailService.sendHtmlMail(email, "【系统注册】验证码", htmlContent);
return Result.success("验证码已发送");
}
}
(2)发送带 Excel 附件的报表邮件
@Service
public class ReportService {
@Autowired
private EmailService emailService;
// 生成日报并发送邮件
public void sendDailyReport() {
// 1. 生成Excel报表(假设已实现,返回文件路径)
String excelPath = generateDailyExcel(); // 自定义方法:生成报表到本地
// 2. 邮件内容
String content = "您好,今日销售报表已生成,详见附件。\n\n系统自动发送,请勿回复。";
// 3. 发送带附件邮件(用异步方法,避免阻塞)
emailService.sendAsyncMail(
"manager@company.com,finance@company.com", // 多个收件人
"【每日报表】" + LocalDate.now().toString(),
content,
excelPath // 附件路径
);
}
}
四、八年踩坑总结:这些细节能让你少走 5 年弯路
1. 必须异步发送!否则高并发下直接崩
早期在订单系统里同步发送邮件,高峰期直接导致接口超时(邮件发送耗时 1-3 秒)。用@Async异步发送,配合线程池配置(避免线程耗尽):
# 配置异步线程池(application.yml)
spring:
task:
execution:
pool:
core-size: 5 # 核心线程数
max-size: 10 # 最大线程数
queue-capacity: 50 # 队列容量
2. 附件大小和数量有限制,提前处理
- 单个附件太大(如超过 20MB)会被邮箱服务器拒绝(QQ 邮箱默认限制 25MB),解决方案:
① 压缩附件(用 Zip);② 上传文件到 OSS,邮件发送下载链接;③ 分多个邮件发送。 - 附件数量太多(如超过 10 个)会触发反垃圾机制,建议打包成一个压缩包。
3. 避免邮件进垃圾箱的 5 个技巧
- 发件人邮箱固定,且提前 “养号”(先给熟人发几封正常邮件,标记 “不是垃圾邮件”)。
- 内容避免大量敏感词(如 “免费”“赚钱”“优惠”,用 “限时活动” 替代)。
- 主题和内容相关(别主题写 “报表”,内容是广告)。
- 加入 “退订” 链接(营销类邮件必需,否则易被标记)。
- 控制发送频率(同一邮箱对同一收件人,一天别超过 3 封)。
4. 一定要加日志和重试机制
-
日志必须记录:收件人、主题、发送时间、是否成功(排查问题的关键)。
-
失败重试:用
@Retryable(Spring Retry)处理临时失败(如网络波动):
<!-- 重试依赖 -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
// 在发送方法上加重试注解
@Retryable(
value = {MessagingException.class}, // 对哪些异常重试
maxAttempts = 3, // 重试3次
backoff = @Backoff(delay = 1000) // 间隔1秒
)
public void sendSimpleMail(String to, String subject, String content) {
// 原发送逻辑...
}
// 重试耗尽后执行(记录到数据库,人工处理)
@Recover
public void recover(MessagingException e, String to, String subject, String content) {
log.error("邮件重试发送失败,记录待处理:{}", to);
// saveToDB(to, subject, content); // 保存到数据库
}
五、最后:发送邮件的本质是 “可靠的信息传递”
八年开发下来,我对邮件发送的理解早已不是 “调个 API” 那么简单。它本质是业务系统的 “消息出口”—— 既要保证发得出去,也要保证对方收得到,更要保证内容有价值。
从技术角度,JavaMailSender已经帮我们简化了 90% 的工作,剩下的 10% 全在细节:异步处理、异常重试、垃圾邮件规避、附件适配…… 这些才是区分 “能发送” 和 “能稳定发送” 的关键。
如果你也在做邮件相关开发,欢迎在评论区分享你的踩坑经历 —— 毕竟,解决问题的最快方式,就是知道别人掉过哪些坑。