chapter11:SpringBoot与消息

Spring Boot与消息视频

1. 消息概述

(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的实现。

JMS与AMQP的对比
chapter11:SpringBoot与消息_第1张图片

(6)Spring支持

  • Spring-jms提供了对JMS的支持;

  • Spring-rabbit提供了AMQP的支持;

  • 需要ConnectionFactory的实现来连接消息代理;

  • 提供JmsTemplate、RabbitTemplate模板类来发送消息;

  • @JmsListener(JMS),@RabbitListener(AMQP)注解在方法上监听消息代理发布的消息;

  • @EnableJms,@EnableRabbit开启支持;

(7)SpringBoot自动配置

  • JmsAutoConfiguration

  • RabbitAutoConfiguration

2.消息应用场景

异步处理
chapter11:SpringBoot与消息_第2张图片
应用解耦,流量削峰
chapter11:SpringBoot与消息_第3张图片

3. RabbitMQ简介

RabbitMQ是一个由erlang语言开发的AMQP(Advanced Message Queue Protocol)的开源实现。

核心概念: Message,Publisher,Exchange,…

3.1 Message

消息,由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成, 这些属性包括路由键(routing-key),相对其他消息的优先权(priority),指出该消息可能需要持久性存储)等;

3.2 Publisher

消息的生产者,也是一个向交换器发布消息的客户端应用程序。

3.3 Exchange

交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。

Exchange有4种类型: direct(默认),fanout,topic和headers;不同类型的Exchange转发消息的策略有所区别。

3.4 Queue

消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列;消息一直在队列里面,等待消费者连接到这个队列将其消费(取出)。

3.5 Binding

绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则, 所以可以将交换器理解成一个由绑定构成的路由表。

3.6 Connection

网络连接, 比如一个TCP连接。

3.7 Channel

信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁TCP都是非常昂贵的开销,所以引入了信道的概念, 以复用一条TCP连接。

3.8 Consumer

消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。

3.9 Virtual Host

虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个vhost本质上是一个mini版的RabbitMQ服务器,拥有自己的队列、交换器、绑定和权限机制。vhost是AMQP概念的基础,必须在连接时指定,RabbitMQ默认的vhost是/

3.10 Broker

表示消息队列服务器实体。
chapter11:SpringBoot与消息_第4张图片

4. RabbitMQ运行机制

AMQP中的消息路由

AMQP中消息的路由过程和Java开发属性的JMS存在一些差异,AMQP中增加了Exchange和Binding的角色。生产者把消息发布到Exchange上,消息最终到达队列并被消费者接收,而根据Binding的routing key决定交换器的消息应该发送到哪个队列上。
chapter11:SpringBoot与消息_第5张图片

5. Exchange类型

RabbitMQ中,Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型: direct、fanout、topic、headers。headers匹配AMQP消息的header而不是路由键,headers交换器和direct交换器完全一致,但性能差很多,目前几乎用不到,所以直接看另外三种类型:

5.1Direct Exchange

典型的点对点发送模型。消息中的路由键(routing key)如果和Binding中的binding key一致,交换器就将消息发送到对于的队列中。路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为dog,则只会转发routing key标记为dog的消息,不会转发dog.puppy,也不会转发dog.guard等等。它是完全匹配、单播的模式。
chapter11:SpringBoot与消息_第6张图片

5.2 Fanout Exchange

广播模式,每个发到fanout类型交换器的消息都会分到所有绑定的队列上去。fanout交换器不处理路由键(routing key),只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息,fanout类型转发消息是最快的。
chapter11:SpringBoot与消息_第7张图片

5.3 Topic Exchange

topic交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式(固定的单词或通配符)进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符,符合#和符号*#匹配0个或多个单词, *匹配一个单词。
chapter11:SpringBoot与消息_第8张图片

6. RabbitMQ安装及配置

6.1 安装RabbitMQ

安装rabbitMQ, 参考 centos7安装rabbitmq教程1, centos7安装rabbitmq教程2

