SpringBoot学习系列(十五)------SpringBoot与消息

SpringBoot学习系列(十五)------SpringBoot与消息

前言

在当前微服务火爆的情况下,消息服务中间件的使用,是我们开发人员必须掌握的一项技能,在大多的应用中,我们通过消息服务来提高系统间的异步通信以及扩展和解耦能力,在当前的市场下,消息服务主要有两种规范:JMS和AMQP

  • JMS : java message service, 基于JVM消息代理的规范,它的实现有:ActiveMQ、HornetMQ
  • AMQP : Advanced Message Queuing Protocol,高级消息队列协议,兼容JMS,它的实现有:RabbitMQ.

关于JMS和AMQP的异同,可以参照下图:

SpringBoot学习系列(十五)------SpringBoot与消息_第1张图片

在本篇博文中,我们将着重来学习RabbitMQ与SpringBoot的整合使用.

正文

1. 了解RabbitMQ

  • 核心概念:

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

    2. Publisher : 消息的生产者,也是一个向交换器发布消息的客户端程序.

    3. Exchange : 交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列.根据消息中不同的路由键来分发给不同的队列.

      他有四种类型:direct(默认)、fanout、topic、headers,不同类型的Exchange转发消息的策略不同.

    4. Binding : 绑定,用于消息队列和交换器之间的关联,一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,也可以将交换器理解为由绑定构成的路由表.Exchange和Binding可以是多对多的关系.

    5. Connection : 网络连接,如TCP连接

    6. Channel : 信道,是一条独立的双向数据流通道,信道是建立在真实的TCP连接内的虚拟连接,AMQP的命令都是通过信道发出去的,它的存在是为了复用一条TCP连接,这样就不用建立很多的TCP连接.

    7. Consumer : 消息的消费者,表示一个从消息队列中取得消息的客户端程序.

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

    9. Broker : 表示消息队列服务器实体

    具体的图解如下:

    SpringBoot学习系列(十五)------SpringBoot与消息_第2张图片

2. RabbitMQ运行机制

AMQP中因为加入了Exchange和Binding这2个角色,所以它的机制和JMS有所区别.区别的主要体现在Exchange上.

Exchange主要有四种类型: direct、fanout、topic、headers,其中headers现在基本用不到,所以我们来看看其他的类型:

  • dircect

    消息中的路由键如果和Binding中的binding的key一致,交换器就将消息发送到对应的队列中.路由键和队列名完全匹配,比如,如果一个队列绑定到交换器时要求路由键为dog,则只会转发routing key标记为dog的消息,不会转发dog.puppy或者其他的,它是完全匹配、单播的模式.

    SpringBoot学习系列(十五)------SpringBoot与消息_第3张图片

  • fanout

    每个发到fanout类型交换器的消息都会分发到所有绑定的队列上去,类似子网广播,它转发消息的速度是最快的.

    SpringBoot学习系列(十五)------SpringBoot与消息_第4张图片

  • Topic

    topic交换器通过模式匹配来分配消息,它将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上.它将路由键和绑定建的字符串切分成单词,这些单词之间用.隔开,它同样也会识别两个通配符:符号#和符号*,#匹配0个或者多个单词,*匹配一个单词.

    SpringBoot学习系列(十五)------SpringBoot与消息_第5张图片

3. RabbitMQ的安装和使用

要使用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控制台的页面如下:

SpringBoot学习系列(十五)------SpringBoot与消息_第6张图片

在WEB端,我们可以选择Exchange来创建一个新的Exchange或者Queue:

SpringBoot学习系列(十五)------SpringBoot与消息_第7张图片

在这个页面:Name代表创建的Exchange的名称;Type可以选择exchange的四种类型;Durabilty表示是否持久化,如果选择是,则rabbitmq重启以后,该Exchange依然存在.点击Add exchange则添加一个新的Exchange.

在Queue的Add页面,也可以创建一个队列使用.

新建了exchange和queue以后,我们可以在exchange的页面上,点击刚才新增的exchange来给它绑定队列:

SpringBoot学习系列(十五)------SpringBoot与消息_第8张图片

接下来我们就可以在WEB页面上直接发送消息来测试他们对应的模式:

SpringBoot学习系列(十五)------SpringBoot与消息_第9张图片

点击Publish message以后,就表示发送消息,我们可以在queue界面查看获取到的消息.

4. RabbitMQ整合SpringBoot使用

我们先创建一个SpringBoot项目,并且引入RabbitMQ的依赖和Web依赖:
SpringBoot学习系列(十五)------SpringBoot与消息_第10张图片

我们可以看到在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系统管理组件,可以用它来创建和管理队列或者交换器

了解了以上的组件以后,我们现在就可以来测试一下:

  1. 首先我们在WEB管理页面创建2个路由器和2个队列

    SpringBoot学习系列(十五)------SpringBoot与消息_第11张图片

    SpringBoot学习系列(十五)------SpringBoot与消息_第12张图片

    这两个交换器,一个是direct类型的,一个是fanout类型的,我们分别将2个队列绑定在交换器上,绑定规则如下:

    SpringBoot学习系列(十五)------SpringBoot与消息_第13张图片

    SpringBoot学习系列(十五)------SpringBoot与消息_第14张图片

  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);
        }
    
    }
    
  3. 发送JSON格式的数据

    我们如果用WEB浏览器查看发送的消息,可以看到rabbitmq使用自己默认的序列化帮我们发送数据,可视化很差:

    SpringBoot学习系列(十五)------SpringBoot与消息_第15张图片

    要发送JSON格式的数据,需要我们自定义消息转换器MessageConverter,我们可以创建一个配置类:

    @Configuration
    public class MyAMQPConfig {
    
        @Bean
        public MessageConverter messageConverter(){
            //这里使用的Jackson2JsonMessageConverter是SpringBoot中已经实现的转换器,我们直接new出来即可
            return new Jackson2JsonMessageConverter();
        }
    }
    

    现在我们查看Web端就可以看到数据被序列化为json发送了:

    SpringBoot学习系列(十五)------SpringBoot与消息_第16张图片

  4. 使用fanout发送时,获取的方式也是一样的.

4. 使用SpringBoot监听注解来消费队列中的消息

在实际的项目中,我们一般会写一个监听器来监听对应队列中的消息,一旦收到以后运行某段业务逻辑.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);
        }
    }
    

    这样在程序运行的时候,只要我们的队列接收到了消息,我们就可以使用监听器来触发函数处理.

5. AmqpAdmin管理组件的使用

AmqpAdmin在SpringBoot中帮助我们创建和删除ExchangeQueue以及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学习系列(十五)------SpringBoot与消息_第17张图片

总结

至此,SpringBoot整合RabbitMQ的使用基本上结束了,还有一些细节和需要注意的地方,在使用的时候多留心即可.

你可能感兴趣的:(消息中间件,后端技术,SpringBoot)