Spring Cloud Stream
是一个用来为微服务应用构建消息驱动能力的框架。它可以基于Spring Boot
来创建独立的、可用于生产的Spring
应用程序。它通过使用Spring Integration
来连接消息代理中间件以实现消息事件驱动。Spring Cloud Stream
为一些供应商的消息中间件产品提供了个性化的自动化配置实现,并且引入了发布-订阅、消费组以及分区这三个核心概念。
简单地说,Spring Cloud Stream
本质上就是整合了Spring Boot
和Spring Integration
,实现了一套轻量级的消息驱动的微服务框架。通过使用Spring Cloud Stream
,可以有效简化开发人员对消息中间件的使用复杂度,让系统开发人员可以有更多的精力关注于核心业务逻辑的处理。
由于Spring Cloud Stream
基于Spring Boot
实现,所以它秉承了Spring Boot
的优点,自动化配置的功能可帮助我们快速上手使用,但是到目前为止,Spring Cloud Stream
只支持RabbitMQ
和Kafka
这两个著名的消息中间件的自动化配置。
核心概念
Spring Cloud Stream
构建的应用程序与消息中间件之间时通过绑定器Binder
相关联的,绑定器对于应用程序而言起到了隔离作用,它使得不同消息中间件的实现细节对应用程序来说是透明的。所以对于每一个Spring Cloud Stream
的应用程序来说,它不需要知晓消息中间件的通信细节,它只需知道Binder
对应程序提供的抽象概念来使用消息中间件来实现业务逻辑即可,而这个抽象概念就是消息通道Channel
。
绑定器
Binder
绑定器是Spring Cloud Stream
中一个非常重要的概念。在没有绑定器这个概念的情况下,我们的Spring Boot
应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性,这使得我们实现的消息交互逻辑就会非常笨重,因为对具体的中间件实现细节有太重的依赖,当中间件有较大的变动升级、或是更换中间件的时候,我们就需要付出非常大的代价来实施。
通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。通过向应用程序暴露统一的Channel
通道,使得应用程序不需要再考虑各种不同的消息中间件实现。当我们需要升级消息中间件,或是更换其他消息中间件产品时,我们要做的就是更换它们对应的Binder
绑定器而不需要修改任何Spring Boot
的应用逻辑。
发布-订阅模式
在Spring Cloud Stream
中的消息通信方式遵循了发布-订阅模式,当一条消息被投递到消息中间件之后,它会通过共享的Topic
主题进行广播,消息消费者在订阅的主题中收到它并触发自身的业务逻辑处理。这里所提到的Topic
主题是Spring Cloud Stream
中的一个抽象概念,用来代表发布共享消息给消费者的地方。在不同的消息中间件中,Topic
可能对应着不同的概念,比如:在RabbitMQ
中的它对应了Exchange
、而在Kafka
中则对应了Kafka
中的Topic
。
相对于点对点队列实现的消息通信来说,Spring Cloud Stream
采用的发布-订阅模式可以有效的降低消息生产者与消费者之间的耦合,当我们需要对同一类消息增加一种处理方式时,只需要增加一个应用程序并将输入通道绑定到既有的Topic
中就可以实现功能的扩展,而不需要改变原来已经实现的任何内容。
消费组
虽然Spring Cloud Stream
通过发布-订阅模式将消息生产者与消费者做了很好的解耦,基于相同主题的消费者可以轻松的进行扩展,但是这些扩展都是针对不同的应用实例而言的,在现实的微服务架构中,我们每一个微服务应用为了实现高可用和负载均衡,实际上都会部署多个实例。很多情况下,消息生产者发送消息给某个具体微服务时,只希望被消费一次,虽然有时两个实例同属一个应用,但是这个消息出现了被重复消费两次的情况。为了解决这个问题,在Spring Cloud Stream
中提供了消费组的概念。
如果在同一个主题上的应用需要启动多个实例的时候,我们可以通过spring.cloud.stream.bindings.input.group
属性为应用指定一个组名,这样这个应用的多个实例在接收到消息的时候,只会有一个成员真正的收到消息并进行处理。如下图所示,我们为Service-A
和Service-B
分别启动了两个实例,并且根据服务名进行了分组,这样当消息进入主题之后,Group-A
和Group-B
都会收到消息的副本,但是在两个组中都只会有一个实例对其进行消费。
默认情况下,当我们没有为应用指定消费组的时候,Spring Cloud Stream
会为其分配一个独立的匿名消费组。所以,如果同一主题下所有的应用都没有指定消费组的时候,当有消息被发布之后,所有的应用都会对其进行消费,因为它们各自都属于一个独立的组中。大部分情况下,我们在创建Spring Cloud Stream
应用的时候,建议最好为其指定一个消费组,以防止对消息的重复处理,除非该行为需要这样做(比如:刷新所有实例的配置等)。
消息分区
通过引入消费组的概念,我们已经能够在多实例的情况下,保障每个消息只被组内一个实例进行消费。通过上面对消费组参数设置后的实验,我们可以观察到,消费组并无法控制消息具体被哪个实例消费。也就是说,对于同一条消息,它多次到达之后可能是由不同的实例进行消费的。但是对于一些业务场景,就需要对于一些具有相同特征的消息每次都可以被同一个消费实例处理,比如:一些用于监控服务,为了统计某段时间内消息生产者发送的报告内容,监控服务需要在自身内容聚合这些数据,那么消息生产者可以为消息增加一个固有的特征ID
来进行分区,使得拥有这些ID
的消息每次都能被发送到一个特定的实例上实现累计统计的效果,否则这些数据就会分散到各个不同的节点导致监控结果不一致的情况。而分区概念的引入就是为了解决这样的问题:当生产者将消息数据发送给多个消费者实例时,保证拥有共同特征的消息数据始终是由同一个消费者实例接收和处理。
Spring Cloud Stream
为分区提供了通用的抽象实现,用来在消息中间件的上层实现分区处理,所以它对于消息中间件自身是否实现了消息分区并不关心,这使得Spring Cloud Stream
为不具备分区功能的消息中间件也增加了分区功能扩展。
消息类型
Spring Cloud Stream
为了让开发者能够在消息中声明它的内容类型,在输出消息中定义了一个默认的头信息:contentType
。对于那些不直接支持头信息的消息中间件,Spring Cloud Stream
提供了自己的实现机制,它会在消息发出前自动将消息包装进它自定义的消息封装格式中,并加入头信息。而对于那些自身就支持头信息的消息中间件,Spring Cloud Stream
构建的服务可以接收并处理来自非Spring Cloud Stream
构建但包含符合规范头信息的应用程序发出的消息。
Spring Cloud Stream
允许使用spring.cloud.stream.bindings.<channelName>.cotent-type
属性以声明式的配置方式为绑定的输入和输出通道设置消息内容的类型。此外,原生的消息类型转换器依然可以轻松地用于我们的应用程序。目前,Spring Cloud Stream
中自带支持了以下几种常用的消息类型转换:
JSON
与POJO
的互相转换。JSON
与org.springframework.tuple.Tuple
的互相转换。Object
与byte[]
的互相转换。为了实现远程传输序列化的原始字节,应用程序需要发送byte
类型的数据,或是通过实现Java
的序列化接口来转换为字节(Object
对象必须可序列化)。String
与byte[]
的互相转换。Object
向纯文本的转换:Object
需要实现toString()
方法。
上面所指的JSON
类型可以表现为一个byte
类型的数组,也可以是一个包含有效JSON
内容的字符串。另外,Object
对象可以由JSON
、byte
数组或者字符串转换而来,但是在转换为JSON
的时候总是以字符串的形式返回。
MIME类型
在Spring Cloud Stream
中定义的content-type
属性采用了Media Type
,即Internet Media Type
(互联网媒体类型),也被称为MIME
类型,常见的有application/json
、text/plain;charset=UTF-8
。
MIME
类型对于标示如何转换为String
或byte[]
非常有用。并且,我们还可以使用MIME
类型格式来表示Java
类型,只需要使用带有类型参数的一般类型:application/x-java-object
。
比如,我们可以使用application/x-java-object;type=java.util.Map
来表示传输的是一个java.util.Map
对象,或是使用application/x-java-object;type=com.study.springcloud.vo.User
来表示传输的是一个com.study.springcloud.vo.User
对象;除此之外,更重要的是,它还提供了自定义的MIME
类型,比如通过application/x-spring-tuple
来指定Spring
的Tuple
类型。
在Spring Cloud Stream
中默认提供了一些可以开箱即用的类型转换器,具体如下所示:
源内容类型 | 目标内容类型 | content-type头 | content-type | 注释 |
---|---|---|---|---|
POJO | JSON String | ignored | application/json | |
Tuple | JSON String | ignored | application/json | JSON is tailored for Tuple |
POJO | String (toString()) | ignored | text/plain,java.lang.String | |
POJO | byte[] (java.io.serialized) | ignored | application/x-java-serialized-object | |
JSON byte[] 或 String | POJO | application/json(or none) | application/x-java-object | |
byte[] 或 String | Serializable | application/x-java-serialized-object | application/x-java-object | |
JSON byte[] 或 String | Tuple | application/json(or none) | application/x-spring-tuple | |
byte[] | String | any | text/plain,java.lang.String | will apply any Charset specified in the content-type header |
String | byte[] | any | application/octet-stream | will apply any Charset specified in the content-type header |
消息类型的转换行为只会在需要进行转换时才被执行,比如,当服务模块产生了一个头信息为application/json
的XML
字符串消息,Spring Cloud Stream
是不会将该XML
字符串转换为JSON
的,这是因为该模块的输出内容已经是一个字符串类型了,所以它并不会将其做进一步的转换。
另外需要注意的是,Spring Cloud Stream
虽然同时支持输入通道和输出通道的消息类型转换,但还是推荐开发者尽量在输出通道中做消息转换。因为对于输入通道的消费者来说,当目标是一个POJO
的时候,使用@StreamListener
注解使能够支持自动对其进行转换的。
Spring Cloud Stream
除了提供上面这些开箱即用的转换器之外,还支持开发者自定义的消息转换器。这使得我们可以使用任意格式(包括二进制)的数据进行发送和接收,并且将这些数据与特定的contentType
相关联。在应用启用的时候,Spring Cloud Stream
会将所有org.springframework.messaging.converter.MessageConverter
接口实现的自定义转换器以及默认实现的那些转换器都加载到消息转换工厂中,以提供给消息处理时使用。