Spring Cloud Stream 是消息中间件组件,它集成了 kafka 和 rabbitmq 。本篇文章以 Rabbit MQ 为消息中间件系统为基础,介绍 Spring Cloud Stream 的使用。如果你没有用过消息中间件,可以到 RabbitMQ 的官网看一下,或者参考这个 http://rabbitmq.mr-ping.com/。理解了消息中间件的设计,才能更好的使用它。

消息中间的几大应用场景

1、异步处理

比如用户在电商网站下单,下单完成后会给用户推送短信或邮件,发短信和邮件的过程就可以异步完成。因为下单付款是核心业务,发邮件和短信并不属于核心功能,并且可能耗时较长,所以针对这种业务场景可以选择先放到消息队列中,有其他服务来异步处理。

2、应用解耦:

假设公司有几个不同的系统,各系统在某些业务有联动关系,比如 A 系统完成了某些操作,需要触发 B 系统及 C 系统。如果 A 系统完成操作,主动调用 B 系统的接口或 C 系统的接口,可以完成功能,但是各个系统之间就产生了耦合。用消息中间件就可以完成解耦,当 A 系统完成操作将数据放进消息队列,B 和 C 系统去订阅消息就可以了。这样各系统只要约定好消息的格式就好了。

3、流量削峰

比如秒杀活动,一下子进来好多请求,有的服务可能承受不住瞬时高并发而崩溃,所以针对这种瞬时高并发的场景,在中间加一层消息队列,把请求先入队列,然后再把队列中的请求平滑的推送给服务,或者让服务去队列拉取。

4、日志处理

kafka 最开始就是专门为了处理日志产生的。

当碰到上面的几种情况的时候,就要考虑用消息队列了。如果你碰巧使用的是 RabbitMQ 或者 kafka ,而且同样也是在使用 Spring Cloud ,那可以考虑下用 Spring Cloud Stream。

使用 Spring Cloud Stream && RabbitMQ

介绍下面的例子之前,假定你已经对 RabbitMQ 有一定的了解。

首先来认识一下 Spring Cloud Stream 中的几个重要概念。

Destination Binders:目标绑定器,目标指的是 kafka 还是 RabbitMQ,绑定器就是封装了目标中间件的包。如果操作的是 kafka 就使用 kafka binder ,如果操作的是 RabbitMQ 就使用 rabbitmq binder。

Destination Bindings:外部消息传递系统和应用程序之间的桥梁,提供消息的“生产者”和“消费者”(由目标绑定器创建)

Message:一种规范化的数据结构,生产者和消费者基于这个数据结构通过外部消息系统与目标绑定器和其他应用程序通信。

可能看完了上面的三个概念仍然是一头雾水,没有关系,实践过程中自然就明白了。

先来一个最简单的例子

因为用到的是 rabbitmq,所以在本地搭好 rabbitmq 环境,然后装好 rabbitmq-management 插件,这样就可以访问 web UI 界面了,默认是 15672 端口。

1、引用对应 rabbitmq 的 stream 包

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

2、在 application.yml 中增加配置

spring:
profiles: stream-rabbit-customer-group1
cloud:
stream:
bindings:
input:
destination: default.messages
binder: local_rabbit
output:
destination: default.messages
binder: local_rabbit
binders:
local_rabbit:
type: rabbit
environment:
spring:
rabbitmq:
host: localhost
port: 32775
username: guest
password: guest
server:
port: 8201

理解配置文件很重要,基本上理解清楚了配置,也就明白 spring cloud stream 是怎么回事了。

spring.cloud.stream.binders,上面提到了 stream 的 3 个重要概念的第一个 「Destination binders」。上面的配置文件中就配置了一个 binder,命名为 local_rabbit,指定 type 为 rabbit ,表示使用的是 rabbitmq 消息中间件,如果用的是 kafka ,则 type 设置为 kafka。environment 就是设置使用的消息中间件的配置信息,包括 host、port、用户名、密码等。可以设置多了个 binder,适配不同的场景。

