官方表述是:Spring Cloud Stream is a framework for building highly scalable event-driven microservices connected with shared messaging systems.意思就是Spring Cloud Stream是一个框架,用于构建与共享消息传递系统相连的高度可伸缩的事件驱动微服务。以下是官方提供的一个应用模型,很明显的看出,应用程序和中间件之间是通过binder以及一系列的input/output进行数据的交互。(特别说明:Spring Cloud Stream (后续简称SCS)目前按照特性写法区分可以分为两大类一个是3.1.x之前的版本,一个是之后的版本,可以说是区别比较大的,本文讲述的是3.2.4的版本)该框架的引入,可以让开发者不再纠结于变更mq框架而需要频繁的改变业务代码以及相关配置,只需要改动相关框架的配置文件,就可以实现不同消息中间件之间自由切换,甚至组合使用。
目前的话呢,Spring Cloud Stream为Kafka和Rabbit MQ提供了Binder实现。当然目前Spring Cloud Alibaba 内部呢也实现了RocketMq的Binder。本文将会讲述RabbitMQ的使用。
Spring Cloud Stream 提供了一种抽象的Binder用来物理连接外部的中间件。生产者可以是任意向通道发送信息的组件,通道可以使用Binder被绑定到外部的消息中间件。消费者也可以通过任意组件从通道中获取消息。同理,消费者的通道也可以被绑定到外部消息中间件。
Binding是连接应用程序跟消息中间件的桥梁,用于配置生产者和消费者。基本上一系列的配置都是在这里。
如果有需要可以前往官网看具体内容(中文版,该版本属于老版本内容)
org.springframework.boot
spring-boot-starter-parent
2.7.9
org.springframework.cloud
spring-cloud-stream-binder-rabbit
org.springframework.cloud
spring-cloud-dependencies
2021.0.3
pom
import
在引入依赖的时候需要注意,SCS的包需要和Spring Boot和Spring Cloud做版本对应,否则启动会出现异常问题,具体的版本对应可以参考官网,小编在此贴个图,方便各位。
在本文中我们采用的是RabbitMq这一消息中间件,如果小伙伴们用的是其他的,直接替换即可。
server:
port: 9009
spring:
# rabbitmq:
# host: 127.0.0.1
# port: 5672
# username: bosen
# password: bosen@888
# virtual-host: /
cloud:
function:
# 定义消费者
definition: ackMessage;normal;delayMessage
stream:
binders: # 定义rabbit还是kafka,可以写多个
bosen-rabbit:
type: rabbit
environment:
spring:
rabbitmq:
host: localhost
port: 5672
username: bosen
password: bosen@888
virtual-host: /
bindings:
delayMessage-in-0:
destination: delayMessage.exchange.cloud # mq对应交换机
content-type: application/json
consumer:
acknowledge-mode: auto # manual手动确认 ,auto 自动确认
concurrency: 10
# autoStartup: true
group: delay-group # 消息组
binder: bosen-rabbit
delayMessage-out-0:
destination: delayMessage.exchange.cloud
content-type: application/json
group: delay-group
binder: bosen-rabbit
ackMessage-in-0:
destination: ackMessage.exchange.cloud
content-type: application/json
consumer:
acknowledge-mode: manual # manual手动确认 ,auto 自动确认
concurrency: 10 # 并发数
group: ackMessage-group
binder: bosen-rabbit
ackMessage-out-0:
destination: ackMessage.exchange.cloud
content-type: application/json
group: ackMessage-group
binder: bosen-rabbit
normal-in-0:
destination: normal.exchange.cloud
content-type: application/json
consumer:
acknowledge-mode: auto # manual手动确认 ,auto 自动确认
concurrency: 10 # 并发数
group: normal-group
binder: bosen-rabbit
normal-out-0:
destination: normal.exchange.cloud
content-type: application/json
group: normal-group
binder: bosen-rabbit
rabbit:
bindings:
ackMessage-in-0:
consumer:
startOffset: latest
acknowledge-mode: manual # manual手动确认 ,auto 自动确认
concurrency: 10 # 并发数
autoRebalanceEnabled: true
delayMessage-in-0:
consumer:
startOffset: latest
delayedExchange: true # 开启延时
concurrency: 10 # 并发数
autoRebalanceEnabled: true
# exchange-type: direct
delayMessage-out-0:
producer:
delayedExchange: true # 开启延时
# exchange-type: direct
application:
name: bosen-message
main:
allow-bean-definition-overriding: true
mvc:
pathmatch:
matching-strategy: ant_path_matcher
特别说明:
在上述配置中,可以看到注释了spring.rabbitmq的相关配置,是因为我们使用spring.cloud.stream.binder.**.enviornment属性进行消息中间件的连接配置。可是在我们配置完成之后启动项目会发现,控制台抛出了一个异常,关键内容就是“Rabbit health check failed”以及“Connection refused: connect”,起初我和大部分小伙伴一样,以为是自己连接配置写错了,特地去检查了一遍,反复确认,发现没有任务问题,于是我试着去发消息调用接口,发现是可以正常发送的,消费也正常。于是可以确定不是配置问题。于是呼,开启了程序员的必经之路,看源码,进入到具体报错的类RabbitHealthIndicator下面,在搜索的时候会发现有两个,点击配置文件
会发现这两个类都是在spring boot actuator这个jar包下面的内容,这个框架是一个健康检测框架,再往下找,会发现在某个配置类下面,需要读取spring.rabbit的配置
此刻我可能在想和你一样的问题,居然是这个问题,那我不要他是不是就可以,答案是必然的,这个框架并不是必要的,看项目需要,如果你不需要,只需要添加配置关闭即可
# 关闭actuator的检测功能,如果要开启,为了避免连接异常,需要指定spring.rabbitmq的相关配置
management:
health:
rabbit:
enabled: false
如果你还是想用这个框架做健康检测,只需要放开spring.rabbitmq的配置即可,也是可以正常启动。
基本配置写完了,接下来就是关于生产者的写法了,3.1.x之前的版本是通过Source来进行数据的发送的,但是在新版本中,使用的是StreamBridge,在项目中只需要引入这个Bean即可使用
@Resource
private StreamBridge streamBridge;
/**
*
* @param mqMessageVO 消息内容
* @return 结果
*/
@Override
public ResponseData sendMsg(SendMQMessageVO mqMessageVO) {
Message message = MessageBuilder.withPayload(mqMessageVO.getMsg()).build();
streamBridge.send(mqMessageVO.getBindingName(), message);
return ResponseData.success();
}
/**
* 发送延迟消息
* @param mqMessageVO 消息
* @return 结果
*/
@Override
public ResponseData sendDelayMsg(SendMQMessageVO mqMessageVO) {
Message message = MessageBuilder.withPayload(mqMessageVO.getMsg()).setHeader("x-delay", mqMessageVO.getDelaySeconds() * 1000).build();
streamBridge.send(mqMessageVO.getBindingName(), message);
return ResponseData.success();
}
消费者的bean其实在配置的时候就已经定义了,只要做具体的bean注入就行
/**
* mq接收ackMessage消息/手动ack确认
**/
@Bean
Consumer> ackMessage() {
log.info("ackMessage-初始化订阅");
return obj -> {
Channel channel = obj.getHeaders().get(AmqpHeaders.CHANNEL, Channel.class);
Long deliveryTag = obj.getHeaders().get(AmqpHeaders.DELIVERY_TAG, Long.class);
try {
log.info("ackMessage-消息接收成功:" + obj.getPayload());
//业务逻辑处理
//ack确认
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
//重新回队列-true则重新入队列,否则丢弃或者进入死信队列。
// channel.basicReject(deliveryTag, true);
log.error(e.getMessage());
}
};
}
/**
* mq接收normal消息
**/
@Bean
Consumer
从上述代码可以看到新版本的SCS的一个特性,那就是函数式编程,这也是相比老版本的一个大的变革。
为了方便,小编自己封装了一个api接口用于通用调用
public interface IMQMessageService {
ResponseData sendMsg(SendMQMessageVO mqMessageVO);
ResponseData sendDelayMsg(SendMQMessageVO mqMessageVO);
}
@Resource
private IMQMessageService imqMessageService;
/**
* 发送普通消息Rabbitmq
* bindingName 绑定队列名称
* @param mqMessageVO 消息内容
*/
@PostMapping("/sendMessage")
public ResponseData sendMessage(@RequestBody @Valid SendMQMessageVO mqMessageVO) {
return imqMessageService.sendMsg(mqMessageVO);
}
/**
* 发送延迟消息
*
* @param mqMessageVO 消息实体
* @return 结果
*/
@PostMapping("/sendDelayedMessage")
public ResponseData sendDelayedMessage(@RequestBody @Valid SendMQMessageVO mqMessageVO) {
return imqMessageService.sendDelayMsg(mqMessageVO);// 延迟时间(秒)
}
@Data
public class SendMQMessageVO implements Serializable {
private static final long serialVersionUID = 945882305703451537L;
/**
* exchange名称,SCS框架中的
*/
@NotBlank(message = "exchange名称不能为空")
private String bindingName;
/**
* 消息类型
*/
@NotNull(message = "消息类型不能为空")
private Integer msgType;
/**
* 消息
*/
@NotBlank(message = "消息内容不能为空")
private String msg;
/**
* 延迟时长
*/
private Integer delaySeconds;
}
3.6、测试
在这里我就直接贴curl,有兴趣的小伙伴可以拿去测试
curl --location --request GET 'localhost:9009/mq/sendDelayedMessage?message=hello&bindingName=delayMessage-out-0'