细聊Spring Cloud Bus

Spring 事件驱动模型

因为Spring Cloud Bus的运行机制也是Spring事件驱动模型所以需要先了解相关知识点:



上面图中是Spring事件驱动模型的实现示意图,以下再补充一些图中未提现的实现细节:抽象类abstract class AbstractApplicationEventMulticaster中根据事件和事件类型获取对应的观察者的方法是:

	protected Collection<ApplicationListener<?>> getApplicationListeners(
ApplicationEvent event, ResolvableType eventType)

该方法内具体检索监听器(观察者的方法)是:

private Collection<ApplicationListener<?>> retrieveApplicationListeners(
ResolvableType eventType, @Nullable Class<?> sourceType, @Nullable ListenerRetriever retriever) .....
// Add programmatically registered listeners, including ones coming
// from ApplicationListenerDetector (singleton beans and inner beans).
for (ApplicationListener<?> listener : listeners) {
if (supportsEvent(listener, eventType, sourceType)) {
if (retriever != null) {
retriever.applicationListeners.add(listener);
}
allListeners.add(listener);
}
}
.....

此方法内根据传入参数事的件对象遍历所有对应(订阅)的监听者,其中有个很重要的方法boolean supportsEvent,此方法用于判断是否是订阅的监听者:

	protected boolean supportsEvent(
ApplicationListener<?> listener, ResolvableType eventType, @Nullable Class<?> sourceType) { GenericApplicationListener smartListener = (listener instanceof GenericApplicationListener ?
(GenericApplicationListener) listener : new GenericApplicationListenerAdapter(listener));
return (smartListener.supportsEventType(eventType) && smartListener.supportsSourceType(sourceType));
}

其中接口GenericApplicationListener和GenericApplicationListenerAdapter类都是为了定义或实现supportsEventType方法和supportsSourceType方法,通过这两个方法确定是否是事件的监听器(观察者、订阅者)。

public interface GenericApplicationListener extends ApplicationListener<ApplicationEvent>, Ordered {

	/**
* Determine whether this listener actually supports the given event type.
* @param eventType the event type (never {@code null})
*/
boolean supportsEventType(ResolvableType eventType); /**
* Determine whether this listener actually supports the given source type.
* <p>The default implementation always returns {@code true}.
* @param sourceType the source type, or {@code null} if no source
*/
default boolean supportsSourceType(@Nullable Class<?> sourceType) {
return true;
} /**
* Determine this listener's order in a set of listeners for the same event.
* <p>The default implementation returns {@link #LOWEST_PRECEDENCE}.
*/
@Override
default int getOrder() {
return LOWEST_PRECEDENCE;
} }

其中判断发布事件的来源对象supportsSourceType方法默认就返回true,意味着如果不重写这个接口方法,是否是订阅事件的监听器不以事件来源对象进行判断,只根据事件类型进行筛选,该方法的具体实现可参考GenericApplicationListenerAdapter类包装的supportsSourceType方法实现:

public boolean supportsSourceType(@Nullable Class<?> sourceType) {
return !(this.delegate instanceof SmartApplicationListener) ||
((SmartApplicationListener) this.delegate).supportsSourceType(sourceType);
}

Spring Cloud Bus的事件、发布、订阅

Spring Cloud Bus的事件都继承于RemoteApplicationEvent类,RemoteApplicationEvent类继承于Spring事件驱动模型的事件抽象类ApplicationEvent,也就说Spring Cloud Bus的事件、发布、订阅也是基于Spring的事件驱动模型,例如Spring Cloud Bus的配置刷新事件RefreshRemoteApplicationEvent:

同理订阅事件也是标准的Spring事件驱动模型,例如配置刷新的监听器源码继承了Spring事件驱动模型中的接口ApplicationListener<E extends ApplicationEvent>:

public class RefreshListener
implements ApplicationListener<RefreshRemoteApplicationEvent> { private static Log log = LogFactory.getLog(RefreshListener.class); private ContextRefresher contextRefresher; public RefreshListener(ContextRefresher contextRefresher) {
this.contextRefresher = contextRefresher;
} @Override
public void onApplicationEvent(RefreshRemoteApplicationEvent event) {
Set<String> keys = this.contextRefresher.refresh();
log.info("Received remote refresh request. Keys refreshed " + keys);
} }

在BusRefreshAutoConfiguration类中会将RefreshListener对象注册到Spring的BeanFactory中(不把监听器类注册到Spring的BeanFactory中就无法利用Spring的事件驱动模型对刷新事件进行处理)。

	@Bean
@ConditionalOnProperty(value = "spring.cloud.bus.refresh.enabled",matchIfMissing = true)
@ConditionalOnBean(ContextRefresher.class)
public RefreshListener refreshListener(ContextRefresher contextRefresher) {
return new RefreshListener(contextRefresher);
}

