Java Email:八年老开发手把手教你搞定邮件发送(附附件实战)

554 阅读9分钟

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.com465需要开启 “SMTP 服务”,用授权码登录
163/126 邮箱smtp.163.com465授权码 = 登录密码(部分账号)
企业邮箱(阿里)smtp.qiye.aliyun.com465用企业邮箱账号 + 密码登录
Gmailsmtp.gmail.com465需科学上网,且开启低安全应用权限

坑点提醒

  • 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% 全在细节:异步处理、异常重试、垃圾邮件规避、附件适配…… 这些才是区分 “能发送” 和 “能稳定发送” 的关键。

如果你也在做邮件相关开发,欢迎在评论区分享你的踩坑经历 —— 毕竟,解决问题的最快方式,就是知道别人掉过哪些坑。