Topic Exchange交换机也叫通配符交换机,我们在发送消息到Topic Exchange的时候不能随意指定route key(应该是由一系列点号连接的字符串,一般会与binding key有关联,route key的长度一般不能超过255个字节)。同理,交换机与队列之间的binding key也应该是点号连接成的字符串,当消息发送者发送信息到Topic Exchage交换机的时候,这时候发送消息的route key会与binding key进行通配符匹配,所有匹配成功的消息都会发送到消息接受者。
Topic Exchange主要有两种通配符:# 和 *
下面我们根据一张图来理解一下Topic Exchange是怎么匹配的:
【a】一条以” com.register.mail”为routing key的消息将会匹配到Register Queue与SaveMail Queue两个队列上,所以消息会发送到消息接收者1和消息接收者2。routing key为“email.register.test”的消息同样会被推送到Register Queue与SaveMail Queue两个队列。
【b】如果routing key为”com.register.wsh”的话,消息只会被推送到Register Queue上;routing key为”email.com.wsh”的消息会被推送到SaveMail Queue上,routing key为"email.com.test”的消息也会被推送到SaveMail Queue上,但同一条消息只会被推送到SaveMail Queue上一次。
注意:如果在发送消息的时候没有匹配到符合条件的binding key,那么这条消息将会被废弃。如:com.register.wsh.test 消息不会被推送到Register Queue上,但是注意 email.com.wsh.test则可以推送到SaveMail Queue上。
基于上一章的学习,我们已经对RabbitMQ有了一定的认识,这一章有些概念就不做详细讲述。这里我们新建一个springboot工程:具体项目结构:
springboot_rabbitmq_topic_exchange : 端口1111
本章主要实现了会员注册之后,发送通知,然后消息接收者接收到消息之后进行保存会员操作以及发送注册成功邮件案例。
需要注意引入amqp的RabbitMQ依赖,具体pom.xml:
4.0.0
com.springboot.wsh
springboot_rabbitmq_topic_exchange
0.0.1-SNAPSHOT
jar
springboot_rabbitmq_topic_exchange
RabbitMQ Topic Exchange通配符交换机
org.springframework.boot
spring-boot-starter-parent
1.5.2.RELEASE
UTF-8
UTF-8
1.8
org.springframework.boot
spring-boot-starter-amqp
com.alibaba
fastjson
1.2.40
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
mysql
mysql-connector-java
runtime
org.springframework.boot
spring-boot-starter-data-jpa
commons-lang
commons-lang
2.6
javax.mail
mail
1.4.7
org.springframework.boot
spring-boot-maven-plugin
配置文件:配置RabbitMQ的一些消息以及数据库连接信息等
server:
port: 1111
spring:
application:
name: rabbitmq_topic_exchange
rabbitmq:
host: localhost
virtual-host: /
username: guest
password: guest
publisher-confirms: true
port: 5672
datasource:
username: root
password: wsh0905
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/rabbitmq?characterEncoding=utf8
jpa:
database: MySQL
show-sql: true
hibernate:
naming_strategy: org.hibernate.cfg.ImprovedNamingStrategy
这里我们采用Spring Data JPA方式操作数据库,我们先建一张member表,建表语句:
CREATE TABLE `member` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(20) DEFAULT NULL,
`password` varchar(20) DEFAULT NULL,
`email` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8
接着建立实体类Member:
/**
* @Title: Member
* @ProjectName springboot_rabbitmq_topic_exchange
* @Description: 会员实体类
* @Author WeiShiHuai
* @Date 2018/9/21 14:53
*/
@Entity
@Table(name = "member")
public class Member implements Serializable {
@Id
@Column(name = "id")
@GeneratedValue
private Long id;
/**
* 用户名
*/
@Column(name = "username")
private String username;
/**
* 密码
*/
@Column(name = "password")
private String password;
/**
* 邮箱
*/
@Column(name = "email")
private String email;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
MemberRepository: 注意是一个接口,继承了JpaRepository,默认实现了增删查改功能
public interface MemberRepository extends JpaRepository{
}
MemberController:
@RestController
public class MemberController {
@Autowired
private MemberService memberService;
@RequestMapping("/registerMember")
public void registerMember() throws Exception {
Member member = new Member();
member.setUsername("weixiaohuai");
member.setPassword("123456");
member.setEmail("[email protected]");
memberService.memberRegister(member);
}
}
MemberService:
@Service
public class MemberService {
@Autowired
private MemberRegisterSender memberRegisterSender;
public Long memberRegister(Member member) throws Exception {
//会员注册
memberRegisterSender.sendMessage(member);
return member.getId();
}
}
由于这些类相对简单一点,就不多说了。
这个类主要存放RabbitMQ 队列名称、交换机、以及两者之间的route key。
/**
* @Title: Constants
* @ProjectName springboot_rabbitmq_topic_exchange
* @Description: 常量类
* @Author WeiShiHuai
* @Date 2018/9/21 15:00
*/
public class Constants {
/**
* 消息队列-topic交换机名称
*/
public static final String MEMBER_TOPIC_EXCHANGE_NAME = "rabbit_mq_topic_exchange_name";
/**
* 消息队列-注册会员-队列名称
*/
public static final String MEMBER_REGISTER_QUEUE_NAME = "rabbit_mq_member_register_queue_name";
/**
* 消息队列-注册会员-队列路由键
*/
public static final String MEMBER_REGISTER_QUEUE_ROUTE_KEY = "register.*";
/**
* 消息队列-发送邮件-队列名称
*/
public static final String MEMBER_SEND_MAIL_QUEUE_NAME = "rabbit_mq_member_send_mail_queue_name";
/**
* 消息队列-发送邮件-队列路由键
*/
public static final String MEMBER_SEND_MAIL_QUEUE_ROUTE_KEY = "register.#";
/**
* 消息队列-topic交换机-路由key
*/
public static final String MEMBER_TOPIC_EXCHANGE_ROUTE_KEY = "register.member";
/**
* 邮件文本类型 - HTML
*/
public static final String SEND_MAIL_HTML_TYPE = "text/html;charset=UTF-8";
/**
* 邮件文本类型 - TEXT
*/
public static final String SEND_MAIL_TEXT_TYPE = "text";
}
这个类主要是创建了一个Topic Exchange通配符 交换机,一个注册会员队列以及与交换机的绑定操作、一个发送邮件队列以及与交换机进行绑定。并加入了日志记录各个对象实例的创建状态。
/**
* @Title: MemberRegistrtRabbitMQConfiguration
* @ProjectName springboot_rabbitmq_topic_exchange
* @Description: 会员注册-RabitMQ-相关配置类
* @Author WeiShiHuai
* @Date 2018/9/21 15:10
*/
@Configuration
public class MemberRegistrtRabbitMQConfiguration {
private static Logger logger = LoggerFactory.getLogger(MemberRegistrtRabbitMQConfiguration.class);
/**
* 创建通配符交换机实例
*
* @return 通配符交换机实例
*/
@Bean
public TopicExchange topicExchange() {
TopicExchange topicExchange = new TopicExchange(Constants.MEMBER_TOPIC_EXCHANGE_NAME);
logger.info("【【【会员注册通配符交换机实例创建成功】】】");
return topicExchange;
}
/**
* 创建会员注册队列实例,并持久化
*
* @return 会员注册队列实例
*/
@Bean
public Queue memberRegisterQueue() {
Queue memberRegisterQueue = new Queue(Constants.MEMBER_REGISTER_QUEUE_NAME, true);
logger.info("【【【会员注册队列实例创建成功】】】");
return memberRegisterQueue;
}
/**
* 创建会员发送邮件队列实例,并持久化
*
* @return 会员发送邮件队列实例
*/
@Bean
public Queue memberSendMailQueue() {
Queue memberRegisterQueue = new Queue(Constants.MEMBER_SEND_MAIL_QUEUE_NAME, true);
logger.info("【【【会员发送邮件队列实例创建成功】】】");
return memberRegisterQueue;
}
/**
* 绑定会员注册队列到交换机
*
* @return 绑定对象
*/
@Bean
public Binding memberRegisterBinding() {
Binding binding = BindingBuilder.bind(memberRegisterQueue()).to(topicExchange()).with(Constants.MEMBER_REGISTER_QUEUE_ROUTE_KEY);
logger.info("【【【会员注册队列与交换机绑定成功】】】");
return binding;
}
/**
* 绑定会员发送邮件队列到交换机
*
* @return 绑定对象
*/
@Bean
public Binding memberSendMailBinding() {
Binding binding = BindingBuilder.bind(memberSendMailQueue()).to(topicExchange()).with(Constants.MEMBER_SEND_MAIL_QUEUE_ROUTE_KEY);
logger.info("【【【会员发送邮件队列与交换机绑定成功】】】");
return binding;
}
}
主要是注入了RabbitTemplate,使用RabbitTemplate提供的convertAndSend方法进行消息的发送,传入定义好的交换机、route-key以及发送的消息对象。
/**
* @Title: MemberRegisterSender
* @ProjectName springboot_rabbitmq_topic_exchange
* @Description: 用户注册消息发送者
* @Author WeiShiHuai
* @Date 2018/9/21 15:33
*/
@Component
public class MemberRegisterSender {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送会员注册通知消息
*
* @param message 消息内容
*/
public void sendMessage(Member message) throws Exception {
rabbitTemplate.convertAndSend(Constants.MEMBER_TOPIC_EXCHANGE_NAME, Constants.MEMBER_TOPIC_EXCHANGE_ROUTE_KEY, message);
}
}
/**
* @Title: MemberRegisterReceiver
* @ProjectName springboot_rabbitmq_topic_exchange
* @Description: 用户注册消息接收者
* @Author WeiShiHuai
* @Date 2018/9/21 15:47
*/
@Component
@RabbitListener(queues = "rabbit_mq_member_register_queue_name")
public class MemberRegisterReceiver {
@Autowired
private MemberRepository memberRepository;
private static Logger logger = LoggerFactory.getLogger(MemberRegisterReceiver.class);
@RabbitHandler
@Transactional
public void handler(Member member) throws Exception {
logger.info("会员用户名: {}, 注册成功, 准备创建会员信息...", member.getUsername());
//保存会员消息
memberRepository.save(member);
}
}
可以看到,我们使用@RabbitListener监听了队列名称为rabbit_mq_member_register_queue_name的队列,这个名称需要对应Constants.MEMBER_REGISTER_QUEUE_NAME,否则消息将不会成功接收。 我们使用@RabbitHandler注解进行消息的处理,注入了MemberRepository进行保存会员操作。
/**
* @Title: MemberRegisterReceiver
* @ProjectName springboot_rabbitmq_topic_exchange
* @Description: 发送邮件消息接收者
* @Author WeiShiHuai
* @Date 2018/9/21 15:47
*/
@Component
@RabbitListener(queues = "rabbit_mq_member_send_mail_queue_name")
public class MemberSendMailReceiver {
private static Logger logger = LoggerFactory.getLogger(MemberSendMailReceiver.class);
@Transactional
@RabbitHandler
public void sendMail(Member member) throws Exception {
logger.info("会员用户名:{},注册成功,准备发送邮件...", member.getUsername());
//执行发送邮件操作
new EMailSender()
.setTitle("会员注册成功通知邮件")
.setContent("恭喜你,你已注册成为我们的会员")
.setContentType(Constants.SEND_MAIL_TEXT_TYPE)
.setSendMailTargets(new ArrayList() {{
add("[email protected]");
}}).send();
}
}
同理注意监听的队列名称需要与创建队列时的名称一致等。从上图看到,我们调用了发送邮件的方法,这里采用Java Mail实现发送邮件的简单功能,主要是演示RabbitMQ, 点到为止即可。下面直接贴一下相关代码:
【a】邮件发送者对象MailEntity.java
/**
* @Title: MailEntity
* @ProjectName springboot_rabbitmq_topic_exchange
* @Description: 发送邮件时需要的参数字段
* @Author WeiShiHuai
* @Date 2018/9/21 22:53
*/
public class MailEntity implements Serializable {
/**
* SMTP服务器
*/
private String smtpService;
/**
* 端口号
*/
private String smtpPort;
/**
* 发送邮箱
*/
private String fromMailAddress;
/**
* 发送邮箱的STMP口令
*/
private String fromMailStmpPwd;
/**
* 邮件标题
*/
private String title;
/**
* 邮件内容
*/
private String content;
/**
* 内容格式(默认采用html)
*/
private String contentType;
/**
* 接受邮件地址集合
*/
private List list = new ArrayList<>();
public String getSmtpService() {
return smtpService;
}
public void setSmtpService(String smtpService) {
this.smtpService = smtpService;
}
public String getSmtpPort() {
return smtpPort;
}
public void setSmtpPort(String smtpPort) {
this.smtpPort = smtpPort;
}
public String getFromMailAddress() {
return fromMailAddress;
}
public void setFromMailAddress(String fromMailAddress) {
this.fromMailAddress = fromMailAddress;
}
public String getFromMailStmpPwd() {
return fromMailStmpPwd;
}
public void setFromMailStmpPwd(String fromMailStmpPwd) {
this.fromMailStmpPwd = fromMailStmpPwd;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getContentType() {
return contentType;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public List getList() {
return list;
}
public void setList(List list) {
this.list = list;
}
}
【b】邮件配置文件以及读取配置文件工具类
mail.properties:位于resources目录下,主要配置授权码以及邮件发送方地址,显示昵称等信息
send.mail.smtp.service=smtp.qq.com
send.mail.smtp.prot=587
#邮件发送方地址
[email protected]
#邮件发送方授权码
send.mail.from.smtp.pwd=对应你qq邮箱的授权码
#邮件发送的时候显示的昵称
send.mail.from.nickname=weixiaohuai
PropertiesUtils:
/**
* @Title: PropertiesUtils
* @ProjectName springboot_rabbitmq_topic_exchange
* @Description: 工具类
* @Author WeiShiHuai
* @Date 2018/9/21 22:54
*/
public class PropertiesUtils {
private final ResourceBundle resource;
private final String fileName;
/**
* 构造函数实例化部分对象,获取文件资源对象
*
* @param fileName
*/
public PropertiesUtils(String fileName) {
this.fileName = fileName;
Locale locale = new Locale("zh", "CN");
this.resource = ResourceBundle.getBundle(this.fileName, locale);
}
/**
* 根据传入的key获取对象的值
*
* @param key properties文件对应的key
* @return String 解析后的对应key的值
*/
public String getValue(String key) {
return this.resource.getString(key);
}
/**
* 获取properties文件内的所有key值
*
* @return
*/
public Enumeration getKeys() {
return resource.getKeys();
}
}
【c】邮件发送逻辑EmailSender.java
package com.springboot.wsh.mail;
import com.springboot.wsh.constants.Constants;
import com.springboot.wsh.entity.MailEntity;
import com.springboot.wsh.utils.PropertiesUtils;
import org.apache.commons.lang.StringUtils;
import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;
import java.util.List;
import java.util.Properties;
/**
* @Title: EMailSender
* @ProjectName springboot_rabbitmq_topic_exchange
* @Description: 邮件发送者实体,主要作用就是用来配置发送邮件参数以及执行发送邮件操作
* @Author WeiShiHuai
* @Date 2018/9/21 22:27
*/
public class EMailSender {
/**
* 邮件实体
*/
private static MailEntity mail = new MailEntity();
/**
* 设置邮件标题
*
* @param title 标题信息
* @return
*/
public EMailSender setTitle(String title) {
mail.setTitle(title);
return this;
}
/**
* 设置邮件内容
*
* @param content
* @return
*/
public EMailSender setContent(String content) {
mail.setContent(content);
return this;
}
/**
* 设置邮件格式
*
* @param typeEnum
* @return
*/
public EMailSender setContentType(String typeEnum) {
mail.setContentType(typeEnum);
return this;
}
/**
* 设置请求目标邮件地址
*
* @param targets
* @return
*/
public EMailSender setSendMailTargets(List targets) {
mail.setList(targets);
return this;
}
/**
* 执行发送邮件
*
* @throws Exception 如果发送失败会抛出异常信息
*/
public void send() throws Exception {
//校验发送邮件对象参数是否设置
this.checkMailParams(mail);
//读取/resource/mail.properties文件内容
final PropertiesUtils properties = new PropertiesUtils("mail");
// 创建Properties 类用于记录邮箱的一些属性
final Properties props = new Properties();
// 表示SMTP发送邮件,进行身份验证
props.put("mail.smtp.auth", "true");
//此处填写SMTP服务器
props.put("mail.smtp.host", properties.getValue("send.mail.smtp.service"));
//设置端口号,QQ邮箱两个端口465/587
props.put("mail.smtp.port", properties.getValue("send.mail.smtp.prot"));
// 设置发送邮箱
props.put("mail.user", properties.getValue("send.mail.from.address"));
// 设置授权码
props.put("mail.password", properties.getValue("send.mail.from.smtp.pwd"));
// 构建授权信息,用于进行SMTP进行身份验证
Authenticator authenticator = new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
// 用户名、密码
String userName = props.getProperty("mail.user");
String password = props.getProperty("mail.password");
return new PasswordAuthentication(userName, password);
}
};
// 使用环境属性和授权信息,创建邮件会话
Session mailSession = Session.getInstance(props, authenticator);
// 创建邮件消息
MimeMessage message = new MimeMessage(mailSession);
// 设置发件人
String nickName = MimeUtility.encodeText(properties.getValue("send.mail.from.nickname"));
InternetAddress form = new InternetAddress(nickName + " <" + props.getProperty("mail.user") + ">");
message.setFrom(form);
// 设置邮件标题
message.setSubject(mail.getTitle());
//html发送邮件
if (mail.getContentType().equals(Constants.SEND_MAIL_HTML_TYPE)) {
// 设置邮件的内容体 默认使用html方式发送
message.setContent(mail.getContent(), StringUtils.isBlank(mail.getContentType()) ? Constants.SEND_MAIL_HTML_TYPE : mail.getContentType());
} else if (mail.getContentType().equals(Constants.SEND_MAIL_TEXT_TYPE)) {
// Text文本方式发送
message.setText(mail.getContent());
}
//发送邮箱地址
List targets = mail.getList();
for (String target : targets) {
try {
// 设置收件人的邮箱
InternetAddress to = new InternetAddress(target);
message.setRecipient(Message.RecipientType.TO, to);
// 发送邮件
Transport.send(message);
} catch (Exception e) {
continue;
}
}
}
/**
* 校验发送邮件的一些参数是否设置
*
* @param mail 邮件发送对象
* @throws Exception
*/
private void checkMailParams(MailEntity mail) throws Exception {
if (StringUtils.isBlank(mail.getTitle())) {
throw new Exception("抱歉,邮件标题不能为空,请先设置邮件标题!");
}
if (StringUtils.isBlank(mail.getContent())) {
throw new Exception("抱歉,邮件内容不能为空,请先设置邮件内容!");
}
if (mail.getList().size() == 0) {
throw new Exception("抱歉,邮件接收方不能为空,请先设置邮件接收目标对象!");
}
}
}
【d】登录邮件发送方qq邮件,对应你配置文件上写的邮箱地址,开启POP3/SMTP服务:
【e】生成授权码
这样邮件相关的设置就已经完成了,下面我们进行测试一下。
可以看到我们RabbitMQ已经启动成功并且交换机、两个队列也成功绑定,访问http://localhost:1111/registerMember,因为我们再两个队列分别进行了保存数据库操作以及发送邮件操作,
可以看到,消息接受者已经成功接收到消息发送者的消息,并且进行了相应的处理。现在查询数据库中是否有对应的会员信息:
下面我们登录邮件接收方[email protected],
可以看到成功接收到了邮件发送方发送的注册成功通知邮件。
Topic Exchange是一个通过通配符匹配的交换机模式,如果route key与绑定的key不一致的话,消息会被丢弃,这样消息接收者也不能接收到发送者发送的消息。本文是在作者我学习Topic Exchange的时候的一些总结以及实践,能力有限,仅供大家参考学习,共同学习,共同进步!