集成互联网架构解耦神器ActiveMQ —— Spring Boot系列

812 阅读9分钟

ps: 本文在草稿箱发现,写于 2018.09.01,里面的内容已经有些老了,读者请斟酌阅读

Apache ActiveMQ 简介

Apache ActiveMQ 是一个开源的、高可扩展的、基于 Java 实现的消息中间件,它支持许多不同语言编写的客户端和协议,比如 Java,C,C ++,C#,Ruby,Perl,Python,PHP 等语言,还支持 AMQP, MQTT, OpenWireSTOMP 等多种协议,同时还支持 JMS 1.1J2EE 1.4 规范,这非常适合异构系统之间的通信和交互问题。

JMS 简介

JMS 1.1 规范定义:

JMS provides a common way for Java programs to create, send, receive and read an enterprise messaging system’s messages.

维基百科定义:

Java 消息服务(Java Message Service,JMS) 应用程序接口是一个 Java 平台中关于面向消息中间件(MOM)的 API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信

从定义上可以看出,JMS 是一套 API 规范,是 Java 平台中用于规范客户端和消息中间件之间如何进行交互(创建、发送、接收和读取消息等)而设计,它提供了一套通用的接口,用于在两个应用程序之间,或分布式系统之间发送消息,进行异步通信。而 spring-jms 正是对JMS规范的实现。

JMS 两种消息模式

1. 发布/订阅(Topic 消息模式)

Publisher 向 ActiveMQ 发布一个 TopicA 并附加(可选)一些交互数据(可以是任何双方约定好的数据格式),监听 TopicA 的 Subscriber(客户端) 都会接收到 ActiveMQ 推送过来的消息,如果此时部分 Subscriber 失去连接的将丢失本次消息,信息包含了发布方传递的数据

2. 点到点(Queue 消息模式)

JMS 规范定义

Point-to-point systems are about working with queues of messages. They are point-to-point in that a client sends a message to a specific queue. Some PTP systems blur the distinction between PTP and Pub/Sub by providing system clients that automatically distribute messages.

The JMS PTP model defines how a client works with queues: how it finds them, how it sends messages to them, and how it receives messages from them.

Producer 向 ActiveMQ 发布了一个 TopicA 并附加(可选)一些交互数据(可以是任何双方约定好的数据格式),消息将保存在 ActiveMQ 的 Queue 中(可以配置成 BD 持久化等),监听 TopicA 的多个 Consumer 中只有一个 Consumer 会收到消息,收到之后会通知 ActiveMQ,ActiveMQ 知道消息被消费之后,会把对应的消息从 Queue 中删除或者其它操作。如果没有消费者消费这条消息,那么这条消息将会一直保留在 ActiveMQ 的 Queue 中,直到它被消费为止

对比两种消息模式的技术特点

对比项 Topic Queue
概要介绍 发布/订阅 生产者/消费者(点对点)
消息状态 无状态,消息不会被保存。 在 ActiveMQ 中会以文件的形式保存,或者可以配置成DB持久化。
消息完整性保障 不能保障所有的 Subscriber 都能够接收到 Publisher 发送的消息,一旦离线就可能会丢失。 可以保障 Consumer 接收到 Producer 发布的每一条消息,但只能被一个 Consumer 接收。
消息的发布和接收策略 监听同一个 topic 的所有 Subscriber 都会收到来自 Publisher 的消息,接收后通知 ActiveMQ,ActiveMQ 记录消息队列中有多少已经出队,未出队的消息不会再次发出。 一对一的消息发布和接收策略,一个 Producer 发送消息,对应只能有一个 Consumer 接收(多个监听同一 topic 的 Consumer 可以通过设置优先级优先让其优先收到) ,接收完之后会通知 ActiveMQ,ActiveMQ 记录多少消息出队,并删除 queue 中对应的消息,未出队的消息会等 Consumer 在线时再发送。

典型的 RPC 通信架构和 ActiveMQ 通信架构对比

典型的 RPC 通信架构

RPC通信架构图 RPC 调用有一个特点,就是调用方会关注调用的执行结果,比如登录、注册等功能,客户端通过 RPC 调用服务端暴露出来的接口,然后服务端对这次调用进行反馈,最后客户端通过反馈来判断接下来的业务流程。

ActiveMQ 通信架构

通过 ActiveMQ 通信有一个特点,就是调用方不关心执行结果,比如,在微信中,你向好友发送了一条消息,你的手机只负责发送,却不需要去关心这条消息是否到达用户的微信中,因为如果用户不在线呢。

ActiveMQ 适用场景

