(二)RocketMq与Spring的集成开发原理之传统Spring和Springboot方式

1.前话

  目前使用RocketMq最主流的无非就三种方式:

  1. 直接使用RocketMq客户端jar包的ProducerConsumer
  2. 对接传统Spring容器,将ProducerConsumer交给Spring容器进行管理;
  3. 使用RocketMq-Springboot-Starter包快速配置集成到Springboot项目。

  对于第一种直接使用RocketMq客户端的方式,这里便不做介绍了,直接跳转至RocketMqGithub文档观看官方给的样例即可。

  这里主要介绍的是传统的Spring项目和Springboot项目集成RocketMq的最佳实践,以及扩展性的简单说明一下Springboot集成的原理。

2.传统Spring集成

  对于传统Spring项目而言,要集成RocketMq和直接使用客户端的ProducerConsumer是差不了太多的,无非是初始化了对象后交给Spring容器进行管理,后续需要使用的时候直接从Spring容器中取出来即可。

2.1 Maven配置

  RocketMq客户端jar包配置如下:

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-acl</artifactId>
    <version>4.5.2</version>
</dependency>

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.5.2</version>
</dependency>

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-common</artifactId>
    <version>4.5.2</version>
</dependency>

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-logging</artifactId>
    <version>4.5.2</version>
</dependency>

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-remoting</artifactId>
    <version>4.5.2</version>
</dependency>

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-srvutil</artifactId>
    <version>4.5.2</version>
</dependency>

2.2 Configuration配置

  将需要使用的ProducerConsumer统一放到Configuration类中进行管理,代码如下:

@Configuration
public class RocketMqConfiguration {

    @Value("${rocketMq.nameSrvAddr}")
    private String nameSrvAddr;

    @Value("${rocketMq.producerGroup}")
    private String producerGroup;

    /**
     * 配置Producer交给Spring进行管理
     *
     * @return
     */
    @Bean(initMethod = "start", destroyMethod = "shutdown")
    public DefaultMQProducer defaultMqProducer() {
        // 使用默认的MQ producerGroup分组
        DefaultMQProducer producer = new DefaultMQProducer(producerGroup);
        // 设置NameServer地址
        producer.setNamesrvAddr(nameSrvAddr);
        // 其它属性看需求
        return producer;
    }

    @Value("${rocketMq.consumerGroup}")
    private String consumerGroup;

    @Value("${rocketMq.consumerTopic}")
    private String consumerTopic;

    @Value("${rocketMq.consumerTag}")
    private String consumerTag;

    /**
     * 注册默认的Consumer
     *
     * @param messageListenerConcurrently
     * @return
     * @throws MQClientException
     */
    @Bean(initMethod = "start", destroyMethod ="shutdown")
    public DefaultMQPushConsumer defaultMqPushConsumer(
            @Qualifier("generalConcurrentlyListener")MessageListenerConcurrently messageListenerConcurrently)
            throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(consumerGroup);
        // 设置NameServer地址
        consumer.setNamesrvAddr(nameSrvAddr);
        // 订阅topic和tag
        consumer.subscribe(consumerTopic, consumerTag);
        // 注册listener监听器
        consumer.registerMessageListener(messageListenerConcurrently);
        // 其它属性配置看需求
        return consumer;
    }

}

  配置方式和直接使用初始化使用差不多,只是把初始化后的ProducerConsumer交给了Spring容器进行管理,具体的RocketMq属性再配置到Spring容器中即可。

2.3 配置MessageListener监听器

  在Configuration类中是引用了MessageListener监听类,因此需要在Spring容器中创建一个名为generalConcurrentlyListenerBean,代码如下:

@Slf4j
@Component("generalConcurrentlyListener")
public class GeneralConcurrentlyListener implements MessageListenerConcurrently {

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        for (Message m : msgs) {
            log.info("timestamp:{} msg body:{} msg data:{}", System.currentTimeMillis(), new String(m.getBody()), m);
        }
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
}

  一个很简单的实现,只需要确保这个Bean交给Spring容器管理即可,到这里就能保证Consumer是可以正常消费TopicTag的消息了。

2.4 使用Producer

  如果要发送消息只需要从Spring容器中获取Producer,再使用对象进行相应的发消息操作即可,这里是使用了最简单的方式,代码如下:

@Slf4j
@Service
public class SendMqService {

    @Value("${rocketMq.consumerTopic}")
    private String consumerTopic;

    @Value("${rocketMq.consumerTag}")
    private String consumerTag;

    @Autowired
    private DefaultMQProducer producer;

    /**
     * 发送mq消息
     *
     * @param messageBody
     */
    public void sendMq(String messageBody) {
        Message message = new Message(consumerTopic, consumerTag, messageBody.getBytes());
        try {
            // 具体发送什么类型的消息看需求
            SendResult sendResult = producer.send(message);
            log.info("sendResult:{}", sendResult);
        } catch (Exception e) {
            log.error("发送mq消息失败", e);
        }
    }

}

  直接调用sendMq()方法即可发送普通的RocketMq消息,如果要自定义不同的消息只需要添加不同的方法即可。

  Spring简单集成RocketMq只需要配置这几步,使用方式和RocketMqGithub使用文档一样。确保Spring容器有如下几个属性:

