Guava的event bus
guava, https://github.com/google/guava 是一个非常有名的Java类库,提供了很多在日常开发中常用的集合、函数接口等。此外,guava还提供了一个模块叫做event bus,生产者往event bus上投递消息,event bus负责回调订阅了此类消息的回调函数,实现了消息生产者和消费者之间的解耦和异步处理。以下是一个简单的例子:
public class SimpleListener { @Subscribe public void task(String s) { System.out.println("do task(" + s + ")"); } }
public class SimpleEventBusExample { public static void main(String[] args) { EventBus eventBus = new EventBus(); eventBus.register(new SimpleListener()); System.out.println("Post Simple EventBus Example"); eventBus.post("Simple EventBus Example"); } }
output
Post Simple EventBus Example do task(Simple EventBus Example)
event bus集成到spring中
在之前的例子和guava的官方文档里面可以看到,guava的event bus使用方式如下
1. 声明一个event bus对象(线程安全,所以可以做到全局唯一,而且订阅者和发布者必须共享这个event bus对象)
2. 对于订阅者,支持 @Subscribe,定义处理消息的回调函数。
3. 对每一个订阅者,需要调用event bus的register方法,才能收到消息订阅。
对于1 和 2,都比较好。但是对于第3步来说,则有一点困难。因为
1. 在spring中,通常的你的订阅者还会依赖其他的spring管理的bean,于是你的订阅者也会被纳入到spring的生命周期的管理中来,这样如果用new的方式来初始化一个订阅者,显得非常的"不spring"。
2. 对于每个订阅者,都要显式的注册到event bus里面,这样并没有做到关注点分离。理想的情况下,订阅者是不应该去关注如何注册到event bus中。它只应该申明处理消息的回调函数,以及该回调函数是否能够并发调用。注册到event bus中这件事,对于订阅者应该是被动且自动的(只需要申明自己是否想注册到event bus,而不需要关心细节)。这一点在多人开发,并且项目人员水平参差不齐的时候,尤其重要。
那么如何做到自动注册呢?其实答案很简单,在spring中,ApplicationContext这个类提供了一系列的方法去获取到当前spring context中的bean,只需要在event bus初始化之后,通过ApplicationContext来获取当前有哪些订阅者,并且主动的去注册就行。由于,guava的实现中,并没有要求订阅者实现某个接口,而是用注解的方式来声明回调函数的,则这篇文章中的实现,也不需要订阅者去实现某个接口,而是用注解的方式来申明自己是一个订阅者。代码如下
先声明一个注解
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Service public @interface EventSubscriber {}
对于一个订阅者,在类的接口上,加上这个注解,并且确定这个bean会在纳入spring的生命周期管理中。
@EventSubscriber public class SimpleSubscriber implements HiDasSubscriber { @Autowired Somebean somebean; @Subscribe @AllowConcurrentEvents public Integer logEventToDbAndUpdateStatus(String event) { System.out.println("receive a event:"+event); } }
声明一个event bus的服务,并且在初始化之后,通过ApplicationContext的 getBeansWithAnnotation 的方法把所有的订阅者获取,并且注册到event bus中。
@Service public class EventBusService implements InitializingBean{ private EventBus innerBus; @Inject private ApplicationContext appContext; public void unRegister(Object eventListener){ innerBus.unregister(eventListener); } public void postEvent(String event){ innerBus.post(event); } public void register(Object eventListener){ innerBus.register(eventListener); } @Override public void afterPropertiesSet() throws Exception { innerBus = new AsyncEventBus("Hidas-event-bus", Executors.newCachedThreadPool()); appContext.getBeansWithAnnotation(EventSubscriber.class).forEach((name, bean) -> { innerBus.register(bean); }); } }
完成了这两步之后。如果其他的消息生产者要往event bus上发消息,只需要注入这个event bus service,并且调用其post方法就好了。
注:这个只是例子,在实际项目中,对所有的消息都会声明一个基类或者接口,每个订阅者对只会处理消息的某个具体实现。这样event bus会根据消息的具体类型,来调用真正关注此类消息的订阅者的回调函数。这样比起让所有消息订阅者去实现一个 onEvent(BaseEvent event)的方法, 实际上是避免了一个多路分配的问题。
总结
对于一些类库在spring中使用,这种方法实际上是一种通用的模式,实现某个接口或者编写某个注解,然后通过ApplicationContext来获取对应的bean,之后进行某些注册或者组装操作。这样的话,可以让业务的代码,和框架的代码做到一定程度的关注点分离。