摘要: 消息订阅与分发机制在系统开发中有非常多的使用场景,有进程内的实现(观察者模式实现,Event, Listener),也系统间的实现(例如 各种 message queue)。Guava提供了一个进程内非常轻量级的实现 EventBus,可以很好的实现模块之间的解耦。并且提供了同步和异步的实现版本。
EventBus 看到这个名字,相信你已经大概明白是个什么东东了。
Event就是事件,Bus 这里不是巴士汽车的意思,在计算机领域一般翻译为总线。那么EventBus就是装载Event,然后做分发的。若做过java swing开发,肯定记得XXEvent,以及XXEventListener接口,都会有一个方法onEvent(XXEvent event)。没错EventBus干的就是相同的事情,但借着采用jdk1.5引入的注解,使得开发消息发布与订阅系统非常简洁方便,无需实现接口或继承基类。
关于消息发布与订阅系统的应用场景,其实在服务端系统开发中也经常用到,游戏中的任务系统或者一些电商系统的活动系统中经常会用到。例如,游戏中收集几个道具就完成某个任务,获取奖励。或者很多平台的签到系统,累计签到多少天获得奖励。这里就使用Guava中的EventBus开发一个签到系统。
public class SignInEvent {
// 签到天数
private int count;
public SignInEvent(int count) {
this.count = count;
}
public int getCount() {
return count;
}
}
public class SignInProcessor {
@Subscribe
public void signIn(SignInEvent event) {
int count = event.getCount();
// TODO 根据签到的天数发放奖励
System.out.println("签到" + count + "天");
}
}
public class AppTest {
@Test
public void eventBusTest() {
EventBus signInEventBus = new EventBus("SignInEventBus");
SignInProcessor processor = new SignInProcessor();
signInEventBus.register(processor);
signInEventBus.post(new SignInEvent(2));
}
}
事件处理类SignInProcessor并不需要实现某个接口,只需要在需要处理的方法上加上@Subscribe注解,这里也并没有限制SignInProcessor只能处理SignInEvent,要在SignInProcessor添加其他事件的处理逻辑也只需要添加一个方法,添加注解@Subscribe,第一个参数传入需要处理的事件实例。
public class SignInProcessor {
@Subscribe
public void signIn(SignInEvent event) {
int count = event.getCount();
// TODO 根据签到的天数发放奖励
System.out.println("签到" + count + "天");
}
@Subscribe
public void logout(LogoutEvent event) {
// 获取登出的时间
Date date = event.getTime();
// TODO
}
}
这里先说下,EventBus是如何确定一个类中的某个方法来处理相应的事件的呢,Guava中提供了一个接口
interface SubscriberFindingStrategy {
Multimap<Class<?>, EventSubscriber> findAllSubscribers(Object source);
}
并提供了一个机遇注解的实现
class AnnotatedSubscriberFinder implements SubscriberFindingStrategy
findAllSubscribers的实现逻辑
@Override
public Multimap<Class<?>, EventSubscriber> findAllSubscribers(Object listener) {
Multimap<Class<?>, EventSubscriber> methodsInListener = HashMultimap.create();
Class<?> clazz = listener.getClass();
for (Method method : getAnnotatedMethods(clazz)) {
Class<?>[] parameterTypes = method.getParameterTypes();
Class<?> eventType = parameterTypes[0];
EventSubscriber subscriber = makeSubscriber(listener, method);
methodsInListener.put(eventType, subscriber);
}
return methodsInListener;
}
1. 获取listener对象类,以及父类中所有被@Subscriber注解的方法,而且这个方法有且只有一个参数
2. 获取这些方法的第一个参数的类型
3. 创建EventSubscriber,保存listener实例,以及对应的Method实例,方便以后反射调用方法处理事件
讲完如何查找处理方法后,再看下具体的事件对象是如何发布出去的呢?这里的具体逻辑就在EventBus中的post方法中
public void post(Object event) {
// 获取事件对象的所有父类以及父类实现的接口
// 获取父类的目的为的是可以把event发布给某些接收父类Event的处理方法
Set<Class<?>> dispatchTypes = flattenHierarchy(event.getClass());
boolean dispatched = false;
for (Class<?> eventType : dispatchTypes) {
// subscribersByType 是一个非线程安全的集合,所以在操作的时候需要添加锁
subscribersByTypeLock.readLock().lock();
try {
Set<EventSubscriber> wrappers = subscribersByType.get(eventType);
if (!wrappers.isEmpty()) {
dispatched = true;
for (EventSubscriber wrapper : wrappers) {
// 放入当前线程对应的Queue中,这里使用到ThreadLocal变量
enqueueEvent(event, wrapper);
}
}
} finally {
subscribersByTypeLock.readLock().unlock();
}
}
// 若未能找到对应Event的处理器而且当前事件的类型不是DeadEvent就把传入的事件包装成DeadEvent
if (!dispatched && !(event instanceof DeadEvent)) {
post(new DeadEvent(this, event));
}
// 从当前线程的对应的队列中事件处理器和事件,并处理事件
dispatchQueuedEvents();
}
从EventBus中的post方法处理逻辑来看,事件的分发和处理是在同一个线程中同步处理的。
但是很多时候事件的处理逻辑比较复杂耗时,需要将事件的分发和处理异步。事件的处理不阻塞分发的主线程。
Guava提供了AsyncEventBus,就是将分发和处理异步化。AsyncEventBus的实现并不复杂。
AsyncEventBus 继承自 EventBus, 将EventBus中的存放待分发的事件队列eventsToDispatch从ThreadLocal<Queue<EventWithSubscriber>>换成了ConcurrentLinkedQueue<EventWithSubscriber> 支持多个线程并发访问获取事件处理,AsyncEventBus的构造函数需要传入一个Executor,可以根据实际需要传入定制的线程池。
某些场景下,在事件处理类的实例中需要保存事件相关状态,多线程并发访问的时候可能出现问题。Guava提供了注解@AllowConcurrentEvents,它的用途标记多个线程能否同时调用同一个事件处理器的处理方法来处理相依的事件。
具体处理逻辑在AnnotatedSubscriberFinder类中的
private static EventSubscriber makeSubscriber(Object listener, Method method) {
EventSubscriber wrapper;
// 这里判断事件处理方法是否有被@AllowConcurrentEvents注解
if (methodIsDeclaredThreadSafe(method)) {
wrapper = new EventSubscriber(listener, method);
} else {
// 若没有被@AllowConcurrentEvents注解,多个线程在处理的时候就需要同步调用该处理器来处理事件
wrapper = new SynchronizedEventSubscriber(listener, method);
}
return wrapper;
}
好了,Guava中的EventBus相关使用及实现基本讲完了。其实并不复杂。需要你对下面的相关类和处理机制比较熟悉
1. ThreadLocal
2. ConcurrentLinkedQueue
3. 反射获取类的父类和接口,这里使用Guava中的TypeToken封装类,已经反射方法调用
4. ReentrantReadWriteLock
5. Cache,Guava对常用的缓存做了一些封装,下一篇将会讲到