也可以使用@EventListener创建监听器,例如TraceListener类:

	@EventListener
public void onAck(AckRemoteApplicationEvent event) {
Map<String, Object> trace = getReceivedTrace(event);
// FIXME boot 2 this.repository.add(trace);
} @EventListener
public void onSend(SentApplicationEvent event) {
Map<String, Object> trace = getSentTrace(event);
// FIXME boot 2 this.repository.add(trace);
}

发布事件也是利用应用程序上下文进行事件发布,比如配置刷新的实现代码:

@Endpoint(id = "bus-refresh") // TODO: document new id
public class RefreshBusEndpoint extends AbstractBusEndpoint { public RefreshBusEndpoint(ApplicationEventPublisher context, String id) {
super(context, id);
} @WriteOperation
public void busRefreshWithDestination(@Selector String destination) { // TODO:
// document
// destination
publish(new RefreshRemoteApplicationEvent(this, getInstanceId(), destination));
} @WriteOperation
public void busRefresh() {
publish(new RefreshRemoteApplicationEvent(this, getInstanceId(), null));
} }

注解@WriteOperation实现POST操作,@Endpoint结合management.endpoints.web.exposure.include=* 配置项可实现一个接入点,接入点的URL是:/actuator/bus-refresh

父类AbstractBusEndpoint内用应用程序上下文实现事件的发布:

public class AbstractBusEndpoint {

	private ApplicationEventPublisher context;

	private String appId;

	public AbstractBusEndpoint(ApplicationEventPublisher context, String appId) {
this.context = context;
this.appId = appId;
} protected String getInstanceId() {
return this.appId;
} protected void publish(ApplicationEvent event) {
this.context.publishEvent(event);
} }

Spring Cloud Bus的底层通讯实现(对使用者透明)

Spring Cloud Bus的底层通讯基础是Spring Cloud Stream,定义发送总线事件和接收总线事件监听器的类是BusAutoConfiguration(在网络上发送和接收其他节点的事件消息),因为继承了ApplicationEventPublisherAware所以该类也具备发布本地事件的功能(可以查询Aware接口作用),发布网络事件消息的方法是:

@EventListener(classes = RemoteApplicationEvent.class)
public void acceptLocal(RemoteApplicationEvent event) {
if (this.serviceMatcher.isFromSelf(event)
&& !(event instanceof AckRemoteApplicationEvent)) {
this.cloudBusOutboundChannel.send(MessageBuilder.withPayload(event).build());
}
}

如果监听到RemoteApplicationEvent类事件,首先检查是否是自己发布并且不是ACK事件,如果是自己发布的非ACK事件就在总线上发送这个事件消息。发送AckRemoteApplicationEvent(ACK事件)已经在接收其他节点发的事件消息时触发了,所以这里不用管发送ACK事件的工作了。

接收事件消息:

@StreamListener(SpringCloudBusClient.INPUT)
public void acceptRemote(RemoteApplicationEvent event) {
if (event instanceof AckRemoteApplicationEvent) {
if (this.bus.getTrace().isEnabled() && !this.serviceMatcher.isFromSelf(event)
&& this.applicationEventPublisher != null) {
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) {
if (!this.serviceMatcher.isFromSelf(event)) {
this.applicationEventPublisher.publishEvent(event);
}
if (this.bus.getAck().isEnabled()) {
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) {
// 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()));
}
}

接收到其他节点发来的事件消息后会将此事件发布到本地的应用程序上下文中(this.applicationEventPublisher),监听此事件类型的订阅者就会相应的进行处理。

两个跟踪事件AckRemoteApplicationEvent和SentApplicationEvent

从他们的继承关系可以看出,AckRemoteApplicationEvent可以发送到其他网络节点(继承于RemoteApplicationEvent),SentApplicationEvent只是本地事件(继承于ApplicationEvent),SentApplicationEvent事件可以显示收到事件消息的类型,AckRemoteApplicationEvent事件只显示收到事件消息的ID,TraceListener类负责监听和记录他们的内容(配置项要打开spring.cloud.bus.trace.enabled=true):

public class TraceListener {

	@EventListener
public void onAck(AckRemoteApplicationEvent event) {
Map<String, Object> trace = getReceivedTrace(event);
// FIXME boot 2 this.repository.add(trace);
} @EventListener
public void onSend(SentApplicationEvent event) {
Map<String, Object> trace = getSentTrace(event);
// FIXME boot 2 this.repository.add(trace);
} protected Map<String, Object> getSentTrace(SentApplicationEvent event) {
.....
} protected Map<String, Object> getReceivedTrace(AckRemoteApplicationEvent event) {
.....
} }

在总线事件发送端和总线事件接收端日志的记录流程如下:

测试A应用和B应用进行“聊天”

首先准备环境:

创建3个项目:spring-cloud-bus-shared-library、spring-cloud-bus-a、spring-cloud-bus-b

  • spring-cloud-bus-shared-library:负责定义事件和监听器还有配置类
  • spring-cloud-bus-a:扮演A应用负责引用shared-library并利用BUS发送消息给B应用(此消息实际为广播消息)
  • spring-cloud-bus-b:扮演B应用负责引用shared-library并利用BUS回复A应用发来的消息(此消息非广播消息)

spring-cloud-bus-shared-library的POM的依赖项:

<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR3</spring-cloud.version>
</properties> <dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies> <dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>

删除构建的Maven插件节点否则构建后其他项目引用不了(格式不对):

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

启动一个rabbitmq:

docker pull rabbitmq:3-management

docker run -d --hostname my-rabbit --name rabbit -p 5672:5672 -p 15672:15672 rabbitmq:3-management

application.properties配置定义:

spring.application.name=spring-cloud-bus-shared-library
server.port=9007
# 开启消息跟踪
spring.cloud.bus.trace.enabled=true
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest #显示的暴露接入点
management.endpoints.web.exposure.include=*

spring-cloud-bus-a、spring-cloud-bus-b的配置信息除了spring.application.name和server.port不一样,其他都是一样的。

自定义一个聊天事件类:

/**
* 聊天事件
*/
public class ChatRemoteApplicationEvent extends RemoteApplicationEvent { private String message; //for serializers
private ChatRemoteApplicationEvent(){} public ChatRemoteApplicationEvent(Object source, String originService,
String destinationService,String message){
super(source, originService, destinationService); this.message = message;
} public void setMessage(String message){
this.message = message;
} public String getMessage(){
return this.message;
}
}

自定义聊天事件监听器:

/**
* 聊天事件监听
*/
public class ChatListener implements ApplicationListener<ChatRemoteApplicationEvent> { private static Log log = LogFactory.getLog(ChatListener.class); public ChatListener(){} @Override
public void onApplicationEvent(ChatRemoteApplicationEvent event){
log.info(String.format("应用%s对应用%s悄悄的说:\"%s\"",
event.getOriginService(),
event.getDestinationService(),
event.getMessage()));
}
}

配置类将监听器注册到BeanFactory中,并需要显示的告诉Spring Cloud Bus我们有一个自定义事件:@RemoteApplicationEventScan(basePackageClasses=ChatRemoteApplicationEvent.class),否则BUS收到消息后无法识别事件类型。

@Configuration
@ConditionalOnClass(ChatListener.class)
@RemoteApplicationEventScan(basePackageClasses=ChatRemoteApplicationEvent.class)
public class BusChatConfiguration { @Bean
public ChatListener ChatListener(){
return new ChatListener();
}
}

发布到本地Maven仓库:

mvn install

spring-cloud-bus-a、spring-cloud-bus-b的POM依赖:

<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR3</spring-cloud.version>
</properties> <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-bus</artifactId>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.bluersw</groupId>
<artifactId>spring-cloud-bus-shared-library</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies> <dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

在spring-cloud-bus-a、spring-cloud-bus-b的启动Main函数上增加@ComponentScan(value = "com.bluersw")注解,否则不会扫描引用spring-cloud-bus-shared-library项目的配置类(也就加载不了自定义的事件和监听器类型)。

spring-cloud-bus-a:

@SpringBootApplication
@ComponentScan(value = "com.bluersw")
public class SpringCloudBusAApplication { public static void main(String[] args) {
SpringApplication.run(SpringCloudBusAApplication.class, args);
} }

spring-cloud-bus-b:

@SpringBootApplication
@ComponentScan(value = "com.bluersw")
public class SpringCloudBusBApplication { public static void main(String[] args) {
SpringApplication.run(SpringCloudBusBApplication.class, args);
} }

spring-cloud-bus-a发送消息给spring-cloud-bus-b(启动spring-cloud-bus-a程序和spring-cloud-bus-b程序):

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringCloudBusAApplicationTests { @Autowired
private ApplicationEventPublisher context; @Autowired
private BusProperties bp; @Test
public void AChat() {
context.publishEvent(new ChatRemoteApplicationEvent(this,bp.getId(),null,"hi!B应用,我是A应用,。"));
} }

执行AChat()之后,spring-cloud-bus-b的控制台会输出:

”应用spring-cloud-bus-a

细聊Spring Cloud Bus的更多相关文章

  1. 跟我学SpringCloud | 第八篇:Spring Cloud Bus 消息总线

    SpringCloud系列教程 | 第八篇:Spring Cloud Bus 消息总线 Springboot: 2.1.6.RELEASE SpringCloud: Greenwich.SR1 如无特 ...

  2. Spring Cloud Bus介绍--Spring Cloud学习第七天(非原创)

    一.什么是Spring Cloud Bus二.Spring Cloud Bus之RabbitMQ介绍三.Spring Cloud Bus整合RabbitMQ四.Spring Cloud Bus整合Ka ...

  3. SpringCloud教程 | 第八篇: 消息总线(Spring Cloud Bus)

    一.安装rabbitmq 二.pom父文件 <?xml version="1.0" encoding="UTF-8"?> <project x ...

  4. 第七篇: 消息总线(Spring Cloud Bus)

    Spring Cloud Bus 将分布式的节点用轻量的消息代理连接起来.它可以用于广播配置文件的更改或者服务之间的通讯,也可以用于监控.本文要讲述的是用Spring Cloud Bus实现通知微服务 ...

  5. spring cloud 使用spring cloud bus自动刷新配置

    Spring Cloud Bus提供了批量刷新配置的机制,它使用轻量级的消息代理(例如RabbitMQ.Kafka等)连接分布式系统的节点,这样就可以通过Spring Cloud Bus广播配置的变化 ...

  6. Spring Boot + Spring Cloud 构建微服务系统(十):配置中心(Spring Cloud Bus)

    技术背景 我们在上一篇讲到,Spring Boot程序只在启动的时候加载配置文件信息,这样在GIT仓库配置修改之后,虽然配置中心服务器能够读取最新的提交信息,但是配置中心客户端却不会重新读取,以至于不 ...

  7. spring cloud bus原理总结

    1.spring cloud bus spring cloud是按照spring的配置对一系列微服务框架的集成,spring cloud bus是其中一个微服务框架,用于实现微服务之间的通信. spr ...

  8. Spring Cloud Bus 消息总线 RabbitMQ

    Spring Cloud Bus将分布式系统中各节点通过轻量级消息代理连接起来. 从而实现例如广播状态改变(例如配置改变)或其他的管理指令. 目前唯一的实现是使用AMQP代理作为传输对象. Sprin ...

  9. 第九章 消息总线: Spring Cloud Bus

    在微服务架构的系统中, 我们通常会使用轻量级的消息代理来构建一个共用的消息主题让系统中所有微服务实例都连接上来, 由于该主题中产生的消息会被所有实例监听和消费, 所以我们称它为消息总线. 在总线上的各 ...

随机推荐

  1. HTML水平居中和垂直居中的实现方式

    父元素是块元素,根据子元素不同分为以下几种: 1.子元素是行内元素: a.水平居中:在父元素上设置text-align:center; b.垂直居中:在行内子元素上设置行高与父元素相同line-hei ...

  2. VMware新加网卡NAT连接(内网)出现本机与虚拟机ping不通的问题

    今新加网卡NAT连接,配置好之后始终出现eth1:link is not ready. 虚拟机与本机不能建立连接. 解决方案:windows里面打开服务开启VMware NAT Service,并关闭 ...

  3. VPS 安装MySQL

    目前Centos下默认支持的数据库是MariaDB,MariaDB是mysql的增强版本,由于mysql被Oracle收购之后,mysql之父担心之后mysql会变成闭源的软件,就又开发了这个版本,支 ...

  4. Zabbix分布式监控系统实践 自定义配置

    https://www.zabbix.com/wiki/templates/start 环境介绍OS: Ubuntu 10.10 Server 64-bitServers:zabbix-server: ...

  5. shiro框架在springboot项目中的应用

    地址:https://blog.csdn.net/taojin12/article/details/88343990 地址2:https://blog.csdn.net/bicheng4769/art ...

  6. window.onload()和$(document).ready的区别( $(document).ready == $(function(){ }) )

    首先$(function(){}) 和 $(document).ready(function(){}) 是一个方法,$(function(){})为简写(用的多) $(document).ready和 ...

  7. mysql服务设置远程连接

    一.前期准备 1.虚拟机/物理机    mysql环境(非本机)2.本机 navicat软件(验证远程连接) 二 .mysql配置 1.在远程主机的本机   使用root用户连接mysql mysql ...

  8. hdu 5963:朋友

    刚看到这题时感觉是树上博弈,然后我开始用一维的数据找规律.发现在一维的树上,如果把各边的值合在一起当成一个二进制数,那么,ans只与奇偶性有关,于是,我提出了一个比较大胆的假设:若连接在root上的所 ...

  9. 2018 ACM-ICPC 中国大学生程序设计竞赛线上赛 D Merchandise (斜率优化)

    Description: The elderly aunts always like to look for bargains and preferential merchandise. Now th ...

  10. 170820-关于JSP页面的知识点

    1.JSP [1] 简介 > HTML - HTML擅长显示一个静态的网页,但是不能调用Java程序. > Servlet - Servlet擅长调用Java程序和后台进行交互,但是它不擅 ...