通过http://192.168.111.129:15672/打开RabbitMQ控制台。
chapter11:SpringBoot与消息_第9张图片

6.2 RabbitMQ配置

根据下面的交换器,路由键进行配置。
chapter11:SpringBoot与消息_第10张图片

点击Exchanges标签页,进行交换器的配置, 新增exchange.direct、exchange.fanout、exchange.topic交换器。durability=Durable表示持久化。
chapter11:SpringBoot与消息_第11张图片
点击Queues标签页,进行队列的配置, 新增atguigu、atguigu.news、atguigu.emps、gulixueyuan.news队列。
chapter11:SpringBoot与消息_第12张图片
对exchange.direct交换器进行队列绑定, 绑定了atguigu、atguigu.news、atguigu.emps、gulixueyuan.news队列。
chapter11:SpringBoot与消息_第13张图片

对exchange.fanout交换器进行队列绑定, 绑定了atguigu、atguigu.news、atguigu.emps、gulixueyuan.news队列。
chapter11:SpringBoot与消息_第14张图片
对exchange.topic交换器进行队列绑定, 绑定了atguigu、atguigu.news、atguigu.emps、gulixueyuan.news队列。routing key使用了通配符进行匹配。
chapter11:SpringBoot与消息_第15张图片

6.3 使用交换器发送消息

使用点对点交换机exchange.direct发送消息, routing key=atguigu。
chapter11:SpringBoot与消息_第16张图片

direct是点对点模式,只有atguigu队列可以接收到exchange.direct交换器发送的消息。
chapter11:SpringBoot与消息_第17张图片

读取消息
chapter11:SpringBoot与消息_第18张图片
使用exchange.fanout交换器发送消息, routing key=atguigu.news
chapter11:SpringBoot与消息_第19张图片

atguigu、atguigu.news、atguigu.emps、gulixueyuan.news队列都会接收到exchange.fanout交换器发送的消息,exchange.fanout发送消息的匹配规则与发送消息的路由键没有关系,是广播模式。
chapter11:SpringBoot与消息_第20张图片
chapter11:SpringBoot与消息_第21张图片

使用exchange.topic交换器发送消息, routing key=atguigu.news。
chapter11:SpringBoot与消息_第22张图片
因为exchange.topic绑定了atguigu.# 和 *.news的路由匹配规则,所以atguigu、atguigu.news、atguigu.emps、gulixueyuan.news队列都会接收到exchange.topic交换器通过atguigu.news发送的消息。
chapter11:SpringBoot与消息_第23张图片
读取消息
chapter11:SpringBoot与消息_第24张图片

使用exchange.topic交换器发送消息, routing key=hello.news。根据*.news的匹配规则, atguigu.news、gulixueyuan.news队列会接收到exchange.topic交换器通过hello.news发送的消息。
chapter11:SpringBoot与消息_第25张图片
读取消息
chapter11:SpringBoot与消息_第26张图片

7. 项目中实践RabbitMQ

7.1 创建SpringBoot项目

引入 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>

7.2 添加rabbitmq配置

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

7.3 RabbitTemplate

7.3.1 点对点模式发送消息

@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=吴承恩)

7.3.2 广播模式发送消息

@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

7.4 监听并接收消息

@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());
    }
}

7.6 AmqpAdmin组件

AmqpAdmin是RabbitMQ系统管理功能组件, 提供了创建和删除Queue,Exchange,Binding的功能。

7.6.1 创建Exchange

@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完成");
    }    
}    

7.6.2 创建Queue

@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完成");
    }
}    

7.6.3 创建Binding

@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组件创建交换器,队列和绑定关系成功。
chapter11:SpringBoot与消息_第27张图片

7.7 RabbitMQ自动配置

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);
		}
    }
}

7.8 遇到的问题

测试过程中,发现一直失败,无法连接到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

你可能感兴趣的:(SpringBoot,spring,boot,后端,java)