之前的开发主要是底层开发,没有深入涉及到消息方面。现在面对的是一个这样的场景:
假设公司项目A用了RabbitMQ,而项目B用了Kafka。这时候就会出现有两个消息框架,这两个消息框架可能编码有所不同,且结构也有所不同,而且之前甚至可能使用的是别的框架,造成了一个不易管理的局面。目前我的需求是不改动或者说少量代码完成两个消息队列之间的切换。我要屏蔽掉切换的成本。
spring cloud stream官方文档
PS:如有英文,是作者纯纯的懒,懒得翻译
市面上大部分消息队列的格局应该是 生产者 -》 broker -》消费者
采用的是发布-订阅的模式,大概的元素有如下几个:
Message:生产者/消费者之间靠消息媒介传递信息内容
MessageChannel:消息必须走特定的通道
队列:假如发消息会先发到消息队列当中
通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。Binder可以生成Binding,Binding用来绑定消息容器的生产者和消费者,它有两种类型,INPUT和OUTPUT,INPUT对应于消费者,OUTPUT对应于生产者。
我们需要选用一个mq,我们使用一个测试环境的mq用于学习demo,我们需要引入一些依赖。
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-stream-rabbit -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
<version>3.2.5</version>
</dependency>
Stream中的消息通信方式遵循了发布-订阅模式,Topic主题进行广播,在RabbitMQ就是Exchange,在Kakfa中就是Topic。
Binder: 很方便的连接中间件,屏蔽差异
Channel: 通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channe对队列进行配置
Source(源:发送者)和Sink(水槽:接受者): 简单的可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入。
通过将@EnableBinding注释应用于应用程序的一个配置类,可以将Spring应用程序转换为Spring Cloud Stream应用程序。@EnableBinding注释本身使用@Configuration进行元注释,并触发Spring Cloud Stream基础设施的配置:
...
@Import(...)
@Configuration
@EnableIntegration
public @interface EnableBinding {
...
Class<?>[] value() default {};
}
@EnableBinding注释可以将一个或多个接口类作为参数,这些接口类包含表示可绑定组件(通常是消息通道)的方法。
使用案例
@EnableBinding({XXXStreamClient.class})
Spring Cloud Stream应用程序可以在接口中定义任意数量的输入和输出通道,分别为@input和@output方法:
public interface Barista {
@Input
SubscribableChannel orders();
@Output
MessageChannel hotDrinks();
@Output
MessageChannel coldDrinks();
}
使用此接口作为@EnableBinding的参数将触发创建三个绑定通道,分别命名为orders、hotDrinks和coldDrinks。
@EnableBinding(Barista.class)
public class CafeConfiguration {
...
}
使用@Input和@Output注释,可以为通道指定自定义通道名称,如以下示例所示:
public interface Barista {
…
@Input(“inboundOrders”)
SubscribableChannel orders();
}
In this example, the created bound channel will be named inboundOrders.
为了简单地解决最常见的用例(包括输入通道、输出通道或两者),Spring Cloud Stream提供了三个开箱即用的预定义接口。
Source 可以用于具有单个出站通道的应用程序。
public interface Source {
String OUTPUT = "output";
@Output(Source.OUTPUT)
MessageChannel output();
}
Sink can be used for an application which has a single inbound channel.
public interface Sink {
String INPUT = "input";
@Input(Sink.INPUT)
SubscribableChannel input();
}
Processor can be used for an application which has both an inbound channel and an outbound channel.
public interface Processor extends Source, Sink {
}
每个绑定的接口,Spring Cloud Stream将生成一个实现该接口的bean。调用其中一个bean的@Input 或@Output 方法将返回相关的绑定通道。
以下示例中的bean在调用其hello方法时在输出通道上发送消息。它在注入的源bean上调用output()来检索目标通道。
@Component
public class SendingBean {
private Source source;
@Autowired
public SendingBean(Source source) {
this.source = source;
}
public void sayHello(String name) {
source.output().send(MessageBuilder.withPayload(name).build());
}
}
Bound channels can be also injected directly:
@Component
public class SendingBean {
private MessageChannel output;
@Autowired
public SendingBean(MessageChannel output) {
this.output = output;
}
public void sayHello(String name) {
output.send(MessageBuilder.withPayload(name).build());
}
}
If the name of the channel is customized on the declaring annotation, that name should be used instead of the method name. Given the following declaration:
public interface CustomSource {
...
@Output("customOutput")
MessageChannel output();
}
The channel will be injected as shown in the following example:
@Component
public class SendingBean {
private MessageChannel output;
@Autowired
public SendingBean(@Qualifier("customOutput") MessageChannel output) {
this.output = output;
}
public void sayHello(String name) {
this.output.send(MessageBuilder.withPayload(name).build());
}
}
作为对Spring Integration支持的补充,Spring Cloud Stream提供了自己的@StreamListener注释,该注释以其他Spring消息传递注释(例如,@MessageMapping、@JmsListener、@RabbitListener等)为模型。@StreamListener注释为处理入站消息提供了一个更简单的模型,尤其是在处理涉及内容类型管理和类型强制的用例时。
Spring Cloud Stream提供了一个可扩展的MessageConverter机制,用于处理绑定通道的数据转换,在本例中,用于向用@StreamListener注释的方法进行调度。以下是处理外部投票事件的应用程序示例:
@EnableBinding(Sink.class)
public class VoteHandler {
@Autowired
VotingService votingService;
@StreamListener(Sink.INPUT)
public void handle(Vote vote) {
votingService.record(vote);
}
}
当考虑具有String有效载荷和application/json的contentType标头的入站消息时,可以看到@StreamListener和Spring Integration@ServiceActivator之间的区别。在@StreamListener的情况下,MessageConverter机制将使用contentType标头将String有效载荷解析为Vote对象。
与其他Spring Messaging方法一样,方法参数可以用@Payload、@Headers和@Header进行注释。
对于返回数据的方法,必须使用@SendTo注释为该方法返回的数据指定输出绑定目标:
@EnableBinding(Processor.class)
public class TransformProcessor {
@Autowired
VotingService votingService;
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public VoteResult handle(Vote vote) {
return votingService.record(vote);
}
}
使用@StreamListener将消息调度到多个方法
在本例中,所有带有值为foo的头类型的消息都将被调度到receiveFoo方法,而所有带有值bar的头类型消息都将调度到receive bar方法。
@EnableBinding(Sink.class)
@EnableAutoConfiguration
public static class TestPojoWithAnnotatedArguments {
@StreamListener(target = Sink.INPUT, condition = "headers['type']=='foo'")
public void receiveFoo(@Payload FooPojo fooPojo) {
// handle the message
}
@StreamListener(target = Sink.INPUT, condition = "headers['type']=='bar'")
public void receiveBar(@Payload BarPojo barPojo) {
// handle the message
}
}
过程 略
配置
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
test: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
testOutput: #生产者消息输出通道 ---> 消息输出通道 = 生产者相关的定义:Exchange & Queue
destination: exchange-test #exchange名称,交换模式默认是topic;把SpringCloud Stream的消息输出通道绑定到RabbitMQ的exchange-test交换器。
content-type: application/json #设置消息的类型,本次为json
default-binder: defaultRabbit #设置要绑定的消息服务的具体设置,默认绑定RabbitMQ
group: testGroup #分组=Queue名称,如果不设置会使用默认的组流水号
testInput: #消费者消息输入通道 ---> 消息输入通道 = 消费者相关的定义:Exchange & Queue
destination: exchange-test #exchange名称,交换模式默认是topic;把SpringCloud Stream的消息输入通道绑定到RabbitMQ的exchange-test交换器。
content-type: application/json
default-binder: defaultRabbit
group: testGroup
testOutput1: #生产者消息输出通道 ---> 消息输出通道 = 生产者相关的定义:Exchange & Queue
destination: exchange-test #exchange名称,交换模式默认是topic;把SpringCloud Stream的消息输出通道绑定到RabbitMQ的exchange-test交换器。
content-type: application/json #设置消息的类型,本次为json
default-binder: test #设置要绑定的消息服务的具体设置,默认绑定RabbitMQ
group: testGroup1 #分组=Queue名称,如果不设置会使用默认的组流水号
testInput1: #消费者消息输入通道 ---> 消息输入通道 = 消费者相关的定义:Exchange & Queue
destination: exchange-test #exchange名称,交换模式默认是topic;把SpringCloud Stream的消息输入通道绑定到RabbitMQ的exchange-test交换器。
content-type: application/json
default-binder: test
group: testGroup1
TestChannelProcessor
package com.anxin.rabbitmq_scz.provider;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.stereotype.Component;
@Component
public interface TestChannelProcessor {
/**
* 生产者消息输出通道(需要与配置文件中的保持一致)
*/
String TEST_OUTPUT = "testOutput";
/**
* 消息生产
*
* @return
*/
@Output(TEST_OUTPUT)
MessageChannel testOutput();
/**
* 消费者消息输入通道(需要与配置文件中的保持一致)
*/
String TEST_INPUT = "testInput";
/**
* 消息消费
*
* @return
*/
@Input(TEST_INPUT)
SubscribableChannel testInput();
}
TestMessageProducer
package com.anxin.rabbitmq_scz.provider;
import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.binding.BinderAwareChannelResolver;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
import java.util.HashMap;
import java.util.Map;
@EnableBinding(value = {TestChannelProcessor.class})
public class TestMessageProducer {
@Autowired
private BinderAwareChannelResolver channelResolver;
/**
* 生产消息
*
* @param msg
*/
public void testSendMessage(String msg) {
Map<String, Object> headers = new HashMap<>();
Map<String, Object> payload = new HashMap<>();
payload.put("msg", msg);
System.err.println("生产者发送消息:" + JSON.toJSONString(payload));
channelResolver.resolveDestination(TestChannelProcessor.TEST_OUTPUT).send(
MessageBuilder.createMessage(payload, new MessageHeaders(headers))
);
}
/**
* 生产消息
*
* @param msg
*/
public void testSendMessage1(String msg) {
Map<String, Object> headers = new HashMap<>();
Map<String, Object> payload = new HashMap<>();
payload.put("msg", msg);
System.err.println("生产者发送消息:" + JSON.toJSONString(payload));
channelResolver.resolveDestination(TestChannelProcessor1.TEST_OUTPUT).send(
MessageBuilder.createMessage(payload, new MessageHeaders(headers))
);
}
}
TestMessageConsumer
package com.anxin.rabbitmq_scz.consumer;
import com.anxin.rabbitmq_scz.provider.TestChannelProcessor;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.messaging.Message;
@EnableBinding(TestChannelProcessor.class)
public class TestMessageConsumer {
@StreamListener(TestChannelProcessor.TEST_INPUT)
public void testConsumeMessage(Message<String> message) {
System.err.println("消费者消费消息:" + message.getPayload());
}
}
SwaggerConfig
package com.anxin.rabbitmq_scz.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.async.DeferredResult;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.genericModelSubstitutes(DeferredResult.class)
.select()
.paths(PathSelectors.any())
.build().apiInfo(apiInfo());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder().title("Stream server")
.description("测试SpringCloudStream")
.termsOfServiceUrl("https://spring.io/projects/spring-cloud-stream")
.version("1.0").build();
}
}
TestController
package com.anxin.rabbitmq_scz.controller;
import com.anxin.rabbitmq_scz.provider.TestMessageProducer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@Autowired
private TestMessageProducer testMessageProducer;
/**
* 发送保存订单消息
*
* @param message
*/
@GetMapping(value = "sendTestMessage")
public void sendTestMessage(@RequestParam("message") String message) {
//发送消息
testMessageProducer.testSendMessage(message);
}
/**
* 发送保存订单消息
*
* @param message
*/
@GetMapping(value = "sendTestMessage1")
public void sendTestMessage1(@RequestParam("message") String message) {
//发送消息
testMessageProducer.testSendMessage1(message);
}
}
启动即可
新入一门技术最简单最快的办法就是观看它的官方文档,学会api的调用,代价是极低的,但是如果要深入一门技术,必须需要阅读其源码且结合其设计模式。