20 消息驱动:如何理解 Spring 中对消息处理机制的抽象过程?
从今天开始,我们将进入到 Spring Cloud 中与消息处理机制相关内容的介绍。Spring Cloud 专门提供了一个 Spring Cloud Stream 框架来实现事件驱动架构,并完成与主流消息中间件的集成。同时,Spring Cloud Stream 背后也整合了 Spring 家族中的消息处理和消息总线方面的几个框架,可以说是 Spring Cloud 中整合程度最高的一个开发框架。
SpringHealth 中的事件驱动架构
在微服务设计和开发过程中经常会存在这样的需求:系统中的某个服务会因为用户操作或内部行为发布一个事件,该服务知道这个事件在将来的某一个时间点会被其他服务所消费,但是并不知道这个服务具体是谁、也不关心什么时候被消费。同样,消费该事件的服务也不一定需要知道该事件是由哪个服务所发布。如下图所示:
事件发送和消费示意图
在上图中,事件生产者和消费者之间的虚线代表的是一种相互松散、没有直接调用的关联关系。满足以上特性的系统代表着一种松耦合的架构,通常被称为事件驱动架构,而这里的事件也可以被理解是服务与服务之间发送的一种消息。事件驱动架构本质上是一种架构设计风格,实现方法和工具有很多。在 Spring Cloud 家族中这个工具就是 Spring Cloud Stream。在接下来的内容中,我们将结合 SpringHealth 案例来分析事件驱动架构的实现需求以及在微服务架构中的应用。
在微服务系统中引入事件驱动架构的主要目的在于提升系统的扩展性。所谓扩展性,举例来说,就是在向现有系统中添加新业务时,不需要改变原有的各个组件,而只需把新业务封闭在一个新的组件中就能完成整体业务的升级,我们认为这样的系统就具有较好的可扩展性。
让我们回到 SpringHealth 系统,在我们的案例中存在健康干预相关的业务场景,常见的健康干预涉及用户、设备和健康干预自身信息维护等功能,而 SpringHealth 分别提取了 user-service、device-service 和 intervention-service 这三个微服务。显然,这三个服务之间需要进行服务之间的调用和协调从而完成业务闭环。如果在不久的将来,SpringHealth 中需要引入其他服务才能形成完整的业务流程,那么这个业务闭环背后的交互模式就需要进行相应的调整。
一般而言,类似 SpringHealth 这样的系统中的用户信息变动并不会太频繁,所以很多时候我们会想到通过缓存来存放用户信息,并在健康干预处理过程中直接从缓存中获取所需的用户信息。在这样的设计和实现方式下,试想一旦某个用户信息发生变化,我们应该如何正确和高效的应对这一场景?
考虑到系统扩展性,显然在 intervention-service 中直接通过访问 user-service 实时获取用户信息的服务交互模式并不是一个好的选择,因为用户信息更新的时机我们无法事先预知,而事件驱动架构为我们提供了一种更好的实现方案。当用户信息变更时,user-service 可以发送一个事件,该事件表明了某个用户信息已经发生了变化,并将传递到所有对该事件感兴趣的微服务,这些微服务会根据自身的业务逻辑来消费这一事件。通过这种方式,某个特定服务就可以获取用户信息变更事件从而正确且高效的更新缓存信息。基于这种设计思想,该场景下交互示意图如下所示:
用户信息更新场景中的事件驱动架构
在上图中,我们看到了有 consumer-service1 和 consumer-service2 这两个消费者服务,事件处理架构的优势就在于当系统中需要添加新的用户信息变更事件处理逻辑来完成整个流程时,我们只需要对该事件添加一个新的 consumer-service2 即可,而不需要对原有的 consumer-service1 中的处理流程做任何修改。这在应对系统扩展性上有很大的优势。
针对上图,在技术上实现上,我们可以使用主流的消息中间件来实现消息的发布与消费,常见的包括 ActiveMQ、RabbitMQ、Kafka 等。这些消息中间件的核心功能就是能够将所收到的消息存储起来并进行转发。有了存储转发机制之后,就可以做到消息发布者和消费者相互独立。关于各个消息中间件的介绍不是本课程的重点,而在 Spring Cloud Stream 中集成了 RabbitMQ 和 Kafka,我们会在下一课时中进行详细展开。在此之前,我们有必要对 Spring 家族中的消息处理机制做一个展开,因为 Spring Cloud Stream 正是构建在 Spring 消息处理机制之上。
Spring 家族中的消息处理机制
在了解了事件驱动架构以及消息中间件的基本概念之后,我们来看一下 Spring 中针对这些概念提供的技术解决方案。在 Spring 家族中,与消息处理机制相关的框架有三个。事实上,本课程要介绍的 Spring Cloud Stream 是基于 Spring Integration 实现了消息发布和消费机制并提供了一层封装,很多关于消息发布和消费的概念和实现方法本质上都是依赖于 Spring Integration。而在 Spring Integration 的背后,则依赖于 Spring Messaging 组件来实现消息处理机制的基础设施。这三个框架之间的依赖关系如下图所示:
Spring 家族中三大消息处理相关框架关系图
接下来的内容,我们先来对位于底层的 Spring Messaging 和 Spring Integration 框架做一些展开,方便你在使用 Spring Cloud Stream 时对其背后的实现原理有更好的理解。
Spring Messaging
Spring Messaging 是 Spring 框架中的一个底层模块,用于提供统一的消息编程模型。例如,消息这个数据单元在 Spring Messaging 中统一定义为如下所示的 Message 接口,包括一个消息头 Header 和一个消息体 Payload:
public interface Message<T> {
T getPayload();
MessageHeaders getHeaders();
}
public interface Message<T> {
T getPayload();
MessageHeaders getHeaders();
}
而消息通道 MessageChannel 的定义也比较简单,我们可以调用 send() 方法将消息发送至该消息通道中,MessageChannel 接口定义如下所示:
public interface MessageChannel {
long INDEFINITE_TIMEOUT = -1;
default boolean send(Message<?> message) {
return send(message, INDEFINITE_TIMEOUT);
}
boolean send(Message<?> message, long timeout);
}
public interface MessageChannel {
long INDEFINITE_TIMEOUT = -1;
default boolean send(Message<?> message) {
return send(message, INDEFINITE_TIMEOUT);
}
boolean send(Message<?> message, long timeout);
}
消息通道的概念比较抽象,可以简单把它理解为是对队列的一种抽象。我们知道在消息传递系统中,队列的作用就是实现存储转发的媒介,消息发布者所生成的消息都将保存在队列中并由消息消费者进行消费。通道的名称对应的就是队列的名称,但是作为一种抽象和封装,各个消息传递系统所特有的队列概念并不会直接暴露在业务代码中,而是通过通道来对队列进行配置。
Spring Messaging 把通道抽象成如下所示的两种基本表现形式,即支持轮询的 PollableChannel 和实现发布-订阅模式的 SubscribableChannel,这两个通道都继承自具有消息发送功能的 MessageChannel:
public interface PollableChannel extends MessageChannel {
Message<?> receive();
Message<?> receive(long timeout);
}
public interface SubscribableChannel extends MessageChannel {
boolean subscribe(MessageHandler handler);
boolean unsubscribe(MessageHandler handler);
}
public interface PollableChannel extends MessageChannel {
Message<?> receive();
Message<?> receive(long timeout);
}
public interface SubscribableChannel extends MessageChannel {
boolean subscribe(MessageHandler handler);
boolean unsubscribe(MessageHandler handler);
}
我们注意到对于 PollableChannel 而言才有 receive 的概念,代表这是通过轮询操作主动获取消息的过程。而 SubscribableChannel 则是通过注册回调函数 MessageHandler 来实现事件响应。MessageHandler 接口定义如下:
public interface MessageHandler {
void handleMessage(Message<?> message) throws MessagingException;
}
public interface MessageHandler {
void handleMessage(Message<?> message) throws MessagingException;
}
Spring Messaging 在基础消息模型之上还提供了很多方便在业务系统中使用消息传递机制的辅助功能,例如各种消息体内容转换器 MessageConverter 以及消息通道拦截器 ChannelInterceptor 等,这里不做展开,你可以参考官方文档做进一步了解。
Spring Integration
Spring Integration 是对 Spring Messaging 的扩展,提供了对系统集成领域的经典著作《企业集成模式:设计构建及部署消息传递解决方案》中所描述的各种企业集成模式的支持,通常被认为是一种企业服务总线 ESB 框架。
在 Spring Messaging 的基础上,Spring Integration 还实现了其他几种有用的通道,包括支持阻塞式队列的 RendezvousChannel,该通道与带缓存的 QueueChannel 都属于点对点通道,但只有在前一个消息被消费之后才能发送下一个消息。PriorityChannel 即优先级队列,而 DirectChannel 是 Spring Integration 的默认通道,该通道的消息发送和接收过程处于同一线程中。另外还有 ExecutorChannel,使用基于多线程的 TaskExecutor 来异步消费通道中的消息。
Spring Integration 的设计目的是系统集成,因此内部提供了大量的集成化端点方便应用程序直接使用。当各个异构系统之间进行集成时,如何屏蔽各种技术体系所带来的差异性,Spring Integration 为我们提供了解决方案。通过通道之间的消息传递,在消息的入口和出口我们可以使用通道适配器和消息网关这两种典型的端点对消息进行同构化处理。Spring Integration 提供的常见集成端点包括 File、FTP、TCP/UDP、HTTP、JDBC、JMS、AMQP、JPA、Mail、MongoDB、Redis、RMI、Web Services 等。
Spring Integration 的功能非常强大,本课程无意对所有这些功能做过多阐述。在下一课时介绍 Spring Cloud Stream 的基本架构时我们会对 Spring Integration 做更详细的介绍。
小结与预告
本课时引入了消息传递机制来应对系统开发中所需要实现的事件驱动架构,而在 Spring Cloud 中也存在强大的 Spring Cloud Stream 框架完成对主流消息中间件的平台化集成。注意到该框架同时也是对 Spring Messaging 和 Spring Intergration 这两个 Spring 家族中消息处理框架的封装,这些都是我们理解并正确使用 Spring Cloud Stream 的前提。
这里给你留一道思考题:在 Spring 家族中存在哪些框架可以用来实现消息处理,而 Spring Cloud Stream 与这些框架又是什么样的关系?
在引入 Spring Cloud Stream 框架之后,下一课时我们将关注于该框架的基本架构。在架构设计上,Spring Cloud Stream 中所包含的理念和实现技巧同样值得我们学习和应用。
21 消息架构:如何把握 Spring Cloud Stream 的基本架构?
上一课时中,我们介绍了事件驱动架构的基本原理,以及 Spring 中对消息传递机制的抽象和对应的开发框架。要想在 SpringHealth 案例系统中添加消息发送和接收的效果有很多种实现方法,我们完全可以直接使用诸如 RabbitMQ、Kafka 等消息中间件来实现消息传递,这种解决方案的主要问题在于需要开发人员考虑不同框架的使用方式以及框架之间存在的功能差异性。而 Spring Cloud Stream 则不同,它在内部整合了多款主流的消息中间件,为开发人员提供了一个平台型解决方案,从而屏蔽各个消息中间件在技术实现上的差异。在今天的内容中,我将首先介绍 Spring Cloud Stream 的基本架构,并给出它与目前主流的各种消息中间件之间的整合机制。
Spring Cloud Stream 基本架构
Spring Cloud Stream 对整个消息发布和消费过程做了高度抽象,并提供了一系列核心组件。我们先介绍通过 Spring Cloud Stream 构建消息传递机制的基本工作流程。区别于直接使用 RabbitMQ、Kafka 等消息中间件,Spring Cloud Stream 在消息生产者和消费者之间添加了一种桥梁机制,所有的消息都将通过 Spring Cloud Stream 进行发送和接收,如下图所示:
Spring Cloud Stream 工作流程图
在上图中,我们不难看出 Spring Cloud Stream 具备四个核心组件,分别是 Binder、Channel、Source 和 Sink,其中 Binder 和 Channel 成对出现,而 Source 和 Sink 分别面向消息的发布者和消费者。
- Source 和 Sink
在 Spring Cloud Stream 中,Source 组件是真正生成消息的组件,相当于是一个输出(Output)组件。而 Sink 则是真正消费消息的组件,相当于是一个输入(Input)组件。根据我们对事件驱动架构的了解,对于同一个 Source 组件而言,不同的微服务可能会实现不同的 Sink 组件,分别根据自身需求进行业务上的处理。
在 Spring Cloud Stream 中,Source 组件使用一个普通的 POJO 对象来充当需要发布的消息,通过将该对象进行序列化(默认的序列化方式是 JSON)然后发布到 Channel 中。另一方面,Sink 组件监听 Channel 并等待消息的到来,一旦有可用消息,Sink 将该消息反序列化为一个 POJO 对象并用于处理业务逻辑。
- Channel
Channel 的概念比较容易理解,就是常见的通道,是对队列的一种抽象。根据上一课时所讨论的结果,我们知道在消息传递系统中,队列的作用就是实现存储转发的媒介,消息生产者所生成的消息都将保存在队列中并由消息消费者进行消费。通道的名称对应的往往就是队列的名称。
- Binder
Spring Cloud Stream 中最重要的概念就是 Binder。所谓 Binder,顾名思义就是一种黏合剂,将业务服务与消息传递系统黏合在一起。通过 Binder,我们可以很方便地连接消息中间件,可以动态的改变消息的目标地址、发送方式而不需要了解其背后的各种消息中间件在实现上的差异。关于 Binder 是如何与不同的消息中间件进行整合的实现原理我们在后续课程中会有源码级的专题进行讨论。
Spring Cloud Stream 集成 Spring 消息处理机制
结合上一课时中了解到的关于 Spring Messaging 和 Spring Integration 的相关概念,我们就不难理解 Spring Cloud Stream 中关于 Source 和 Sink 的定义。Source 和 Sink 都是接口,其中 Source 接口的定义如下
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
public interface Source {
String OUTPUT = "output";
@Output(Source.OUTPUT)
MessageChannel output();
}
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
public interface Source {
String OUTPUT = "output";
@Output(Source.OUTPUT)
MessageChannel output();
}
注意到这里通过 MessageChannel 来发送消息,而 MessageChannel 类来自 Spring Messaging 组件。我们在 MessageChannel 上发现了一个 @Output 注解,该注解定义了一个输出通道。
类似的,Sink 接口定义如下:
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.messaging.SubscribableChannel;
public interface Sink{
String INPUT = "input";
@Input(Sink.INPUT)
SubscribableChannel input();
}
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.messaging.SubscribableChannel;
public interface Sink{
String INPUT = "input";
@Input(Sink.INPUT)
SubscribableChannel input();
}
同样,这里通过 Spring Messaging 中的 SubscribableChannel 来实现消息接收,而 @Input 注解定义了一个输入通道。
注意到 @Input 和 @Output 注解使用通道名称作为参数,如果没有名称,会使用带注解的方法名字作为参数,也就是默认情况下分别使用“input”和“output”作为通道名称。从这个角度讲,一个 Spring Cloud Stream 应用程序中的 Input 和 Output 通道数量和名称都是可以任意设置的,我们只需要在这些通道的定义上添加 @Input 和 @Output 注解即可。例如在如下所示的代码中,我们定义了 SpringHealthChannel 接口并声明了一个 Input 通道和两个 Output 通道,说明使用该通道的服务会从外部的一个通道中获取消息并向外部的两个通道发送消息:
public interface SpringHealthChannel {
@Input
SubscribableChannel input1();
@Output
MessageChannel output1();
@Output
MessageChannel output2();
}
public interface SpringHealthChannel {
@Input
SubscribableChannel input1();
@Output
MessageChannel output1();
@Output
MessageChannel output2();
}
可以看到上述接口定义中同时使用到了 Spring Messaging 中的 SubscribableChannel 和 MessageChannel。Spring Cloud Stream 对 Spring Messaging 和 Spring Integration 提供了原生支持。在常规情况下,我们不需要使用这些框架中提供的API就能完成常见的开发需求。但如果确实有需要,我们也可以使用更为底层 API 直接操控消息发布和接收过程。
Spring Cloud Stream 集成消息中间件
对于 Spring Cloud Stream 而言,最核心的无疑是 Binder 组件。Binder 组件是服务与消息中间件之间的一层抽象,但我们知道各种消息中间件在消息传递机制的设计和实现上存在一定的差异性,那么 Spring Cloud Stream 如何屏蔽这些差异性从而打造自身的消息模型呢?在接下来的内容中,我们将梳理 Spring Cloud Stream 中的消息传递模型,并给出 Binder 与消息中间件如何进行整合的过程。
Spring Cloud Stream 中的消息传递模型
Spring Cloud Stream 将消息发布和消费抽象成如下三个核心概念,并结合目前主流的一些消息中间件对这些概念提供了统一的实现方式。
- 发布-订阅模型
我们知道点对点模型和发布-订阅模型是传统消息传递系统的两大基本模型,其中点对点模型实际上可以被视为发布-订阅模型在订阅者数量为 1 时的一种特例。因此,在 Spring Cloud Stream 中,统一通过发布-订阅模型完成消息的发布和消费,如下所示:
消息发布-订阅模型示意图
- 消费者组
设计消费者组(Consumer Group)的目的是应对集群环境下的多服务实例问题。显然,如果采用发布-订阅模式就会导致一个服务的不同实例都消费到了同一条消息。为了解决这个问题,Spring Cloud Stream 中提供了消费者组的概念。一旦使用了消费组,一条消息就只能被同一个组中的某一个服务实例所消费。消费者的基本结构如下图所示(其中虚线表示不会发生的消费场景):
消费者组结构示意图
- 消息分区
假如我们希望相同的消息都被同一个微服务实例来处理,但又有多个服务实例组成了负载均衡结构,那么通过上述的消费组概念仍然不能满足要求。针对这一场景,Spring Cloud Stream 又引入了消息分区(Partition)的概念。引入分区概念的意义在于,同一分区中的消息能够确保始终是由同一个消费者实例进行消费。尽管消息分区的应用场景并没有那么广泛,但如果想要达到类似的效果,Spring Cloud Stream 也为我们提供了一种简单的实现方案,消息分区的基本结构如下图所示:
消息分区结构示意图
Binder 与消息中间件
Binder 组件本质上是一个中间层,负责与各种消息中间件的交互。目前,Spring Cloud Stream 中集成的消息中间件包括 RabbitMQ和Kafka。在具体介绍如何使用 Spring Cloud Stream 进行消息发布和消费之前,我们先来结合消息传递机制给出 Binder 对这两种不同消息中间件的整合方式。
- RabbitMQ
RabbitMQ 是 AMQP(Advanced Message Queuing Protocol,高级消息队列协议)协议的典型实现框架。在 RabbitMQ 中,核心概念是交换器(Exchange)。我们可以通过控制交换器与队列之间的路由规则来实现对消息的存储转发、点对点、发布-订阅等消息传递模型。在一个 RabbitMQ 中可能会存在多个队列,交换器如果想要把消息发送到具体某一个队列,就需要通过两者之间的绑定规则来设置路由信息。路由信息的设置是开发人员操控 RabbitMQ 的主要手段,而路由过程的执行依赖于消息头中的路由键(Routing Key)属性。交换器会检查路由键并结合路由算法来决定将消息路由到哪个队列中去。下图就是交换器与队列之间的路由关系图:
RabbitMQ 中交换器与队列的路由关系图
可以看到一条来自生产者的消息通过交换器中的路由算法可以发送给一个或多个队列,从而分别实现点对点和发布订阅功能。同时,我们基于上图也不难得出消费者组的实现方案。因为 RabbitMQ 里每个队列是被消费者竞争消费的,所以指定同一个组的消费者消费同一个队列就可以实现消费者组。
- Kafka
从架构上讲,在 Kafka 中,生产者使用推模式将消息发布到服务器,而消费者使用拉模式从服务器订阅消息。在 Kafka 中还使用到了 Zookeeper,其作用在于实现服务器与消费者之间的负载均衡,所以启动 Kafka 之前必须确保 Zookeeper 正常运行。同时,Kafka 也实现了消费者组机制,如下图所示:
Kafka 消费者分组
可以看到多个消费者构成了一种组结构,消息只能传输给某个组中的某一个消费者。也就是说,Kafka 中消息的消费具有显式的分布式特性,天生就内置了 Spring Cloud Stream 中的消费组概念。
在 SpringHealth 案例中,我们将同时基于 RabbitMQ 和 Kafka 来展示 Spring Cloud Stream 的各项功能特性。
小结与预告
Spring Cloud Stream 是 Spring Cloud 中针对消息处理的一款平台型框架,该框架的核心优势在于在内部集成了 RabbitMQ、Kafka 等主流消息中间件,而对外则提供了统一的 API 接入层。通过今天的课程,我们知道 Spring Cloud Stream 是通过 Binder 来实现了这一目标。同时,针对消息处理场景下的消费者分组、消息分区等需求,该框架也内置了抽象层并完成与不同消息中间件之间的整合。
这里给你留一道思考题:在 Spring Cloud Stream 中,消费者组和消费分区分别用于解决什么应用场景?
在明确了 Spring Cloud Stream 的基本架构之后,在接下来的两个课时中,我们将介绍如何使用它来实现消息发布者和消费者的详细步骤和方法。
22 消息发布:如何使用 Spring Cloud Stream 实现消息发布者和消费者?(上)
从上一课时的内容中,我们对 Spring Cloud Stream 的基本架构有了全面的了解。今天,就让我们回到案例,来看看如何使用 Spring Cloud Stream 来完成消息发布者和消费者的构建。
设计 SpringHealth 中的消息发布场景
在《消息驱动:如何理解 Spring 中对消息处理机制的抽象过程?》课时中,我们已经给出了在 SpringHealth 案例系统中应用消息处理机制的一个典型场景。类似 SpringHealth 这样的系统中的用户信息变动并不会太频繁,所以很多时候我们会想到通过缓存系统来存放用户信息。而一旦用户信息发生变化,user-service 可以发送一个事件,给到相关的订阅者并更新缓存信息,如下图所示:
用户信息更新场景中的事件驱动架构
一般而言,事件在命名上通常采用过去时态以表示该事件所代表的动作已经发生。所以,我们把这里的用户信息变更事件命名为 UserInfoChangedEvent。通常,我们也会建议使用一个独立的事件消费者来订阅这个事件,就像上图中的 consumer-service1 一样。但为了保持 SpringHealth 系统的简单性,我们不想再单独构建一个微服务,而是选择把事件订阅和消费的相关功能同样放在了 intervention-service 中,如下图所示:
简化之后的用户信息更新场景处理流程
接下来我们关注于上图中的事件发布者 user-service。在 user-service 中需要设计并实现使用 Spring Cloud Stream 发布消息的各个组件,包括 Source、Channel 和 Binder。我们围绕 UserInfoChangedEvent 事件给出 user-service 内部的整个实现流程,如下图所示:
user-service 消息发布实现流程
在 user-service 中,势必会存在一个对用户信息的修改操作,这个修改操作会上图中的触发 UserInfoChangedEvent 事件,然后该事件将被构建成一个消息并通过 UserInfoChangedSource 进行发送。UserInfoChangedSource 就是一种 Spring Cloud Stream 中的具体 Source 实现。然后 UserInfoChangedSource 使用默认的名为“output”的 Channel 进行消息发布。在案例中,我们将同时演示 Kafka 和 RabbitMQ,所以 Binder 组件分别封装了这两个消息中间件。
实现消息发布者
站在消息处理的角度讲,这个消息发布流程并不复杂,主要的实现过程是如何使用 Spring Cloud Stream 完成 Source 组件的创建、Binder 组件的配置以及如何与 user-service 进行集成,让我们一起来看一下。
使用 @EnableBinding 注解
无论是消息发布者还是消息消费者,首先都需要引入 spring-cloud-stream 依赖,如下所示:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
而在 SpringHealth 案例中,如果我们使用 Kafka 作为我们的消息中间件系统,那么也需要引入 spring-cloud-starter-stream-kafka 依赖,如下所示。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-kafka</artifactId>
</dependency>
对应的,RabbitMQ 就需要引入 spring-cloud-starter-stream-rabbit 依赖,如下所示:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
对于消息发布者而言,它在 Spring Cloud Stream 体系中扮演着 Source 的角色,所以我们需要在 user-service 的 Bootstrap 类中标明这个 SpringBoot 应用程序是一个 Source 组件。调整之后的 UserApplication 类如下所示。
@SpringCloudApplication
@EnableBinding(Source.class)
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
@SpringCloudApplication
@EnableBinding(Source.class)
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
可以看到,我们在原有 UserApplication 上我们添加了一个 @EnableBinding(Source.class) 注解,该注解的作用就是告诉 Spring Cloud Stream 这个 Spring Boot 应用程序是一个消息发布者,需要绑定到消息中间件,实现两者之间的连接。@EnableBinding 注解定义比较简单,如下所示:
public @interface EnableBinding {
Class<?>[] value() default {};
}
public @interface EnableBinding {
Class<?>[] value() default {};
}
我们可以使用一个或者多个接口作为该注解的参数。在上面的代码中,我们使用了 Source 接口,表示与消息中间件绑定的是一个消息发布者。在下一课时中,我们在介绍消息消费者时同样也会使用到这个 @EnableBinding 注解。
定义 Event
接下来,需要给出 UserInfoChangedEvent 的定义。对于事件的定义也存在一些通用的做法,事件类型、事件所对应的操作、事件对应的业务领域对象等是一个完整事件定义所必需的元素。因此,我们将 UserInfoChangedEvent 定义如下:
public class UserInfoChangedEvent{
//事件类型
private String type;
//事件所对应的操作
private String operation;
//事件对应的领域模型
private User user;
}
public class UserInfoChangedEvent{
//事件类型
private String type;
//事件所对应的操作
private String operation;
//事件对应的领域模型
private User user;
}
定义完事件的数据结构之后,接下来我们就需要通过 Source 接口来具体实现消息的发布。
创建 Source
在 Spring Cloud Stream 中,Source 是一个接口,包含了一个发送消息的 MessageChannel,让我们简单回顾一下该接口的定义,如下所示:
public interface Source {
String OUTPUT = "output";
@Output(Source.OUTPUT)
MessageChannel output();
}
public interface Source {
String OUTPUT = "output";
@Output(Source.OUTPUT)
MessageChannel output();
}
使用这个接口的方式也很简单,我们只需要在业务代码中直接进行注入即可,就像在使用一个普通的 Javabean 一样。完整的 UserInfoChangedSource 类如下所示:
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.support.MessageBuilder;
…
@Component
public class UserInfoChangedSource {
private Source source;
private static final Logger logger = LoggerFactory.getLogger(UserInfoChangedSource.class);
@Autowired
public UserInfoChangedSource(Source source){
this.source = source;
}
private void publishUserInfoChangedEvent(UserInfoOperation operation, User user){
logger.debug("Sending message for UserId: {}", user.getId());
UserInfoChangedEvent change = new UserInfoChangedEvent(
UserInfoChangedEvent.class.getTypeName(),
operation.toString(),
user);
source.output().send(MessageBuilder.withPayload(change).build());
}
public void publishUserInfoAddedEvent(User user) {
publishUserInfoChangedEvent(UserInfoOperation.ADD, user);
}
public void publishUserInfoUpdatedEvent(User user) {
publishUserInfoChangedEvent(UserInfoOperation.UPDATE, user);
}
public void publishUserInfoDeletedEvent(User user) {
publishUserInfoChangedEvent(UserInfoOperation.DELETE, user);
}
}
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.support.MessageBuilder;
…
@Component
public class UserInfoChangedSource {
private Source source;
private static final Logger logger = LoggerFactory.getLogger(UserInfoChangedSource.class);
@Autowired
public UserInfoChangedSource(Source source){
this.source = source;
}
private void publishUserInfoChangedEvent(UserInfoOperation operation, User user){
logger.debug("Sending message for UserId: {}", user.getId());
UserInfoChangedEvent change = new UserInfoChangedEvent(
UserInfoChangedEvent.class.getTypeName(),
operation.toString(),
user);
source.output().send(MessageBuilder.withPayload(change).build());
}
public void publishUserInfoAddedEvent(User user) {
publishUserInfoChangedEvent(UserInfoOperation.ADD, user);
}
public void publishUserInfoUpdatedEvent(User user) {
publishUserInfoChangedEvent(UserInfoOperation.UPDATE, user);
}
public void publishUserInfoDeletedEvent(User user) {
publishUserInfoChangedEvent(UserInfoOperation.DELETE, user);
}
}
可以看到我们创建了一个 publishUserInfoChangedEvent 方法,在该方法中,我们首先构建了 UserInfoChangedEvent 事件并通过 Spring Messaging 模块所提供的 MessageBuilder 工具类将它转换为消息中间件所能发送的Message对象。然后,我们调用 Source 接口的 output() 方法将事件发送出去,这里的 output() 方法背后使用的就是一个具体的 MessageChannel。
配置 Binder
为了通过 UserInfoChangedSource 将代表 UserInfoChangedEvent 的消息发送到正确的地址,我们需要在 application.yml 配置文件中配置 Binder 信息。Binder 信息中存在一些通用的配置项,例如如果要想把消息发布到消息中间件,就需要知道消息所发送的通道或者说目的地 Destination,以及序列化方式,如下所示:
spring:
cloud:
stream:
bindings:
output:
destination: userInfoChangedTopic
content-type: application/json
另一方面,因为 Binder 完成了与具体消息中间件的整合过程,所以需要针对特定的消息中间件来提供专门的配置项。我们先来看在使用 Kafka 的场景下 Binder 的配置方法,相关配置项如下所示:
spring:
cloud:
stream:
bindings:
output:
destination: userInfoChangedTopic
content-type: application/json
kafka:
binder:
zk-nodes: localhost
brokers: localhost
在以上配置项中,除了前面介绍的通用配置型之外,因为 Kafka 的运行依赖于 Zookeeper,所以“kafka”配置段使用 Kafka 作为消息中间件平台,并将其 Zookeeper 地址以及 Kafka 自身的地址都指向了本地。
相比 Kafka,RabbitMQ 的配置稍微复杂一点,如下所示:
spring:
cloud:
stream:
bindings:
default:
content-type: application/json
binder: rabbitmq
output:
destination: userInfoChangedExchange
contentType: application/json
binders:
rabbitmq:
type: rabbit
environment:
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtual-host: /
在以上配置项中,我们设置了 destination为userInfoChangedExchange 后会在 RabbitMQ 中创建一个名为“userInfoChangedExchange”的交换器,并把 Spring Cloud Stream 的消息输出通道绑定到该交换器。同时,我们在 bindings 配置段中指定了一个“default”子配置段,用于指定默认所使用的 binder。在这个示例中,我们将这个默认 binder 命名为“rabbitmq”并在“binders”配置段中指定了运行 RabbitMQ 的相关参数。请注意 RabbitMQ 和 Kafka 这两款消息中间件在配置方式上各个配置项的层级以及内容上的差别。
集成服务
最后,我们要做的事情就是在 user-service 中集成消息发布功能。在前一版本的 UserService 类的基础之上,我们添加对 UserInfoChangedSource 的使用过程,如下所示:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private UserInfoChangedSource userInfoChangedSource;
public User getUserById(Long userId) {
return userRepository.findById(userId).orElse(null);
}
public User getUserByUserName(String userName) {
return userRepository.findUserByUserName(userName);
}
public void addUser(User user){
userRepository.save(user);
userInfoChangedSource.publishUserInfoAddedEvent(user);
}
public void updateUser(User user){
userRepository.save(user);
userInfoChangedSource.publishUserInfoUpdatedEvent(user);
}
public void deleteUser(User user){
userRepository.delete(user);
userInfoChangedSource.publishUserInfoDeletedEvent(user);
}
}
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private UserInfoChangedSource userInfoChangedSource;
public User getUserById(Long userId) {
return userRepository.findById(userId).orElse(null);
}
public User getUserByUserName(String userName) {
return userRepository.findUserByUserName(userName);
}
public void addUser(User user){
userRepository.save(user);
userInfoChangedSource.publishUserInfoAddedEvent(user);
}
public void updateUser(User user){
userRepository.save(user);
userInfoChangedSource.publishUserInfoUpdatedEvent(user);
}
public void deleteUser(User user){
userRepository.delete(user);
userInfoChangedSource.publishUserInfoDeletedEvent(user);
}
}
可以看到,我们在增加、修改和删除用户操作时都添加了发布用户信息变更事件的机制。注意到在 UserService 中我们并没有构建具体的 UserInfoChangedEvent 事件,而是把这部分操作放在了 UserInfoChangedSource中,目的也是为了降低各个层次之间的依赖关系,并封装对事件的统一操作。
至此,完整的消息发布者实现完毕。接下来,我们来看看消息消费场景应该如何进行设计。
设计 SpringHealth 中的消息消费场景
我们继续讨论 SpringHealth 案例,根据整个消息交互流程,intervention-service 就是 UserInfoChangedEvent 事件的消费者。作为该事件的消费者,intervention-service 需要把变更后的用户信息更新到缓存中。
在 Spring Cloud Stream 中,负责消费消息的是 Sink 组件,因此,我们同样围绕 UserInfoChangedEvent 事件给出 intervention-service 内部的整个实现流程,如下图所示:
intervention-service 消息消费实现流程
在上图中,UserInfoChangedEvent 事件通过消息中间件发送到 Spring Cloud Stream 中,Spring Cloud Stream 通过 Sink 获取消息并交由 UserInfoChangedSink 实现具体的消费逻辑。可以想象在这个 UserInfoChangedSink 中会负责实现缓存相关的处理逻辑。
让我们把消息消费过程与 intervention-service 中的业务流程串联起来。我们知道在 intervention-service 中存在 UserServiceClient 类,其核心方法 getUserByUserName 如下所示:
public UserMapper getUserByUserName(String userName){
ResponseEntity<UserMapper> restExchange =
restTemplate.exchange(
"http://zuulservice:5555/springhealth/user/users/username/{userName}",
HttpMethod.GET,
null, UserMapper.class, userName);
UserMapper user = restExchange.getBody();
return user;
}
public UserMapper getUserByUserName(String userName){
ResponseEntity<UserMapper> restExchange =
restTemplate.exchange(
"http://zuulservice:5555/springhealth/user/users/username/{userName}",
HttpMethod.GET,
null, UserMapper.class, userName);
UserMapper user = restExchange.getBody();
return user;
}
这里我们直接通过调用 user-service 远程获取 User 信息。我们知道用户账户信息变更是一个低频事件,而每次通过 UserServiceClient 实现远程调用的成本很高且没有必要。现在我们可以通过 Spring Cloud Stream 获取用户信息更新的消息了,UserServiceClient 就有了优化的空间。基本思路就是缓存用户信息,并通过消息触发缓存更新,然后我们先从缓存中获取用户信息,只有在缓存中找不到对应的用户信息时才会发起远程调用。下图展示了采用这一设计思想之后的流程图:
用户账户更新流程图
在上图中,我们看到 user-service 异步发送的 UserInfoChangedEvent 事件会被消费,该消息的处理器 UserInfoChangedSink 所消费,然后 UserInfoChangedSink 将更新后的用户账户信息进行缓存以供 intervertion-service 使用。显然,UserInfoChangedSink 是整个流程的关键。至于如何实现这个 UserInfoChangedSink,我们放在下一课时中进行详细展开并给出代码示例。
小结与预告
今天,我们基于用户信息更新这一特定业务场景,介绍了使用 Spring Cloud Stream 来完成对 SpringHealth 系统中消息发布消费流程的建模,并提供了针对消息发布者的实现过程。可以看到,只要理解了 Spring Cloud Stream 的基本架构,使用该框架发送消息的开发更多的是配置工作。
这里给你留一道思考题:在 Spring Cloud Stream 配置不同的 Binder 时,有哪些公共配置项,又有哪些是针对具体消息中间件的特定配置项?
下一课时将继续讨论基于 Spring Cloud Stream 的开发过程,我们关注于消息消费者的实现,以及自定义消息通道、消费者分组以及消息分区等高级主题的实现方式。
23 消息消费:如何使用 Spring Cloud Stream 实现消息发布者和消费者?(下)
在上一课时中,我们给出了 SpringHealth 案例中基于 Spring Cloud Stream 的消息发布场景以及实现方式,同时也给出了消息消费的应用场景。今天我们将延续上一课时的内容,来具体讲解如何在服务中添加消息消费者,以及使用各项消息消费的高级主题,并给出案例的运行效果。
在服务中添加消息消费者
在介绍消息消费者的具体实现方法之前,我们先来回顾消息消费的实现流程,如下图所示:
消息消费实现流程
针对上图中各个消费者组件的实现过程,我们采用与介绍发布者时相同的方式进行展开。首当其冲的还是要使用 @EnableBinding 注解。
使用 @EnableBinding 注解
与初始化消息发布环境一样,我们同样需要在 intervention-service 需要引入 spring-cloud-stream、spring-cloud-starter-stream-kafka 或 spring-cloud-starter-stream-rabbit 这几个Maven依赖,并构建 Bootstrap 类。intervention-service 中的 Bootstrap 类是 InterventionApplication,其代码如下所示:
@SpringCloudApplication
@EnableBinding(Sink.class)
public class InterventionApplication{
public static void main(String[] args) {
SpringApplication.run(InterventionApplication.class, args);
}
}
@SpringCloudApplication
@EnableBinding(Sink.class)
public class InterventionApplication{
public static void main(String[] args) {
SpringApplication.run(InterventionApplication.class, args);
}
}
显然,对于作为消息消费者的 Bootstrap 类而言,@EnableBinding 注解所绑定的应该是 Sink 接口。
创建 Sink
UserInfoChangedSink 负责处理具体的消息消费逻辑,代码如下所示:
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
...
public class UserInfoChangedSink {
@Autowired
private UserInfoRedisRepository userInfoRedisRepository;
private static final Logger logger = LoggerFactory.getLogger(UserInfoChangedSink.class);
@StreamListener("input")
public void handleChangedUserInfo(UserInfoChangedEventMapper userInfoChangedEventMapper) {
logger.debug("Received a message of type " + userInfoChangedEventMapper.getType());
logger.debug("Received a {} event from the user-service for user name {}",
userInfoChangedEventMapper.getOperation(),
userInfoChangedEventMapper.getUser().getUserName());
if(userInfoChangedEventMapper.getOperation().equals("ADD")) {
userInfoRedisRepository.saveUser(userInfoChangedEventMapper.getUser());
} else if(userInfoChangedEventMapper.getOperation().equals("UPDATE")) {
userInfoRedisRepository.updateUser(userInfoChangedEventMapper.getUser());
} else if(userInfoChangedEventMapper.getOperation().equals("DELETE")) {
userInfoRedisRepository.deleteUser(userInfoChangedEventMapper.getUser().getUserName());
} else {
logger.error("Received an UNKNOWN event from the user-service of type {}", userInfoChangedEventMapper.getType());
}
}
}
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
...
public class UserInfoChangedSink {
@Autowired
private UserInfoRedisRepository userInfoRedisRepository;
private static final Logger logger = LoggerFactory.getLogger(UserInfoChangedSink.class);
@StreamListener("input")
public void handleChangedUserInfo(UserInfoChangedEventMapper userInfoChangedEventMapper) {
logger.debug("Received a message of type " + userInfoChangedEventMapper.getType());
logger.debug("Received a {} event from the user-service for user name {}",
userInfoChangedEventMapper.getOperation(),
userInfoChangedEventMapper.getUser().getUserName());
if(userInfoChangedEventMapper.getOperation().equals("ADD")) {
userInfoRedisRepository.saveUser(userInfoChangedEventMapper.getUser());
} else if(userInfoChangedEventMapper.getOperation().equals("UPDATE")) {
userInfoRedisRepository.updateUser(userInfoChangedEventMapper.getUser());
} else if(userInfoChangedEventMapper.getOperation().equals("DELETE")) {
userInfoRedisRepository.deleteUser(userInfoChangedEventMapper.getUser().getUserName());
} else {
logger.error("Received an UNKNOWN event from the user-service of type {}", userInfoChangedEventMapper.getType());
}
}
}
这里引入了一个新的注解 @StreamListener,将该注解添加到某个方法上就可以使之接收处理流中的事件。在上面的例子中,@StreamListener 注解添加在了 handleChangedUserInfo() 方法上并指向了“input”通道,意味着所有流经“input”通道的消息都会交由这个 handleChangedUserInfo() 方法进行处理。
而在 handleChangedUserInfo() 方法中,我们调用 UserInfoRedisRepository 类完成各种缓存相关的处理。UserInfoRedisRepository 的实现代码参考如下:
@Repository
public class UserInfoRedisRepositoryImpl implements UserInfoRedisRepository {
private static final String HASH_NAME = "user";
private RedisTemplate<String, UserMapper> redisTemplate;
private HashOperations<String, String, UserMapper> hashOperations;
public UserInfoRedisRepositoryImpl() {
super();
}
@Autowired
private UserInfoRedisRepositoryImpl(RedisTemplate<String, UserMapper> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@PostConstruct
private void init() {
hashOperations = redisTemplate.opsForHash();
}
@Override
public void saveUser(UserMapper user) {
hashOperations.put(HASH_NAME, user.getUserName(), user);
}
@Override
public void updateUser(UserMapper user) {
hashOperations.put(HASH_NAME, user.getUserName(), user);
}
@Override
public void deleteUser(String userName) {
hashOperations.delete(HASH_NAME, userName);
}
@Override
public UserMapper findUserByUserName(String userName) {
return (UserMapper) hashOperations.get(HASH_NAME, userName);
}
}
@Repository
public class UserInfoRedisRepositoryImpl implements UserInfoRedisRepository {
private static final String HASH_NAME = "user";
private RedisTemplate<String, UserMapper> redisTemplate;
private HashOperations<String, String, UserMapper> hashOperations;
public UserInfoRedisRepositoryImpl() {
super();
}
@Autowired
private UserInfoRedisRepositoryImpl(RedisTemplate<String, UserMapper> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@PostConstruct
private void init() {
hashOperations = redisTemplate.opsForHash();
}
@Override
public void saveUser(UserMapper user) {
hashOperations.put(HASH_NAME, user.getUserName(), user);
}
@Override
public void updateUser(UserMapper user) {
hashOperations.put(HASH_NAME, user.getUserName(), user);
}
@Override
public void deleteUser(String userName) {
hashOperations.delete(HASH_NAME, userName);
}
@Override
public UserMapper findUserByUserName(String userName) {
return (UserMapper) hashOperations.get(HASH_NAME, userName);
}
}
这里,我们使用了 Spring Data 提供的 RedisTemplate 和 HashOperations 工具类来封装对Redis的数据操作。关于 Spring Data 的使用方法不是本课程的重点,你可以参考相关资料进行进一步了解。
配置 Binder
对于消息消费者而言,配置 Binder 的方式和消息发布者非常类似。如果使用默认的消息通道,那么我们只需要把用于发送的“output”通道改为接收的“input”通道就可以了。这里以 Kafka 为例,给出 Binder 的配置信息,如下所示:
spring:
cloud:
stream:
bindings:
input:
destination: userInfoChangedTopic
content-type: application/json
kafka:
binder:
zk-nodes: localhost
brokers: localhost
Spring Cloud Stream 高级主题
在分别介绍完消息发布者和消费者的基本实现过程之后,我们将在此基础上讨论 Spring Cloud Stream 的高级主题,包括自定义消息通道、使用消费者组以及消息分区。
自定义消息通道
在前面的示例中,无论是消息发布还是消息消费,我们都使用了 Spring Cloud Stream 中默认提供的通道名“output”和“input”。显然,在有些场景下,为了更好地管理系统中存在的所有通道,为通道进行命名是一项最佳实践,这点对于消息消费的场景尤为重要。在接下来的内容中,针对消息消费的场景,我们将不使用 Sink 组件默认提供的“input”通道,而是尝试通过自定义通道的方式来实现消息消费。
在 Spring Cloud Stream 中,实现一个面向消息消费场景的自定义通道的方法也非常简单,只需要定义一个新的接口,并在该接口中通过 @Input 注解声明一个新的 Channel 即可。例如我们可以定义一个新的 UserInfoChangedChannel 接口,然后通过 @Input 注解就可以声明一个“userInfoChangedChannel”通道,代码如下所示。
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.messaging.SubscribableChannel;
public interface UserInfoChangedChannel{
String USER_INFO = "userInfoChangedChannel";
@Input(UserInfoChangedChannel.USER_INFO)
SubscribableChannel userInfoChangedChannel();
}
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.messaging.SubscribableChannel;
public interface UserInfoChangedChannel{
String USER_INFO = "userInfoChangedChannel";
@Input(UserInfoChangedChannel.USER_INFO)
SubscribableChannel userInfoChangedChannel();
}
注意到该通道的类型为 Spring Intergration 中用于消费消息的 SubscribableChannel。同时,我们也注意到这个 UserInfoChangedChannel 的代码风格与 Spring Cloud Stream 自带的Sink接口完全一致。作为回顾,这里也给出 Sink 接口的定义,如下所示:
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.messaging.SubscribableChannel;
public interface Sink{
String INPUT = "input";
@Input(Sink.INPUT)
SubscribableChannel input();
}
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.messaging.SubscribableChannel;
public interface Sink{
String INPUT = "input";
@Input(Sink.INPUT)
SubscribableChannel input();
}
一旦我们完成了自定义的消息通信,就可以在 @StreamListener 注解中设置这个通道。以前面介绍的 UserInfoChangedSink 为例,添加了自定义通道之后的重构代码结构如下所示:
@EnableBinding(UserInfoChangedChannel.class)
public class UserInfoChangedSink{
@StreamListener(UserInfoChangedChannel.USER_INFO)
public void handleChangedUserInfo(UserInfoChangedEventMapper userInfoChangedEventMapper) {
…
}
}
@EnableBinding(UserInfoChangedChannel.class)
public class UserInfoChangedSink{
@StreamListener(UserInfoChangedChannel.USER_INFO)
public void handleChangedUserInfo(UserInfoChangedEventMapper userInfoChangedEventMapper) {
…
}
}
可以看到,这里我们继续使用 @EnableBinding 注解绑定了自定义的 UserInfoChangedChannel。因为 UserInfoChangedChannel 中通过 @Input 注解提供了“userInfoChangedChannel”通道,所以这种用法实际上和 @EnableBinding(Sink.class) 是完全一致的。因此,对于 Binder 的配置而言,我们要做的也只是调整通道的名称。再次以 Kafka 为例,重构后的 Binder 配置信息如下所示:
spring:
cloud:
stream:
bindings:
userInfoChangedChannel:
destination: userInfoChangedTopic
content-type: application/json
kafka:
binder:
zk-nodes: localhost
brokers: localhost
使用消费者分组
在微服务架构中,服务多实例部署的场景非常常见。在集群环境下,我们希望服务的不同实例被放置在竞争的消费者关系中,同一服务集群中只有一个实例能够处理给定消息。Spring Cloud Stream 提供的消费者分组可以很方便地实现这一需求,效果图如下所示:
intervention-service 消息分组效果示意图
在上图中,两个 intervention-service 实例构成了一个 interventionGroup。在这个 interventionGroup 中,UserInfoChangedEvent 事件只会被一个 intervention-service 实例所消费。
要想实现上图所示的消息消费效果,我们唯一要做的事情也是重构Binder配置,即在配置Binder时指定消费者分组信息即可,如下所示:
spring:
cloud:
stream:
bindings:
userInfoChangedChannel:
destination: userInfoChangedTopic
content-type: application/json
group: interventionGroup
kafka:
binder:
zk-nodes: localhost
brokers: localhost
以上基于Kafka的配置信息中,我们关注“bindings”段中的通道名称使用了自定义的“userInfoChangedChannel”,并且在该配置项中设置了“group”为“interventionGroup”。
使用消息分区
最后一项 Spring Cloud Stream 使用上的高级主题是使用消费分区。同样是在集群环境下,假设存在两个 intervention-service 实例,我们希望用户信息中 id 为单号的 UserInfoChangedEvent 始终由第一个 intervention-service 实例进行消费,而id为双号的 UserInfoChangedEvent 则始终由第二个 intervention-service 实例进行消费。基于类似这样的需求,我们就可以构建消息分区,如下所示:
intervention-service 消息分区效果示意图
要想实现上图所示的消息消费效果,我们唯一要做的事情还是重构 Binder 配置。这次以 RabbitMQ 为例给出示例配置,如下所示:
spring:
cloud:
stream:
bindings:
default:
content-type: application/json
binder: rabbitmq
output:
destination: userInfoChangedExchange
group: interventionGroup
producer:
partitionKeyExpression: payload.user.id % 2
partitionCount: 2
binders:
rabbitmq:
type: rabbit
environment:
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtual-host: /
首先,我们明确上述配置项针对的是消息发布者 Source 组件,因为我们看到了“output”配置项。注意到,我们指定了交换器和消费者分组分别为 “userInfoChangedExchange”和“interventionGroup”。同时,这里还出现了两个新的配置项“partitionKeyExpression”和“partitionCount”,这两个配置项就与消息分区有关。我们指定了“partitionKeyExpression”为“payload.user.id”,意味着 Spring Cloud Stream 会根据传入的 UserInfoChangedEvent 中的 User 对象的 id 对 2 进行取模操作。如果取模值为 1 表示只有分区Id为 1 的 intervention-service 能接收到该信息,如果是取模值为 0 表示只有分区 Id 为 2 的 intervention-service 能接收到该信息。显然,通过这样的分区策略,分区的数量“partitionCount”应该为 2。
对应的,作为消息消费者的 Sink 组件的配置项如下所示:
spring:
cloud:
stream:
bindings:
default:
content-type: application/json
binder: rabbitmq
input:
destination: userInfoChangedExchange
group: interventionGroup
consumer:
partitioned: true
instanceIndex: 0
instanceCount: 2
binders:
rabbitmq:
type: rabbit
environment:
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtual-host: /
上述配置中同样包含了分区信息,其中 partitioned=true 表示启用消息分区功能,instanceCount=2 表示消息分区的消费者节点数量为 2 个。特别要注意的是 instanceIndex 参数用来设置当前消费者实例的索引号。instanceIndex 是从 0 开始的,我们在这里就把当前服务实例的索引号为 0。显然我们在另外一个 intervention-service 实例中需要将 instanceIndex 设置为 1。
为了演示消息分区功能,我们需要运行一个 user-service 作为 Source 组件,以及两个独立的 intervention-service 作为 Sink 组件,从而构建一个完整的示例并给出运行时应用系统的控制台输出效果。两个独立的 Sink 组件就按照前面给出的分区策略进行消息的处理。然后在两个 Sink 组件的输出中,UserInfoChangedEvent 中 User 对象的 Id 成单双数交替出现。你可以自己做一些尝试和练习。
小结与预告
承接上一课时内容,今天我们继续讨论使用 Spring Cloud Stream 实现消息消费者的实现方法。同样,我们发现通过合理配置 Binder 组件,这一实现过程也比较简单。另一方面,Spring Cloud Stream 中还存在一些高级主题,例如自定义消息通道、消费者组以及消费分区,本课时同样也介绍了如何在 SpringHealth 案例系统中使用这些高级主题的方法。
这里给你留一道思考题:在 Spring Cloud Stream 中,如何配置消费者组和消费分区功能?
通过前面课程的学习,我们感受到了 Spring Cloud Stream 中 Binder 组件的强大功能。基于这个组件,我们可以使用同一套开发模式来分别集成 RabbitMQ 和 Kafka 等主流的消息中间件。介绍完消息发布和消费之后,我们有必要对 Binder 组件的内部实现机制做深入分析,这就是下一课时的内容。
24 消息集成:如何剖析 Spring Cloud Stream 集成消息中间件的实现原理?
Spring Cloud Stream 中的内容比较多,今天我们重点关注的是如何实现 Spring Cloud Stream 与其他消息中间件的整合过程,因此只介绍消息发送和接收的主流程。我们将分别从Spring Cloud Stream以及消息中间件的角度出发,分析如何基于这一主流程,完成两者之间的无缝集成。
Spring Cloud Stream 中的 Binder
通过前面几个课时的介绍,我们明确了 Binder 组件是 Spring Cloud Stream 与各种消息中间件进行集成的核心组件,而 Binder 组件的实现过程涉及一批核心类之间的相互协作。接下来,我们就对 Binder 相关的核心类做源码级的展开。
BindableProxyFactory
我们知道在发送和接收消息时,需要使用 @EnableBinding 注解,该注解的作用就是告诉 Spring Cloud Stream 将该应用程序绑定到消息中间件,从而实现两者之间的连接。我们来到 org.springframework.cloud.stream.binding 包下的 BindableProxyFactory 类。根据该类上的注释,BindableProxyFactory 是用于初始化由 @EnableBinding 注解所提供接口的工厂类,该类的定义如下所示:
public class BindableProxyFactory implements MethodInterceptor, FactoryBean<Object>, Bindable, InitializingBean
public class BindableProxyFactory implements MethodInterceptor, FactoryBean<Object>, Bindable, InitializingBean
注意到 BindableProxyFactory 同时实现了 MethodInterceptor 接口和 Bindable 接口。其中前者是 AOP 中的方法拦截器,而后者是一个标明能够绑定 Input 和 Output 的接口。我们先来看 MethodInterceptor 中用于拦截的 invoke 方法,如下所示:
@Override
public synchronized Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
Object boundTarget = targetCache.get(method);
if (boundTarget != null) {
return boundTarget;
}
Input input = AnnotationUtils.findAnnotation(method, Input.class);
if (input != null) {
String name = BindingBeanDefinitionRegistryUtils.getBindingTargetName(input, method);
boundTarget = this.inputHolders.get(name).getBoundTarget();
targetCache.put(method, boundTarget);
return boundTarget;
}
else {
Output output = AnnotationUtils.findAnnotation(method, Output.class);
if (output != null) {
String name = BindingBeanDefinitionRegistryUtils.getBindingTargetName(output, method);
boundTarget = this.outputHolders.get(name).getBoundTarget();
targetCache.put(method, boundTarget);
return boundTarget;
}
}
return null;
}
@Override
public synchronized Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
Object boundTarget = targetCache.get(method);
if (boundTarget != null) {
return boundTarget;
}
Input input = AnnotationUtils.findAnnotation(method, Input.class);
if (input != null) {
String name = BindingBeanDefinitionRegistryUtils.getBindingTargetName(input, method);
boundTarget = this.inputHolders.get(name).getBoundTarget();
targetCache.put(method, boundTarget);
return boundTarget;
}
else {
Output output = AnnotationUtils.findAnnotation(method, Output.class);
if (output != null) {
String name = BindingBeanDefinitionRegistryUtils.getBindingTargetName(output, method);
boundTarget = this.outputHolders.get(name).getBoundTarget();
targetCache.put(method, boundTarget);
return boundTarget;
}
}
return null;
}
这里的逻辑比较简单,可以看到 BindableProxyFactory 保存了一个缓存对象 targetCache。如果所调用方法已经存在于缓存中,则直接返回目标对象。反之,会根据 @Input 和 @Output 注解从 inputHolders 和 outputHolders 中获取对应的目标对象并放入缓存中。这里使用缓存的作用仅仅是为了加快每次方法调用的速度,而系统在初始化时通过重写 afterPropertiesSet 方法,已经将所有的目标对象都放置在 inputHolders 和 outputHolders 这两个集合中。至于这里提到的这个目标对象,暂时可以把它理解为就是一种 MessageChannel 对象,后面会对其进行展开。
然后我们来看 Bindable 接口的定义,如下所示:
public interface Bindable {
default Collection<Binding<Object>> createAndBindInputs(BindingService adapter) {
return Collections.<Binding<Object>>emptyList();
}
default void bindOutputs(BindingService adapter) {}
default void unbindInputs(BindingService adapter) {}
default void unbindOutputs(BindingService adapter) {}
default Set<String> getInputs() {
return Collections.emptySet();
}
public interface Bindable {
default Collection<Binding<Object>> createAndBindInputs(BindingService adapter) {
return Collections.<Binding<Object>>emptyList();
}
default void bindOutputs(BindingService adapter) {}
default void unbindInputs(BindingService adapter) {}
default void unbindOutputs(BindingService adapter) {}
default Set<String> getInputs() {
return Collections.emptySet();
}
default Set<String> getOutputs(){
return Collections.emptySet();
}
}
显然,这个接口提供了对 Input 和 Output 的绑定和解绑操作。在 BindableProxyFactory 中,对以上几个方法的实现过程基本都类似,我们随机挑选一个 bindOutputs 方法进行展开,如下所示:
@Override
public void bindOutputs(BindingService bindingService) {
for (Map.Entry<String, BoundTargetHolder> boundTargetHolderEntry : this.outputHolders.entrySet()) {
BoundTargetHolder boundTargetHolder = boundTargetHolderEntry.getValue();
String outputTargetName = boundTargetHolderEntry.getKey();
if (boundTargetHolderEntry.getValue().isBindable()) {
if (log.isDebugEnabled()) {
log.debug(String.format("Binding %s:%s:%s", this.namespace, this.type, outputTargetName));
}
bindingService.bindProducer(boundTargetHolder.getBoundTarget(), outputTargetName);
}
}
}
@Override
public void bindOutputs(BindingService bindingService) {
for (Map.Entry<String, BoundTargetHolder> boundTargetHolderEntry : this.outputHolders.entrySet()) {
BoundTargetHolder boundTargetHolder = boundTargetHolderEntry.getValue();
String outputTargetName = boundTargetHolderEntry.getKey();
if (boundTargetHolderEntry.getValue().isBindable()) {
if (log.isDebugEnabled()) {
log.debug(String.format("Binding %s:%s:%s", this.namespace, this.type, outputTargetName));
}
bindingService.bindProducer(boundTargetHolder.getBoundTarget(), outputTargetName);
}
}
}
这里需要引入另一个重要的工具类 BindingService,该类提供了对 Input 和 Output 目标对象进行绑定的能力。但事实上,通过类上的注释可以看到,这也是一个外观类,它将底层的绑定动作委托给了 Binder。我们以绑定生产者的 bindProducer 方法为例展开讨论,该方法如下所示:
public <T> Binding<T> bindProducer(T output, String outputName) {
String bindingTarget = this.bindingServiceProperties
.getBindingDestination(outputName);
Binder<T, ?, ProducerProperties> binder = (Binder<T, ?, ProducerProperties>) getBinder(
outputName, output.getClass());
ProducerProperties producerProperties = this.bindingServiceProperties
.getProducerProperties(outputName);
if (binder instanceof ExtendedPropertiesBinder) {
Object extension = ((ExtendedPropertiesBinder) binder)
.getExtendedProducerProperties(outputName);
ExtendedProducerProperties extendedProducerProperties = new ExtendedProducerProperties<>(
extension);
BeanUtils.copyProperties(producerProperties, extendedProducerProperties);
producerProperties = extendedProducerProperties;
}
validate(producerProperties);
Binding<T> binding = doBindProducer(output, bindingTarget, binder, producerProperties);
this.producerBindings.put(outputName, binding);
return binding;
}
public <T> Binding<T> bindProducer(T output, String outputName) {
String bindingTarget = this.bindingServiceProperties
.getBindingDestination(outputName);
Binder<T, ?, ProducerProperties> binder = (Binder<T, ?, ProducerProperties>) getBinder(
outputName, output.getClass());
ProducerProperties producerProperties = this.bindingServiceProperties
.getProducerProperties(outputName);
if (binder instanceof ExtendedPropertiesBinder) {
Object extension = ((ExtendedPropertiesBinder) binder)
.getExtendedProducerProperties(outputName);
ExtendedProducerProperties extendedProducerProperties = new ExtendedProducerProperties<>(
extension);
BeanUtils.copyProperties(producerProperties, extendedProducerProperties);
producerProperties = extendedProducerProperties;
}
validate(producerProperties);
Binding<T> binding = doBindProducer(output, bindingTarget, binder, producerProperties);
this.producerBindings.put(outputName, binding);
return binding;
}
显然,这里的 doBindProducer 方法完成了真正的绑定操作,如下所示:
public <T> Binding<T> doBindProducer(T output, String bindingTarget, Binder<T, ?, ProducerProperties> binder,
ProducerProperties producerProperties) {
if (this.taskScheduler == null || this.bindingServiceProperties.getBindingRetryInterval() <= 0) {
return binder.bindProducer(bindingTarget, output, producerProperties);
}
else {
try {
return binder.bindProducer(bindingTarget, output, producerProperties);
}
catch (RuntimeException e) {
LateBinding<T> late = new LateBinding<T>();
rescheduleProducerBinding(output, bindingTarget, binder, producerProperties, late, e);
return late;
}
}
}
public <T> Binding<T> doBindProducer(T output, String bindingTarget, Binder<T, ?, ProducerProperties> binder,
ProducerProperties producerProperties) {
if (this.taskScheduler == null || this.bindingServiceProperties.getBindingRetryInterval() <= 0) {
return binder.bindProducer(bindingTarget, output, producerProperties);
}
else {
try {
return binder.bindProducer(bindingTarget, output, producerProperties);
}
catch (RuntimeException e) {
LateBinding<T> late = new LateBinding<T>();
rescheduleProducerBinding(output, bindingTarget, binder, producerProperties, late, e);
return late;
}
}
}
从这个方法中,我们终于看到了 Spring Cloud Stream 中最核心的概念 Binder,通过 Binder 的 bindProducer 方法完成了目标对象的绑定。
Binder
Binder 是一个接口,分别提供了绑定生产者和消费者的方法,如下所示:
public interface Binder<T, C extends ConsumerProperties, P extends ProducerProperties> {
Binding<T> bindConsumer(String name, String group, T inboundBindTarget, C consumerProperties);
Binding<T> bindProducer(String name, T outboundBindTarget, P producerProperties);
}
public interface Binder<T, C extends ConsumerProperties, P extends ProducerProperties> {
Binding<T> bindConsumer(String name, String group, T inboundBindTarget, C consumerProperties);
Binding<T> bindProducer(String name, T outboundBindTarget, P producerProperties);
}
在介绍 Binder 接口的具体实现类之前,我们先来看一下如何获取一个 Binder,getBinder 方法如下所示。
protected <T> Binder<T, ?, ?> getBinder(String channelName, Class<T> bindableType) {
String binderConfigurationName = this.bindingServiceProperties.getBinder(channelName);
return binderFactory.getBinder(binderConfigurationName, bindableType);
protected <T> Binder<T, ?, ?> getBinder(String channelName, Class<T> bindableType) {
String binderConfigurationName = this.bindingServiceProperties.getBinder(channelName);
return binderFactory.getBinder(binderConfigurationName, bindableType);
显然,这里用到了个工厂模式。工厂类 BinderFactory 的定义如下所示:
public interface BinderFactory {
<T> Binder<T, ? extends ConsumerProperties, ? extends ProducerProperties> getBinder(String configurationName,
Class<? extends T> bindableType);
}
public interface BinderFactory {
<T> Binder<T, ? extends ConsumerProperties, ? extends ProducerProperties> getBinder(String configurationName,
Class<? extends T> bindableType);
}
BinderFactory 只有一个方法,根据给定的配置名称 configurationName 和绑定类型 bindableType 获取 Binder 实例。而 BinderFactory 的实现类也只有一个,即 DefaultBinderFactory。在该实现类的 getBinder 方法中对配置信息进行了校验,并通过 getBinderInstance 获取真正的 Binder 实例。在 getBinderInstance 方法中,我们通过一系列基于 Spring 容器的步骤构建了一个上下文对象 ConfigurableApplicationContext,并通过该上下文对象获取实现了 Binder 接口的 Java bean,核心代码就是下面这句:
Binder<T, ?, ?> binder = binderProducingContext.getBean(Binder.class);
Binder<T, ?, ?> binder = binderProducingContext.getBean(Binder.class);
当然,对于 BinderFactory 而言,缓存也是需要的。在 DefaultBinderFactory 中存在一个 binderInstanceCache 变量,使用了一个 Map 来保存配置名称所对应的 Binder 对象。
AbstractMessageChannelBinder
既然我们已经能够获取 Binder 实例,接下去就来讨论 Binder 实例中对 bindConsumer 和 bindProducer 方法的实现过程。在 Spring Cloud Stream 中,Binder 接口的类层关系如下所示,注意到这里还展示了 spring-cloud-stream-binder-rabbit 代码工程中的 RabbitMessageChannelBinder 类,这个类在本课时讲到 Spring Cloud Stream 与 RabbitMQ 进行集成时会具体展开:
Binder 接口类层结构图
Spring Cloud Stream 首先提供了一个 AbstractBinder,这是一个抽象类,提供的 bindConsumer 和 bindProducer 方法实现如下所示:
@Override
public final Binding<T> bindConsumer(String name, String group, T target, C properties) {
if (StringUtils.isEmpty(group)) {
Assert.isTrue(!properties.isPartitioned(), "A consumer group is required for a partitioned subscription");
}
return doBindConsumer(name, group, target, properties);
}
protected abstract Binding<T> doBindConsumer(String name, String group, T inputTarget, C properties);
@Override
public final Binding<T> bindProducer(String name, T outboundBindTarget, P properties) {
return doBindProducer(name, outboundBindTarget, properties);
}
protected abstract Binding<T> doBindProducer(String name, T outboundBindTarget, P properties);
@Override
public final Binding<T> bindConsumer(String name, String group, T target, C properties) {
if (StringUtils.isEmpty(group)) {
Assert.isTrue(!properties.isPartitioned(), "A consumer group is required for a partitioned subscription");
}
return doBindConsumer(name, group, target, properties);
}
protected abstract Binding<T> doBindConsumer(String name, String group, T inputTarget, C properties);
@Override
public final Binding<T> bindProducer(String name, T outboundBindTarget, P properties) {
return doBindProducer(name, outboundBindTarget, properties);
}
protected abstract Binding<T> doBindProducer(String name, T outboundBindTarget, P properties);
可以看到,它对 Binder 接口中相关方法只是提供了空实现,并把具体实现过程通过 doBindConsumer 和 doBindProducer 抽象方法交由子类进行完成。显然,从设计模式上讲,AbstractBinder 应用了很典型的模板方法模式。
AbstractBinder 的子类是 AbstractMessageChannelBinder,它同样也是一个抽象类。我们来看它的 doBindProducer 方法,并对该方法中的核心语句进行提取和整理:
@Override
public final Binding<MessageChannel> doBindProducer(final String destination, MessageChannel outputChannel,
final P producerProperties) throws BinderException {
@Override
public final Binding<MessageChannel> doBindProducer(final String destination, MessageChannel outputChannel,
final P producerProperties) throws BinderException {
…
…
final MessageHandler producerMessageHandler;
final ProducerDestination producerDestination;
try {
producerDestination = this.provisioningProvider.provisionProducerDestination(destination,
producerProperties);
SubscribableChannel errorChannel = producerProperties.isErrorChannelEnabled()
? registerErrorInfrastructure(producerDestination) : null;
producerMessageHandler = createProducerMessageHandler(producerDestination, producerProperties,
errorChannel);
…
postProcessOutputChannel(outputChannel, producerProperties);
((SubscribableChannel) outputChannel).subscribe(
new SendingHandler(producerMessageHandler, HeaderMode.embeddedHeaders
.equals(producerProperties.getHeaderMode()), this.headersToEmbed,
producerProperties.isUseNativeEncoding()));
Binding<MessageChannel> binding = new DefaultBinding<MessageChannel>(destination, null, outputChannel,
producerMessageHandler instanceof Lifecycle ? (Lifecycle) producerMessageHandler : null) {
…
};
doPublishEvent(new BindingCreatedEvent(binding));
return binding;
}
上述代码的核心逻辑在于,Source 里的 output 发送消息到 outputChannel 通道之后会被 SendingHandler 这个 MessageHandler 进行处理。从设计模式上讲,SendingHandler 是一个静态代理类,因此它又将这个处理过程委托给了由 createProducerMessageHandler 方法所创建的 producerMessageHandler,这点从 SendingHandler 的定义中可以得到验证,如下所示的 delegate 就是传入的 producerMessageHandler:
private final class SendingHandler extends AbstractMessageHandler implements Lifecycle {
private final MessageHandler delegate;
@Override
protected void handleMessageInternal(Message<?> message) throws Exception {
Message<?> messageToSend = (this.useNativeEncoding) ? message
: serializeAndEmbedHeadersIfApplicable(message);
this.delegate.handleMessage(messageToSend);
}
// 省略其他方法
}
private final class SendingHandler extends AbstractMessageHandler implements Lifecycle {
private final MessageHandler delegate;
@Override
protected void handleMessageInternal(Message<?> message) throws Exception {
Message<?> messageToSend = (this.useNativeEncoding) ? message
: serializeAndEmbedHeadersIfApplicable(message);
this.delegate.handleMessage(messageToSend);
}
// 省略其他方法
}
请注意,同样作为一个模板方法类,AbstractMessageChannelBinder 具有三个抽象方法,即 createProducerMessageHandler、postProcessOutputChannel 和 afterUnbindProducer,这三个方法都需要由它的子类进行实现。也就是说,SendingHandler 所使用的 producerMessageHandler 需要由 AbstractMessageChannelBinder 子类负责进行创建。
需要注意的是,作为统一的数据模型,SendingHandler 以及 producerMessageHandler 中使用的都是 Spring Messaging 组件中的 Message 消息对象,而 createProducerMessageHandler 内部会把这个 Message 消息对象转换成对应中间件的消息数据格式并进行发送。
下面转到消息消费的场景,我们来看 AbstractMessageChannelBinder 的 doBindConsumer 方法。该方法的核心语句是创建一个消费者端点 ConsumerEndpoint,如下所示:
MessageProducer consumerEndpoint = createConsumerEndpoint(destination, group, properties);
consumerEndpoint.setOutputChannel(inputChannel);
MessageProducer consumerEndpoint = createConsumerEndpoint(destination, group, properties);
consumerEndpoint.setOutputChannel(inputChannel);
这两行代码有两个注意点。首先,createConsumerEndpoint 是一个抽象方法,需要 AbstractMessageChannelBinder 的子类进行实现。与 createProducerMessageHandler 一样,createConsumerEndpoint 需要把中间件对应的消息数据结构转换成 Spring Messaging 中统一的 Message 消息对象。
然后,我们注意到这里的 consumerEndpoint 类型是 MessageProducer。MessageProducer 在 Spring Integration 中代表的是消息的生产者,它会把从第三方消息中间件中收到的消息转发到 inputChannel 所指定的通道中。基于 @StreamListener 注解,在 Spring Cloud Stream 中存在一个 StreamListenerMessageHandler 类,用于订阅 inputChannel 消息通道中传入的消息并进行消费。
作为总结,我们可以用如下所示的流程图来概括整个消息发送和消费流程:
消息发送和消费整体流程图
Spring Cloud Stream 集成 RabbitMQ
到目前为止,Spring Cloud Stream 提供了对 RabbitMQ 和 Kafka 这两款主流消息中间件的集成。今天,我们选择使用 RabbitMQ 作为示例,讲解如何通过 Spring Cloud Stream 所提供的 Binder 完成与具体消息中间件的整合。
Spring Cloud Stream 团队提供了 spring-cloud-stream-binder-rabbit 作为与 RabbitMQ 集成的代码工程。这个工程只有四个类,我们需要重点关注的就是实现了 AbstractMessageChannelBinder 中几个抽象方法的 RabbitMessageChannelBinder 类。
集成消息发送
首先找到 RabbitMessageChannelBinder中 的 createProducerMessageHandler 方法,我们知道该方法用于完成消息的发送。我们在 createProducerMessageHandler 中找到了以下核心代码:
final AmqpOutboundEndpoint endpoint = new AmqpOutboundEndpoint( buildRabbitTemplate(producerProperties.getExtension(), errorChannel != null));
endpoint.setExchangeName(producerDestination.getName());
final AmqpOutboundEndpoint endpoint = new AmqpOutboundEndpoint( buildRabbitTemplate(producerProperties.getExtension(), errorChannel != null));
endpoint.setExchangeName(producerDestination.getName());
首先,在 buildRabbitTemplate 方法中,我们看到了 RabbitTemplate 的构建过程。RabbitTemplate 是 Spring Amqp 组件中提供的专门用于封装与 RabbitMQ 底层交互 API 的模板类。在构建 RabbitTemplate 的整个过程中,涉及设置与 RabbitMQ 相关的 ConnectionFactory 等众多参数。
然后,我们发现 RabbitMessageChannelBinder 也是直接集成了 Spring Integration 中用于整合 AQMP 协议的 AmqpOutboundEndpoint。AmqpOutboundEndpoint 提供了如下所示的 send 方法进行消息的发送:
private void send(String exchangeName, String routingKey,
final Message<?> requestMessage, CorrelationData correlationData) {
if (this.amqpTemplate instanceof RabbitTemplate) {
MessageConverter converter = ((RabbitTemplate) this.amqpTemplate).getMessageConverter();
org.springframework.amqp.core.Message amqpMessage = MappingUtils.mapMessage(requestMessage, converter,
getHeaderMapper(), getDefaultDeliveryMode(), isHeadersMappedLast());
addDelayProperty(requestMessage, amqpMessage);
((RabbitTemplate) this.amqpTemplate).send(exchangeName, routingKey, amqpMessage, correlationData);
}
else {
this.amqpTemplate.convertAndSend(exchangeName, routingKey, requestMessage.getPayload(),
message -> {
getHeaderMapper().fromHeadersToRequest(requestMessage.getHeaders(),
message.getMessageProperties());
return message;
});
}
}
private void send(String exchangeName, String routingKey,
final Message<?> requestMessage, CorrelationData correlationData) {
if (this.amqpTemplate instanceof RabbitTemplate) {
MessageConverter converter = ((RabbitTemplate) this.amqpTemplate).getMessageConverter();
org.springframework.amqp.core.Message amqpMessage = MappingUtils.mapMessage(requestMessage, converter,
getHeaderMapper(), getDefaultDeliveryMode(), isHeadersMappedLast());
addDelayProperty(requestMessage, amqpMessage);
((RabbitTemplate) this.amqpTemplate).send(exchangeName, routingKey, amqpMessage, correlationData);
}
else {
this.amqpTemplate.convertAndSend(exchangeName, routingKey, requestMessage.getPayload(),
message -> {
getHeaderMapper().fromHeadersToRequest(requestMessage.getHeaders(),
message.getMessageProperties());
return message;
});
}
}
可以看到这里依赖于 Spring Amqp 提供的 AmqpTemplate 接口进行消息的发送,而 RabbitTemplate 是 AmqpTemplate 的一个实现类。同时,通过 Spring Integration 组件中提供的 MessageConverter 工具类完成了从 org.springframework.messaging.Message 到 org.springframework.amqp.core.Message 这两个消息数据结构之间的转换。
集成消息消费
RabbitMessageChannelBinder 中与消息消费相关的是 createConsumerEndpoint 方法。这个方法中大量使用了 Spring Amqp 和 Spring Integration 中的工具类。该方法最终返回的是一个 AmqpInboundChannelAdapter 对象。在 Spring Integration 中,AmqpInboundChannelAdapter 是一种 InboundChannelAdapter,代表面向输入的通道适配器,提供了消息监听功能,如下所示
protected class Listener implements ChannelAwareMessageListener, RetryListener {
@Override
public void onMessage(final Message message, final Channel channel) throws Exception {
//省略相关实现
}
}
protected class Listener implements ChannelAwareMessageListener, RetryListener {
@Override
public void onMessage(final Message message, final Channel channel) throws Exception {
//省略相关实现
}
}
在这个 onMessage 方法中,调用了 createAndSend 方法完成消息的创建和发送,如下所示:
private void createAndSend(Message message, Channel channel) {
org.springframework.messaging.Message<Object> messagingMessage = createMessage(message, channel);
setAttributesIfNecessary(message, messagingMessage);
sendMessage(messagingMessage);
}
private org.springframework.messaging.Message<Object> createMessage(Message message, Channel channel) {
Object payload = AmqpInboundChannelAdapter.this.messageConverter.fromMessage(message);
Map<String, Object> headers = AmqpInboundChannelAdapter.this.headerMapper
.toHeadersFromRequest(message.getMessageProperties());
if (AmqpInboundChannelAdapter.this.messageListenerContainer.getAcknowledgeMode()
== AcknowledgeMode.MANUAL) {
headers.put(AmqpHeaders.DELIVERY_TAG, message.getMessageProperties().getDeliveryTag());
headers.put(AmqpHeaders.CHANNEL, channel);
}
if (AmqpInboundChannelAdapter.this.retryTemplate != null) {
headers.put(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT, new AtomicInteger());
}
final org.springframework.messaging.Message<Object> messagingMessage = getMessageBuilderFactory()
.withPayload(payload)
.copyHeaders(headers)
.build();
return messagingMessage;
}
private void createAndSend(Message message, Channel channel) {
org.springframework.messaging.Message<Object> messagingMessage = createMessage(message, channel);
setAttributesIfNecessary(message, messagingMessage);
sendMessage(messagingMessage);
}
private org.springframework.messaging.Message<Object> createMessage(Message message, Channel channel) {
Object payload = AmqpInboundChannelAdapter.this.messageConverter.fromMessage(message);
Map<String, Object> headers = AmqpInboundChannelAdapter.this.headerMapper
.toHeadersFromRequest(message.getMessageProperties());
if (AmqpInboundChannelAdapter.this.messageListenerContainer.getAcknowledgeMode()
== AcknowledgeMode.MANUAL) {
headers.put(AmqpHeaders.DELIVERY_TAG, message.getMessageProperties().getDeliveryTag());
headers.put(AmqpHeaders.CHANNEL, channel);
}
if (AmqpInboundChannelAdapter.this.retryTemplate != null) {
headers.put(IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT, new AtomicInteger());
}
final org.springframework.messaging.Message<Object> messagingMessage = getMessageBuilderFactory()
.withPayload(payload)
.copyHeaders(headers)
.build();
return messagingMessage;
}
显然,在上述 createMessage 方法中,我们完成了消息数据格式从 org.springframework.amqp.core.Message 到 org.springframework.messaging.Message 的反向转换。
小结与预告
Binder 是 Spring Cloud Stream 的核心组件,通过这个组件,Spring Cloud Stream 完成了与第三方消息中间件的集成。在本课时中,我们花了较大篇幅系统分析了 Binder 组件相关的核心类。然后,基于这些核心类以及 RabbitMQ,我们给出了 Spring Cloud Stream 集成RabbitMQ 的实现原理。
这里给你留一道思考题:在 Spring Cloud Stream 中,Binder 组件对于消息发送和消费做了哪些抽象?
介绍完 Spring Cloud Stream 之后,我们又将启动一个新的话题,即安全性。在微服务架构中,安全性的重要性往往被忽略,值得我们系统的进行分析和实现。下一课时,我们首先关注如何理解微服务访问的安全需求和实现方案。