RabbitMQ异步发送邮件
1.邮件发送接口(伪代码):
public void toSendMsg(HttpServletRequest request, HttpServletResponse response) { //1.校验是否为非法请求 //xxx //2.接收参数 String emails = request.getParameter("emails");//多封邮件(可接收分隔符拼接的emails,也可以选择接受email数组) String emailContent = request.getParameter("emailContent");//邮件的固定格式内容 // ... try { new SendEmailThread(email,emailContent).start(); //实例化发送邮件的线程。此处可以用局部内部类或者java8的lambda代替第二步,不过上面的局部变量要用final修饰 //操作间隔时间限制 //xxx } catch (Exception e) e.printStackTrace(); } finally { //xxx } }
2.构建线程对象
class SendEmailThread extends Thread{ private String emails; private String emailContent; SendEmailThread(String emails,String emailContent){ this.emails=emails; this.emailContent=emailContent; } public void run(){ try { sendEmail(emails,emailContent);//将参数传入负责执行发送逻辑的方法 }catch (Exception e) { e.printStackTrace(); } } }
3.发送逻辑方法sendEmail
//此处根据自己的业务逻辑自行设计,可将邮件及其内容写入redis队列,或者有MQ软件环境的写入MQ,再用定时任务进行发送。发送之后将发送结果写入数据库或者logger记录,方便查询发送是否成功。具体实现待发布
----------------------------------------------------我是分隔符---------------------------------------------------------------------------------------------------------------
22年由于项目需要接触了RabbitMQ,并用于异步发送邮件,可以更快地响应用户请求
基于springboot项目,相关配置:
spring:
rabbitmq:
host: #机器ip
port: #端口号5672
username: xxx
password: xxx
virtual-host: /
# 开启confirms回调 P -> Exchange 解决消息没有到达交换机问题
publisher-confirm-type: correlated
# 开启returnedMessage回调 Exchange -> Queue 解决消息没从交换机路由到队列问题
publisher-returns: true
# 设置手动确认(ack) Queue -> C
listener:
simple:
acknowledge-mode: manual #消费者确认后,消息才会从队列中删除
prefetch: 100
retry: #重试机制
enabled: true #是否开启消费者重试
max-attempts: 3 #最大重试次数
initial-interval: 200 #重试间隔时间(单位毫秒)
max-interval: 10000 #重试最大时间间隔(单位毫秒)
multiplier: 2 #间隔时间乘子,间隔时间*乘子=下一次的间隔时间,最大不能超过设置的最大间隔时间
声明一个消息队列的配置类RabbitMqConfig,本次采用的消息模型是Routing直连,使用了DirectExchange,并初始化绑定了交换机和队列
package com.xx; import org.springframework.amqp.core.*; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; @Configuration public class RabbitMqConfig2 { @Value("${spring.rabbitmq.host}") private String host; @Value("${spring.rabbitmq.port}") private int port; @Value("${spring.rabbitmq.username}") private String username; @Value("${spring.rabbitmq.password}") private String password; @Value("${spring.rabbitmq.virtual-host}") private String virtualHost; public static final String QUEUE_MAIL = "QUEUE_MAIL";//邮件队列 public static final String ROUTINGKEY_MAIL = "ROUTINGKEY_MAIL"; public static final String EXCHANGE_MAIL = "EXCHANGE_MAIL"; //建立一个连接 @Bean public ConnectionFactory connectionFactory() { CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host, port); connectionFactory.setUsername(username); connectionFactory.setPassword(password); connectionFactory.setVirtualHost(virtualHost); //connectionFactory.setPublisherConfirms(true);//确认机制 connectionFactory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED); connectionFactory.setPublisherReturns(true); //发布确认,template要求CachingConnectionFactory的publisherConfirms属性设置为true return connectionFactory; } @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) //必须是prototype类型 public RabbitTemplate rabbitTemplate() { RabbitTemplate template = new RabbitTemplate(this.connectionFactory()); template.setMessageConverter(this.jsonMessageConverter()); template.setMandatory(true); return template; } @Bean public Queue queueMail() { /* * 1、name: 队列名称 * 2、durable: 是否持久化 * 3、exclusive: 是否独享、排外的。如果设置为true,定义为排他队列。则只有创建者可以使用此队列。也就是private私有的。 * 4、autoDelete: 是否自动删除。也就是临时队列。当最后一个消费者断开连接后,会自动删除。 */ return new Queue(QUEUE_MAIL, true, false, false); //队列持久 } /** * 针对消费者配置 * 1. 设置交换机类型 * 2. 将队列绑定到交换机 * FanoutExchange: 将消息分发到所有的绑定队列,无routingkey的概念 * HeadersExchange :通过添加属性key-value匹配 * DirectExchange:按照routingkey分发到指定队列 * TopicExchange:多关键字匹配 */ @Bean public DirectExchange exchangeMail() { return new DirectExchange(EXCHANGE_MAIL, true, false); } //将队列和交换机绑定, 并设置用于匹配键 @Bean public Binding bindDirect() { return BindingBuilder.bind(queueMail()).to(exchangeMail()).with(ROUTINGKEY_MAIL); } @Bean public MessageConverter jsonMessageConverter() { return new Jackson2JsonMessageConverter(); } }
开发生产者
@Slf4j
@Service
public class MailProduceServiceImpl implements MailProduceService {
//RabbitMqConfig配置类
private final RabbitMqConfig rabbitMqConfig;
@Autowired
public MailProduceServiceImpl(RabbitMqConfig rabbitMqConfig) {
this.rabbitMqConfig = rabbitMqConfig;
}
@Override
public void productMessage(Message message) { //message是自定义封装的类,存的是一些业务信息(例如邮件收发人、邮件内容等)
//交换机确认 回调接口
//消息没有到达交换机问题:对应yml配置:开启生产者确认publisher-confirm-type: correlated
// correlationData 存储消息的ID和自己存储的关于该条消息的信息
// boolean ack 判断是否发送成功
// cause 发送失败是失败的原因
rabbitMqConfig.rabbitTemplate().setConfirmCallback((correlationData, ack, cause) -> {
if (!ack) {
log.warn("消息发送到exchange失败,correlationData={}, cause={}", correlationData, cause);
// 消息确认失败的处理操作
message.setFailureCause(cause);
message.setStatus("2");
} else {
message.setStatus("1");
if (log.isDebugEnabled()) {
log.debug("消息{},交换机已收到", correlationData);
}
}
//根据需要此处可将message入库,方便以后追踪
});
//队列消息回退接口
//消息没从交换机路由到队列问题:yml开启生产回执publisher-returns: true
rabbitMqConfig.rabbitTemplate().setReturnsCallback(returned -> {
log.info("未找到路由为{}的队列", returned.getRoutingKey());
message.setStatus("2");
message.setFailureCause(returned.getReplyText());
// 根据需要此处可将message入库,方便以后追踪
});
//创建uuid
String msgId = UUID.randomUUID().toString().replaceAll("-", "");
message.setCid(msgId);// CorrelationData ID (消息ID)
//避免重复消息:CorrelationData 对象,每个发送的消息都需要配备一个 CorrelationData 相关数据对象,
//CorrelationData 对象内部只有一个 id 属性,用来表示当前消息唯一性。
CorrelationData correlationData = new CorrelationData(msgId);
//发送消息到rabbitMQ
rabbitMqConfig.rabbitTemplate().convertAndSend("EXCHANGE_MAIL", "ROUTINGKEY_MAIL", message, correlationData);
log.warn("productMail message:{}" , message);
// 根据需要此处可将message入库,方便以后追踪
}
}
开发消费者
/**
* 消费消息
*/
@RequiredArgsConstructor
@Slf4j
@Service
public class RabbitListenterServiceImpl2 {
//发送邮件的业务类
private final MailSenderService mailSenderService;
@RabbitListener(queues = RabbitMqConfig.QUEUE_FACULTY_MAIL)
public void sendFacultyFundProcess(Message message, Channel channel) throws IOException {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
MessageProperties properties = message.getMessageProperties();
long tag = properties.getDeliveryTag();
log.warn("消费者获取消息:{}"
, MessageHelper.msgToObj(message, com.xxMessage.class).toString());
//发送邮件业务代码
mailSenderService.send(message);
channel.basicAck(tag, false);//消息确认
}
}
其他,使用javaMail发送邮件相关代码
@Slf4j @Primary @Service @RequiredArgsConstructor public class DefaultMailServiceImpl implements MailService { private final JavaMailSender javaMailSender; @Value("${spring.mail.username}") private String from; @Value("${spring.mail.nickname}") private String nickname; /** * @param to 接收者 * @param subject 标题 * @param content 内容 * @param isHTML 是否是HTML字段 * @desc 方法功能: 发送邮件 */ @Override public void sendMail(String to, String subject, String content, boolean isHTML) { SendMailDTO dto = new SendMailDTO(); dto.setToList(Collections.singletonList(to)); dto.setSubject(subject); dto.setContent(content); dto.setHtml(isHTML); sendMail(dto); } @Override public void sendMail(SendMailDTO dto) { MimeMessage message = javaMailSender.createMimeMessage(); if (CollectionUtil.isEmpty(dto.getToList())) { throw new CustomException(BUSINESS.getCode(), BUSINESS.getMessage()); } try { //true表示需要创建一个multipart message MimeMessageHelper helper = new MimeMessageHelper(message, true); helper.setFrom(from, nickname); helper.setTo(dto.getToList().toArray(new String[0])); if (CollectionUtil.isNotEmpty(dto.getCcList())) { helper.setCc(dto.getCcList().toArray(new String[0])); } if (CollectionUtil.isNotEmpty(dto.getBccList())) { helper.setBcc(dto.getBccList().toArray(new String[0])); } helper.setSubject(dto.getSubject()); helper.setText(dto.getContent(), dto.isHtml()); javaMailSender.send(message); } catch (Exception e) { String contextStr = String.format("to=%s,subject=%s,content=%s,ishtml=%s" , dto.getToList(), dto.getSubject(), dto.getContent(), dto.isHtml()); log.info(String.format("send_mail_error!errorMsg=%s, context=%s" , e.getMessage(), contextStr), e); throw new CustomException(BUSINESS.getCode(), BUSINESS.getMessage()); } } }