当调用方不关心执行返回的结果时,宜使用 ActiveMQ。

举个反例来说明,假设上游(客户端 or 服务端)并不关注下游的返回结果,如果使用 RPC 与下游进行交互时,会出现什么问题呢?

比如:

需求模拟:用户通过扫垃圾桶上的二维码来投放垃圾桶,投放完之后会触发以下的操作
1. 用户投放垃圾之后,需要调用微信服务接口推送模板消息通知用户投放信息和获得积分。
2. 调用垃圾分类引擎接口对用户投放的垃圾进行质量评分。
3. 刚好现阶段对于部分垃圾分类需要人工检查,调用信息管理系统接口通知相关的负责人。

此时 RPC 调用在这种需求场景下会出现上下游严重耦合的问题

  1. 一旦这个垃圾投放的业务有新的业务需求扩展,那么除了下游要增加新业务功能之外,上游还得增加一个 RPC 调用新业务暴露的接口,垃圾桶的开发者就不爽了,为什么你们加需求,改的人是我,而且还得记住下游到底有多少业务依赖。
  2. 一旦下游业务出问题,就可能会影响到其它基础服务,比如微信消息业务出问题了,如果调用方异常处理没做好,就可能直接报错影响其它业务的正常运行。

解决方案:上下游之间的通信改成由 ActiveMQ 来负责。

那么就变成下面这样子:

需求模拟:用户通过扫垃圾桶上的二维码来投放垃圾桶,投放完之后会触发以下的操作
1. 用户投放垃圾之后,需要通知微信服务推送模板消息通知用户投放信息和获得积分。
2. 通知垃圾分类引擎对用户投放的垃圾进行质量评分。
3. 刚好现阶段对于部分垃圾分类需要人工检查,则会通知相关的负责人。

转变之后,MQ 能够做到上下游物理上逻辑上双解耦

  1. 物理上解耦:上下游互相不需要知道彼此的存在,互相不会建立物理连接,彼此只与 ActiveMQ 进行物理连接。

  2. 逻辑上解耦:上游不需要知道下游具体有多少个业务依赖于它,它只负责向 ActiveMQ 发布垃圾投放消息即可,下游只需要订阅对应的 topic ,新业务扩展不会影响上游。

ActiveMQ 不适用场景

当调用方关心执行返回的结果时,不宜使用 ActiveMQ,应该使用 RPC。

比如登录功能,直接通过 RPC 调用下游服务的接口,下游服务反馈结果就可以了。

如果上游通过 ActiveMQ 发送一个 login.topic,并订阅 一个 login.result.topic,然后对程序进行阻塞,下游处理逻辑之后发送一个 login.result.topic 通知上游,把简单的事情复杂化了。

Spring Boot 集成外部 Apache ActiveMQ

Github 有一个 spring boot 集成 ActiveMQ 的 spring-boot-sample-activemq,它只用到了内置在内存中的 ActiveMQ,而通常项目中用到外部 ActiveMQ 比较多,in memory 的配置就自行看demo吧,非常简单。

集成外部 Apache ActiveMQ 也是非常简单,只要根据下面的步骤做就好。

1. 安装 Apache ActiveMQ

官网下载地址,根据自己的操作系统下载对应的版本,下载后启动即可。

Mac or linux:

Window: 双击 activemq.bat 文件启动 activemq

日志文件:/your_activemq_dir/data/activemq.log

2. 增加 activemq 的依赖

gradle

// activemq
compile group: 'org.springframework.boot', name: 'spring-boot-starter-activemq'

or

maven

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-activemq</artifactId>
	<version>${spring-boot-version}</artifactId>
</dependency>

最后保证依赖下载完毕。

3. application.(yml | properties) 增加 activemq 的配置

增加 activemq  jms 配置
spring: 
  activemq:
    # activemq 代理地址
    broker-url: tcp://0.0.0.0:61616
    user: admin
    password: admin
    # 外部 activemq 不需要使用 in memory 的 activemq
    in-memory: false
    send-timeout: 3000
    pool:
      enabled: false
      # 发生异常重新连接 activemq
      reconnect-on-exception: true
#  jms:
    # activemq 默认只是启动点到点的消息模式,如果还要启动 发布/订阅 需要设置为 true,由于一些问题,这里注释掉,接下来会讲到
#    pub-sub-domain: true
    
# activemq 发布/订阅 模式 topic
topics:
  sample: sample-topic

# activemq 生产者/消费者 模式 queue
queues:
  sample: sample-queue
  
# jms 发布订阅模式监听器名字
jms.pub-sub-listener: topicListenerFactory

