事件驱动编码
概述
Spring 事件(Event)机制 是一种基于观察者模式的实现,它允许应用组件在保持松耦合的同时进行通信。在Spring框架中,事件机制主要涉及三个核心概念:事件源(ApplicationEvent)、事件发布器(ApplicationEventPublisher)和事件监听器(ApplicationListener)
事件发布流程中,有三个核心概念,他们之间的关系如下图所示:
- 事件源(
ApplicationEvent
):这个就是你要发布的事件对象。 - 事件发布器(
ApplicationEventPublisher
):这是事件的发布工具。 - 事件监听器(
ApplicationListener
):这个相当于是事件的消费者。
使用 Spring 事件机制,可以有以下好处:
- 可以快捷地实现代码异步执行,不会影响主线程代码执行逻辑
- 实现业务逻辑解耦,让代码逻辑更加简洁。例如:事件源(
ApplicationEvent
)代码在模块A中,事件监听器(ApplicationListener
)在模块B中,这样就无须编写强关联代码。
[一]本地事件
[1]使用方法
接下来,让我们通过一个简单的案例来演示一下 Spring 中事件的用法。
首先,我们需要自定义一个事件对象,自定义的事件继承自 ApplicationEvent
类,如下:
public class MyEvent extends ApplicationEvent {
private String name;
public MyEvent(Object source, String name) {
super(source);
this.name = name;
}
@Override
public String toString() {
return "MyEvent{" +
"name='" + name + '\'' +
"} " + super.toString();
}
}
事件发布方式,通过 ApplicationContext
中的 publishEvent
方法,将事件发送出去即可。还可以使用 ApplicationEventPublisher
中的 publishEvent
方法
提示
使用 ApplicationContext
方法,可以有很多种,常用的两种方式如下:
- 实现
ApplicationContextAware
接口 - 以 @Autowire 的方式,注入
ApplicationContext
使用 ApplicationEventPublisher
方法,可以有很多种:
- 实现
ApplicationEventPublisherAware
接口 - 以 @Autowire 的方式,注入
ApplicationEventPublisher
为了方便使用,Dante Cloud 在 ServiceContextHolder
中提供便捷获取ApplicationContext
方法,可以直接发送 Event,如下所示:
ServiceContextHolder.getApplicationContext().publishEvent(new MyEvent("测试","测试名称"))
事件发布之后,还需要一个事件消费者去消费这个事件,或者也可以称之为事件监听器。
事件监听器有两种定义方式,第一种是自定义类实现 ApplicationListener
接口:
@Component
public class MyEventListener implements ApplicationListener<MyEvent> {
@Override
public void onApplicationEvent(MyEvent event) {
System.out.println("event = " + event);
}
}
第二种方式则是通过注解去标记事件消费(监听)方法:
@Component
public class MyEventListener02 {
@EventListener(value = MyEvent.class)
public void hello(MyEvent event) {
System.out.println("event02 = " + event);
}
}
就这样,一个简单的事件发布订阅系统就完成了。现在,如果发布事件,对应的事件监听器中就可以接收到事件。
[2]系统应用
Dante Cloud 中,大量使用了 Spring 事件(Event)机制,主要的事件定义如下,你可以在自己的代码中直接使用:
系统类事件
AccountStatusChangedEvent
:用户账号状态变更事件,主要用于账号锁定以及自动解锁用途DisableAuthenticationEvent
:关闭认证事件EnableAuthenticationEvent
:开启认证事件EnumDictionaryGatherEvent
:枚举字典收集事件OidcClientRegistrationEvent
:OIDC 客户端自动注册事件PrincipalConnectedEvent
:用户上线事件PrincipalDisconnectedEvent
:用户下线事件RestAuditEvent
:Rest 接口审计记录事件RestMappingGatherEvent
:Rest 接口映射收集事件SignOutComplianceEvent
:系统用户登出合规性记录事件SysAttributeChangeEvent
:权限元数据变更事件SignOutComplianceEvent
:系统用户登出合规性记录事件
消息类事件
DialogueMessageReceivingEvent
:接收到私信事件MailNotificationSendingEvent
:Email通知发送事件MessageSendingEvent
:统一消息发送事件MqttMessageReceivingEvent
:Mqtt消息接收事件MqttMessageSendingEvent
:Mqtt消息发送事件RSocketBroadcastMessageSendingEvent
:RSocket 广播发送事件RSocketFireAndForgetMessageReceivingEvent
:RSocket 个人消息接收事件RSocketFireAndForgetMessageSendingEvent
:RSocket 个人消息发送事件StreamMessageSendingEvent
:Spring Cloud Stream 发送事件WebSocketBroadcastMessageSendingEvent
:WebSocket 广播发送事件WebSocketUserMessageSendingEvent
:WebSocket 个人消息发送事件
[3]代码抽象
当我们编写自定义 Event 时会发现,ApplicationEvent
构造函数有一个 Object
类型的参数。这个参数就是我们希望通过 Event 传递数据。Object
类型的数据,在实际使用中并不方便,接收到数据还是需要进行类型转换。
为了方便使用,Dante Cloud 抽象定义了一个 AbstractApplicationEvent
类。这个类在使用时,需要指定数据类型的泛型,那么在拿数据时就可以直接获取到对应的对象。
public abstract class AbstractApplicationEvent<T> extends ApplicationEvent {
private final T data;
public AbstractApplicationEvent(T data) {
super(data);
this.data = data;
}
public AbstractApplicationEvent(T data, Clock clock) {
super(data, clock);
this.data = data;
}
public T getData() {
return data;
}
}
[4]注意事项
本章节,之所以称之为“本地事件”,是因为标注的 Spring 事件,只能用于单独的服务或者系统之中。不管 ApplicationEvent
和 ApplicationListener
是在相同的代码模块中还是不同代码模块中,必须保证两者在同一个服务或者应用中才能生效。
换句话说,跨应用或者服务之间使用 Spring 事件是无法进行通信的。
[二]远程事件
Spring 事件(Event)机制,在 Spring 生态的各个组件被大量使用。但是无法解决跨服务或者跨应用进行事件通信。Spring 生态中的 Spring Cloud Bus
组件对此提供了解决方案。
Spring Cloud Bus
是 Spring Cloud Stream
的扩展组件。Spring Cloud Stream
主要用于实现在服务间便捷的使用消息队列,由此可以看出 Spring Cloud Bus
实现远程事件通信,是依赖于外部消息队列实现,同时采用和 Spring 事件机制一致的编码方式,以保持代码风格的一致性。
[1]使用方法
Spring Cloud Bus
发送远程 Spring 事件,与本地事件用法基本一致。也是需要定义 ApplicationEvent
和 ApplicationListener
。
区别在于:
- 自定义远程事件需要实现
RemoteApplicationEvent
而不是ApplicationEvent
- 不管是事件发送端还是接收端,都需要使用使用注解
@RemoteApplicationEventScan
扫描到所需使用的RemoteApplicationEvent
。否则事件不会被准确接收到。 RemoteApplicationEvent
比ApplicationEvent
多了一个发送目标
参数。这个参数用于指定 Event 具体发送到的应用或服务。
[2]系统应用
Dante Cloud 中,大量使用了 Spring Cloud Bus
远程事件,主要的事件定义如下,你可以在自己的代码中直接使用:
系统类事件
RemoteAccountStatusChangedEvent
:用户账号状态变更事件,主要用于账号锁定以及自动解锁用途RemoteAttributeTransmitterSyncEvent
:权限数据同步事件RemoteDisableAuthenticationEvent
:关闭认证事件RemoteEnableAuthenticationEvent
:开启认证事件RemoteEnumDictionaryGatherEvent
:枚举字典收集事件RemoteOidcClientRegistrationEvent
:OIDC 客户端自动注册事件RemoteRestAuditEvent
:Rest 接口审计记录事件RemoteRestMappingGatherEvent
:Rest 接口映射收集事件
消息类事件
RemoteMessageSendingEvent
:统一消息发送事件
[3]注意事项
远程事件也支持数据的传递,传递的数据默认还是为 Object
类型,当然你也可以传递具体的数据对象。Spring Cloud Bus
也是和 Spring 生态其它组件一样,使用 Jackson 来处理序列化和反序列化。
但是这里需要注意的是,由于对象的定义千变万化,任何 JSON 组件都不可能 100% 的、自动化的实现对象的反序列化。在远程事件中传递自定义的对象时,很可能出现接收端反序列化异常。
Dante Cloud 为了规避这方面问题,直接选择了最“笨拙”的方式,发送事件时将对象数据转换成 JSON 字符串,监听器接收到数据时在将 JSON 字符串手动反序列化为对象。
提示
有些时候选择一些“笨拙”的方式实现代码未必不好,很多让人摸不着头脑、不好解决的问题通常都是由于为了使用某些“简单”的方式而导致。
[三]混合事件
在微服务的开发过程中,本地事件和远程事件往往并不是孤立使用的,为了满足一些需求和场景,可能既需要使用本地事件又需要使用远程事件。
在 Dante Cloud 中就大量存在这样的场景,比如:REST 接口的扫描、枚举字典的聚合等。这种类型的代码往往都是模版化的,实现逻辑大同小异,会导致编写出大量逻辑相似的代码。
为了减少重复代码的出现,针对同时需要本地事件和远程事件场景,抽象出一个同一个的策略化事件接口 cn.herodotus.stirrup.message.core.definition.strategy.StrategyEventManager
以及 cn.herodotus.stirrup.message.core.definition.strategy.ApplicationStrategyEventManager
。
通过实现 StrategyEventManager
以及 ApplicationStrategyEventManager
这两个接口就可以大量减少模版化代码的产生,同时便捷实现本地事件和远程事件的混合使用
示例代码,如下所示:
public class DefaultAccountStatusChangedEventManager implements AccountStatusChangedEventManager {
@Override
public String getDestinationServiceName() {
return ServiceContextHolder.getUpmsServiceName();
}
@Override
public void postLocalProcess(AccountStatus data) {
publishEvent(new AccountStatusChangedEvent(data));
}
@Override
public void postRemoteProcess(String data, String originService, String destinationService) {
publishEvent(new RemoteAccountStatusChangedEvent(data, originService, destinationService));
}
}