简介这是一份面向Java开发者、企业级应用开发人员及自动化流程设计者的邮件发送实战示例聚焦于通过JavaMail API实现带正文与附件的邮件发送功能。资源围绕SMTP协议配置、Session会话管理、MimeMessage与Multipart组合、附件数据源处理及Transport发送等核心环节展开适合需要快速掌握邮件集成方案的初中级开发者参考。压缩包共41个文件约1.33MB包含9个java源码、11个class编译文件、7个jar依赖库以及xml配置、log日志、html页面和Eclipse工程相关文件结构完整可直接导入运行。目前已有816人学习下载读者可从中获取完整的邮件发送代码示例、异常处理与调试思路并参考其工程组织方式快速构建自己的邮件功能模块。1. 从一次线上告警说起为什么Java发邮件值得单独拆一遍凌晨两点监控群里弹出一条告警某业务系统的对账文件连续三次发送失败。值班同事排查了一圈发现不是网络问题也不是对方拒收而是代码里用FileDataSource读附件时文件路径写成了相对路径打包成 jar 之后工作目录变了文件根本找不到。这个坑不复杂但很典型——Java 发送邮件这件事看起来就是调几个 API真到生产环境附件路径、编码、超时、认证、抄送密送、HTML 正文里嵌图片每一项都能让你加班。这份资源围绕「Java 实现发送邮件内容 附件」展开核心是 JavaMail 这套标准方案。它适合两类人一类是刚接手「系统要发通知邮件」需求的后端开发需要一份能直接跑通的代码骨架另一类是做二次开发的老手想看看附件流处理、连接超时、多收件人拆分这些细节有没有更稳的写法。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序把这份代码拆开讲。2. 项目定位与技术选型JavaMail 这套组合拳怎么打2.1 为什么是 JavaMail 而不是自己拼 SMTP 协议Java 发邮件的主流方案就是 Jakarta Mail早期叫 JavaMail。它把 SMTP 协议的握手、认证、MIME 编码这些脏活全封装了你只需要关心「发给谁、发什么、附件是什么」。这份资源用的就是这套标准 API没有引入 Spring 的JavaMailSender好处是依赖干净不绑框架放到任何 Java 项目里都能用。核心依赖就一个!-- pom.xml 中引入 Jakarta Mail 实现 -- dependency groupIdcom.sun.mail/groupId artifactIdjakarta.mail/artifactId version2.0.1/version /dependency如果你用的是老项目可能还是javax.mail的坐标包名是javax.mail.*新项目统一用jakarta.mail.*。这两个不能混用混了就是ClassNotFoundException这是第一个容易翻车的地方。2.2 整体结构一个工具类扛下所有发送逻辑这份代码的结构很直白没有过度设计。核心是一个MailUtil工具类对外暴露一个发送方法内部完成 Session 构建、Message 组装、Transport 发送三步。配套一个MailConfig承载 SMTP 服务器地址、端口、账号、密码这些参数。调用方只需要准备收件人、主题、正文、附件路径剩下的交给工具类。这种「工具类 配置对象」的写法适合中小项目快速落地。如果你的系统里发邮件的地方超过十处建议再往上抽一层MailService接口把「发对账邮件」「发告警邮件」这些业务语义封进去工具类只负责技术实现。这是二次开发时第一个可以动刀的点。2.3 参数配置哪些必须改哪些可以默认SMTP 相关的参数集中在配置里下面这张表是我整理的关键项参数作用常见值mail.smtp.host邮件服务器地址smtp.example.commail.smtp.port端口465SSL/ 587STARTTLSmail.smtp.auth是否认证truemail.smtp.ssl.enable是否启用 SSL465 端口时为 truemail.smtp.timeout读超时毫秒10000mail.smtp.connectiontimeout连接超时毫秒10000端口和 SSL 是绑定的465 一般直接 SSL587 走 STARTTLS。这两个配错表现是连接被拒绝或者握手失败。超时参数不配默认是无限等待一旦对方服务器卡住你的线程就挂在那儿了生产环境必须显式设置。3. 核心代码拆解一封带附件的邮件是怎么组装出来的3.1 构建 Session认证与超时的落点Session 是整个发送过程的上下文认证信息和超时策略都在这里注入。// 构建邮件会话注入认证与超时配置 Properties props new Properties(); props.put(mail.smtp.host, config.getHost()); props.put(mail.smtp.port, config.getPort()); props.put(mail.smtp.auth, true); props.put(mail.smtp.ssl.enable, true); // 超时必须显式设置否则线程可能被无限阻塞 props.put(mail.smtp.timeout, 10000); props.put(mail.smtp.connectiontimeout, 10000); Session session Session.getInstance(props, new Authenticator() { Override protected PasswordAuthentication getPasswordAuthentication() { // 注意这里的密码通常是邮箱的授权码不是登录密码 return new PasswordAuthentication(config.getUsername(), config.getPassword()); } });这段代码里有两个点值得说。第一getPasswordAuthentication返回的密码绝大多数邮件服务商要求的是「授权码」而不是账号登录密码用登录密码会直接认证失败报535 Authentication failed。第二Session.getInstance和getDefaultInstance的区别前者每次新建后者复用。多线程环境下建议用getInstance避免配置串味。3.2 组装 MimeMessage正文与附件的混合体一封带附件的邮件MIME 结构是multipart/mixed里面套一个multipart/alternative放正文纯文本 HTML再挂若干个附件体。JavaMail 帮你把这层嵌套封装好了你只需要按顺序添加。// 创建邮件消息 MimeMessage message new MimeMessage(session); message.setFrom(new InternetAddress(config.getUsername())); message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to1example.com,to2example.com)); message.setSubject(对账文件, UTF-8); // 主题指定编码避免中文乱码 // 正文部分用 MimeMultipart 承载文本和附件 MimeMultipart multipart new MimeMultipart(); // 1. 先放正文 MimeBodyPart textPart new MimeBodyPart(); textPart.setContent(h3附件请查收/h3, text/html;charsetUTF-8); multipart.addBodyPart(textPart); // 2. 再放附件 MimeBodyPart filePart new MimeBodyPart(); FileDataSource source new FileDataSource(new File(/data/report.xlsx)); filePart.setDataHandler(new DataHandler(source)); // 关键用 MimeUtility 编码文件名否则中文附件名会乱码 filePart.setFileName(MimeUtility.encodeText(对账报表.xlsx)); multipart.addBodyPart(filePart); message.setContent(multipart);逻辑说明setRecipients支持逗号分隔的多个地址内部会解析成数组。正文用text/html可以写 HTML 标签但要注意不是所有客户端都渲染复杂样式。附件部分FileDataSource负责把文件读成流setFileName设置显示名。参数上MimeUtility.encodeText是处理中文文件名的关键不加这行收件人看到的附件名就是一串?UTF-8?B?...?的编码串。3.3 发送与资源释放Transport 的正确用法组装完 Message最后一步是交给 Transport 发出去。这里有个容易忽略的点Transport 用完必须关闭否则连接会泄漏。// 发送邮件try-with-resources 确保连接关闭 try (Transport transport session.getTransport(smtp)) { transport.connect(config.getHost(), config.getUsername(), config.getPassword()); transport.sendMessage(message, message.getAllRecipients()); } catch (MessagingException e) { // 记录日志包含收件人和主题方便排查 log.error(邮件发送失败, 收件人{}, 主题{}, recipients, subject, e); throw new RuntimeException(邮件发送失败, e); }transport.connect显式传入账号密码和 Session 里的认证是两回事这里才是真正建立连接。sendMessage的第二个参数是收件人数组getAllRecipients会把 TO、CC、BCC 都取出来。用 try-with-resources 是 Java 7 以后的写法比手动finally里 close 更稳不会因为异常漏掉关闭。4. 跑起来从零到发出第一封邮件的完整步骤4.1 环境准备与依赖确认先把 JDK 和构建工具准备好。这份代码对 JDK 版本要求不高JDK 8 及以上都能跑。如果你用 Maven确认pom.xml里引入了 Jakarta Mail如果用 Gradle对应坐标是com.sun.mail:jakarta.mail:2.0.1。检查依赖是否生效可以在项目里执行mvn dependency:tree | grep mail能看到 mail 相关的包就说明引入成功。4.2 配置参数把示例值换成你自己的代码里所有需要改的地方都集中在配置对象里。你需要准备四样东西SMTP 服务器地址、端口、发件邮箱账号、授权码。授权码在邮箱服务商的设置里生成不是登录密码。把这几项填进配置其余的超时参数可以先保持默认的 10000 毫秒。提示第一次跑建议先把收件人设成你自己的邮箱确认能收到再改业务地址避免误发给真实用户。4.3 运行验证看日志、看收件箱、看附件运行主类或者单元测试观察控制台。发送成功不会有太多输出失败会抛MessagingException异常信息里通常包含 SMTP 返回码。收到邮件后重点检查三件事主题中文是否正常、正文 HTML 是否渲染、附件名是否乱码、附件能否正常打开。这四项都通过说明基础链路没问题。4.4 常见失败现象对照现象大概率原因535 Authentication failed用了登录密码而非授权码Connection refused端口和 SSL 配置不匹配附件名乱码没调 MimeUtility.encodeText发送卡住不返回没设 timeout中文正文乱码setContent 没指定 charset5. 避坑与排查那些让我加班到凌晨的细节5.1 附件路径相对路径在 jar 里必翻车现象本地 IDE 跑得好好的打成 jar 部署后报FileNotFoundException。原因new File(report.xlsx)是相对当前工作目录的jar 启动时工作目录和你以为的不一样。解决附件路径统一用绝对路径或者把附件作为 classpath 资源用getResourceAsStream读成流再挂到ByteArrayDataSource上。我一般会在配置里强制要求绝对路径启动时校验文件是否存在。5.2 中文乱码三个地方都要管现象主题、正文、附件名任意一处出现乱码。原因Java 默认编码和邮件客户端解码方式不一致。解决主题用setSubject(subject, UTF-8)正文用setContent(content, text/html;charsetUTF-8)附件名用MimeUtility.encodeText(name)。三处都加上基本不会再乱。5.3 连接泄漏Transport 不关的后果现象跑一段时间后发送变慢最终报连接数超限。原因每次发送都新建 Transport 但没关闭连接池被占满。解决用 try-with-resources 包住 Transport确保异常时也能关闭。如果发送频率高考虑复用 Transport但要注意线程安全。5.4 收件人过多一次群发被限流现象发给几十个收件人时部分人收不到或者服务器返回限流错误。原因单次 SMTP 会话里塞太多收件人触发了服务商的频率限制。解决把收件人分批每批 10 到 20 个批之间加短暂休眠。这个策略在发通知类邮件时特别有用。5.5 超时缺失线程被无限挂起现象某次发送后线程一直不返回监控显示线程数飙升。原因没设mail.smtp.timeout对方服务器不响应时一直等。解决连接超时和读超时都设上一般 10 秒足够。超过这个时间还没响应基本可以判定对方有问题直接失败重试比干等更合理。6. 二次开发把工具类改造成能扛业务的邮件服务6.1 从工具类到服务接口的演进原始代码是一个静态工具类调用方直接传参。业务一多你会发现「发对账邮件」和「发告警邮件」的模板、收件人、附件规则都不一样全堆在调用方会乱。我的做法是抽一个MailService接口定义send(MailRequest request)方法MailRequest里放业务字段比如bizType、bizId。实现类里根据bizType决定用哪个模板、抄送给谁。工具类退居底层只负责技术发送。// 业务层接口屏蔽底层发送细节 public interface MailService { void send(MailRequest request); } // 实现里根据业务类型组装不同的邮件内容 public class BizMailService implements MailService { Override public void send(MailRequest request) { // 根据 bizType 选择模板和收件人策略 MailTemplate template templateFactory.get(request.getBizType()); MailUtil.send(template.render(request), request.getAttachments()); } }这样改完新增一种邮件只需要加一个模板不用动发送逻辑。6.2 附件处理的进阶内存流与临时文件原始代码用FileDataSource读磁盘文件。如果附件是动态生成的比如从数据库导出的 Excel没必要先落盘再读。可以用ByteArrayDataSource直接挂内存流// 动态生成的附件直接挂内存流省去落盘 ByteArrayOutputStream bos exportService.exportToExcel(params); MimeBodyPart part new MimeBodyPart(); part.setDataHandler(new DataHandler( new ByteArrayDataSource(bos.toByteArray(), application/vnd.ms-excel))); part.setFileName(MimeUtility.encodeText(导出数据.xlsx));注意ByteArrayDataSource的第二个参数是 MIME 类型写错会导致客户端识别不了文件类型。大附件不建议走内存容易 OOM还是落临时文件再读更稳。6.3 验证改造效果三个必测场景改完之后我一般会跑三个场景验证一是正常发送带一个附件确认基础链路没断二是发送带中文名附件和 HTML 正文确认编码没问题三是模拟 SMTP 超时确认异常能被捕获且线程不挂起。这三个过了基本可以上线。另外建议加一个发送日志表记录每次发送的收件人、主题、状态、耗时出问题时能快速定位。6.4 一个我踩过的坑有次改造时我把 Transport 复用成了一个单例想着省连接。结果并发发送时出现消息串台A 的附件发给了 B。原因是 Transport 不是线程安全的多个线程共用一个连接消息体互相覆盖。从那以后我每次做邮件改造都强制走一遍并发测试确认每个线程独立建连或者用连接池。希望帮到你少走这段弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?