spring boot 官网提供 activemqjms 的所有配置,可以根据自己的实际需求去配置。

4. 配置两种消息模式共存

很遗憾地告诉你,加上上面的配置,queue 和 topic 两种消息模式是不能共存的(pub-sub-domain 默认为false,只是 queue),就算设置 spring.jms.pub-sub-domain: true 用于启动 topic 的消息模式,此时 queue 会被替换成 topic,注意是被替换,而不是增加 topic 消息模式,这是因为默认情况下,@JmsListener 注解定义的方法 Spring Boot 会在必要时注册一个默认的 ContainerFactory 侦听 destination,这个默认的 ContainerFactory 并没有帮我们把两种消息模式都保留,而是直接读配置,如果启动 topic,那么 queue 就会被覆盖,queue 全部变成 topic 消息模式(读者自己可以去验证,),除非你定制容器工厂。

解决方案:增加一个 topic 消息模式的 ContainerFactory 监听器专门处理 topic 消息。

import org.springframework.boot.autoconfigure.jms.DefaultJmsListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerContainerFactory;
import org.springframework.jms.support.converter.MappingJackson2MessageConverter;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.jms.support.converter.MessageType;

import javax.jms.ConnectionFactory;

/**
 * Jms 配置
 * Created by Blink on 1/14/2018 AD.
 *
 * @author Blink
 */
@Configuration
public class JmsConfiguration {

    /**
     * 创建一个独立的 topic 监听器,当使用到 发布/订阅 消息模式时,订阅者需要声明 containerFactory="${jms.pub-sub-listener}"
     *
     * @param connectionFactory
     * @param configurer
     * @return
     */
    @Bean(name = "topicListenerFactory")
    public JmsListenerContainerFactory<?> topicListenerFactory(ConnectionFactory connectionFactory,
                                                               DefaultJmsListenerContainerFactoryConfigurer configurer) {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        // This provides all boot's default to this factory, including the message converter
        configurer.configure(factory, connectionFactory);
        // You could still override some of Boot's default if necessary.
        factory.setPubSubDomain(true);
        return factory;
    }

    /**
     * 将接收的json数据转成对象
     *
     * @return
     */
    @Bean
    public MessageConverter jacksonJmsMessageConverter() {
        MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
        converter.setTargetType(MessageType.TEXT);
        converter.setTypeIdPropertyName("_type");
        return converter;
    }
}

4. 使用 生产者/消费者 模式

增加生产者代码

import lombok.extern.slf4j.Slf4j;
import org.apache.activemq.command.ActiveMQQueue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.jms.core.JmsMessagingTemplate;
import org.springframework.stereotype.Service;

/**
 * 生产者
 * Created by Blink on 1/10/2018 AD.
 *
 * @author Blink
 */
@Slf4j
@Service
public class Producer implements CommandLineRunner {

    @Value("${queues.sample}")
    public String sampleQueue;

    @Autowired
    private JmsMessagingTemplate jmsMessagingTemplate;

    public void send(String destination, String message) {
        log.info("============>>>>> 发布queue消息 " + message);
        this.jmsMessagingTemplate.convertAndSend(new ActiveMQQueue(destination), message);
    }

    @Override
    public void run(String... args) throws Exception {
        this.send(this.sampleQueue, "测试生产者消费者消息模式。");
    }
}

增加消费者代码

import lombok.extern.slf4j.Slf4j;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Service;

/**
 * 消费者
 * Created by Blink on 1/10/2018 AD.
 *
 * @author Blink
 */
@Slf4j
@Service
public class Consumer {

    @JmsListener(destination = "${queues.sample}")
    public void receive1(String text) {
        log.info("Consume1 " + text);
    }
    
    @JmsListener(destination = "${queues.sample}")
    public void receive2(String text) {
        log.info("Consume2 " + text);
    }
}

启动项目就可以看到控制台输出: 生产者消费者运行日志1 or 生产者消费者运行日志2

这证实了一开说的,生产者/消费者 消息模式,多个 Consumer 监听同一个 topic,只有一个消费者可以收到 Producer 发送的消息。

这里还说明了,具体会被哪一个 Consumer(相同 priority 时) 接收到是不一定,可以通过设置 Consumer 的 priority 来指定优先由哪个 consumer 收到。

5. 使用 发布/订阅 模式

记得在 application.(yml | properties) 设置:spring.jms.pub-sub-domain: true,否则 activemq 会输出警告消息 没有设置pub-sub消息模式警告消息

增加发布者代码

