在当前微服务火爆的情况下,消息服务中间件的使用,是我们开发人员必须掌握的一项技能,在大多的应用中,我们通过消息服务来提高系统间的异步通信以及扩展和解耦能力,在当前的市场下,消息服务主要有两种规范:JMS和AMQP
关于JMS和AMQP的异同,可以参照下图:
在本篇博文中,我们将着重来学习RabbitMQ与SpringBoot的整合使用.
核心概念:
Message : 消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出
该消息可能需要持久性存储)等.
Publisher : 消息的生产者,也是一个向交换器发布消息的客户端程序.
Exchange : 交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列.根据消息中不同的路由键来分发给不同的队列.
他有四种类型:direct(默认)、fanout、topic、headers,不同类型的Exchange转发消息的策略不同.
Binding : 绑定,用于消息队列和交换器之间的关联,一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,也可以将交换器理解为由绑定构成的路由表.Exchange和Binding可以是多对多的关系.
Connection : 网络连接,如TCP连接
Channel : 信道,是一条独立的双向数据流通道,信道是建立在真实的TCP连接内的虚拟连接,AMQP的命令都是通过信道发出去的,它的存在是为了复用一条TCP连接,这样就不用建立很多的TCP连接.
Consumer : 消息的消费者,表示一个从消息队列中取得消息的客户端程序.
Virtual Host : 虚拟主机,表示一批交换器、消息队列和相关的对象,虚拟主机是共享相同的身份认证和加密环境的独立服务器域.每个vhost本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列,交换器,绑定和权限机制.vhost是AMQP概念的基础,默认是/
.
Broker : 表示消息队列服务器实体
具体的图解如下:
AMQP中因为加入了Exchange和Binding这2个角色,所以它的机制和JMS有所区别.区别的主要体现在Exchange上.
Exchange主要有四种类型: direct、fanout、topic、headers,其中headers现在基本用不到,所以我们来看看其他的类型:
dircect
消息中的路由键如果和Binding中的binding的key一致,交换器就将消息发送到对应的队列中.路由键和队列名完全匹配,比如,如果一个队列绑定到交换器时要求路由键为dog
,则只会转发routing key标记为dog
的消息,不会转发dog.puppy
或者其他的,它是完全匹配、单播的模式.
fanout
每个发到fanout类型交换器的消息都会分发到所有绑定的队列上去,类似子网广播,它转发消息的速度是最快的.
Topic
topic交换器通过模式匹配来分配消息,它将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上.它将路由键和绑定建的字符串切分成单词,这些单词之间用.
隔开,它同样也会识别两个通配符:符号#
和符号*
,#
匹配0个或者多个单词,*
匹配一个单词.
要使用RabbitMQ,首先要在服务器上安装,这里我们使用docker来安装,docker的使用在我的系列博文中都有较少,需要的小伙伴可以去参考一下,首先我们下载docker:
#1.使用命令下载RabbitMQ,这里因为国外的下载速度比较慢,所以使用到了镜像加速,使用镜像加速的时候,只需要在下载命令前加上registry.docker-cn.com/library/即可,例如:pull registry.docker-cn.com/library/ubuntu:16.04
[root@localhost ~]# docker pull registry.docker-cn.com/library/rabbitmq:3-management
# 带management的版本是有web页面来查看的
#2.下载完成后,运行镜像,需要注意的是,由于我们下载的是有web页面管理的版本,因此需要映射2个端口号,命令如下:
[root@localhost ~]# docker run -d -p 5672:5672 -p 15672:15672 --name myrabbitmq 镜像id
#5672是rabitmq服务的端口号,15672是web端的端口号,现在我们可以通过 主机IP:15672/ 来访问rabbitmq的管理页面了,默认的账号和密码都是guest
登录WEB控制台的页面如下:
在WEB端,我们可以选择Exchange
来创建一个新的Exchange或者Queue:
在这个页面:Name
代表创建的Exchange的名称;Type
可以选择exchange的四种类型;Durabilty
表示是否持久化,如果选择是,则rabbitmq重启以后,该Exchange依然存在.点击Add exchange
则添加一个新的Exchange.
在Queue的Add页面,也可以创建一个队列使用.
新建了exchange和queue以后,我们可以在exchange的页面上,点击刚才新增的exchange来给它绑定队列:
接下来我们就可以在WEB页面上直接发送消息来测试他们对应的模式:
点击Publish message
以后,就表示发送消息,我们可以在queue
界面查看获取到的消息.
我们先创建一个SpringBoot项目,并且引入RabbitMQ的依赖和Web依赖:
我们可以看到在pom文件中SpringBoot帮我们引入了相关依赖,下面来看一下它帮我们配置了什么,根据前面对SpringBoot的了解,它会帮我们自动配置一个叫RabbitAutoConfiguration
的类,我们看看它帮我们做了什么:
@Configuration
@ConditionalOnClass({RabbitTemplate.class, Channel.class})
@EnableConfigurationProperties({RabbitProperties.class})
@Import({RabbitAnnotationDrivenConfiguration.class})
public class RabbitAutoConfiguration {
public RabbitAutoConfiguration() {
}
@Configuration
@ConditionalOnClass({RabbitMessagingTemplate.class})
@ConditionalOnMissingBean({RabbitMessagingTemplate.class})
@Import({RabbitAutoConfiguration.RabbitTemplateConfiguration.class})
protected static class MessagingTemplateConfiguration {
protected MessagingTemplateConfiguration() {
}
@Bean
@ConditionalOnSingleCandidate(RabbitTemplate.class)
public RabbitMessagingTemplate rabbitMessagingTemplate(RabbitTemplate rabbitTemplate) {
return new RabbitMessagingTemplate(rabbitTemplate);
}
}
@Configuration
@Import({RabbitAutoConfiguration.RabbitConnectionFactoryCreator.class})
protected static class RabbitTemplateConfiguration {
private final RabbitProperties properties;
private final ObjectProvider<MessageConverter> messageConverter;
private final ObjectProvider<RabbitRetryTemplateCustomizer> retryTemplateCustomizers;
public RabbitTemplateConfiguration(RabbitProperties properties, ObjectProvider<MessageConverter> messageConverter, ObjectProvider<RabbitRetryTemplateCustomizer> retryTemplateCustomizers) {
this.properties = properties;
this.messageConverter = messageConverter;
this.retryTemplateCustomizers = retryTemplateCustomizers;
}
@Bean
@ConditionalOnSingleCandidate(ConnectionFactory.class)
@ConditionalOnMissingBean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
PropertyMapper map = PropertyMapper.get();
RabbitTemplate template = new RabbitTemplate(connectionFactory);
MessageConverter messageConverter = (MessageConverter)this.messageConverter.getIfUnique();
if (messageConverter != null) {
template.setMessageConverter(messageConverter);
}
template.setMandatory(this.determineMandatoryFlag());
Template properties = this.properties.getTemplate();
if (properties.getRetry().isEnabled()) {
template.setRetryTemplate((new RetryTemplateFactory((List)this.retryTemplateCustomizers.orderedStream().collect(Collectors.toList()))).createRetryTemplate(properties.getRetry(), Target.SENDER));
}
properties.getClass();
map.from(properties::getReceiveTimeout).whenNonNull().as(Duration::toMillis).to(template::setReceiveTimeout);
properties.getClass();
map.from(properties::getReplyTimeout).whenNonNull().as(Duration::toMillis).to(template::setReplyTimeout);
properties.getClass();
map.from(properties::getExchange).to(template::setExchange);
properties.getClass();
map.from(properties::getRoutingKey).to(template::setRoutingKey);
properties.getClass();
map.from(properties::getQueue).whenNonNull().to(template::setQueue);
return template;
}
private boolean determineMandatoryFlag() {
Boolean mandatory = this.properties.getTemplate().getMandatory();
return mandatory != null ? mandatory : this.properties.isPublisherReturns();
}
@Bean
@ConditionalOnSingleCandidate(ConnectionFactory.class)
@ConditionalOnProperty(
prefix = "spring.rabbitmq",
name = {"dynamic"},
matchIfMissing = true
)
@ConditionalOnMissingBean
public AmqpAdmin amqpAdmin(ConnectionFactory connectionFactory) {
return new RabbitAdmin(connectionFactory);
}
}
@Configuration
@ConditionalOnMissingBean({ConnectionFactory.class})
protected static class RabbitConnectionFactoryCreator {
protected RabbitConnectionFactoryCreator() {
}
@Bean
public CachingConnectionFactory rabbitConnectionFactory(RabbitProperties properties, ObjectProvider<ConnectionNameStrategy> connectionNameStrategy) throws Exception {
PropertyMapper map = PropertyMapper.get();
CachingConnectionFactory factory = new CachingConnectionFactory((com.rabbitmq.client.ConnectionFactory)this.getRabbitConnectionFactoryBean(properties).getObject());
properties.getClass();
map.from(properties::determineAddresses).to(factory::setAddresses);
properties.getClass();
map.from(properties::isPublisherConfirms).to(factory::setPublisherConfirms);
properties.getClass();
map.from(properties::isPublisherReturns).to(factory::setPublisherReturns);
org.springframework.boot.autoconfigure.amqp.RabbitProperties.Cache.Channel channel = properties.getCache().getChannel();
channel.getClass();
map.from(channel::getSize).whenNonNull().to(factory::setChannelCacheSize);
channel.getClass();
map.from(channel::getCheckoutTimeout).whenNonNull().as(Duration::toMillis).to(factory::setChannelCheckoutTimeout);
Connection connection = properties.getCache().getConnection();
connection.getClass();
map.from(connection::getMode).whenNonNull().to(factory::setCacheMode);
connection.getClass();
map.from(connection::getSize).whenNonNull().to(factory::setConnectionCacheSize);
connectionNameStrategy.getClass();
map.from(connectionNameStrategy::getIfUnique).whenNonNull().to(factory::setConnectionNameStrategy);
return factory;
}
private RabbitConnectionFactoryBean getRabbitConnectionFactoryBean(RabbitProperties properties) throws Exception {
PropertyMapper map = PropertyMapper.get();
RabbitConnectionFactoryBean factory = new RabbitConnectionFactoryBean();
properties.getClass();
map.from(properties::determineHost).whenNonNull().to(factory::setHost);
properties.getClass();
map.from(properties::determinePort).to(factory::setPort);
properties.getClass();
map.from(properties::determineUsername).whenNonNull().to(factory::setUsername);
properties.getClass();
map.from(properties::determinePassword).whenNonNull().to(factory::setPassword);
properties.getClass();
map.from(properties::determineVirtualHost).whenNonNull().to(factory::setVirtualHost);
properties.getClass();
map.from(properties::getRequestedHeartbeat).whenNonNull().asInt(Duration::getSeconds).to(factory::setRequestedHeartbeat);
Ssl ssl = properties.getSsl();
if (ssl.isEnabled()) {
factory.setUseSSL(true);
ssl.getClass();
map.from(ssl::getAlgorithm).whenNonNull().to(factory::setSslAlgorithm);
ssl.getClass();
map.from(ssl::getKeyStoreType).to(factory::setKeyStoreType);
ssl.getClass();
map.from(ssl::getKeyStore).to(factory::setKeyStore);
ssl.getClass();
map.from(ssl::getKeyStorePassword).to(factory::setKeyStorePassphrase);
ssl.getClass();
map.from(ssl::getTrustStoreType).to(factory::setTrustStoreType);
ssl.getClass();
map.from(ssl::getTrustStore).to(factory::setTrustStore);
ssl.getClass();
map.from(ssl::getTrustStorePassword).to(factory::setTrustStorePassphrase);
ssl.getClass();
map.from(ssl::isValidateServerCertificate).to((validate) -> {
factory.setSkipServerCertificateValidation(!validate);
});
ssl.getClass();
map.from(ssl::getVerifyHostname).to(factory::setEnableHostnameVerification);
}
properties.getClass();
map.from(properties::getConnectionTimeout).whenNonNull().asInt(Duration::toMillis).to(factory::setConnectionTimeout);
factory.afterPropertiesSet();
return factory;
}
}
}
我们可以看到,这个autoConfiguration帮我们配置了以下组件:
自动配置了连接工厂ConnectionFactory
RabbitProperties 封装了RabbitMQ的配置,我们可以使用spring.rabbitmq.xxx
来指定属性,可以指定的属性如下:
private String host = "localhost";
private int port = 5672;
private String username = "guest";
private String password = "guest";
private final RabbitProperties.Ssl ssl = new RabbitProperties.Ssl();
private String virtualHost;
private String addresses;
@DurationUnit(ChronoUnit.SECONDS)
private Duration requestedHeartbeat;
private boolean publisherConfirms;
private boolean publisherReturns;
private Duration connectionTimeout;
private final RabbitProperties.Cache cache = new RabbitProperties.Cache();
private final RabbitProperties.Listener listener = new RabbitProperties.Listener();
private final RabbitProperties.Template template = new RabbitProperties.Template();
private List<RabbitProperties.Address> parsedAddresses;
RabbitTemplate : 给RabbitMQ发送和接收消息
AmqpAdmin : RabbitMQ系统管理组件,可以用它来创建和管理队列或者交换器
了解了以上的组件以后,我们现在就可以来测试一下:
首先我们在WEB管理页面创建2个路由器和2个队列
这两个交换器,一个是direct类型的,一个是fanout类型的,我们分别将2个队列绑定在交换器上,绑定规则如下:
现在我们可以在SpringBoot中发送和接收消息了:
首先我们在配置文件中指定rabbitmq的服务器ip:spring.rabbitmq.host=服务器IP
账号和密码在SpringBoot2.0以后都有了默认值guest
,如果没有修改的话可以不配置
测试代码如下:
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootAmqpApplicationTests {
/**
*我们可以直接在这里注入rabbitTemplat来操作mq
*在SpringBoot的自动配置中已经帮我们配置好了
*/
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void contextLoads() {
//Message需要自己构造一个;定义消息体内容和消息头
//rabbitTemplate.send(exchage,routeKey,message);
//object默认当成消息体,只需要传入要发送的对象,自动序列化发送给rabbitmq;
//rabbitTemplate.convertAndSend(exchage,routeKey,object);
Map<String, Object> msg = new HashMap<>();
msg.put("msg", "rabbitTemplate发送的消息");
msg.put("date", "hello word!");
rabbitTemplate.convertAndSend("xiaojian.direct",
"rabbitmq.direct", msg);
}
//从队列中接收消息
@Test
public void getMsg() {
Map<String, Object> o = (Map<String, Object>) rabbitTemplate.receiveAndConvert("xiaojian.direct");
System.out.println(o.getClass());
System.out.println(o);
}
}
发送JSON格式的数据
我们如果用WEB浏览器查看发送的消息,可以看到rabbitmq使用自己默认的序列化帮我们发送数据,可视化很差:
要发送JSON格式的数据,需要我们自定义消息转换器MessageConverter
,我们可以创建一个配置类:
@Configuration
public class MyAMQPConfig {
@Bean
public MessageConverter messageConverter(){
//这里使用的Jackson2JsonMessageConverter是SpringBoot中已经实现的转换器,我们直接new出来即可
return new Jackson2JsonMessageConverter();
}
}
现在我们查看Web端就可以看到数据被序列化为json发送了:
使用fanout发送时,获取的方式也是一样的.
在实际的项目中,我们一般会写一个监听器来监听对应队列中的消息,一旦收到以后运行某段业务逻辑.SpringBoot为我们简化了监听器的使用,只要使用简单的注解即可实现监听消息:
在主运行类中增加注解@EnbaleRabbit
@SpringBootApplication
@EnableRabbit//开启rabbit注解支持
public class SpringbootAmqpApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootAmqpApplication.class, args);
}
}
创建对应的服务类来监听消息:
@Service
public class RabbirListener {
/**
* 使用rabbitMQ包提供的Message来接收消息的头和体,以便于定制化的需求
* @param message
*/
@RabbitListener(queues = {"xiaojian.direct"})
public void getMsg(Message message) {
System.out.println(message.getMessageProperties());
System.out.println(message.getBody());
}
/**
* 如果发送的消息是具体的javaBean,可以直接在方法的参数中来接收
* @param user
*/
@RabbitListener(queues = {"xiaojian.fanout"})
public void getMes(User user) {
System.out.println(user);
}
}
这样在程序运行的时候,只要我们的队列接收到了消息,我们就可以使用监听器来触发函数处理.
AmqpAdmin在SpringBoot中帮助我们创建和删除Exchange
、Queue
以及Binding
.
/**
* 注入amqpAdmin的管理组件
*/
@Autowired
private AmqpAdmin amqpAdmin;
@Test
public void create() {
//创建Exchange
amqpAdmin.declareExchange(new DirectExchange("admin.direct"));
//创建Queue
amqpAdmin.declareQueue(new Queue("admin.queue"));
//设置Binding
//Binding的构造参数:
//destination:目的地,表示要绑定到的队列名
//destinationType:目的地的类型,有QUEUE和EXCHANGE两种
//exchange: 要绑定的交换器的name
//routingKey: 要匹配的路右键
//arguments: 参数,可以为空
amqpAdmin.declareBinding(new Binding("admin.queue", Binding.DestinationType.QUEUE,
"admin.direct", "admin.helloword", null));
}
我们查看WEB页面对应的Exchange,可以看到Binding已经存在:
至此,SpringBoot整合RabbitMQ的使用基本上结束了,还有一些细节和需要注意的地方,在使用的时候多留心即可.