邮件发送的背景和重要性
在交付型项目中,消息提醒系统扮演着至关重要的角色,直接影响到甲方的验收标准。四大消息系统的实现是项目成功的关键,它们分别是:
-
邮件提醒:作为最传统且广泛使用的通知方式,邮件提醒能够有效传达重要信息,确保用户及时获取项目进展、关键事项和紧急通知。
-
短信提醒:短信的即时性和高打开率使其成为与用户沟通的重要工具,尤其在需要快速反馈时,短信提醒能够迅速传递信息。
-
社交媒体推送:通过社交媒体平台推送通知,可以增强用户的参与感和互动性,为用户提供更为便捷的信息获取方式,尤其适用于面向年轻用户群体的项目。
-
业务系统网页推送:网页推送是现代互联网项目中不可或缺的一部分,能够在用户使用系统时,实时推送重要消息和更新,提升用户体验。
邮件发送的基本概念
-
邮件的构成:
- 发件人:发送邮件的人的邮箱地址。
- 收件人:接收邮件的人的邮箱地址,支持多个收件人。
- 主题:邮件的标题,简洁明了地概括邮件内容。
- 正文:邮件的主要内容,可以是文本、HTML格式或附件。
-
常见邮件协议:
- SMTP(简单邮件传输协议):用于发送邮件的协议,负责将邮件从发件人服务器传递到收件人服务器。
- POP3(邮局协议版本3):用于从邮件服务器接收邮件的协议,通常将邮件下载到本地,删除服务器上的副本。
- IMAP(互联网消息访问协议):也用于接收邮件,但与POP3不同,它允许用户在邮件服务器上管理邮件,支持多设备同步。
选择邮件服务器
- QQ邮箱
- 163邮箱
个人理解总结
在开发中,常见的协议大多数符合OSI七层网络模型,并采用C/S架构(客户端/服务端)。在项目启动前,我们需要明确选择合适的客户端和服务端。这样,解题思路会变得更加清晰。
即使没有接触过相关内容,我们也可以选择一个例如JavaMail的客户端,并了解其使用的协议。这将帮助我们在筛选服务端时,锁定具体的实现方案。
以QQ邮箱为例,我们可以探讨如何实现邮件发送功能。通过明确使用的协议和架构,能够有效指导开发过程,确保项目的顺利推进。
项目实际需求及开发背景
我们正在开发一个商城系统,用户浏览商品,下单购买,支付成功之后,用户通过可开票订单列表选择某个订单申请开票,开票页面会要求用户填写发票购买方的抬头,以及接收邮件的地址,接收短信的手机号,填写完毕直接开票,开具成功之后短信提醒,同时将pdf文件发送到指定的邮箱。用户查看邮件时能看到一个相对美观的网页,因为邮件服务器提供商是不会为你的内容生成任何模板的,毕竟是免费使用的嘛。
到这里大概的功能点已经比较清晰了,大概的接口是修改发票状态接口,读取网络流的接口(第三方返回发票信息pdf的url),发短信接口,发邮件接口,修改订单表的接口(同一个订单不能重复开发票),发票状态修改之后需要发送邮件。
需要考虑到的点
邮箱地址不可达或者邮箱地址填写不正确,如果修改发票状态的接口和邮件接口都在一个方法中,事务也会是同一个,会因为发送邮件失败时的异常导致修改发票状态回滚,这会导致开票流程上bug,因为事务具备传播特性,会导致同一个线程中各个事务一起回滚。为了不让他回滚,我们需要控制事务的传播特性
控制事务传播特性的几种手段
1,使用spring的@Async注解执行异步任务
2,修改发邮件serivce方法的事务传播特性,@Transaction注解的属性可以指定事务传播特性
3,创建子线程来执行
因为我们项目的事务是使用拦截器根据方法名来设置事务隔离级别和传播特性的,我这里就不使用@Transaction注解演示了
发送邮件的交给调度器去做,我可以创建一个ExecutorService,让子线程去处理任务,默认情况下,子线程是获取不到主线程创建的事务管理器的,也就不会有事务传播了,解决完这几个问题点之后,着手开发。
以SpringBoot框架为例,实现一个QQ邮箱发邮件功能
如果没有接触过,会很懵,客户端其实就是一行行的代码,添加依赖可以直接去看,但是服务端在哪里,会很疑惑,这太正常了,配置服务端的地方,不值得浪费时间探讨,直接贴图
首先配置服务端
提示:需要设置独立密码,否则影响使用
接下来,搞客户端,动手之前先清楚自己要做什么
1,引入某种适配框架的依赖
2,引入进来的依赖需要用什么对象?哪些API?如何用API设置发件人,收件人,发什么内容,采用什么邮件主题(即邮件标题)
直接上手操作,遇到问题一个个解决:
按照上述思路,第一步引入如下依赖
此处的用法是对maven进行版本管理,maven不做介绍: <javax.mail.version>1.6.2</javax.mail.version> <spring-boot-starter-mail.version>3.2.5</spring-boot-starter-mail.version>
<!-- mail start --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> <version>${spring-boot-starter-mail.version}</version> </dependency> <dependency> <groupId>com.sun.mail</groupId> <artifactId>javax.mail</artifactId> <version>${javax.mail.version}</version> </dependency> <!-- mail end -->
javax.mail是需要的,我的项目是Springboot2.7.6,至少我这个版本需要,不加javax的依赖,springboot找不到运行时实例。
第二步:将qq邮箱中心返回的授权码写入application.yml配置文件,灵活切换测试账号,正式账号
tencent: email: sender: [email protected] # 发建人邮箱 password: dlkdsubjtgfbuhccd # 邮箱授权码,必须设置独立密码,否则无法开启smtp subject: 《xxx商城》-电子发票 #邮件的主题 host: smtp.qq.com # host是邮件服务器目标地址,简单点理解就是你告诉服务器你要找这个地址,服务器会调用DNS服务器去解析这个host的实际IP,最终通过网关或者路由器将载体中的信息发送给目标服务来处理,返回给客户端。 port: 587 # smtp协议对应的端口号,默认用这个
第三步:读取yml属性到javaBean,将javaBean的生命周期交给spring管理
@Component //标注为spring组件 @Data //Lombok提供get,set方法,使代码更清爽 @ConfigurationProperties(prefix = "tencent.email")//匹配yml文件中对应的前缀,set到java对象中 public class EmailProperties { private String sender; private String password; private String subject; private String host; private int port; }
第四步: 配置模板
@Configuration public class TencentQQEMailConfig { @Autowired private EmailProperties emailProperties; @Bean public JavaMailSender javaMailSender() { JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); mailSender.setHost(emailProperties.getHost()); // SMTP 服务器地址 mailSender.setPort(emailProperties.getPort()); // SMTP 服务器端口 mailSender.setUsername(emailProperties.getSender()); // 发送邮件的邮箱用户名 mailSender.setPassword(emailProperties.getPassword()); // 发送邮件的邮箱密码 // 设置其他属性 Properties props = mailSender.getJavaMailProperties(); props.put("mail.transport.protocol", "smtp"); //指定协议 props.put("mail.smtp.auth", "true"); //安全认证,你不认证,服务器就不认你 props.put("mail.smtp.starttls.enable", "true"); //开启tls协议 props.put("mail.smtp.starttls.required", "true"); //指定安全传输使用的协议为tls //props.put("mail.debug", "true"); //开发环境才用debug打印日志,生成环境不用 return mailSender; } }
为什么我会称它为模板?客户端与服务端之间的通信,都是有标准协议的,不管是传输层,还是应用层,你并不需要处理与传输相关,路由相关的,但这不代表不需要传输,路由,我们开发层面只需要准备好发送内容,以及异常处理。
而模板通常指的是一种结构化的方法或模式,用于执行特定类型的任务。它包含了一系列的步骤或规则,可以重复使用,并且通常具有一定的灵活性以适应不同的场景。
上述代码中,有两处是值得提醒一下的,红色标记部分,为什么是get出来,最后也没有再set一遍props?通过查看源码,Proterties是继承了HashTable,这是一个特殊的集合类,粘贴部分源码:
Properties部分源码
package java.util;
public class Properties extends Hashtable<Object,Object> {}
JavaMailSenderImpl部分源码:
public class JavaMailSenderImpl implements JavaMailSender {
private Properties javaMailProperties = new Properties();
public void setJavaMailProperties(Properties javaMailProperties) { this.javaMailProperties = javaMailProperties; synchronized(this) { this.session = null; } }
public Properties getJavaMailProperties() { return this.javaMailProperties; }
}
额外的一些思考
上述问题其实是在问:java的参数传递是值传递,还是引用传递?答案是:
Java 的参数传递机制是值传递,这个“值”根据参数类型的不同而有所不同:
-
基本数据类型:对于基本数据类型(如
int
、char
、boolean
等),传递的是参数的值的副本。在方法内部对参数的修改不会影响到外部变量。 -
对象引用:对于对象,传递的是对象引用的副本。虽然可以通过这个引用访问和修改对象的属性,但如果在方法内部改变这个引用,使其指向另一个对象,外部的引用并不会受到影响。
可以说 Java 既不是传统意义上的“引用传递”,也不是“值传递”,而是一个结合了这两者特性的值传递机制。
所以,当props调用put方法时,引用(props)会发生变化,而JavaMailSenderImpl持有的是引用,引用发生变化,引用地址也会随着发生变化。
另外一个点,是我注释掉的代码,开启之后才会打印发送邮件相关的日志
第五步:编写接口,发送邮件
接口我不写了,我直接贴实现层代码
@Service @Slf4j public class TencentQQEmailServiceImpl implements TencentQQEmailService { @Autowired private JavaMailSender javaMailSender; @Autowired private EmailProperties properties; private ExecutorService executor = Executors.newCachedThreadPool(); public Result sendEmail(TencentEmailDTO dto) throws MessagingException, IOException { Result result = new Result(); MimeMessage message = javaMailSender.createMimeMessage(); try { MimeMessageHelper helper = new MimeMessageHelper(message, true,StandardCharsets.UTF_8.name()); helper.setTo(dto.getTo());//这里设置收件人 helper.setFrom(properties.getSender());//这里设置发送人 helper.setSubject(properties.getSubject());//这里设置主题 // 使用RestTemplate从URL获取附件的InputStream // 添加网络文件作为附件 if(StringUtils.hasLength(dto.getFilePath())){ DataSource source = null; try { URL url = new URL(dto.getFilePath()); source = new URLInputStreamDataSource(url); } catch (MalformedURLException e) { throw new RuntimeException("无法创建的URL:"+dto.getFilePath()); } helper.addAttachment("附件.pdf", source); } if(dto.getContent().isEmpty()){ helper.setText("<html lang=\"en\">\n" + "<head>\n" + " <meta charset=\"UTF-8\">\n" + " <title>Email Content</title>\n" + "</head>\n" + "<body>\n" + " <p>尊敬的用户:</p>\n" + " <p>【请下载附件,查收您的电子发票】</p>\n" + "</body>\n" + "</html>", true); }else { helper.setText("<html lang=\"en\">\n" + "<head>\n" + " <meta charset=\"UTF-8\">\n" + " <title>Email Content</title>\n" + "</head>\n" + "<body>\n" + " <p>尊敬的xx用户:</p>\n" + " <p>您在xx申请的电子发票已开具成功,您可以:</p>" + "<a href=\"" + dto.getContent() + "\" style=\"display: block;\n" + " width: 120px;\n" + " height: 28px;\n" + " line-height: 28px;\n" + " border: 1px solid #E54043;\n" + " background-color: #E54043;\n" + " font-size: 12px;\n" + " color: #ffffff;\n" + " text-align: center;\n" + " margin-top: 16px;\n" + " cursor: pointer;\n" + " text-decoration: none;\n" + " transition: all .3s ease-in-out;\" target=\"_blank\" rel=\"noopener\">下载电子发票</a >" + " <p style=\"font-size:12px;line-height:22px;\"> <span style=\"color:#9b9ea0;\">(提醒:此下载链接的有效期为 30 天,如链接失效,请登录xx小程序再次发送。)</span></p>\n" + " <p> 同时,您也可以登录小程序查看此发票关联的订单明细。</p>\n" + " <p> 电子发票与纸质发票具有同等法律效力,可用于报销入账。</p>\n" + "</body>\n" + "</html>", true); } //异步执行 executor.submit(()->{ javaMailSender.send(message); }); } catch (MessagingException e) { result.setSuccess(false); result.setMsg( "email send fail: reason by "+e.getMessage()); throw new RuntimeException(e); } finally { log.error("sendmail-message资源关闭......"); message.getInputStream().close(); } result.setSuccess(true); result.setMsg("email sending....."); return result; } public static class URLInputStreamDataSource implements DataSource { private final URL url; public URLInputStreamDataSource(URL url) { this.url = url; } @Override public InputStream getInputStream() throws IOException { HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); connection.setConnectTimeout(5000); connection.connect(); // 显式建立连接 int code = connection.getResponseCode(); log.info("正在查找邮件附件地址。。。。。状态码:"+code+""); if (code != 200) { throw new RuntimeException("连接异常:请检查URL,状态码"+code); } return connection.getInputStream(); } @Override public OutputStream getOutputStream() throws IOException { throw new UnsupportedOperationException("Output stream not supported"); } @Override public String getContentType() { // 根据实际情况返回合适的内容类型,例如 application/pdf return "application/octet-stream"; } @Override public String getName() { return url.getFile(); } } }
以上发送邮件功能,能做到让用户打开邮箱之后看到一个网页,并附有一个链接,以及还算看过去的前端页面,附上一张实际的效果图:
收件人的邮件截图
最后的寄语
大概我个人就是这样考虑问题,遇到没做过的需求,都是先考虑整体如何完成,细节暂时不考虑,实现完功能之后,会开始处理细节。
标签:Java,private,发送,服务器,mail,public,邮件 From: https://blog.csdn.net/Ta20220617/article/details/143436760