在实际开发中经常会遇到“发送邮件”的场景。这个功能的开发非常的简单,我们可以引入JavaMail组件进行开发,编码简单,功能强大,可以实现多种邮件发送功能(纯文本、单附件、多附件...)。但是邮件发送需要调用第三方邮件提供商的服务,这一过程往往需要消耗大量时间。而在传统的业务层中开发者又是通过同步的方式来实现功能。这就不能让用户获得一个良好的用户体验。所以我们可以考虑使用异步的方式实现邮件的发送。
说道异步,立马可以想到MQ消息中间件。市面上比较主流的MQ有,RabbitMQ、kafka、ActiveMQ以及“阿里爸爸”旗下大名鼎鼎的RocketMQ等等。RabbitMQ是一款开源且高效的AMQP消息队列实现,也是市面上比较主流的消息队列组件。这次我就选择RabbitMQ来实现异步邮件通知的功能。
废话不多说,先上代码。
pom.xml文件中引入java mail 以及amqp的相关jar包。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
</dependencies>
application.yml
server:
port: 8080
spring:
mail:
host: smtp.163.com
username: xxxxx #这里写发送邮件的邮箱地址
password: xxxxx #这里是第三方授权码,需要去邮件设置中申请
default-encoding: utf-8
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
thymeleaf:
encoding: UTF-8
cache: false
servlet:
content-type: text/html
suffix: .html
首先先写一个邮件发送类,代码如下。
@Service(value = "myMailSender")
public class MailSender {
private static Logger logger = LoggerFactory.getLogger(MailSender.class);
@Resource
JavaMailSender javaMailSender;
@Value("${spring.mail.username}")
private String fromMailAddress;
public void send(MailMsg msg) {
SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
simpleMailMessage.setFrom(fromMailAddress);
simpleMailMessage.setTo(msg.getReceiver());
simpleMailMessage.setSubject(msg.getSubject());
simpleMailMessage.setText(msg.getContent());
long begin = System.currentTimeMillis();
javaMailSender.send(simpleMailMessage);
long end = System.currentTimeMillis();
logger.info("use time: {} ms", (end - begin));
}
}
然后在controller层中调用。
@Controller
public class MailController {
@Resource
private MailSendService mailSendService;
@GetMapping("/mail/sendMail")
@ResponseBody
public String send() {
String msg = null;
try {
Long useTime = mailSendService.send(new MailMsg("826935261@qq.com", "邮件测试", "这是一测试邮件"));
msg = "success, uset time " + useTime + " ms";
} catch (Exception e) {
msg = e.getMessage();
}
return msg;
}
}
新增一个html ,加入一个a标签。我这里使用了thymeleaf模板引擎,将href 属性直接路由到了 /mail/sendMail 路径下。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<a href="" th:href="@{/mail/sendMail}">点击同步发送邮件</a> <br>
</div>
</body>
</html>
打开浏览器,访问http://localhost:8080。点击同步发送邮件。
经过了几十秒的等待,我们看到我接受到了邮件。并且页面上也有了返回结果。可以看到,邮件发送的功能是完成了,但是我让用户等了40多秒钟,可以说用户体验非常差,不够友好。
接下来,我换一种方式,使用消息队列先将邮件对象存储起来。然后再由消费者程序来发送邮件。
需要完成这一功能,首先应该在application.yml中加入RabbitMQ的配置。
rabbitmq:
host: localhost
port: 5672
username: itsu
password: itsu
virtual-host: /app
publisher-returns: true
publisher-confirm-type: correlated
template:
mandatory: true
connection-timeout: 1000ms
listener:
simple:
acknowledge-mode: manual
prefetch: 10
concurrency: 1
max-concurrency: 10
然后我来定义一个生产者, 创建一个exchange交换机“email_exchange”, 并定义了一个route key 为test.email。 值得一说的是CorrelationData 对象是用来定义附加的参数。一般用来保存生产者用户自定义的唯一标识。我这里用UUID来实现。confirmCallback 和 returnCallBack 分别代表了RabbitMQ的 confirm - return 机制。这里不细说,有需要的小伙伴可以去访问RabbitMQ的官网了解。
@Resource
RabbitTemplate rabbitTemplate;
RabbitTemplate.ConfirmCallback confirmCallback = (correlationData, ack, cause) -> {
Gson gson = new Gson();
logger.info(gson.toJson(correlationData));
if (!ack) {
logger.warn(cause);
}
};
RabbitTemplate.ReturnCallback returnCallback = (message, replyCode, replyText, exchange, routingKey) -> {
Gson gson = new Gson();
logger.info(gson.toJson(message));
System.err.println(replyCode);
System.err.println(replyText);
System.err.println(exchange);
System.err.println(routingKey);
};
public Long sendToQueue(MailMsg mailMsg) {
long begin = System.currentTimeMillis();
rabbitTemplate.setConfirmCallback(confirmCallback);
rabbitTemplate.setReturnCallback(returnCallback);
CorrelationData cd = new CorrelationData();
cd.setId(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("email_exchange", "test.email", mailMsg, cd);
long end = System.currentTimeMillis();
return end - begin;
}
接下来定义消费者。@RabbitListener用来监听绑定交换机和队列,并且可以在其中定义交换机的类型,route key的值。在消费者这一边需要将生产者定义的route key用来接收消息。@RabbitHandler注解是指被这个注解标记的方法是用来处理消费者接受到的消息对象的。@Payload 表示消息队列需要被反序列化到MailMsg对象中。 @Headers 可以和Http请求中的HttpHeaders类比。是用来存储RabbitMQ 返回给消费者的一些基本信息。
@Service
public class MailConsumer {
private static Logger logger = LoggerFactory.getLogger(MailConsumer.class);
@Resource(name = "myMailSender")
private MailSender mailSender;
@RabbitListener(
bindings = {
@QueueBinding(value = @Queue(
value = "email_queue", durable = "false", autoDelete = "false"
), exchange = @Exchange(
value = "email_exchange", durable = "false", autoDelete = "false", type = "topic"
),
key = "test.email"
)
}
)
@RabbitHandler
public void realSendMail(@Payload MailMsg mailMsg, Channel channel, @Headers Map headers) {
Gson gson = new Gson();
logger.info("===============================received msg ===========================================");
logger.info(gson.toJson(mailMsg));
mailSender.send(mailMsg);
try {
channel.basicAck((Long) headers.get(AmqpHeaders.DELIVERY_TAG), false);
} catch (IOException e) {
e.printStackTrace();
}
}
}
在controller中加上一个方法用来处理异步邮件通知。
@GetMapping("/mail/sendMailByMq")
@ResponseBody
public String sendByMq() {
String msg = null;
try {
Long useTime = mailSendService.sendToQueue(new MailMsg("826935261@qq.com", "邮件测试", "这是一测试邮件"));
msg = "success, use time " + useTime;
} catch (Exception e) {
msg = e.getMessage();
}
return msg;
}
然后我们在页面上加上一个a标签。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<a href="" th:href="@{/mail/sendMail}">点击同步发送邮件</a> <br>
<a href="" th:href="@{/mail/sendMailByMq}">点击异步发送邮件</a>
</div>
</body>
</html>
然后重启应用,测试一下。可以看到这一次仅仅用了9ms就返回结果给前端了。相比于同步处理时让用户等待40多秒,这样的用户体验肯定是大大提高了。
我们打开Rabbitmq management 可以看到,刚刚的邮件发送功能。MQ 作为Broker接收到了一个消息,并且通过Queue派发给了一个消费者。
40 秒后,我又收到了邮件提醒。需要一提的是,RabbitMQ 并不会真正加快你的邮件发送速度。只是通过异步的方式,提前返回结果给到前端。实际邮件发送的时间任然需要40秒,只是在用户体验上提高了等级。消息队列比较常用的使用场景有,高并发场景下的流量削峰,分布式系统中的信息传递等等。