博客邮件提醒功能的实现(异步编程和消息队列Rabbitmq两种方式)

  • 作者:ChenZhen

  • 本人不常看CSDN消息,有问题通过下面的方式联系:

  • 我的个人博客地址:https://www.chenzhen.space/

  • 版权:本文为博主的原创文章,本文版权归作者所有,转载请附上原文出处链接及本声明。

  • 如果对你有帮助,请给一个小小的star⭐

邮件提醒功能:当你收到某个人的回复时,会给你发送一封提醒邮件,并展示回复的内容。

博客邮件提醒功能的实现(异步编程和消息队列Rabbitmq两种方式)_第1张图片

我觉得对于一个博客,邮件回复的功能是必不可少的,能让你及时的回复别人的评论,还能让我更方便的和网上的人对线

其实这个功能还是蛮好实现的,我们先演示怎么用java发送一封简单的邮件

1.开启POP3/SMTP服务

以QQ邮箱为例:

进入设置
在这里插入图片描述

在下方开启POP3/SMTP服务,此处已开启
博客邮件提醒功能的实现(异步编程和消息队列Rabbitmq两种方式)_第2张图片

经过身份认证过后,会获得一串授权码,请务必保存下来

博客邮件提醒功能的实现(异步编程和消息队列Rabbitmq两种方式)_第3张图片

2.引入spring-boot-starter-mail 依赖

由于Spring推出了关于Mail的JavaMailSender类,基于该类Spring Boot又对其进行了进一步封装,从而实现了轻松发送邮件的集成。而且JavaMailSender类提供了强大的邮件发送能力,支持各种类型的邮件发送。


        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-mailartifactId>
        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("[email protected]");
            //邮件接收者
            msg.setTo("[email protected]");
            //邮件主题
            msg.setSubject("测试邮件主题");
            //邮件正文
            msg.setText("测试邮件内容");
            //邮件发送时间
            msg.setSentDate(new Date());
            //邮件发送
            javaMailSender.send(msg);

        }

运行测试方法,需要等待一会便能收到

博客邮件提醒功能的实现(异步编程和消息队列Rabbitmq两种方式)_第4张图片

博客邮件提醒功能的实现(异步编程和消息队列Rabbitmq两种方式)_第5张图片

5.发送静态邮件模板

上面演示了如何发送普通邮件,接下来我们要实现发送一封静态邮件模板,就像开头的实例一样

这里我们使用Thymeleaf作为模板

引入Thymeleaf启动器


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-thymeleafartifactId>
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放置模板文件的默认路径

如果觉得这个模板不好看的话,也可以去我的项目源码使用我的邮件模板,就是开头那一个。

博客邮件提醒功能的实现(异步编程和消息队列Rabbitmq两种方式)_第6张图片

运行测试方法

  • 这里使用MimeMessageHelper来构建邮件,利用Context对象可以设置模板里面的Thymeleaf表达式的值
  • 调用springTemplateEngine的process方法来解析模板,第一个参数为模板文件的名字
 @Test
        public void test4() throws MessagingException {
            MimeMessage msg = javaMailSender.createMimeMessage();//构建邮件
            MimeMessageHelper helper = new MimeMessageHelper(msg, true);//设置可选文本或添加内联元素或附件,

            helper.setFrom("[email protected]");//发件人
            helper.setSentDate(new Date());//发送日期
            helper.setSubject("这是测试主题(thymeleaf)");//发送主题
            helper.setTo("[email protected]");//收件人

            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);//发送
        }

成功收到

博客邮件提醒功能的实现(异步编程和消息队列Rabbitmq两种方式)_第7张图片

博客邮件提醒功能的实现(异步编程和消息队列Rabbitmq两种方式)_第8张图片

  • 现在我们将一些操作进行封装,做成一个工具类,并且新建一个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, "[email protected]", "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);

            }

        }
    }
}

以上准备做好之后下面是两种通知的方式。

方法一:异步编程的方式实现邮件提醒功能:

接下来我们展示异步编程的方式实现邮件提醒功能:

  • 有一个地方需要额外注意,发送提醒邮件的功能是在用户进行评论后的操作,但是邮件的发送过程需要花一段时间,会造成用户点了评论发送按钮之后很久才得到反馈,为了解决这个问题,我们需要运用到多线程思想。

首先在启动类上加上@EnableAsync注解开启异步编程
在这里插入图片描述

我们将MailUtil中发送邮件的方法sendThymeleafEmail()加上@Async注解,Springboot会将该方法标记为一个异步方法,这样在执行该方法的时候springboot会为我们开辟一个另外的线程来运行邮件的发送功能。这样不会造成线程的堵塞。

@Async("asyncThreadPoolTaskExecutor")  //设置为一个异步方法

博客邮件提醒功能的实现(异步编程和消息队列Rabbitmq两种方式)_第9张图片

自定义线程池配置类

  • @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两种方式)_第10张图片

  • 自此邮件提醒功能就完成了!

方法二:使用消息队列Rabbitmq的方式完成邮件提醒功能

这里默认你已经在云服务器上安装好了MQ,如果你还没安装可以参考我的另一篇文章,安装好Rabbitmq:

https://www.chenzhen.space/blog/28

导入依赖:


        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-amqpartifactId>
        dependency>
        
        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
            <version>1.2.83version>
        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的官方配置,不要搞混了

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();


        }
    }
}

最终同样只要在新增评论接口处添加代码,在评论信息保存到数据库后,调用生产者的方法,将消息投递到队列即可。

博客邮件提醒功能的实现(异步编程和消息队列Rabbitmq两种方式)_第11张图片

  • 作者:ChenZhen

  • 本人不常看CSDN消息,有问题通过下面的方式联系:

  • 我的个人博客地址:https://www.chenzhen.space/

  • 版权:本文为博主的原创文章,本文版权归作者所有,转载请附上原文出处链接及本声明。

  • 如果对你有帮助,请给一个小小的star⭐

你可能感兴趣的:(rabbitmq,分布式,经验分享,springboot)