rocketMq.nameSrvAddr=http://***
rocketMq.producerGroup=GID_DEV_SPRINGBOOT_DEMO
rocketMq.consumerGroup=GID_DEV_SPRINGBOOT_DEMO
rocketMq.consumerTopic=Hello-Test
rocketMq.consumerTag=TAG_DEV_HELLO_WORLD

3.SpringBoot快速集成

  一般而言,使用较广的框架都会针对Springboot出一个Starter包以方便开发快速集成,RocketMq同理。

3.1 Maven配置

  SpringbootStarter包配置如下:

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.0.4</version>
</dependency>

3.2 Yaml配置

rocketmq:
  name-server: http://***
  producer:
    group: GID_DEV_SPRINGBOOT_DEMO

3.3 MessageListener配置

  如果有消费者则需要配置MessageLisenter监听器来监听队列消息,代码如下:

@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "GID_DEV_SPRINGBOOT_DEMO", 
        topic = "Hello-Test", selectorExpression = "TAG_DEV_HELLO_WORLD",
        messageModel = MessageModel.CLUSTERING)
public class ClusterMqListener implements RocketMQListener<String> {

    @Override
    public void onMessage(String message) {
        log.info("clustering consume message: {}", message);
    }
}

  consumerGrouptopicselectorExpression都可以将属性配置在Spring容器中再通过${}通配符去获取。

  注:实现类一定要实现RocketMQListener接口,否则会抛异常

3.4 使用RocketMQTemplate模板类发送

  RocketMqSpringboot包提供了发送消息模板类RocketMQTemplate,这个类提供了如下发送方式的快捷方法:

  1. syncSend():同步发送消息;
  2. syncSendOrderly():同步发送严格顺序消息,默认使用的SelectorSelectMessageQueueByHash
  3. asyncSend():异步发送,但必须要填入SendCallboack回调实现类;
  4. asyncSendOrderly:异步发送严格顺序消息,同样需要填入回调实现类,默认使用的SelectorSelectMessageQueueByHash
  5. sendOneWay():单方发送消息;
  6. sendOneWayOrderly():单方发送严格顺序消息,默认使用的SelectorSelectMessageQueueByHash

  通用的方法大概有这六大类,事务性的用得少便不写上去了,这六大类快捷发送消息方法将一些通用流程都封装到了方法中,如果没有特殊的需求直接使用这些模板方法可以节省一定的开发时间。使用代码如下:

@Slf4j
@Service
public class SendMqService {

    @Value("${rocketMq.consumerTopicTag:Hello-Test:TAG_DEV_HELLO_WORLD}")
    private String topicTag;

    @Autowired
    private RocketMQTemplate rocketMqTemplate;

    /**
     * 发送mq消息
     *
     * @param messageBody
     */
    public void sendMq(String messageBody) {
        // 具体发送什么类型的消息看需求
        SendResult sendResult = rocketMqTemplate.syncSend(topicTag, content);
        log.info("sendResult:{}", sendResult);
    }

}

  相较于直接使用DefaultMQProducer类去自己封装消息体及确定各种对象,使用模板类发送更为简单,如有特殊需求同样可以参考传统Spring那种使用方式,直接获取DefaultMQProducer类,再自己封装消息体和发送方式。

  这种配置使用方式个人是十分推荐的,快捷方便,简单直接,基本能覆盖大部分的使用场景,但这种使用方式并没有很常见,更多的还是传统Spring的那种配置方式。


4.Springboot自动集成的原理

4.1 RocketMq的自动装填类

  只要稍微熟悉Springboot自动装填机制,就可以知道RocketMq一定是写了个AutoConfiguration类去嵌入Springboot,从rocketmq-spring-bootjar包中就很容易看到RocketMQAutoConfiguration类,这个类就是自动集成的核心了。

  这个类做的事也很简单,就是把要集成到Spring框架的事情在这个类中帮我们做了,其源码如下:

@Configuration
public class RocketMQAutoConfiguration {

    @Autowired
    private Environment environment;

    @Bean
    public DefaultMQProducer defaultMQProducer(RocketMQProperties rocketMQProperties) {
        // **配置略过,感兴趣的可以自行去看看
        DefaultMQProducer producer;
        // **配置略过,感兴趣的可以自行去看看
        return producer;
    }

    @Bean(destroyMethod = "destroy")
    public RocketMQTemplate rocketMQTemplate(DefaultMQProducer mqProducer,
        ObjectMapper rocketMQMessageObjectMapper,
        RocketMQMessageConverter rocketMQMessageConverter) {
        RocketMQTemplate rocketMQTemplate = new RocketMQTemplate();
        // **配置略过,感兴趣的可以自行去看看
        return rocketMQTemplate;
    }

}

  贴出来的代码省略了很多,但里面大致的关键要素代码就这几段,是不是和传统的Spring配置方式相差不大?