spring.cloud.stream.bindings ,对应上面提到到 「Destination Bindings」。这里面可以配置多个 input 或者 output,分别表示消息的接收通道和发送通道,对应到 rabbitmq 上就是不同的 exchange。这个配置文件里定义了两个input 、两个output,名称分别为 input、log_input、output、log_output。这个名称不是乱起的,在我们的程序代码中会用到,用来标示某个方法接收哪个 exchange 或者发送到哪个 exchange 。

每个通道下的 destination 属性指 exchange 的名称,binder 指定在 binders 里设置的 binder,上面配置中指定了 local_rabbit 。

可以看到 input、output 对应的 destination 是相同的,log_input、log_output 对应的 destination 也相同, 也就是对应相同的 exchange。一个表示消息来源,一个表示消息去向。

另外还可以设置 group 。因为服务很可能不止一个实例,如果启动多个实例,那么没必要每个实例都消费同一个消息,只要把功能相同的实例的 group 设置为同一个,那么就会只有一个实例来消费消息,避免重复消费的情况。如果设置了 group,那么 group 名称就会成为 queue 的名称,如果没有设置 group ,那么 queue 就会根据 destination + 随机字符串的方式命名。

3、接下来做一个最简单的例子,来演示如何接收消息。

首先来介绍一下 stream 内置的简单消息通道(消息通道也就是指消息的来源和去向)接口定义,一个 Source 和 一个 Sink 。

Source.java

import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel; public interface Source {
String OUTPUT = "output"; @Output("output")
MessageChannel output();
}

消息发送通道定义,定义了一个 MessageChannel 类型的 output() 方法,用 @Output 注解标示,并指定了 binding 的名称为 output。

Sink.java

import org.springframework.cloud.stream.annotation.Input;
import org.springframework.messaging.SubscribableChannel; public interface Sink {
String INPUT = "input"; @Input("input")
SubscribableChannel input();
}

消息接收通道定义,定义了一个 SubscribableChannel 类型的 input() 方法,表示订阅一个消息的方法,并用 @Input 注解标识,并且指定了 binging 的名称为 input 。

创建一个简单的消息接收方法:

@SpringBootApplication
@EnableBinding(value = {Processor.class})
@Slf4j
public class DefaultApplication { public static void main(String[] args) {
SpringApplication.run(DefaultApplication.class, args);
}
}

在项目启动类上加上注解 @EnableBinding(value = {Processor.class}) ,表明启用 stream ,并指定定义的 Channel 定义接口类。

然后,创建一个 service 服务类,用来订阅消息,并对消息进行处理。

@Slf4j
@Component
public class DefaultMessageListener { @StreamListener(Processor.INPUT)
public void processMyMessage(String message) {
log.info("接收到消息:" + message);
}
}

在方法 processMyMessage() 上使用 @StreamListener 注解,表示对消息进行订阅监控,指定 binding 的名称,其中 Processor.INPUT 就是 Sink 的 input ,也就是字符串 input ,对应的上面的配置文件,就是 spring.cloud.stream.bindings.input。

启动 DefaultApplication ,可以在 rabbitmq 管理控制台的 exchanges 中看到增加的这几个 bindings 。

可以看到 exchange 的名称对应的就是 bindings 的两个 input 和 两个 output 的 destination 的值。

用 rabbitmq UI 控制台发送消息测试

点击上图的 default.input.messages 进入 exchange 详请页面,在 publish message 部分填写上 Payload ,然后点击 Publish message 按钮。

之后回到 DefaultApplication 的输出控制台,会看到消息已经被接收。

模拟一个日志处理

接下来模拟生产者和消费者处理消息的过程,模拟一个日志处理的过程。

  • 原始日志发送到 kite.log.messages exchange
  • 接收器在 kite.log.messages exchange 接收原始日志,经过处理格式化,发送到 kite.log.format.messages exchange
  • 接收器在 kite.log.format.messages exchange 接收格式化后的日志

