EventBus 确保触发消息的对象和使用消息的对象解耦,在很多场景下都有作用,通常用在生产者触发消息的时候并不关心消费这个消息的对象是谁的时候最适合。
笔者最早使用 Java Swing 的时候就用到了很多 UI Event 和自定义 listener 和 event。现在 Spring boot 的很多项目下多个 service 之间,有的是强关联,直接在一个 service 里引用另外一个 service ,有的时候会导致相互引用,另外有很多情况 service 之间本来就没有什么关联,解耦会更合适。
举例:比如删除一个用户是在 UserService 下实现的,用户的删除可能会触发很多相应的处理,分别在不同的 service 下处理,这个时候不需要在 UserService 里引起所有处理的 service,只需要利用 EventBus 触发一个 "USER_DELETE" 的消息就可以。然后所有订阅这个消息的 service 收到消息后自行去处理。
1. 自己的实现
EventBus 实现的原理很简单,早期都是自己实现的。
通常就是一个单例的 EventBus 对象里面包含一个 Map,对应的 key 是监听的 topic ,对应的 value 是所有对这个 topic 感兴趣的监听者对象,所以 value 是一个数组
private final HashMap> dict = new HashMap<>();
通过注册方法,把消费者对象注册进来,通常这些消费者都 implement 一个特定的接口或直接使用一个嵌入类。
public interface IEvent {
/**
* 接收到消息后触发消息的处理调用
*
* @param data 消息带的数据
*/
void invoke(String message, Object data);
}
public void on(String message, IEvent userListener) {
synchronized (EventBus.class) {
if (!dict.containsKey(message)) {
dict.put(message, new ArrayList<>());
}
List listeners = dict.get(message);
listeners.add(userListener);
}
}
最后是触发消息函数,包含 topic 和附带的数据对象
public void fire(String message, Object data) {
if (dict.containsKey(message)) {
List listeners = dict.get(message);
listeners.forEach(listener -> listener.invoke(message, data));
}
}
使用也很简单,就是生产和消费者约定一个 topic 名称,然后消费者通过 EventBus 订阅 topic,生产者通过 EventBus 触发 topic
class Test1 {
public void fireEvent() {
//1.自己实现的eventbus使用方式
Test2 test2 = new Test2();
Test3 test3 = new Test3();
EventBus.getInstance().on("Event1", test2);
EventBus.getInstance().fire("Event1", "data1");
}
class Test2 implements IEvent {
@Override
public void invoke(String message, Object data) {
System.out.println(Thread.currentThread() + " test2接收到消息,主题是" + message + ",内容是" + data);
}
}
class Test3 {
public Test3() {
EventBus.getInstance().on("Event1", new IEvent() {
@Override
public void invoke(String message, Object data) {
System.out.println(Thread.currentThread() + " test3接收到消息,主题是" + message + ",内容是" + data);
}
});
}
}
2. guava的实现
guava 库带了一个 EventBus ,实现原理和使用都差不多,它也是用一个 map 来保存订阅者。
ConcurrentMap, CopyOnWriteArraySet> subscribers =
Maps.newConcurrentMap()
相比自己的实现,它当然多了很多优点:
- 线程安全
- 允许构建多个消息中心的实例
- 省略了 topic 的概念,发送消息的时候,用消息对象的类型(class)来区分
- 订阅者不需要实现特定的接口,只需要在方法前加特定的注解(@Subscribe)
- 支持同步和异步二种方式,如果是同步方式,在一些场景下,订阅者对消息的处理时间太长会影响生产者的正常执行。
直接看代码例子:
class Test1 {
public void fireEvent() {
//2.google guava库的eventbus使用方式
//2.1同步消息中心
com.google.common.eventbus.EventBus eventBus = new com.google.common.eventbus.EventBus();
Test4 test4 = new Test4();
eventBus.register(test4);
eventBus.post("Event2");
eventBus.post(new EventObject("Event3", "Event3Name"));
//2.2异步消息中心
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 20, 5, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10), new ThreadPoolExecutor.AbortPolicy());
AsyncEventBus asyncEventBus = new AsyncEventBus(executor);
asyncEventBus.register(new Test4());
asyncEventBus.post("Event4");
asyncEventBus.post(new EventObject("Event5", "Event5Name"));
}
}
class Test4 {
@Subscribe
public void invoke(String event) {
//函数名随意,参数只能一个
System.out.println(Thread.currentThread() + " test4接收到guava实现的string消息,内容是" + event);
}
@Subscribe
public void invoke(EventObject event) {
//函数名随意,参数只能一个
System.out.println(Thread.currentThread() + " test4接收到guava实现的object消息,内容是" + event.toString());
}
}
异步方式的使用也很简单,就是在构建 EventBus 的时候传递一个线程池对象,这样触发消息的时候会自动从线程池获取一个线程来处理消息,避免消息的处理影响生产者。
执行的结果如下:
Thread[main,5,main] test3接收到消息,主题是Event1,内容是data1
Thread[main,5,main] test2接收到消息,主题是Event1,内容是data1
Thread[main,5,main] test4接收到guava实现的string消息,内容是Event2
Thread[main,5,main] test4接收到guava实现的object消息,内容是EventObject{id='Event3', name='Event3Name'}
Thread[pool-1-thread-1,5,main] test4接收到guava实现的string消息,内容是Event4
Thread[pool-1-thread-2,5,main] test4接收到guava实现的object消息,内容是EventObject{id='Event5', name='Event5Name'}
可以注意到最后2个消息的处理是在线程池里线程执行的,不影响 main 线程。
总体来说如果项目引用了 guava,就直接用它自带的 EventBus,方便好用。
示例代码可以从 GitHub 上下载。