4.2 Producer的启动

  看完上段代码是知道了自动装填类帮我们把DefaultMQProducerRocketMQTemplate类注册到了Spring容器中,但并没有看到调用Producerstart()方法,那么Producer启动是在那里完成的呢?请看到RocketMQTemplate类的关键源码:

public class RocketMQTemplate extends AbstractMessageSendingTemplate<String> 
                        implements InitializingBean, DisposableBean {
    @Override
    public void afterPropertiesSet() throws Exception {
        if (producer != null) {
            producer.start();
        }
    }
    @Override
    public void destroy() {
        if (Objects.nonNull(producer)) {
            producer.shutdown();
        }
        for (Map.Entry<String, TransactionMQProducer> kv : cache.entrySet()) {
            if (Objects.nonNull(kv.getValue())) {
                kv.getValue().shutdown();
            }
        }
        cache.clear();
    }
}

  看到这个类实现了InitializingBeanDisposableBean接口是不是就豁然开朗了?原来Producer是借助RocketMQTemplate还魂了。

4.3 MessageListener的读取解析

  那么疑问又来了,Consumer是在哪里创建监听消息的呢?这就需要看到RocketMQAutoConfiguration类中的一段关键代码了:

@Import({ListenerContainerConfiguration.class})
public class RocketMQAutoConfiguration {
}

  在RocketMQAutoConfiguration类中引入了ListenerContainerConfiguration类,在这个类中将会完成对@RocketMQMessageListener注解的解析并生成对应的Consumer,其关键源码如下:

@Configuration
public class ListenerContainerConfiguration implements
                SmartInitializingSingleton {
    @Override
    public void afterSingletonsInstantiated() {
        // 获取Spring容器中的所有被@RocketMQMessageListener注解的bean
        Map<String, Object> beans = this.applicationContext
                .getBeansWithAnnotation(RocketMQMessageListener.class);
        // 如果有则依次将bean传进registerContainer()方法执行
        if (Objects.nonNull(beans)) {
            beans.forEach(this::registerContainer);
        }
    }
    private void registerContainer(String beanName, Object bean) {
        // 不重要代码略过,感兴趣的可以自行去看看
        // 从bean获取@RocketMQMessageListener注解信息
        RocketMQMessageListener annotation = clazz.getAnnotation(RocketMQMessageListener.class);

        // 使用placeholder类解析注解的属性,这就是为什么可以在注解中
        // 使用${}通配符
        String consumerGroup = this.environment
                .resolvePlaceholders(annotation.consumerGroup());
        String topic = this.environment
                .resolvePlaceholders(annotation.topic());
                
        // 不重要代码略过,感兴趣的可以自行去看看

        // createRocketMQListenerContainer()方法会根据注解的其它属性创建
        // DefaultRocketMQListenerContainer类,有兴趣可以自行观看
        genericApplicationContext.registerBean(containerBeanName, DefaultRocketMQListenerContainer.class,
            () -> createRocketMQListenerContainer(containerBeanName, bean, annotation));
        // 这里直接使用Spring上下文类的getBean()方法,此时将会在容器内
        // 实例化刚刚创建的DefaultRocketMQListenerContainer类
        DefaultRocketMQListenerContainer container = genericApplicationContext.getBean(containerBeanName,
            DefaultRocketMQListenerContainer.class);
        if (!container.isRunning()) {
            try {
                // 如果容器尚未执行则调用start()方法,这里面将会调用
                // Consumer的start()方法启动消费者
                container.start();
            } catch (Exception e) {
                // 不重要代码略过,感兴趣的可以自行去看看
            }
        }
        // 不重要代码略过,感兴趣的可以自行去看看
    }
}

  简要说明一下这个类的执行流程和作用:

  1. RocketMQAutoConfiguration被自动装填到Spring容器后,将会使用@Import注解引入ListenerContainerConfiguration
  2. 当被引入的ListenerContainerConfiguration类被Spring容器实例化后将会执行afterSingletonsInstantiated方法;
  3. 在该方法中将会从Spring容器中取出所有被RocketMQMessageListener注解过的Bean
  4. BeanDefaultRocketMQListenerContainer类封装,并注册到Spring容器中实例化;
  5. 调用DefaultRocketMQListenerContainerstart()方法,启动消费者。

  总而言之就是实现了RocketMQListener接口且被RocketMQMessageListener注解的Bean,最终会被封装成DefaultRocketMQListenerContainer类,在这里面的Consumer消费消息,并经过一系列处理(诸如转换消息内容)最终调用到Bean的接口方法实现中。

  这样是不是就清晰多了?如果感兴趣推荐自行去看源码,很多疑惑将会在源码中给出答案。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值