一、前言
我们之前通过SpringBoot整合mail可以很方便的实现邮件的发送,但是这类服务通常不稳定,当出现网络抖动的时候,会导致邮件自动推送失败。现在我们学习SpringBoot整合RabbitMQ实现邮件的完美投递,以便实现邮件 100% 被投递成功。
内容涵盖了 RabbitMQ 很多知识点,如:
- 生产者和消费者模型
- 消息发送机制
- 消费确认机制
- 消息的重新投递
- 消息消费失败的处理方案
二、项目准备
1.安装RabbitMQ服务
安装 RabbitMQ 服务,这一步比较简单,可以访问下面的官方地址,下载软件包并依次按照步骤进行安装即可。
https://rabbitmq.org.cn/docs/download
安装成功之后,登陆 RabbitMQ 控制台,可以看到类似于如下界面。
2.创建交换器
点击“Exchanges”菜单,进入“交换器”管理界面。
进入之后,点击最下方“Add a new exchange”按钮,创建一个类型为topic
,名称叫mail.exchange
的交换器,并提交。
3.创建消息队列
接着,点击“Queues”菜单,进入消息队列管理界面。
同样的,点击最下方“Add a new queue”按钮,创建一个名称叫mq.mail.ack
的消息队列,并提交。
保存之后,在列表中可以看到刚刚创建的消息队列,然后点击进入详情。
在详情中,将当前消息队列与上文创建的交换器进行绑定,便于后续通过交换器来发送消息到队列,操作如下。
对于topic
类型的交换器,通常不直接与消息队列进行交互,而是通过一个路由键,将消息路由到目标消息队列,这样设计的目的是让消息投递更加灵活。路由键,可以简单理解为类似于路由器,对数据进行路由分发处理。
4.配置邮箱发送服务器
为了实现邮件自动发送功能,我们还需要准备一个邮箱发送服务器,以 QQ 邮箱为例,登陆进去之后,在设置里面开启 POP3/SMTP 服务,并获取授权码记录下来。
三、项目实践
1.引入依赖
<!--mail 支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!--amqp 支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2.添加相关配置
在application.properties
中添加 rabbitmq、邮箱相关配置,示例如下:
# 配置邮件发送主机地址
spring.mail.host=smtp.qq.com
# 配置邮件发送服务端口号
spring.mail.port=25
# 配置邮件发送服务协议
spring.mail.protocol=smtp
# 配置邮件发送者用户名或者账户
spring.mail.username=xxxx
# 配置邮件发送者密码或者授权码
spring.mail.password=xxxx
# 配置邮件默认编码
spring.mail.default-encoding=UTF-8
# 配置smtp相关属性
spring.mail.properties.mail.smtp.auth=false
spring.mail.properties.mail.smtp.ssl.enable=false
spring.mail.properties.mail.smtp.ssl.required=false
#rabbitmq配置
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=qx
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#开启confirms回调 P -> Exchange
spring.rabbitmq.publisher-confirms=true
# 开启returnedMessage回调 Exchange -> Queue
spring.rabbitmq.publisher-returns=true
# 设置手动确认(ack) Queue -> C
spring.rabbitmq.listener.simple.acknowledge-mode=manual
spring.rabbitmq.listener.simple.prefetch=100
其中,spring.mail.username
和spring.mail.password
指的就是上文中创建的邮箱账号和授权码,将其配置进去即可。
3.编写RabbitMQ配置类
package com.example.dataproject.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author qx
* @date 2024/9/11
* @des 监听消息的发送情况配置
*/
@Configuration
@Slf4j
public class RabbitConfig {
@Autowired
private CachingConnectionFactory connectionFactory;
@Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
//设置消息转换器为json格式
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
//消息是否成功发送到Exchange
rabbitTemplate.setConfirmCallback((correlationData, b, s) -> {
if (b) {
log.info("消息发送到Exchange成功:{}", correlationData);
} else {
log.error("消息发送到Exchange失败,{},cause:{}", correlationData, s);
}
});
//触发setReturnCallback回调必须设置mandatory=true,否则Exchange没有找到Queue就会丢弃掉消息,而不会触发回调
rabbitTemplate.setMandatory(true);
//消息是否从Exchange路由到Queue,注意:这是一个失败回调,只有消息从Exchange路由到Queue失败才会回调这个方法
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
log.error("消息从Exchange路由到Queue失败:exchange:{},route:{},replyCode:{},replyText:{},message{}", exchange, routingKey, replyCode, replyText, message);
});
return rabbitTemplate;
}
}
4.编写生产者服务
在 Spring Boot 中,我们可以利用RabbitTemplate
工具,将数据通过交换器发送到目标消息队列,示例如下。
先创建一个实体类
package com.example.dataproject.entity;
import lombok.Getter;
import lombok.Setter;
/**
* @author qx
* @date 2024/9/11
* @des
*/
@Getter
@Setter
public class Mail {
/**
* 目标邮箱地址
*/
private String to;
/**
* 标题不能为空
*/
private String title;
/**
* 正文不能为空
*/
private String content;
/**
* 消息id
*/
private String msgId;
}
接下来创建一个工具类根据实体生成对应Message
package com.example.dataproject.utils;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessageProperties;
/**
* @author qx
* @date 2024/9/11
* @des
*/
public class MessageHelper {
/**
* 将对象序列化成消息数据
*
* @param obj
* @return
*/
public static Message objToMsg(Object obj) {
if (null == obj) {
return null;
}
if (obj instanceof String) {
}
Message message = MessageBuilder.withBody(JsonUtil.objToStr(obj).getBytes()).build();
// 设置消息持久化
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
// 设置消息为json格式
message.getMessageProperties().setContentType(MessageProperties.CONTENT_TYPE_JSON);
return message;
}
/**
* 将消息数据反序列化成对象
*
* @param message
* @param clazz
* @param <T>
* @return
*/
public static <T> T msgToObj(Message message, Class<T> clazz) {
if (null == message || null == clazz) {
return null;
}
String json = new String(message.getBody());
return JsonUtil.strToObj(json, clazz);
}
}
创建生产者服务类
package com.example.dataproject.service;
import com.example.dataproject.entity.Mail;
import com.example.dataproject.utils.MessageHelper;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.UUID;
/**
* @author qx
* @date 2024/9/11
* @des 生产者服务
*/
@Service
public class ProduceService {
@Autowired
private RabbitTemplate rabbitTemplate;
public boolean sendData(Mail mail) {
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
mail.setMsgId(uuid);
CorrelationData correlationData = new CorrelationData(uuid);
rabbitTemplate.convertAndSend("mail.exchange", "route.mail.ack", MessageHelper.objToMsg(mail), correlationData);
return true;
}
}
5.编写消费者服务
package com.example.dataproject.service;
import com.example.dataproject.entity.Mail;
import com.example.dataproject.utils.MessageHelper;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @author qx
* @date 2024/9/11
* @des 消费者
*/
@Component
@Slf4j
public class ConsumerService {
@Autowired
private SendMailService sendMailService;
@RabbitListener(queues = {"mq.mail.ack"})
public void consume(Message message, Channel channel) throws IOException {
log.info("收到消息:{}", message);
Mail mail = MessageHelper.msgToObj(message, Mail.class);
boolean falg = sendMailService.send(mail);
//手动确认模式
long tag = message.getMessageProperties().getDeliveryTag();
if (falg) {
//发送成功 删除消息
channel.basicAck(tag, falg);
} else {
//发送失败 重试返回消息队列
channel.basicNack(tag, falg, true);
}
}
}
6.编写邮件发送服务
package com.example.dataproject.service;
import com.example.dataproject.entity.Mail;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
/**
* @author qx
* @date 2024/9/11
* @des 邮件发送服务层
*/
@Service
@Slf4j
public class SendMailService {
@Value("${spring.mail.username}")
private String from;
@Autowired
private JavaMailSender mailSender;
public boolean send(Mail mail) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(mail.getTo());
message.setSubject(mail.getTitle());
message.setText(mail.getContent());
try {
mailSender.send(message);
log.info("邮件发送成功");
return true;
} catch (Exception e) {
log.error("邮件发送失败:from:{},to:{},desc:{}", from, mail.getTo(), e.getMessage());
return false;
}
}
}
7.编写 controller 接口
接着,编写一个 controller 接口,将邮件发送服务暴露出去,示例如下:
package com.example.dataproject.controller;
import com.example.dataproject.entity.Mail;
import com.example.dataproject.service.ProduceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author qx
* @date 2024/9/11
* @des
*/
@RestController
public class MailController {
@Autowired
private ProduceService produceService;
@PostMapping("/send")
public String sendMail(Mail mail) {
boolean flag = produceService.sendData(mail);
return flag ? "success" : "fail";
}
}
8.服务测试
启动服务进行测试
点击发送后我们查看控制台的日志,发送邮件已经成功发送了,并且发送成功之后消息被删除,不会重新进行发送。
我们查看收件人的邮箱也接收到了邮件。
可以清楚的看到,邮件发送成功!当大批量的发送邮件,也不用担心,因为整个邮件的发送都是异步的,不会阻塞主流程的运行。