作者:ChenZhen
博客地址:https://www.chenzhen.space/
版权:本文为博主 ChenZhen 的原创文章,本文版权归作者所有,转载请附上原文出处链接及本声明。
如果对你有帮助,请给一个小小的star⭐
邮件提醒功能:当你收到某个人的回复时,会给你发送一封提醒邮件,并展示回复的内容。
我觉得对于一个博客,邮件回复的功能是必不可少的,能让你及时的回复别人的评论,还能让我更方便的和网上的人对线
其实这个功能还是蛮好实现的,我们先演示怎么用java发送一封简单的邮件
1.开启POP3/SMTP服务
以QQ邮箱为例:
进入设置
在下方开启POP3/SMTP服务,此处已开启
经过身份认证过后,会获得一串授权码,请务必保存下来
2.引入spring-boot-starter-mail 依赖
由于Spring推出了关于Mail的JavaMailSender类,基于该类Spring Boot又对其进行了进一步封装,从而实现了轻松发送邮件的集成。而且JavaMailSender类提供了强大的邮件发送能力,支持各种类型的邮件发送。
<!-- 邮件发送功能 启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
3.yaml文件配置
spring:
mail:
host: smtp.qq.com # 配置 smtp 服务器地址
port: 25 # smtp 服务器端口
username: xxxxx #配置你的邮箱地址
password: xxxxx #配置申请到的授权码
protocol: smtp #协议
thymeleaf-html: mail #设置要解析发送的html模板(需要你将.html文件放到/resources/templates下面)
4.测试发送普通邮件
配置完后,我们来测试发送一封普通的邮件
- 在这里有两个关键的对象,JavaMailSender负责发送邮件,SimpleMailMessage负责构建邮件内容对象,我们使用这两个对象来发送邮件。
由于Spring Boot的starter模块提供了自动化配置,在引入了spring-boot-starter-mail依赖之后,会根据配置文件中的内容去创建JavaMailSender实例并交给spring管理,因此我们可以直接在需要使用的地方直接@Autowired来引入 JavaMailSender 邮件发送对象
注意:使用@Autowired注解的类必须交由spring管理,即加上@Component注解
使用SimpleMailMessage对象来构建一封邮件
@Test
public void test5(){
//构建邮件内容对象
SimpleMailMessage msg = new SimpleMailMessage();
//邮件发送者
msg.setFrom("1583296383@qq.com");
//邮件接收者
msg.setTo("1583296383@qq.com");
//邮件主题
msg.setSubject("测试邮件主题");
//邮件正文
msg.setText("测试邮件内容");
//邮件发送时间
msg.setSentDate(new Date());
//邮件发送
javaMailSender.send(msg);
}
运行测试方法,需要等待一会便能收到
5.发送静态邮件模板
上面演示了如何发送普通邮件,接下来我们要实现发送一封静态邮件模板,就像开头的实例一样
这里我们使用Thymeleaf作为模板
引入Thymeleaf启动器
<!--Thymeleaf 静态模板jar包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
自定义静态模板
这里可以自己自定义好看的邮件模板,这里我就简单的测试一下
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml"><head>
<meta charset="UTF-8">
<title>Thymeleaf邮件模板</title>
</head>
<body>
<p>这是一封Thymeleaf邮件模板</p>
<table border="1">
<tr>
<td>姓名</td>
<td th:text="${name}"></td>
</tr>
<tr>
<td>年龄</td>
<td th:text="${age}"></td>
</tr>
</table>
<div style="color: red;">这是一封Thymeleaf邮件模板</div>
</body>
</html>
将该html文件放到templates目录下,这是springboot放置模板文件的默认路径
如果觉得这个模板不好看的话,也可以去我的项目源码使用我的邮件模板,就是开头那一个。
运行测试方法
- 这里使用MimeMessageHelper来构建邮件,利用Context对象可以设置模板里面的Thymeleaf表达式的值
- 调用springTemplateEngine的process方法来解析模板,第一个参数为模板文件的名字
@Test
public void test4() throws MessagingException {
MimeMessage msg = javaMailSender.createMimeMessage();//构建邮件
MimeMessageHelper helper = new MimeMessageHelper(msg, true);//设置可选文本或添加内联元素或附件,
helper.setFrom("1583296383@qq.com");//发件人
helper.setSentDate(new Date());//发送日期
helper.setSubject("这是测试主题(thymeleaf)");//发送主题
helper.setTo("1583296383@qq.com");//收件人
Context context = new Context();//构建上下文环境
context.setVariable("name","高级工程师");
context.setVariable("age", 19);
String process = springTemplateEngine.process("table", context);//将模板解析成静态字符串
helper.setText(process,true);//内容是否设置成html,true代表是
javaMailSender.send(msg);//发送
}
成功收到
- 现在我们将一些操作进行封装,做成一个工具类,并且新建一个Mail对象来简化发送的过程,完成博客的邮件提醒功能。
Mail类
package com.chenzhen.blog.pojo;
import java.util.Date;
/**
* @author ChenZhen
* @Description
* @create 2022/9/27 11:52
* @QQ 1583296383
* @WeXin(WeChat) ShockChen7
*/
public class Mail {
//发件人邮箱账号(固定为我自己 即博主本人)
private String sendMailAccount;
//收件人邮箱账号
private String acceptMailAccount;
//收件人姓名
private String name;
//收件人评论的内容
private String comment;
//回复收件人的 回复者的姓名
private String respondent;
//回复者的回复内容
private String reply;
//评论发生的地方链接(回复者是在哪里回复收件人的)
private String address;
//邮件主题
private String theme;
//发送时间
private Date sendTime = new Date();
public Mail() {
}
public Mail(String sendMailAccount, String acceptMailAccount, String name, String comment, String respondent, String reply, String address, String theme) {
this.sendMailAccount = sendMailAccount;
this.acceptMailAccount = acceptMailAccount;
this.name = name;
this.comment = comment;
this.respondent = respondent;
this.reply = reply;
this.address = address;
this.theme = theme;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getSendMailAccount() {
return sendMailAccount;
}
public void setSendMailAccount(String sendMailAccount) {
this.sendMailAccount = sendMailAccount;
}
public String getAcceptMailAccount() {
return acceptMailAccount;
}
public void setAcceptMailAccount(String acceptMailAccount) {
this.acceptMailAccount = acceptMailAccount;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public String getRespondent() {
return respondent;
}
public void setRespondent(String respondent) {
this.respondent = respondent;
}
public String getReply() {
return reply;
}
public void setReply(String reply) {
this.reply = reply;
}
public String getTheme() {
return theme;
}
public void setTheme(String theme) {
this.theme = theme;
}
public Date getSendTime() {
return sendTime;
}
public void setSendTime(Date sendTime) {
this.sendTime = sendTime;
}
}
邮件工具类MailUtil
package com.chenzhen.blog.util;
import com.chenzhen.blog.pojo.Mail;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring5.SpringTemplateEngine;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
/**
* @author ChenZhen
* @Description
* @create 2022/9/27 11:59
* @QQ 1583296383
* @WeXin(WeChat) ShockChen7
*/
@Component
public class MailUtil {
@Autowired
private JavaMailSender javaMailSender;//引入 JavaMailSender 邮件发送对象 来实现发送邮件的功能
@Autowired
private SpringTemplateEngine springTemplateEngine;//Spring 模板引擎
@Value("${spring.mail.username}") //从yaml配置文件中获取
private String from; //发送方邮箱地址
@Value("${spring.mail.thymeleaf-html}")//从yaml配置文件中获取
private String html;
/**
* 发送 thymeleaf 页面邮件
*/
public void sendThymeleafEmail(Mail mail) throws MessagingException {
MimeMessage msg = javaMailSender.createMimeMessage();//构建邮件
MimeMessageHelper helper = new MimeMessageHelper(msg, true);//构建邮件收发信息。
helper.setFrom(from);//发件人(默认固定为自己)
helper.setSentDate(mail.getSendTime());//发送日期
helper.setSubject(mail.getTheme());//发送主题
Context context = new Context();//将mail中的值设置进context交由模板引擎渲染
// WebContext context = new WebContext(request, response, request.getServletContext(), request.getLocale());
context.setVariable("name",mail.getName());
context.setVariable("theme", mail.getTheme());
context.setVariable("comment", mail.getComment());
context.setVariable("respondent", mail.getRespondent());
context.setVariable("reply", mail.getReply());
context.setVariable("address", mail.getAddress());
String process = springTemplateEngine.process(html, context);
helper.setText(process,true);//内容是否设置成html,true代表是
helper.setTo(mail.getAcceptMailAccount());//收件人
javaMailSender.send(msg);//发送
}
}
在Service层添加发送邮件方法
定义EmailService
接口类,接口方法sendMail
public interface EmailService {
/**
* 新增邮件回复功能,有回复消息会有邮件提醒
*/
void sendMail(User user, Message message) throws MessagingException;
}
实现接口方法sendMail
对用户进行判断:
- 如果是游客评论则会通知管理员,如果是管理员自己发的评论则不需要通知自己
- 如果是回复别人的评论,则都进行邮件通知
package com.chenzhen.blog.service.impl;
import com.chenzhen.blog.mapper.MessageMapper;
import com.chenzhen.blog.pojo.Mail;
import com.chenzhen.blog.pojo.Message;
import com.chenzhen.blog.pojo.User;
import com.chenzhen.blog.service.EmailService;
import com.chenzhen.blog.util.MailUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.mail.MessagingException;
@Service
public class EmailServiceImpl implements EmailService {
@Autowired
private MailUtil mailUtil;
@Autowired
private MessageMapper messageMapper;
@Override
public void sendMail(User user, Message message) throws MessagingException {
if (user!=null){
//如果是管理员发的评论
if (message.getParentMessage().getId()==null || message.getParentMessage()==null){
//如果是根评论
//不需要发给自己邮件
return;
}else {
//如果不是根评论,则给[我回复的对象]发一封提醒邮件
Message parentMessage = messageMapper.selectById(message.getParentMessage().getId());//获取父评论
Mail mail = new Mail(null, parentMessage.getEmail(), parentMessage.getNickname(), parentMessage.getContent(),
message.getNickname(), message.getContent(),
"/message", "您在《ChenZhen的客栈-留言板》中的评论有了新的回复!");
mailUtil.sendThymeleafEmail(mail);
}
}else {
//如果不是管理员发的评论
if (message.getParentMessage().getId()==null || message.getParentMessage()==null){
//如果是根评论
//发给我自己,提醒有人在留言板留言了
Mail mail = new Mail(null, "1583296383@qq.com", "ChenZhen", null,
message.getNickname(), message.getContent(),
"/message","在《ChenZhen的客栈-留言板》中有了新的留言!");
mailUtil.sendThymeleafEmail(mail);
}else{
//如果不是根评论
//给回复者[回复的对象]发一份提醒邮件
Message parentMessage = messageMapper.selectById(message.getParentMessage().getId());//获取父评论
Mail mail = new Mail(null,parentMessage.getEmail(),parentMessage.getNickname(),
parentMessage.getContent(),message.getNickname(),message.getContent(),
"/message","您在《ChenZhen的客栈-留言板》中的评论有了新的回复!");
mailUtil.sendThymeleafEmail(mail);
}
}
}
}
以上准备做好之后下面是两种通知的方式。
方法一:异步编程的方式实现邮件提醒功能:
接下来我们展示异步编程的方式实现邮件提醒功能:
-
有一个地方需要额外注意,发送提醒邮件的功能是在用户进行评论后的操作,但是邮件的发送过程需要花一段时间,会造成用户点了评论发送按钮之后很久才得到反馈,为了解决这个问题,我们需要运用到多线程思想。
-
我们将
MailUtil
中发送邮件的方法sendThymeleafEmail()
加上@Async
注解,将该方法标记为一个异步方法,这样在执行该方法的时候springboot会为我们开辟一个另外的线程来运行邮件的发送功能。这样不会造成线程的堵塞。
@Async("asyncThreadPoolTaskExecutor") //设置为一个异步方法
自定义线程池配置类
- @Async后的属性参数值是指定使用哪个线程池,这里我们自己配置一个线程池来让Async指定
package com.chenzhen.blog;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @author ChenZhen
* @Description 自定义线程池
* @create 2022/9/28 18:31
* @QQ 1583296383
* @WeXin(WeChat) ShockChen7
*/
@Configuration
public class AsyncPoolConfig {
@Bean(name = "asyncThreadPoolTaskExecutor")
public ThreadPoolTaskExecutor asyncThreadPoolTaskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(200);
executor.setQueueCapacity(25);
executor.setKeepAliveSeconds(200);
executor.setThreadNamePrefix("asyncThread");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
只要在新增评论接口处添加代码,在评论信息保存到数据库后,调用Service层的方法,异步地给用户发送提醒邮件
- 自此邮件提醒功能就完成了!
方法二:使用消息队列Rabbitmq的方式完成邮件提醒功能
这里默认你已经在云服务器上安装好了MQ,如果你还没安装可以参考我的另一篇文章,安装好Rabbitmq:
https://www.chenzhen.space/blog/28
导入依赖:
<!-- Spring Boot整合RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- fast Json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
第一个是rabbitmq整合springboot的启动器,第二个是用来序列化对象的
配置文件:
spring:
rabbitmq:
host: xxx.xxx.xxx.xxx # RabbitMQ服务器主机名
port: 5672 # RabbitMQ服务器端口号
username: admin # 连接用户名
password: xxxxx # 连接密码
virtual-host: / # 默认虚拟主机
publisher-returns: true # 启用发布者返回确认
publisher-confirm-type: correlated # 发布者确认类型 correlated 意味着生产者将使用带有关联 ID 的回调来确认发布。
template:
mandatory: true # 强制消息必须被路由到一个队列
connection-timeout: 1000ms # 连接超时时间
listener:
simple:
acknowledge-mode: manual # 手动消息确认模式
prefetch: 10 # 预取消息数
concurrency: 1 # 并发消费者数量
max-concurrency: 10 # 最大并发消费者数量
rabbitmq:
email:
queue: email-queue #这个配置项是自定义配置项,配置队列的名称值而已 ,不是rabbitmq的官方配置,不要搞混了
解释一下配置项中的:
concurrency: 1
表示使用Spring AMQP来处理消息时,每个消费者实例的并发处理级别为1。这意味着每个消费者将一次处理一个消息。
具体来说,这个配置指定了以下行为:
-
concurrency: 1
:每个消费者实例一次只能处理一个消息。这是一种常见的配置,适用于需要确保每个消息都得到完全处理,不会出现并发处理的情况的场景。这对于需要精确控制消息处理顺序或需要避免并发问题的情况非常有用。 -
prefetch: 10
:这个配置表示每个消费者从队列预获取10条消息,但由于concurrency: 1
,每个消费者实际上只会处理其中的一条消息,然后才会获取更多消息。这有助于提高效率,减少消费者不断请求新消息的开销。 -
max-concurrency: 10
:这是一个最大并发数的配置,指定了在需要时最多可以创建的消费者实例数量。尽管concurrency: 1
指定了每个消费者的并发级别为1,但你可以有多个消费者实例,每个实例都以concurrency: 1
的方式处理消息。如果队列中有足够的消息,Spring AMQP可以根据需要创建多个实例来处理它们,最多可以同时运行10个这样的实例。
总之,concurrency: 1
表示每个消费者实例一次只处理一个消息,但你可以配置多个实例以提高并发处理能力,每个实例都会处理一个消息。这有助于在需要的情况下并行处理多个消息,但保持每个消费者实例的顺序性。
最后总结下rabbitmq并发消费的两个参数prefetchCount
和concurrentConsumers
concurrentConsumers
是设置并发消费者的个数,可以进行初始化-最大值动态调整,并发消费者可以提高消息的消费能力,防止消息的堆积
prefechCount
是每个消费者一次性从broker中取出的消息个数,提高这个参数并不能对消息实现并发消费,仅仅是减少了网络传输的时间
Rabbitmq配置类:
package com.chenzhen.blog.config;
import org.springframework.amqp.core.Queue;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitConfig {
@Value("${rabbitmq.email.queue}")
private String emailQueue;
// 声明emailQueue队列
@Bean
public Queue emailQueue() {
return new Queue(emailQueue);
}
}
生产者代码EmailSender
:
package com.chenzhen.blog.sender;
import com.alibaba.fastjson.JSON;
import com.chenzhen.blog.pojo.EmailMessage;
import com.chenzhen.blog.pojo.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
//邮件生产者
@Component
public class EmailSender {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private RabbitTemplate rabbitTemplate;
// 从配置文件读取邮件队列的名称
@Value("${rabbitmq.email.queue}")
private String emailQueue;
/**
* 发送邮件消息到队列
*
* @param user 收件人信息
* @param message 邮件消息内容
*/
public void send(User user, com.chenzhen.blog.pojo.Message message) {
// 1.创建 EmailMessage 对象,封装收件人和邮件消息
EmailMessage emailMessage = new EmailMessage(user,message);
// 2.将 EmailMessage 对象转换为字节数组
byte[] bytes = JSON.toJSONBytes(emailMessage);
// 3.创建消息对象,并设置消息体和持久化属性
Message rabbitMessage = MessageBuilder.withBody(bytes)
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.build();
// 4.发送消息到邮件队列
rabbitTemplate.send(emailQueue,rabbitMessage);
}
// init 方法被标记为 @PostConstruct,这意味着它会在 目前该Bean 被创建并完成依赖注入后调用。
// 当 Spring 容器创建 MyBean 时,会自动调用 initialize 方法。
@PostConstruct
public void init() {
// 设置发送成功消息确认回调函数
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
// 发送成功的处理逻辑
logger.info("邮件消息投递成功!");
} else {
// 发送失败的处理逻辑
logger.error("邮件消息投递失败!");
logger.error("ConfirmCallback: 相关数据 :{}",correlationData);
logger.error("ConfirmCallback: 确认情况 :{}",ack);
logger.error("ConfirmCallback: 原因 :{}",cause);
}
}
});
// 设置消息退回回调函数
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
// 处理退回的消息
String returnedMessage = new String(message.getBody());
logger.error("消息退回到生产者!");
logger.error("退回的消息 : {}",returnedMessage);
logger.error("回复代码 : {}",replyCode);
logger.error("回复文本 : {}",replyText);
logger.error("交换机 : {}",exchange);
logger.error("路由键 : {}",routingKey);
// 在这里进行退回消息的处理逻辑
}
});
}
}
消费者代码EmailConsumer
package com.chenzhen.blog.consumer;
import com.alibaba.fastjson.JSON;
import com.chenzhen.blog.pojo.EmailMessage;
import com.chenzhen.blog.pojo.User;
import com.chenzhen.blog.service.EmailService;
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class EmailConsumer implements ChannelAwareMessageListener {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private EmailService emailService;
@Override
@RabbitListener(queues = "${rabbitmq.email.queue}")
public void onMessage(Message message, Channel channel) throws Exception {
try {
// 从消息中获取消息体(消息的字节数组)
byte[] messageBody = message.getBody();
// 将消息体转换为 EmailMessage 对象
EmailMessage emailMessage = JSON.parseObject(messageBody, EmailMessage.class);
// 从 EmailMessage 对象中提取用户和邮件消息内容
User user = emailMessage.getUser();
com.chenzhen.blog.pojo.Message userMessage = emailMessage.getMessage();
emailService.sendMail(user, userMessage);
// 手动确认消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
// 发生异常时选择拒绝消息
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
logger.error("邮件发送时发生异常!丢弃该消息!");
e.printStackTrace();
}
}
}
最终同样只要在新增评论接口处添加代码,在评论信息保存到数据库后,调用生产者的方法,将消息投递到队列即可。