Spring Boot与消息视频
(1)大多数应用中,可通过消息服务中间件来提升系统异步通信、扩展解耦能力;
(2)消息服务中两个重要概念:消息代理(message broker)和目的地(destination);
当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地。
(3)消息队列主要有两种形式的目的地
队列(queue):点对点消息通信;
受
者,但并不是说只能有一个接收
者;主题(topic):发布(publish)/订阅(subscribe)消息通信;
(4) JMS(Java Message Service) Java消息服务; 基于JVM消息代理的规范。 ActiveMQ是JMS实现。
(5)AMQP(Advanced Message Queuing Protocol):高级消息队列协议,也是一个消息代理的规范,兼容JMS。RabbitMQ是AMQP的实现。
(6)Spring支持
Spring-jms提供了对JMS的支持;
Spring-rabbit提供了AMQP的支持;
需要ConnectionFactory的实现来连接消息代理;
提供JmsTemplate、RabbitTemplate模板类来发送消息;
@JmsListener(JMS),@RabbitListener(AMQP)注解在方法上监听消息代理发布的消息;
@EnableJms,@EnableRabbit开启支持;
(7)SpringBoot自动配置
JmsAutoConfiguration
RabbitAutoConfiguration
RabbitMQ是一个由erlang语言开发的AMQP(Advanced Message Queue Protocol)的开源实现。
核心概念: Message,Publisher,Exchange,…
消息,由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成, 这些属性包括路由键(routing-key),相对其他消息的优先权(priority),指出该消息可能需要持久性存储)等;
消息的生产者,也是一个向交换器发布消息的客户端应用程序。
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
Exchange有4种类型: direct(默认),fanout,topic和headers;不同类型的Exchange转发消息的策略有所区别。
消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列;消息一直在队列里面,等待消费者连接到这个队列将其消费(取出)。
绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则, 所以可以将交换器理解成一个由绑定构成的路由表。
网络连接, 比如一个TCP连接。
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁TCP都是非常昂贵的开销,所以引入了信道的概念, 以复用一条TCP连接。
消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个vhost本质上是一个mini版的RabbitMQ服务器,拥有自己的队列、交换器、绑定和权限机制。vhost是AMQP概念的基础,必须在连接时指定,RabbitMQ默认的vhost是/
。
AMQP中的消息路由
AMQP中消息的路由过程和Java开发属性的JMS存在一些差异,AMQP中增加了Exchange和Binding的角色。生产者把消息发布到Exchange上,消息最终到达队列并被消费者接收,而根据Binding的routing key决定交换器的消息应该发送到哪个队列上。
RabbitMQ中,Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型: direct、fanout、topic、headers。headers匹配AMQP消息的header而不是路由键,headers交换器和direct交换器完全一致,但性能差很多,目前几乎用不到,所以直接看另外三种类型:
典型的点对点发送模型。消息中的路由键(routing key)如果和Binding中的binding key一致,交换器就将消息发送到对于的队列中。路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为dog
,则只会转发routing key标记为dog
的消息,不会转发dog.puppy
,也不会转发dog.guard
等等。它是完全匹配、单播的模式。
广播模式,每个发到fanout类型交换器的消息都会分到所有绑定的队列上去。fanout交换器不处理路由键(routing key),只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息,fanout类型转发消息是最快的。
topic交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式(固定的单词或通配符)进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符,符合#
和符号*
。 #
匹配0个或多个单词, *
匹配一个单词。
安装rabbitMQ, 参考 centos7安装rabbitmq教程1, centos7安装rabbitmq教程2
通过http://192.168.111.129:15672/
打开RabbitMQ控制台。
点击Exchanges标签页,进行交换器的配置, 新增exchange.direct、exchange.fanout、exchange.topic交换器。durability=Durable表示持久化。
点击Queues标签页,进行队列的配置, 新增atguigu、atguigu.news、atguigu.emps、gulixueyuan.news队列。
对exchange.direct交换器进行队列绑定, 绑定了atguigu、atguigu.news、atguigu.emps、gulixueyuan.news队列。
对exchange.fanout交换器进行队列绑定, 绑定了atguigu、atguigu.news、atguigu.emps、gulixueyuan.news队列。
对exchange.topic交换器进行队列绑定, 绑定了atguigu、atguigu.news、atguigu.emps、gulixueyuan.news队列。routing key使用了通配符进行匹配。
使用点对点交换机exchange.direct发送消息, routing key=atguigu。
direct是点对点模式,只有atguigu队列可以接收到exchange.direct交换器发送的消息。
读取消息
使用exchange.fanout交换器发送消息, routing key=atguigu.news
atguigu、atguigu.news、atguigu.emps、gulixueyuan.news队列都会接收到exchange.fanout交换器发送的消息,exchange.fanout发送消息的匹配规则与发送消息的路由键没有关系,是广播模式。
使用exchange.topic交换器发送消息, routing key=atguigu.news。
因为exchange.topic绑定了atguigu.# 和 *.news的路由匹配规则,所以atguigu、atguigu.news、atguigu.emps、gulixueyuan.news队列都会接收到exchange.topic交换器通过atguigu.news发送的消息。
读取消息
使用exchange.topic交换器发送消息, routing key=hello.news。根据*.news的匹配规则, atguigu.news、gulixueyuan.news队列会接收到exchange.topic交换器通过hello.news发送的消息。
读取消息
引入 spring-boot-starter-amqp
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>1.5.10.RELEASEversion>
<relativePath/>
parent>
<groupId>cn.cryswgroupId>
<artifactId>springboot02-amqpartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>springboot02-amqpname>
<description>springboot02-amqpdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
application.properties配置
# rabbitmq config
spring.rabbitmq.host=192.168.111.129
spring.rabbitmq.username=root
spring.rabbitmq.password=123456
spring.rabbitmq.port=5672
spring.rabbitmq.connection-timeout=30000
#spring.rabbitmq.virtual-host=/
# debug
#debug=true
@RunWith(SpringRunner.class)
@SpringBootTest
public class AmqpApplicationTests {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void test() {
String exchange = "exchange.direct";
String routingKey = "atguigu.news";
// Message需要自己构造一个;定义消息体内容和消息头
// rabbitTemplate.send("", "", message);
Map<String, Object> map = new HashMap<>();
map.put("msg", "这是第一个点对点的消息");
map.put("data", Arrays.asList("hello rabbit world", "123"));
// 只需要传入要发送的对象,自动转成Message对象发送给rabbitmq; 对象被默认序列化
// rabbitTemplate.convertAndSend(exchange, routingKey, map);
rabbitTemplate.convertAndSend(exchange, routingKey,
Book.builder().bookName("西游记").author("吴承恩").build());
}
/**
* 接收消息
*/
@Test
public void testReadMsg() {
Object msg = rabbitTemplate.receiveAndConvert("atguigu.news");
System.out.println("接收消息类型:" + (msg.getClass()));
System.out.println("接收消息:" + msg);
}
}
测试发现读取的消息是乱码, 因为RabbitTemplate模板中默认使用的是SimpleMessageConverter消息转换器进行序列化的。
接收消息类型:class [B
接收消息:[B@333c8791
可以自定义消息转换器来覆盖默认的配置
/**
* 自定义AMQP配置
*
* @Author crysw
* @Version 1.0
* @Date 2023/7/17 22:38
*/
@Configuration
public class MyAmqpConfig {
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
再次测试ok
rabbitConnectionFactory#65e21ce3:0/SimpleConnection@2cae9b8 [delegate=amqp://[email protected]:5672/, localPort= 9067]
接收消息类型:class cn.crysw.bean.Book
接收消息:Book(bookName=西游记, author=吴承恩)
@RunWith(SpringRunner.class)
@SpringBootTest
public class AmqpApplicationTests {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 广播模式,不用指定routingKey,所有绑定的队列都会收到消息
*/
@Test
public void testExchangeFanout() {
String routingKey="";
rabbitTemplate.convertAndSend("exchange.fanout", routingKey,
Book.builder().bookName("三国演义").author("罗贯中").build());
}
}
其他模式的也都有相应的api支持。
HeadersExchange,TopicExchange,CustomExchange,DirectExchange,FanoutExchange
@EnableRabbit + @RabbitListener监听消息队列中的消息。
在启动类上开启rabbitmq监听配置开关。
@SpringBootApplication
@EnableRabbit // 开启rabbitmq配置
public class Springboot02AmqpApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot02AmqpApplication.class, args);
}
}
在方法上添加@RabbitListener声明要监听的队列,就能实现自动监听并接收队列消息。
@Service
public class BookService {
@RabbitListener(queues = {"atguigu.news"})
public void receive(Book book) {
System.out.println("收到消息:" + book);
}
@RabbitListener(queues = {"atguigu"})
public void receiveMsg(Message message) {
System.out.println("消息头:" + message.getMessageProperties());
System.out.println("消息体:" + message.getBody());
}
}
AmqpAdmin是RabbitMQ系统管理功能组件, 提供了创建和删除Queue,Exchange,Binding的功能。
@RunWith(SpringRunner.class)
@SpringBootTest
public class AmqpApplicationTests {
@Autowired
private AmqpAdmin amqpAdmin;
/**
* 使用amqpAdmin创建Exchange
*/
@Test
public void createExchange() {
amqpAdmin.declareExchange(new DirectExchange("exchange.amqpadmin"));
System.out.println("创建Exchange完成");
}
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class AmqpApplicationTests {
@Autowired
private AmqpAdmin amqpAdmin;
/**
* 使用amqpAdmin创建Queue
*/
@Test
public void createQueue() {
amqpAdmin.declareQueue(new Queue("amqpadmin.news", true));
System.out.println("创建Queue完成");
}
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class AmqpApplicationTests {
@Autowired
private AmqpAdmin amqpAdmin;
/**
* 使用amqpAdmin创建Binding
*/
@Test
public void createBinding() {
amqpAdmin.declareBinding(new Binding("amqpadmin.news", Binding.DestinationType.QUEUE, "exchange.amqpadmin", "*.news", null));
System.out.println("创建binding关系完成");
}
}
查看rabbitmq控制台, AmqpAdmin组件创建交换器,队列和绑定关系成功。
RabbitMQ的自动配置在RabbitAutoConfiguration类中, 配置属性封装在RabbitProperties中,使用spring.rabbitmq.xxx修改配置属性。
自动配置类中创建了连接工厂CachingConnectionFactory,RabbitTemplate模板,AmqpAdmin管理组件等;
@Configuration
@ConditionalOnClass({ RabbitTemplate.class, Channel.class })
@EnableConfigurationProperties(RabbitProperties.class)
@Import(RabbitAnnotationDrivenConfiguration.class)
public class RabbitAutoConfiguration {
@Configuration
@ConditionalOnMissingBean(ConnectionFactory.class)
protected static class RabbitConnectionFactoryCreator {
//连接工厂
@Bean
public CachingConnectionFactory rabbitConnectionFactory(RabbitProperties config)
throws Exception {
RabbitConnectionFactoryBean factory = new RabbitConnectionFactoryBean();
if (config.determineHost() != null) {
factory.setHost(config.determineHost());
}
factory.setPort(config.determinePort());
if (config.determineUsername() != null) {
factory.setUsername(config.determineUsername());
}
if (config.determinePassword() != null) {
factory.setPassword(config.determinePassword());
}
if (config.determineVirtualHost() != null) {
factory.setVirtualHost(config.determineVirtualHost());
}
if (config.getRequestedHeartbeat() != null) {
factory.setRequestedHeartbeat(config.getRequestedHeartbeat());
}
RabbitProperties.Ssl ssl = config.getSsl();
if (ssl.isEnabled()) {
factory.setUseSSL(true);
if (ssl.getAlgorithm() != null) {
factory.setSslAlgorithm(ssl.getAlgorithm());
}
factory.setKeyStore(ssl.getKeyStore());
factory.setKeyStorePassphrase(ssl.getKeyStorePassword());
factory.setTrustStore(ssl.getTrustStore());
factory.setTrustStorePassphrase(ssl.getTrustStorePassword());
}
if (config.getConnectionTimeout() != null) {
factory.setConnectionTimeout(config.getConnectionTimeout());
}
factory.afterPropertiesSet();
CachingConnectionFactory connectionFactory = new CachingConnectionFactory(
factory.getObject());
connectionFactory.setAddresses(config.determineAddresses());
connectionFactory.setPublisherConfirms(config.isPublisherConfirms());
connectionFactory.setPublisherReturns(config.isPublisherReturns());
if (config.getCache().getChannel().getSize() != null) {
connectionFactory
.setChannelCacheSize(config.getCache().getChannel().getSize());
}
if (config.getCache().getConnection().getMode() != null) {
connectionFactory
.setCacheMode(config.getCache().getConnection().getMode());
}
if (config.getCache().getConnection().getSize() != null) {
connectionFactory.setConnectionCacheSize(
config.getCache().getConnection().getSize());
}
if (config.getCache().getChannel().getCheckoutTimeout() != null) {
connectionFactory.setChannelCheckoutTimeout(
config.getCache().getChannel().getCheckoutTimeout());
}
return connectionFactory;
}
}
@Configuration
@Import(RabbitConnectionFactoryCreator.class)
protected static class RabbitTemplateConfiguration {
// RabbitTemplate模板
@Bean
@ConditionalOnSingleCandidate(ConnectionFactory.class)
@ConditionalOnMissingBean(RabbitTemplate.class)
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
MessageConverter messageConverter = this.messageConverter.getIfUnique();
if (messageConverter != null) {
rabbitTemplate.setMessageConverter(messageConverter);
}
rabbitTemplate.setMandatory(determineMandatoryFlag());
RabbitProperties.Template templateProperties = this.properties.getTemplate();
RabbitProperties.Retry retryProperties = templateProperties.getRetry();
if (retryProperties.isEnabled()) {
rabbitTemplate.setRetryTemplate(createRetryTemplate(retryProperties));
}
if (templateProperties.getReceiveTimeout() != null) {
rabbitTemplate.setReceiveTimeout(templateProperties.getReceiveTimeout());
}
if (templateProperties.getReplyTimeout() != null) {
rabbitTemplate.setReplyTimeout(templateProperties.getReplyTimeout());
}
return rabbitTemplate;
}
// AmqpAdmin管理组件
@Bean
@ConditionalOnSingleCandidate(ConnectionFactory.class)
@ConditionalOnProperty(prefix = "spring.rabbitmq", name = "dynamic", matchIfMissing = true)
@ConditionalOnMissingBean(AmqpAdmin.class)
public AmqpAdmin amqpAdmin(ConnectionFactory connectionFactory) {
return new RabbitAdmin(connectionFactory);
}
}
}
测试过程中,发现一直失败,无法连接到centOS7的rabbitmq服务;设置超时时间也没有用。
Exception in thread "main" java.util.concurrent.TimeoutException...
超时问题分析及解决方案 :
客户端远程登录linux主机,每次登录输入密码后都会等很长一段时间才会进入,这是因为linux主机在返回信息时需要解析ip导致连接很慢。如果在linux主机的hosts文件事先加入客户端的ip地址,这时再从客户端远程登录linux就会变很快,ip解析优先从本机hosts文件读取,其实是加快了DNS解析过程。
这里所说的远程登录不仅仅是ssh,还可能是mysql远程登录, 连接rabbitmq服务等。
centOS7服务器的/etc/hosts
文件添加本机ip,客户端ip解析配置;
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
127.0.0.1 centos7-1
192.168.111.1 crysw-PC
客户端(PC电脑)的C:\Windows\System32\drivers\etc\hosts
文件添加本机ip,原远程服务端ip解析配置;
# localhost name resolution is handled within DNS itself.
# 127.0.0.1 localhost
# ::1 localhost
192.168.111.129 centos7-1
127.0.0.1 crysw-PC
刷新windows系统的网络 : ipconfig /flushdns
参考1:https://blog.csdn.net/qq_34958326/article/details/116832748
参考2:https://www.cnblogs.com/jerryqi/p/9714566.html