import lombok.extern.slf4j.Slf4j;
import org.apache.activemq.command.ActiveMQTopic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.jms.core.JmsMessagingTemplate;
import org.springframework.stereotype.Service;

/**
 * 发布者
 * Created by Blink on 1/11/2018 AD.
 *
 * @author Blink
 */
@Slf4j
@Service
public class Publisher implements CommandLineRunner {

    @Autowired
    private JmsMessagingTemplate jmsMessagingTemplate;

    @Value("${topics.sample}")
    public String sampleTopic;

    public void publish(String destination, String message) {
        log.info("============>>>>> 发布topic消息 " + message);
        jmsMessagingTemplate.convertAndSend(new ActiveMQTopic(destination), message);
    }

    @Override
    public void run(String... args) throws Exception {
        this.publish(this.sampleTopic, "测试 发布/订阅 消息模式。");
    }
}

增加订阅者代码

import lombok.extern.slf4j.Slf4j;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Service;

/**
 * 订阅者
 * Created by Blink on 1/11/2018 AD.
 *
 * @author Blink
 */
@Slf4j
@Service
public class Subscriber {

    @JmsListener(destination = "${topics.sample}")
    public void subscribe1(String text) {
        log.info("Subscribe1 " + text);
    }

    @JmsListener(destination = "${topics.sample}")
    public void subscribe2(String text) {
        log.info("Subscribe2 " + text);
    }

    @JmsListener(destination = "${topics.sample}")
    public void subscribe3(String text) {
        log.info("Subscribe3 " + text);
    }
}

启动项目就可以看到控制台输出: 发布订阅日志消息

这样验证了之前说的,监听同一个 topic 的所有 Subscriber 都会收到来自 Publisher 的消息。

6. 增加单元测试

添加测试之前,修改一下之前的生产者和消费者代码,让测试尽量简单

// 只保留一个 consumer
@Slf4j
@Service
public class Consumer {

    @JmsListener(destination = "${queues.sample}")
    public void receive(String text) {
        log.info("Consume " + text);
    }
}

// 删除掉 CommandLineRunner 接口
@Slf4j
@Service
public class Producer {

    @Value("${queues.sample}")
    public String sampleQueue;

    @Autowired
    private JmsMessagingTemplate jmsMessagingTemplate;

    public void send(String destination, String message) {
        log.info("============>>>>> 发布queue消息 " + message);
        this.jmsMessagingTemplate.convertAndSend(new ActiveMQQueue(destination), message);
    }
}

生产者/消费者 单元测试

import static org.assertj.core.api.Assertions.*;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.rule.OutputCapture;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * 生产者单元测试
 * Created by Blink on 1/11/2018 AD.
 *
 * @author Blink
 */
@RunWith(SpringRunner.class)
@SpringBootTest
@Import(Producer.class)
public class ProducerTest {

    @Autowired
    private Producer producer;

    @Rule
    public final OutputCapture capture = new OutputCapture();

    @Value("${queues.sample}")
    public String queue;

    /**
     * 期望 当生产者发布消息时 消息将会被接收
     *
     * @throws InterruptedException
     */
    @Test
    public void sendSimpleMessageShouldReceived() throws InterruptedException {
        this.producer.send(this.queue, "Test message");
        Thread.sleep(1000L);
        assertThat(this.capture.toString().contains("Consumer Test message")).isTrue();
    }

}

发布/订阅 单元测试

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.rule.OutputCapture;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;

/**
 * Publisher 单元测试
 * Created by Blink on 1/11/2018 AD.
 *
 * @author Blink
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class PublisherTest {

    @Rule
    public final OutputCapture capture = new OutputCapture();

    @Autowired
    private Publisher publisher;

    @Value("${topics.sample}")
    public String topic;

    /**
     * 期望 当发布者发送消息时 必须被所有订阅者接收到
     *
     * @throws InterruptedException
     */
    @Test
    public void sendSimpleMessageShouldReceivedByAllSubscribers() throws InterruptedException {
        this.publisher.publish(this.topic, "Test message");
        Thread.sleep(1000L);
        assertThat(this.capture.toString().contains("Subscribe1 Test message")).isTrue();
        assertThat(this.capture.toString().contains("Subscribe2 Test message")).isTrue();
        assertThat(this.capture.toString().contains("Subscribe3 Test message")).isTrue();
    }
}

运行单元测试的结果 测试结构

总结

本文开通过介绍 JMS、ActiveMQ ,让读者认识ActiveMQ, Spring Boot 集成 Apache ActiveMQ 的例子和使用场景,来介绍 Apache ActiveMQ 这款互联网架构解耦神器