1、自定义消息通道接口,上面介绍了 stream 自带的 Sink 和 Source,也仅仅能做个演示,真正的业务中还是需要自己定义更加灵活的接口。

@Component
public interface MyProcessor { String MESSAGE_INPUT = "log_input"; String MESSAGE_OUTPUT = "log_output"; String LOG_FORMAT_INPUT = "log_format_input"; String LOG_FORMAT_OUTPUT = "log_format_output"; @Input(MESSAGE_INPUT)
SubscribableChannel logInput(); @Output(MESSAGE_OUTPUT)
MessageChannel logOutput(); @Input(LOG_FORMAT_INPUT)
SubscribableChannel logFormatInput(); @Output(LOG_FORMAT_OUTPUT)
MessageChannel logFormatOutput(); }

2、创建消费者应用

**配置文件如下 **:

spring:
profiles: stream-rabbit-customer-group1
cloud:
stream:
bindings:
log_input:
destination: kite.log.messages
binder: local_rabbit
group: logConsumer-group1
log_output:
destination: kite.log.messages
binder: local_rabbit
group: logConsumer-group1
log_format_input:
destination: kite.log.format.messages
binder: local_rabbit
group: logFormat-group1
log_format_input:
destination: kite.log.format.messages
binder: local_rabbit
group: logFormat-group1
binders:
local_rabbit:
type: rabbit
environment:
spring:
rabbitmq:
host: localhost
port: 32775
username: guest
password: guest
server:
port: 8201

此配置文件要参照 MyProcessor 接口查看,定义了 4 个 binding,但是 destination 两两相同,也就是两个 exchange。

创建 spring boot 启动类

@SpringBootApplication
@EnableBinding(value = {MyProcessor.class})
@Slf4j
public class CustomerApplication { public static void main(String[] args) {
SpringApplication.run(CustomerApplication.class, args);
}
}

用 @EnableBinding(value = {MyProcessor.class}) 注解引入 MyProcessor

创建消息接收处理服务

@Slf4j
@Component
public class LogMessageListener { /**
* 通过 MyProcessor.MESSAGE_INPUT 接收消息
* 然后通过 SendTo 将处理后的消息发送到 MyProcessor.LOG_FORMAT_OUTPUT
* @param message
* @return
*/
@StreamListener(MyProcessor.MESSAGE_INPUT)
@SendTo(MyProcessor.LOG_FORMAT_OUTPUT)
public String processLogMessage(String message) {
log.info("接收到原始消息:" + message);
return "「" + message +"」";
} /**
* 接收来自 MyProcessor.LOG_FORMAT_INPUT 的消息
* 也就是加工后的消息,也就是通过上面的 SendTo 发送来的
* 因为 MyProcessor.LOG_FORMAT_OUTPUT 和 MyProcessor.LOG_FORMAT_INPUT 是指向同一 exchange
* @param message
*/
@StreamListener(MyProcessor.LOG_FORMAT_INPUT)
public void processFormatLogMessage(String message) {
log.info("接收到格式化后的消息:" + message);
}
}

3、创建一个消息生产者,用于发送原始日志消息

配置文件

spring:
cloud:
stream:
bindings:
log_output:
destination: kite.log.messages
binder: local_rabbit
group: logConsumer-group1
binders:
local_rabbit:
type: rabbit
environment:
spring:
rabbitmq:
host: localhost
port: 32775
username: guest
password: guest
server:
port: 8202

仅仅指定了一个 binding log_output,用来发送消息,如果只做生产者就不要指定 log_input,如果指定了 log_input ,应用就会认为这个生产者服务也会消费消息,如果这时没有在此服务中订阅消息,当消息被发送到这个服务时,因为并没有订阅消息,也就是没有 @StreamListener 注解的方法,就会出现如下异常:

org.springframework.messaging.MessageDeliveryException: Dispatcher has no subscribers for channel

创建 spring boot 启动类

