背景
- 在上文章中我们已经实现了一个简单的用户邮箱登记的web应用,将数据保存到mysql数据库中,并利用安全框架对web页面进行保护及实现了管理员的注册登录,又通过Spring的配置属性完成了自定义的各种配置。并了解了Spring与应用的集成的基本概念,实现集成REST API服务。
- 本文将继续深入Spring的集成应用,实现邮件发送及集成消息队列的功能。
JavaMailSender
Spring框架提供了一种使用JavaMailSender接口发送电子邮件的简单抽象方法,而Spring Boot为其提供了自动配置以及启动程序模块。
- JavaMailSender接口具有特殊的JavaMail功能,例如MIME消息支持。
public interface JavaMailSender extends MailSender {
MimeMessage createMimeMessage();
MimeMessage createMimeMessage(InputStream var1) throws MailException;
void send(MimeMessage var1) throws MailException;
void send(MimeMessage... var1) throws MailException;
void send(MimeMessagePreparator var1) throws MailException;
void send(MimeMessagePreparator... var1) throws MailException;
}
Spring集成邮件发送功能
代码语言:javascript复制Spirng实现邮件发送功能,需要以下步骤:
1. 添加maven依赖
2. 添加Spring邮件配置
3. 创建邮件管理Bean并注入Spring应用上下文
4. 修改业务逻辑,调用邮件发送功能
1. 添加maven依赖
代码语言:javascript复制 <!-- pom.xml -->
<dependencies>
<!-- 邮件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
</dependencies>
2. 添加Spring邮件配置
代码语言:javascript复制### application.yml
spring:
#mail配置
mail:
host: smtp.163.com
username: zhuhuix@163.com
password: 自行设置邮箱密码
default-encoding: UTF-8
3. 创建邮件管理Bean并注入Spring应用上下文
代码语言:javascript复制/**
* 邮件发送Bean
*
* @author zhuhuix
* @date 2020-07-13
*/
@Service
@Component
public class MailManager {
private final org.slf4j.Logger logger = LoggerFactory.getLogger(Logger.class);
// 发件人
@Value("${spring.mail.username}")
private String from;
@Autowired
private JavaMailSender javaMailSender;
/**
* 普通文本邮件发送
*
* @param to 收件人
* @param subject 主题
* @param text 内容
*/
public void sendSimpleMail(String to, String subject, String text) {
SimpleMailMessage msg = new SimpleMailMessage();
msg.setFrom(this.from);
msg.setTo(to);
msg.setSubject(subject);
msg.setText(text);
try {
this.javaMailSender.send(msg);
logger.info(msg.toString());
} catch (MailException ex) {
logger.error(ex.getMessage());
}
}
/**
* HTML邮件发送
*
* @param to 收件人
* @param subject 主题
* @param text 内容
*/
public void sendHTMLMail(String to, String subject, String text) {
try {
MimeMessage msg = javaMailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(msg, true);
mimeMessageHelper.setFrom(this.from);
mimeMessageHelper.setTo(to);
mimeMessageHelper.setSubject(subject);
mimeMessageHelper.setText(text, true);
this.javaMailSender.send(msg);
logger.info("to=" to "," "subject=" subject "," "text=" text);
} catch (MailException ex) {
logger.error(ex.getMessage());
} catch (MessagingException ex) {
logger.error(ex.getMessage());
}
}
/**
* 发送带有附件的邮件
* @param to 收件人
* @param subject 主题
* @param text 内容
* @param filePath 附件
*/
public void sendAttachmentMail(String to, String subject, String text, String filePath) {
try {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true);
mimeMessageHelper.setFrom(from);
mimeMessageHelper.setTo(to);
mimeMessageHelper.setSubject(subject);
mimeMessageHelper.setText(text, true);
FileSystemResource fileSystemResource = new FileSystemResource(new File(filePath));
String fileName = fileSystemResource.getFilename();
mimeMessageHelper.addAttachment(fileName, fileSystemResource);
javaMailSender.send(mimeMessage);
logger.info("to=" to "," "subject=" subject "," "text=" text);
} catch (MailException ex) {
logger.error(ex.getMessage());
} catch (MessagingException ex) {
logger.error(ex.getMessage());
}
}
}
4. 修改业务逻辑,调用邮件发送功能
- 业务流程图
- 业务逻辑
/**
* 基于SpringMVC框架开发web应用--用户服务类
*
* @author zhuhuix
* @date 2020-07-03
* @date 2020-07-04 增加通过jdbcTemplate处理数据
* @date 2020-07-07 将jdbcTemplate处理数据程序改为Spring Data JPA的处理方式
* @date 2020-07-10 增加删除deleteUser和查找findUser
* @date 2020-07-13 首次保存用户后通过邮件管理器发送通知邮件
*/
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private MailManager mailManager;
// 返回所有的用户
public List<User> listUsers() {
return (List<User>) userRepository.findAll();
}
// 保存用户
public User saveUser(User user) {
boolean firstRegister = false;
if ((user.getId() == null || user.getId().equals(0L))) {
firstRegister = true;
}
// 首次保存用户成功后发送通知邮件
if (userRepository.save(user) != null && firstRegister == true) {
sendMail(user);
}
return user;
}
// 发送邮件
private void sendMail(User user) {
// 发送文本邮件
String text = "您的邮箱信息已登记!";
mailManager.sendSimpleMail(user.getEmail(), "用户通知(文本邮件)", text);
// 发送HTML邮件
String content = "<html>n"
"<body>n"
"<h3> <font color="red"> " text "</font> </h3>n"
"</body>n"
"</html>";
mailManager.sendHTMLMail(user.getEmail(), "用户通知(HTML邮件)", content);
// 发送带有附件的邮件
String attachFilePath = "c:\csdn-logo.png";
mailManager.sendAttachmentMail(user.getEmail(), "用户通知(带有附件的邮件)", content, attachFilePath);
}
// 删除用户
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
// 查找用户
public User findUser(Long id) {
return userRepository.findById(id).get();
}
// 根据名称查找用户
public List<User> searchUser(String name) {
return userRepository.findByName(name);
}
}
邮件发送功能测试
- 登记用户
- 登录邮箱查看
Spring集成JavaMailSender实现邮件发送小结
以上我们通过JavaMailSender接口实现了文本、超文本及带有附件的邮件的发送功能。 在书写这些程序时,采用了硬编码,可能会碰到如下问题:
- 用Java代码创建基于HTML的电子邮件内容很繁琐且容易出错。
- UI和业务逻辑之间没有明确区分。
- 更改电子邮件内容及重新排列UI时,需要编写Java代码,重新编译,重新部署。 解决这些问题的方法是使用模板库(例如我们已经用到的thymelea或者freemaker),当需要发送的邮件的内容变得相当复杂时,就变得非常必要,读者可自行尝试。
RabbitMQ
RabbitMQ可以说是AMQP(Advanced Message Queue,高级消息队列协议)最杰出的实现。
RabbitMQ的基本概念
概念 | 描述 |
---|---|
发送者 | 消息的生产者,也可以是一个向交换器发布消息的客户端应用程序 |
接收者 | 消息的消费者,也可以认为是向消息队列接收消息的服务端程序 |
Exchange(交换器) | 用来接收发送者发送的消息并将这些消息路由给服务器中的队列 |
Binding (绑定) | 用于消息队列和交换器之间的关联 |
队列 | 用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。 |
Binding key | 在绑定(Binding)Exchange与Queue的同时,一般会指定一个Binding key;Routing key结合Exchange可实现路由规则。 |
Routing key | 通过指定Routing key,结合Exchange和Routing key,可以决定消息流向哪里。 |
- RabbitMQ还有象Channel 信道、Virtual Host 虚拟主机、Broker 消息队列服务器实体等概念,请读者自行研究。
RabbitMQ的消息路由走向
- RabbitMQ的消息路由走向由Exchange的类型决定;分发消息时根据Exchange类型的不同分发策略有区别,见下表:
类型 | 描述 |
---|---|
Direct | 如果消息的routing key与队列的binding key相同,那么消息将会路由到该队列上。 |
Topic | 如果消息的routing key与队列binding key(可能会包含通配符)匹配,那么消息将会路由到一个或多个这样的队列上。 |
Fanout | 不管routing key和binding key是什么,消息都将会路由到所有绑定队列上。 |
Headers | 与Topic Exchange类似,只不过要基于消息的头信息进行路由,而不是routing key。 |
本文只对Direct模型进行展开处理,其他类型请读者自行研究。关于如何绑定队列到Exchange的更详细的描述,可以参考Alvaro Videla和Jason J.W. Williams编写的RabbitMQ in Action (RabbitMQ实战)。
Spring集成RabbitMQ实现异步消息处理
1. 添加maven依赖
代码语言:javascript复制 <!-- pom.xml rabbitmq -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2. Spring添加RabbitMQ配置
代码语言:javascript复制### application.yml
spring:
#RabbitMQ配置
rabbitmq:
host: 192.168.0.1
port: 5672
username: rabbitmq
password: rabbitmq
virtual-host: /
connection-timeout: 10000
listener:
simple:
acknowledge-mode: auto # 自动应答
auto-startup: true
default-requeue-rejected: false # 不重回队列
concurrency: 5
max-concurrency: 20
prefetch: 1 # 每次只处理一个信息
retry:
enabled: false
template:
exchange: web.demo
routing-key: user.key
3. 创建RabbitMQ配置类
代码语言:javascript复制/**
* rabbitmq 配置类
*
* @author zhuhuix
* @date 2020-07-14
*/
@Configuration(value = "rabbitMQConfig")
public class RabbitMQConfig {
// 获取exchange和routing-key定义
@Value("${spring.rabbitmq.template.exchange}")
private String exchange;
@Value("${spring.rabbitmq.template.routing-key}")
private String routingKey;
public String getExchange() {
return exchange;
}
public String getRoutingKey() {
return routingKey;
}
// 自定义消息转换器
@Bean
public MessageConverter messageConverter() {
return new SimpleMessageConverter() {
@Override
protected Message createMessage(Object object, MessageProperties messageProperties) throws MessageConversionException {
Message message = super.createMessage(object, messageProperties);
return message;
}
};
}
}
4. 创建接收消息监听程序
- 监听消息队列,收到完整消息后,调用邮件发送程序
/**
* rabbitmq 接收器
*
* @author zhuhuix
* @date 2020-07-14
*/
@Component
public class RabbitMQReceiver {
private final org.slf4j.Logger logger = LoggerFactory.getLogger(Logger.class);
// 邮件集成请参考上篇文章《Spring全家桶的深入学习(八):Spring集成JavaMailSender实现邮件发送》
@Autowired
private MailManager mailManager;
@Autowired
private RabbitTemplate rabbitTemplate;
// 监听消息队列
@RabbitHandler
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange("#{rabbitMQConfig.getExchange()}"),
key = "#{rabbitMQConfig.getRoutingKey()}",
value = @Queue("user.queue")))
public void receiveMessage(Message message) {
try {
User user = (User) rabbitTemplate.getMessageConverter().fromMessage(message);
logger.info("接收到消息:[{}]", user.toString());
// 收到完整消息后,调用邮件发送程序,发送通知邮件
if (user != null) {
sendMail(user);
}
} catch (Exception ex) {
logger.error(ex.getMessage());
}
}
// 发送邮件
// 请参考上篇文章《Spring全家桶的深入学习(八):Spring集成JavaMailSender实现邮件发送》
private void sendMail(User user) {
// 发送文本邮件
String text = "您的邮箱信息已登记!";
mailManager.sendSimpleMail(user.getEmail(), "用户通知(文本邮件)", text);
// 发送HTML邮件
String content = "<html>n"
"<body>n"
"<h3> <font color="red"> " text "</font> </h3>n"
"</body>n"
"</html>";
mailManager.sendHTMLMail(user.getEmail(), "用户通知(HTML邮件)", content);
// 发送带有附件的邮件
String attachFilePath = "c:\csdn-logo.png";
mailManager.sendAttachmentMail(user.getEmail(), "用户通知(带有附件的邮件)", content, attachFilePath);
}
}
5. 修改业务逻辑,实现发送消息功能
代码语言:javascript复制/**
* 基于SpringMVC框架开发web应用--用户服务类
*
* @author zhuhuix
* @date 2020-07-03
* @date 2020-07-04 增加通过jdbcTemplate处理数据
* @date 2020-07-07 将jdbcTemplate处理数据程序改为Spring Data JPA的处理方式
* @date 2020-07-10 增加删除deleteUser和查找findUser
* @date 2020-07-13 首次保存用户后通过邮件管理器发送通知邮件
* @date 2020-07-14 将同步发送通知邮件的功能变更为通过消息队列异步发送
*/
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private RabbitTemplate rabbitTemplate;
// 返回所有的用户
public List<User> listUsers() {
return (List<User>) userRepository.findAll();
}
// 保存用户
public User saveUser(User user) {
boolean firstRegister = false;
if ((user.getId() == null || user.getId().equals(0L))) {
firstRegister = true;
}
// 首次保存用户成功后发送消息队列实现异步发送通知邮件
if (userRepository.save(user) != null && firstRegister == true) {
rabbitTemplate.convertAndSend(user);
}
return user;
}
// 删除用户
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
// 查找用户
public User findUser(Long id) {
return userRepository.findById(id).get();
}
// 根据名称查找用户
public List<User> searchUser(String name) {
return userRepository.findByName(name);
}
}
功能测试
Spring集成RabbitMQ实现异步消息处理小结
- 异步消息在要通信的应用程序之间提供了一个中间层,这样能够实现更松散的耦合和更强的可扩展性。利用消息队列的这种特性我们可以很方便地实现系统应用间的解耦:
- 用户登记成功后,向客户端返回登记成功的同时,只是向消息队列发送消息,并不等待邮件的发送事件的结果;
- 而消息队列接收者收到消息后,对消息进行解析,并根据解析中的邮件地址,发送通知邮件。
- Spring支持集成RabbitMQ实现异步消息,通过使用消息监听器注解@RabbitListener,消息也可以推送至消费者的bean方法中。