简介这份资源面向需要在Java应用中集成邮件功能的开发者尤其适合企业级项目与自动化流程场景。内容围绕SMTP协议与JavaMail API展开覆盖Session会话构建、MimeMessage邮件组装、Multipart正文与附件组合、Transport发送以及调试与异常处理等关键环节帮助读者理解带附件邮件的完整实现思路。压缩包共41个文件以9个java源码、11个class编译文件、7个jar依赖库为主另含xml配置、log日志与工程配置文件整体约1.33MB可直接导入IDE参考运行。目前已有816人学习下载适合作为邮件发送模块的练手与二次开发起点。读者可从中获取可运行的邮件发送示例代码、附件与正文组合的写法、依赖库配置方式以及常见认证与网络异常的排查线索便于快速迁移到自己的项目中。1. 从一次线上告警说起为什么我又把 Java 发邮件翻出来重写了一遍上周凌晨两点监控群里弹出一条告警某业务系统的对账文件连续三次没发出去下游合作方直接打电话过来问。我爬起来登服务器一看日志里躺着一行javax.mail.MessagingException: Could not connect to SMTP host再往下翻附件路径是对的收件人也没问题卡在连接超时。那一刻我特别想抽自己——这套发邮件的代码是两年前随手写的连接超时、重试、附件流关闭全都没处理平时跑得好好的一到网络抖动就原形毕露。Java 发送邮件这个需求几乎每个后端都碰过定时报表、告警通知、对账文件、激活链接场景多到数不清。但真正把它写扎实的人不多大部分人都是搜一段JavaMailSender的示例代码复制粘贴能发出去就算完事。这份资源就是围绕「Java 实现发送邮件内容 附件」这个核心场景把纯文本、HTML 正文、单附件、多附件、抄送密送、连接池配置这些点一次性讲透适合正在做通知模块、定时任务或者需要给业务系统补邮件能力的开发者。下面我按自己重写这套代码的思路从依赖选型一路拆到二次开发。2. 技术选型与工程结构JavaMail 还是 Spring 封装先想清楚再动手2.1 两种主流方案的取舍Java 发邮件底层都是走 SMTP 协议绕不开jakarta.mail老版本叫javax.mail这套 API。但实际写业务代码时通常有两条路第一条是直接用原生jakarta.mail自己管理Session、MimeMessage、Transport。好处是可控性最强连接参数、超时、认证方式全在手里坏处是样板代码多附件流、编码、异常都得自己兜。第二条是用 Spring 的JavaMailSender它把Session和MimeMessage封装了一层配合MimeMessageHelper写正文和附件非常顺手连接池、超时也能通过配置项统一管理。如果你的项目本身就是 Spring Boot我一般会直接选这条省下来的时间够多写两个业务接口。这份资源里的实现走的是 Spring 封装路线但底层原理部分会对照原生 API 讲这样你换到非 Spring 环境也能迁移。2.2 依赖引入与版本注意Maven 项目里Spring Boot 的 starter 已经把邮件依赖带进来了!-- pom.xml 片段 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-mail/artifactId /dependency如果你不用 Spring Boot那就单独引 Jakarta Maildependency groupIdcom.sun.mail/groupId artifactIdjakarta.mail/artifactId version2.0.1/version /dependency这里有个血泪经验javax.mail和jakarta.mail的包名不一样JDK 升级或者 Spring Boot 升到 3.x 之后老代码里的javax.mail.*会直接编译不过。迁移时把 import 全换成jakarta.mail.*别想着两个共存类加载器会教你做人。2.3 目录结构长什么样一个能落地的邮件模块我一般会拆成这么几层src/main/java/com/example/mail/ ├── config/ │ └── MailConfig.java # 连接池、超时、编码配置 ├── service/ │ ├── MailService.java # 发送入口对外暴露方法 │ └── impl/ │ └── MailServiceImpl.java # 具体组装逻辑 ├── model/ │ ├── MailRequest.java # 收件人、主题、正文、附件载体 │ └── MailAttachment.java # 附件名 字节流/路径 └── util/ └── MailFileUtil.java # 附件读取、文件名编码处理MailRequest这个载体类很关键它把「发给谁、发什么、带什么附件」和「怎么发」解耦开。业务层只管塞数据发送层负责组装MimeMessage后面要加模板、加签名、加重试都只动发送层业务代码不用改。2.4 配置文件里的关键参数application.yml里至少要配这些spring: mail: host: smtp.example.com port: 465 username: notifyexample.com password: your-auth-code protocol: smtps default-encoding: UTF-8 properties: mail: smtp: auth: true connectiontimeout: 5000 timeout: 5000 writetimeout: 5000 starttls: enable: trueconnectiontimeout、timeout、writetimeout这三个超时是很多人会漏的默认值在某些实现里是「无限等待」网络一抖线程就挂死。设成 5 秒是我踩过坑之后的习惯宁可快速失败走重试也别让线程池被拖垮。password这里填的通常是授权码而不是登录密码具体看你的邮件服务商要求。3. 核心发送逻辑拆解正文、附件、抄送怎么组装3.1 发送入口方法的设计对外暴露的方法我一般只留一个参数是MailRequest返回布尔值或者抛自定义异常public interface MailService { /** * 发送邮件支持正文 多附件 * param request 邮件请求载体 * return 是否发送成功 */ boolean sendMail(MailRequest request); }为什么不返回void因为调用方需要知道结果来决定要不要重试、要不要记失败日志。返回布尔值是最简单的契约复杂场景可以换成SendResult对象带上错误码和失败原因。3.2 组装 MimeMessage 的完整过程实现类的核心方法长这样我逐段拆Override public boolean sendMail(MailRequest request) { try { // 1. 创建 MimeMessage传入配置好的 Session MimeMessage message mailSender.createMimeMessage(); // 2. true 表示开启 multipart附件必须开 MimeMessageHelper helper new MimeMessageHelper( message, true, StandardCharsets.UTF_8.name()); // 3. 发件人、收件人、抄送、密送 helper.setFrom(request.getFrom()); helper.setTo(request.getTo().toArray(new String[0])); if (CollectionUtils.isNotEmpty(request.getCc())) { helper.setCc(request.getCc().toArray(new String[0])); } if (CollectionUtils.isNotEmpty(request.getBcc())) { helper.setBcc(request.getBcc().toArray(new String[0])); } // 4. 主题和正文isHtml 控制是否按 HTML 渲染 helper.setSubject(request.getSubject()); helper.setText(request.getContent(), request.isHtml()); // 5. 附件遍历添加 if (CollectionUtils.isNotEmpty(request.getAttachments())) { for (MailAttachment att : request.getAttachments()) { // 第二个参数是展示文件名中文要保证编码正确 helper.addAttachment(att.getFileName(), new ByteArrayResource(att.getContent())); } } // 6. 真正发送 mailSender.send(message); return true; } catch (MessagingException | UnsupportedEncodingException e) { log.error(邮件发送失败, to{}, subject{}, request.getTo(), request.getSubject(), e); return false; } }逻辑说明第 2 步的multipart参数必须为true否则addAttachment会直接抛异常这是新手最容易翻车的地方。第 3 步的setTo接收数组单个收件人也得包成数组。第 5 步用ByteArrayResource而不是FileSystemResource是为了让附件内容在内存里流转避免临时文件清理问题也方便从数据库或对象存储直接读字节流。参数说明request.isHtml()决定正文按纯文本还是 HTML 解析发 HTML 时记得内容里别带未转义的特殊字符。att.getFileName()是收件人看到的附件名中文名要确保MimeMessageHelper的编码参数是 UTF-8否则会变成乱码。3.3 附件载体的设计MailAttachment我一般这么定义public class MailAttachment { private String fileName; // 展示文件名如 对账单_20240101.xlsx private byte[] content; // 文件字节内容 // 从本地文件构造的便捷方法 public static MailAttachment fromFile(File file) throws IOException { MailAttachment att new MailAttachment(); att.setFileName(file.getName()); att.setContent(Files.readAllBytes(file.toPath())); return att; } }用字节数组而不是File对象好处是附件来源可以多样化本地文件、数据库 BLOB、HTTP 下载流统一转成byte[]就能塞进去。代价是大附件会占内存超过 10MB 的附件我建议改成流式处理或者走网盘链接别硬发。3.4 连接池与超时配置Spring Boot 从 2.x 开始支持spring.mail下的连接池配置但需要额外引依赖dependency groupIdcom.sun.mail/groupId artifactIdjakarta.mail/artifactId version2.0.1/version /dependency然后在配置里加spring: mail: properties: mail: smtp: connectionpoolsize: 5 timeout: 5000 connectiontimeout: 5000连接池的意义在于高频发送场景下复用 TCP 连接避免每次发信都重新握手认证。如果你的系统一天就发几封不开也行但要是做告警通知一分钟几十封不开池子性能会很难看。4. 避坑与排查那些年发邮件踩过的雷4.1 附件名乱码现象收件人收到的附件名变成?UTF-8?B?xxx?这种编码串或者直接是乱码。原因MimeMessageHelper构造时没指定编码或者指定了但附件名本身编码不一致。解决构造MimeMessageHelper时显式传StandardCharsets.UTF_8.name()并且确保附件名字符串在传入前没有被二次编码。如果还乱检查邮件客户端的解码方式有些老客户端对 RFC 2231 支持不好可以退回用MimeUtility.encodeText()手动编码。4.2 发送成功但收件人没收到现象代码没报错日志显示send执行完了但对方说没收到。原因大概率进了垃圾箱或者被对方邮件网关拦截。常见触发点是发件人域名没配 SPF/DKIM 记录、正文含敏感词、发送频率过高。解决先让对方查垃圾箱再检查发件域名的 DNS 记录。代码层面可以加Reply-To头、控制发送频率、避免正文全是链接。这个坑排查起来最玄学因为代码本身没问题问题在邮件生态。4.3 连接超时导致线程挂死现象某个时段开始发邮件的线程全部卡住线程池被占满接口大面积超时。原因没设connectiontimeout和timeoutSMTP 服务器不响应时线程无限等待。解决三个超时参数全部设上建议 5 到 10 秒。同时给发送逻辑加熔断或降级邮件发不出去不该拖垮主业务。我一般会把发邮件放到独立线程池和核心业务隔离。4.4 大附件导致 OOM现象发几十兆的附件时服务内存飙升甚至 OOM。原因ByteArrayResource把整个文件读进内存多个并发发送时内存翻倍。解决限制单附件大小超过阈值改成发送下载链接或者用FileSystemResource配合流式写入但要注意临时文件的生命周期管理。生产环境我一般卡在 10MB超过就走对象存储链接。4.5 授权码当成密码用现象本地测试能发部署到服务器就认证失败。原因很多邮件服务商要求用「授权码」而不是登录密码本地可能配了环境变量服务器上没配或者配错。解决统一用配置中心或环境变量管理凭据别硬编码在代码里。部署前用telnet smtp.example.com 465先探一下网络通不通再确认授权码有没有过期。5. 二次开发与验证把邮件模块改造成你想要的样子5.1 加模板引擎别拼字符串原始实现里正文是直接传字符串业务代码里到处StringBuilder拼 HTML维护起来很痛苦。二次开发第一步我建议接模板引擎Thymeleaf 或者 FreeMarker 都行// 用 Thymeleaf 渲染模板 Context context new Context(); context.setVariable(userName, 张三); context.setVariable(orderNo, 20240101001); String html templateEngine.process(mail/order-notify, context); request.setContent(html); request.setHtml(true);模板文件放resources/templates/mail/下改文案不用动 Java 代码运营同学也能自己调。这一步做完邮件模块的复用性会上一个台阶。5.2 加重试与异步同步发送在业务链路里是隐患我一般会包一层异步Async(mailExecutor) public CompletableFutureBoolean sendMailAsync(MailRequest request) { return CompletableFuture.completedFuture(sendMail(request)); }配合自定义线程池核心线程数别开太大邮件发送是 IO 密集型8 到 16 个够用。重试策略用 Spring Retry 或者手写循环注意重试要幂等别把同一封邮件发三遍。5.3 验证清单改完之后怎么确认没问题我习惯跑一遍这个清单验证项操作预期结果纯文本正文发一封无附件邮件正文正常无乱码HTML 正文发带样式的 HTML客户端渲染正确单附件发一个中文名 xlsx附件名不乱码能打开多附件发三个不同类型文件全部收到顺序无关抄送密送加 cc 和 bcc收件人看不到 bcc 地址超时故意填错 host5 秒内失败不挂死大附件发 15MB 文件按策略拒绝或走链接5.4 一个具体技巧附件名兼容处理有些邮件客户端对长附件名和特殊字符支持差我一般会做一层兼容private String encodeFileName(String fileName) throws UnsupportedEncodingException { // 先按 RFC 2231 编码兼容大多数现代客户端 return MimeUtility.encodeText(fileName, StandardCharsets.UTF_8.name(), B); }这个B表示 Base64 编码比Q编码对中文更友好。改完记得在 Outlook、Foxmail、网页邮箱各测一遍不同客户端解码行为不一样这一步没法省。从那以后我每次写邮件模块都会先把超时和附件大小限制写死在配置里再动业务逻辑。发邮件看着简单但它是那种「平时不出事出事就是线上事故」的模块值得多花半小时把边界情况想全。希望帮到你。本文还有配套的精品资源点击获取