@Slf4j
@RestController
@EnableBinding(value = {MyProcessor.class})
public class MyMessageController { @Autowired
private MyProcessor myProcessor; @GetMapping(value = "sendLogMessage")
public void sendLogMessage(String message){
Message<String> stringMessage = org.springframework.messaging.support.MessageBuilder.withPayload(message).build();
myProcessor.logOutput().send(stringMessage);
}
}

同样的引入 @EnableBinding(value = {MyProcessor.class})

创建一个 Controller 用来发送消息

@Slf4j
@RestController
@EnableBinding(value = {MyProcessor.class})
public class MyMessageController { @Autowired
private MyProcessor myProcessor; @GetMapping(value = "sendLogMessage")
public void sendLogMessage(String message){
Message<String> stringMessage = org.springframework.messaging.support.MessageBuilder.withPayload(message).build();
myProcessor.logOutput().send(stringMessage);
}
}

之后,访问链接:

http://localhost:8202/sendLogMessage?message=原始日志

可以在消费服务端看到如下输出:

其他

消息除了可以是字符串类型,还可以是其他类型,也可以是实体类型,例如

@GetMapping(value = "sendObjectLogMessage")
public void sendObjectLogMessage() {
LogInfo logInfo = new LogInfo();
logInfo.setClientIp("192.168.1.111");
logInfo.setClientVersion("1.0");
logInfo.setUserId("198663383837434");
logInfo.setTime(Date.from(Instant.now()));
Message < LogInfo > stringMessage = org.springframework.messaging.support.MessageBuilder.withPayload(logInfo).build();
myProcessor.logOutput().send(stringMessage);
}

上面代码发送了一个 LogInfo 实体对象,在消费者端依然可以用字符串类型接收,因为 @StreamListener 注解会默认把实体转为 json 字符串。

另外,可以试着启动两个消费者端,把 group 设置成相同的,这时,发送的消息只会被一个消费者接收。

如果把 group 设置成不一样的,那么发送的消息会被两个消费者接收。

还可以看其他 Spring Cloud 系列:

如果你也打算学习和使用 Spring Cloud

Spring Cloud Eureka 实现服务注册与发现

为 Eureka 服务注册中心实现安全控制

Spring Cloud Eureka 实现高可用服务发现注册中心

Spring Cloud Config 配置中心 看这一篇就够了

服务注册发现、配置中心集一体的 Spring Cloud Consul

不要吝惜你的「推荐」呦

欢迎关注,不定期更新本系列和其他文章

古时的风筝 ,进入公众号可以加入交流群

