Spring Cloud Bus 定义了
RemoteApplicationEvent远程事件,远程事件的定位是跨JVM发送/接收事件。 远程事件的发送/接收底层依赖Spring Cloud Stream消息的发送。其中,远程事件的发送相当于消息发送到指定的Topic,远程事件的接收相当于订阅指定的topic的消息。
消息总线概述
在分布式系统中,如果一个服务对应100个应用实例,那么在这100个节点手动刷新配置显然是不合理的。本文的消息总线可以解决这个问题。
Enterprise Integration Patterns(企业集成模式)对消息总线的定义为:消息总线是一种消息传递基础结构,它允许不同的系统通过一组共享的接口(消息总线)进行通信。
3个应用分别表示3个系统,它们连接在消息总线上,当某个系统发送消息到消息总线上时,其它系统可以接收到这个消息并做出相应的处理。从消息总线的定义可知,这个功能完全可以用消息队列完成。
-
消息发送/订阅不同的消息队列MQ实现方式不一样,如何统一?
通过Spring Cloud Stream统一发送消息。
-
开发者并不想了解消息队列,只想添加监听器监听变化并做出相应的业务处理。
定义
RemoteApplicationEvent远程事件,用于屏蔽消息发送/接收细节,开发者只需接收远程事件,与Sping原生事件机制完美整合。 -
应用A发送消息,并不想让所有监听Topic的应用都进行业务处理,只想让指定的应用做处理。
RemoteApplicationEvent远程事件定义源服务(originService)和目标服务(destinationService),用于过滤事件接收方。
Spring Cloud Bus 的使用
spring:
application:
name: scb-node1
cloud:
bus:
id: scb-node1
trace:
enabled: true
server:
port: 8081
management:
endpoints:
web:
exposure:
include: '*'
@SpringBootApplication
// 使用@RemoteApplicationEventScan注解扫描远程事件。
// 如果自定义远程事件不使用该注解扫描,这些事件会被识别成UnknownRemoteApplicationEvent
@RemoteApplicationEventScan(basePackages = "org.example")
public class BusApplication {
public static void main(String[] args) {
SpringApplication.run(BusApplication.class, args);
}
@Autowired
ApplicationContext applicationContext;
// 读取配置文件获取busId表示远程事件的来源
@Value("${spring.cloud.bus.id}")
String originServiceName;
@RestController
class BusController {
@GetMapping("/event")
String event(@RequestBody User user,
// destination表示事件目的地,空表示所有应用都会接收
@RequestParam(required = false) String destination) {
// 发送自定义远程事件
applicationContext.publishEvent(new CustomEvent("source", originServiceName, destination, user));
return "ok";
}
}
// 接收CustomEvent事件
@Service
class EventReceiver {
@EventListener
public void receive(CustomEvent event) {
System.out.println("receive:" + event.getUser());
}
}
}
// 自定义远程事件
public class CustomEvent extends RemoteApplicationEvent {
@Getter
private User user;
public CustomEvent() {
}
public CustomEvent(Object source, String originService, String destinationService, User user) {
super(source, originService, destinationService);
this.user = user;
}
}
@Data
@ToString
public class User {
private long id;
private String name;
}
启动三个实例scb-node1(8081)、scb-node2(8082)、scb-node3(8083)
GET http://localhost:8081/event
Content-Type: application/json
{
"id": 1,
"name": "张三"
}
调用成功后所有应用都会收到,控制台会打印receive:User(id=1, name=张三)。
GET http://localhost:8081/event?destination=scb-node3
Content-Type: application/json
{
"id": 1,
"name": "张三"
}
调用成功后scb-node1和scb-node3应用都会收到,控制台会打印receive:User(id=1, name=张三)。
Spring Cloud Bus的原理
Spring Cloud Bus强依赖Spring Cloud Stream。 目前Spring Cloud Bus的实现Spring Cloud AMQP Bus、Spring Cloud Kafka或Spring Cloud RocketMQ Bus底层强依赖Spring Cloud Stream Rabbit Binder、Spring Cloud Stream Kafka Binder以及Spring Cloud Stream RocketMQ Binder。 如果想要更换消息总线实现,只需要修改对应的依赖即可,代码层面没有任何变化。
Spring Cloud Bus在初始化的时候会通过Spring Cloud Stream提供的
@EnableBinding注解构造Spring Cloud Bus对应的Binding(Spring Cloud Bus依赖Spring Cloud Stream,意味着如果没有MQ对应的Spring Cloud Stream,则无法使用Spring Cloud Bus组件)。
@Configuration(proxyBeanMethods = false)
@ConditionalOnBusEnabled
@EnableBinding(SpringCloudBusClient.class)
@EnableConfigurationProperties(BusProperties.class)
@AutoConfigureBefore(BindingServiceConfiguration.class)
// so stream bindings work properly
@AutoConfigureAfter(LifecycleMvcEndpointAutoConfiguration.class)
// so actuator endpoints have needed dependencies
public class BusAutoConfiguration implements ApplicationEventPublisherAware {
...
}
public interface SpringCloudBusClient {
/**
* Name of the input channel for Spring Cloud Bus.
*/
String INPUT = "springCloudBusInput";
/**
* Name of the output channel for Spring Cloud Bus.
*/
String OUTPUT = "springCloudBusOutput";
@Output(SpringCloudBusClient.OUTPUT)
MessageChannel springCloudBusOutput();
@Input(SpringCloudBusClient.INPUT)
SubscribableChannel springCloudBusInput();
}
Output Binding:接收RemoteApplicationEvent远程事件的时候,将事件封装成消息通过Spring Cloud Stream发送到MQ。Input Binding:读取MQ上的消息,把消息封装成事件,再通过Spring的事件机制将事件发送出去,消息总线上的应用通过EventListener接收这个时间。
Spring Cloud Bus 远程事件的发送/接收流程如下图:
Spring Cloud Stream 暴露的
Output Binding和Input Binding必须读取MQ上的同一个Topic。默认情况下springCloudBus这个Topic,可以通过Binding读取进行覆盖:
spring.cloud.stream.bindings.springCloudBusOutput.destination=myTopic
spring.cloud.stream.bindings.springCloudBusInput.destination=myTopic
spring.cloud.stream.bindings.springCloudBusInput.group=myGroup
Spring Cloud Bus事件
-
EnvironmentChangeRemoteApplicationEvent:配置信息修改远程事件。Spring Cloud Bus对外提供了ID为
bus-env的EnvironmentBusEndpoint,用于修改当前应用的配置信息。EnvironmentBusEndpoint内部会发送EnvironmentChangeRemoteApplicationEvent远程事件。同样,Spring Cloud Bus内部提供EnvironmentChangeListener监听器,用于EnvironmentChangeRemoteApplicationEvent远程事件的接收。curl --header "Content-Type: application/json" -XPOST 'http://localhost:8080/actuator/bus-env?name=name&value=springcloudbus'public class EnvironmentChangeListener implements ApplicationListener<EnvironmentChangeRemoteApplicationEvent> { private static Log log = LogFactory.getLog(EnvironmentChangeListener.class); @Autowired private EnvironmentManager env; @Override public void onApplicationEvent(EnvironmentChangeRemoteApplicationEvent event) { Map<String, String> values = event.getValues(); log.info("Received remote environment change request. Keys/values to update " + values); for (Map.Entry<String, String> entry : values.entrySet()) { this.env.setProperty(entry.getKey(), entry.getValue()); } } } -
ACKRemoteApplicationEvent:远程事件发送成功确认事件。远程事件发送出去后,需要机制确认该事件是否发送成功。
-
RefreshRemoteApplicationEvent:配置刷新远程事件。Spring Cloud Bus对外提供了ID为
bus-refresh的RefreshBusEndpoint,RefreshListener内部接收该事件并调用ContextRefresher进行全局配置的刷新。# 所有节点刷新全局配置 curl -XPOST 'http://localhost:8080/actuator/bus-refresh' # node3节点刷新全局配置 curl -XPOST 'http://localhost:8080/actuator/bus-refresh/scb-node3' -
UnKnownRemoteApplicationEvent:未知远程事件。
Spring Cloud Bus 源码分析
Spring Cloud Bus几乎所有核心代码都在
BusAutoConfiguration自动化配置类里,下列代码是其中一段对远程事件发送/接收的分析:
@EventListener(classes = RemoteApplicationEvent.class) // 1
public void acceptLocal(RemoteApplicationEvent event) {
if (this.serviceMatcher.isFromSelf(event)
&& !(event instanceof AckRemoteApplicationEvent)) { // 2
this.cloudBusOutboundChannel.send(MessageBuilder.withPayload(event).build()); //3
}
}
@StreamListener(SpringCloudBusClient.INPUT) // 4
public void acceptRemote(RemoteApplicationEvent event) {
if (event instanceof AckRemoteApplicationEvent) {
if (this.bus.getTrace().isEnabled() && !this.serviceMatcher.isFromSelf(event)
&& this.applicationEventPublisher != null) { // 5
this.applicationEventPublisher.publishEvent(event);
}
// If it's an ACK we are finished processing at this point
return;
}
if (this.serviceMatcher.isForSelf(event)
&& this.applicationEventPublisher != null) { // 6
if (!this.serviceMatcher.isFromSelf(event)) { // 7
this.applicationEventPublisher.publishEvent(event);
}
if (this.bus.getAck().isEnabled()) { // 8
AckRemoteApplicationEvent ack = new AckRemoteApplicationEvent(this,
this.serviceMatcher.getServiceId(),
this.bus.getAck().getDestinationService(),
event.getDestinationService(), event.getId(), event.getClass());
this.cloudBusOutboundChannel
.send(MessageBuilder.withPayload(ack).build());
this.applicationEventPublisher.publishEvent(ack);
}
}
if (this.bus.getTrace().isEnabled() && this.applicationEventPublisher != null) { //9
// We are set to register sent events so publish it for local consumption,
// irrespective of the origin
this.applicationEventPublisher.publishEvent(new SentApplicationEvent(this,
event.getOriginService(), event.getDestinationService(),
event.getId(), event.getClass()));
}
}
- 利用Spring事件的监听机制监听本地所有的
RemoteApplicationEvent远程事件。 - 判断本地接收到的事件不是
AckRemoteApplicationEvent远程确认事件,以及该事件是应用自身发送出去的,如果都满足执行3处的代码 - 构造Message并将该远程事件作为Payload,然后使用Spring Cloud Stream构造的Binding name为
springCloudBusOutput的MessageChannel将消息发送到broker。 @StreamListener注解消费Spring Cloud Stream 构造的Binding name为springCloudBusOutput的MessageChannel,接收的消息为远程消息。- 如果该远程事件是
AckRemoteApplicationEvent远程确认事件,并且应用开启消息追踪trace开关,同时该远程事件不是应用自身发送的,那么本地发送AckRemoteApplicationEvent远程确认事件,表示应用确认收到了其他应用发送过来的远程事件。流程结束。 - 如果该远程事件是其他应用发送给应用自身的那么只需7、8处代码,否则只需9处的代码。
- 该远程事件若不是应用自身发送的,将该事件以本地的方式发送出去。应用自身一开始已经在本地被对应的消息接收方处理了,无需再次发送。
- 如果开启了
AckRemoteApplicationEvent远程确认事件的开关,构造AckRemoteApplicationEvent事件,并在远程和本地都发生该事件。 - 如果开启了消息
trace的开关,本地构造并发送SentApplicationEvent事件。