Spring Cloud 系列之 Spring Cloud Stream的更多相关文章

  1. Spring框架系列(2) - Spring简单例子引入Spring要点

    上文中我们简单介绍了Spring和Spring Framework的组件,那么这些Spring Framework组件是如何配合工作的呢?本文主要承接上文,向你展示Spring Framework组件 ...

  2. Spring框架系列(6) - Spring IOC实现原理详解之IOC体系结构设计

    在对IoC有了初步的认知后,我们开始对IOC的实现原理进行深入理解.本文将帮助你站在设计者的角度去看IOC最顶层的结构设计.@pdai Spring框架系列(6) - Spring IOC实现原理详解 ...

  3. Spring框架系列(7) - Spring IOC实现原理详解之IOC初始化流程

    上文,我们看了IOC设计要点和设计结构:紧接着这篇,我们可以看下源码的实现了:Spring如何实现将资源配置(以xml配置为例)通过加载,解析,生成BeanDefination并注册到IoC容器中的. ...

  4. Spring框架系列(8) - Spring IOC实现原理详解之Bean实例化(生命周期,循环依赖等)

    上文,我们看了IOC设计要点和设计结构:以及Spring如何实现将资源配置(以xml配置为例)通过加载,解析,生成BeanDefination并注册到IoC容器中的:容器中存放的是Bean的定义即Be ...

  5. Spring框架系列(9) - Spring AOP实现原理详解之AOP切面的实现

    前文,我们分析了Spring IOC的初始化过程和Bean的生命周期等,而Spring AOP也是基于IOC的Bean加载来实现的.本文主要介绍Spring AOP原理解析的切面实现过程(将切面类的所 ...

  6. Spring框架系列(10) - Spring AOP实现原理详解之AOP代理的创建

    上文我们介绍了Spring AOP原理解析的切面实现过程(将切面类的所有切面方法根据使用的注解生成对应Advice,并将Advice连同切入点匹配器和切面类等信息一并封装到Advisor).本文在此基 ...

  7. Spring框架系列(11) - Spring AOP实现原理详解之Cglib代理实现

    我们在前文中已经介绍了SpringAOP的切面实现和创建动态代理的过程,那么动态代理是如何工作的呢?本文主要介绍Cglib动态代理的案例和SpringAOP实现的原理.@pdai Spring框架系列 ...

  8. Spring框架系列(12) - Spring AOP实现原理详解之JDK代理实现

    上文我们学习了SpringAOP Cglib动态代理的实现,本文主要是SpringAOP JDK动态代理的案例和实现部分.@pdai Spring框架系列(12) - Spring AOP实现原理详解 ...

  9. 【Spring Boot&&Spring Cloud系列】Spring Boot配置文件

    很多的参数可以配置在application.properties或application.yml文件中 一.BANNER banner.charset=UTF-8 # Banner file enco ...

随机推荐

  1. .net测试篇之单元测试/集成测试神器Autofixture

    autofixture简介 有了单元测试框架加上Moq(后面我们会用单独章节来介绍moq),可以说测试问题基上都能搞定了.然而有了AutoFixture对单元测试来说可以说是如虎添翼,AutoFixt ...

  2. docker学习1:docker前世今生

    Docker简介 Docker是2013发起的一个项目,早在2013年,Docker自诞生起,就是整个技术界的明星项目,当时我还在上海实习,就在各种技术媒体上看到了Docker的介绍文章,很多技术媒体 ...

  3. SynchronousQueue队列程序的执行结果分析

    public static void main(String[] args) throws Exception { /** * SynchronousQueue队列程序的执行结果分析 * Blocki ...

  4. Leetcode 5. Longest Palindromic Substring(最长回文子串, Manacher算法)

    Leetcode 5. Longest Palindromic Substring(最长回文子串, Manacher算法) Given a string s, find the longest pal ...

  5. js 前端实现打印功能

      // 此处是一个打印的方法  可以在点击事件的时候调用 dayin = () =>{ // 获取当前页面要打印的内容  // 这里的className(‘print’)是我给要打印的区域起的 ...

  6. linux 如何初始化密码(解决mysql root用户登录不了的问题)

    这是我遇到的问题 然后就想这可能是mysql安全模式的问题,解决思路:首先改变mysql的安全模式及密码校验问题,jinrumysql后在更改用户名密码. 1.首先将my.ini中加入在[mysqld ...

  7. Python数据类型详解——字典

    Python数据类型详解--字典 引子 已经学习了列表,现在有个需求--把公司每个员工的姓名.年龄.职务.工资存到列表里,你怎么存? staff_list = [ ["Kwan", ...

  8. JMeter更改语言为英文

    1. 进入目录apache-jmeter-2.13\bin 2. 打开jmeter.properties 3. 取消“language=en”前的注释 4. 重新打开JMeter,即可看到语言已经变成 ...

  9. 最短路 dijkstra+优先队列+邻接表

    http://acm.hdu.edu.cn/showproblem.php?pid=2544 #include<iostream> #include<queue> #inclu ...

  10. Codeforces 369 C Valera and Elections

    Valera and Elections 题意:现在有n个候选人, 有n-1条路, 如果选择了这个候选人, 这个候选人就会将从自己这个城市到1号城市上所有坏的路都修复一下,现在求最小的候选人数目, 如 ...