说下你可能没用过的EventBus

最近在Code Review的时候发现了这样一个业务场景,某个业务处理完成之后需要通知审核人员,通知的方式包含短信和邮件,所以代码大致是这样:

//业务校验
validate();
//处理业务逻辑
doBusiness();
//发送邮件或者发送其他类型消息
sendMsg(); 

这个对不对呢?

基于这种普遍的业务场景来说,一般首先我们会考虑同步或者异步发送的问题。

同步的话对接口RT有影响,而且和业务逻辑耦合在一起,这样的做法肯定不太好。

一般情况下,我们会做成异步的方式,使用MQ自己发送自己消费,或者说一个线程池搞定,这样的话不影响主业务逻辑,可以提高性能,并且代码做到了解耦。

然后还有就是数据一致性的问题,邮件一定要发送成功吗?

大多数时候其实我们并不要求邮件一定100%发送成功,失败了就失败好了,监控告警打点做好失败率不要超过阈值就好,还有就是消息服务一旦收到请求应该自己保证消息能够投递。

所以总的来说,使用MQ发送消息自己消费处理,或者线程池异步处理,最后自己搞个补偿的逻辑就能处理好这类问题。

那么,今天要说的是这两个解决方案之外的处理方式,对于这种场景其实我们可以用EventBus来解决。

EventBus使用

看名字就知道,EventBus是事件总线的意思,它是Google Guava库的一个工具,基于观察者模式可以做到进程内的代码解耦作用。

就拿上面的例子来说,引入一个MQ太重了,其实不太需要这样做,EventBus也能达到这个效果,和MQ相比他只能提供进程内的消息事件传递,这对于我们这种业务场景来说足够了不是吗?

我们先看EventBus怎么来使用,一般先创建一个EventBus实例。

//1.创建EventBus
private static EventBus eventBus = new EventBus();

第二步,创建一个事件消息订阅者,处理方式非常简单,只要在我们希望去处理事件的方法上加上@Subscribe注解即可。

形参只能有一个,如果定义0个或者多个的话运行就会报错。

public class EmailMsgHandler {

    @Subscribe
    public void handle(Long businessId) {
        System.out.println("send email msg" + businessId);
    }
}

第三步,注册事件。

eventBus.register(new EmailMsgHandler());

第四步,发送事件。

eventBus.post(1L);

这就是一个EventBus使用的最简单例子,下面我们看看结合开头说的例子怎么处理。

结合实际

比如上面说的案例,举例来说比如注册和用户下单的场景,都需要发送消息和邮件给用户。

EventBus并不强制说我们一定要用单例模式,因为他的创建销毁成本比较低,所以更多是根据我们的业务场景和上下文自己来选择。

public class UserService {
    private static EventBus eventBus = new EventBus();

    public void regist(){
        Long userId = 1L;
        eventBus.register(new EmailMsgHandler());
        eventBus.register(new SmsMsgHandler());
        eventBus.post(userId);
    }
}

public class BookingService {
    private static EventBus eventBus = new EventBus();

    public void booking(){
        //业务逻辑
        Long bookingId = 2L;
        eventBus.register(new EmailMsgHandler());
        eventBus.register(new SmsMsgHandler());
        eventBus.post(bookingId);
    }
}

然后在业务逻辑处理完成之后,分别去注册了邮件和短信两个事件订阅者。

public class EmailMsgHandler {

    @Subscribe
    public void handle(Long businessId) {
        System.out.println("send email msg" + businessId);
    }
}

public class SmsMsgHandler {

    @Subscribe
    public void handle(Long businessId) {
        System.out.println("send sms msg" + businessId);
    }
}

最后我们发送事件,用户注册我们发送了一个用户ID,下单成功我们发送了一个订单ID。

再写一个测试类去测试一下,分别创建两个service,然后分别调用方法。

public class EventBusTest {

    public static void main(String[] args) {
        UserService userService = new UserService();
        userService.regist();

        BookingService bookingService = new BookingService();
        bookingService.booking();

    }
}

执行测试类,我们可以看到输出,分别去执行了我们的事件订阅的方法。

send email msg1send sms msg1send email msg2send sms msg2

使用起来你会发现非常简单,对于希望轻量级简单地做到解耦使用EventBus非常合适。

注意别踩坑

首先,注意一下例子中的参数都是Long类型,如果事件的参数是其他类型的话,那么消息是无法接受到的,比如我们把下单中发送的订单ID改成String类型然后会发现没有消费了,因为我们没有定义一个参数类型是String的方法。

public class BookingService {    private static AsyncEventBus eventBus = new AsyncEventBus(Executors.newFixedThreadPool(3));    public void booking(){        //业务逻辑        String bookingId = "2";        eventBus.register(new EmailMsgHandler());        eventBus.register(new SmsMsgHandler());        eventBus.post(bookingId);    }}//输出send email msg1send sms msg1

EmailMsgHandlerSmsMsgHandler都新增一个接收String类型的订阅方法,这样就可以接收到了。

@Subscribepublic void handle(String businessId) {   System.out.println("send email msg for string" + businessId);}@Subscribepublic void handle(String businessId) { System.out.println("send sms msg for string" + businessId);}//输出send sms msg1send email msg1send email msg for string2send sms msg for string2

除此之外,其实我们可以定义一个DeadEvent来处理这种情况,它相当于是一个默认的处理方式,当没有匹配的事件类型参数的话就会默认发送一个DeadEvent事件。

定义一个默认处理器。

public class DefaultEventHandler {    @Subscribe    public void handle(DeadEvent event) {        System.out.println("no subscriber," + event);    }}

BookingService新增一个pay()支付方法,下单完了去支付,注册我们的默认事件。

public void pay(){  //业务逻辑  eventBus.register(new DefaultEventHandler());  eventBus.post(new Payment(UUID.randomUUID().toString()));}@ToString@Data@NoArgsConstructor@AllArgsConstructorpublic class Payment {    private String paymentId;}

执行测试bookingService.pay()看到输出结果:

no subscriber,DeadEvent{source=AsyncEventBus{default}, event=Payment(paymentId=255da942-7128-4bd1-baca-f0a8e569ed88)}

源码分析

OK,简单的介绍就到这里,那其实到目前为止我们说的这个都是同步调用的,这不太符合我们的要求,我们当然使用异步处理更好。

那就看看源码它是怎么实现的。

@Betapublic class EventBus {  private static final Logger logger = Logger.getLogger(EventBus.class.getName());  private final String identifier;  private final Executor executor;  private final SubscriberExceptionHandler exceptionHandler;  private final SubscriberRegistry subscribers = new SubscriberRegistry(this);  private final Dispatcher dispatcher;    public EventBus() {    this("default");  }  public EventBus(String identifier) {    this(        identifier,        MoreExecutors.directExecutor(),        Dispatcher.perThreadDispatchQueue(),        LoggingHandler.INSTANCE);  }}

identifier就是个名字,标记,默认就是default

executor执行器,默认创建一个MoreExecutors.directExecutor(),事件订阅者根据你自己提供的executor来决定如何执行事件订阅的处理方式。

exceptionHandler是异常处理器,默认创建的就是打点日志。

subscribers就是我们的消费者,订阅者。

dispatcher用来做事件分发。

默认创建的executor是一个MoreExecutors.directExecutor(),看到command.run()你就会发现他这不就是同步执行嘛。

public static Executor directExecutor() {   return DirectExecutor.INSTANCE;}private enum DirectExecutor implements Executor {   INSTANCE;@Overridepublic void execute(Runnable command) {   command.run();}@Overridepublic String toString() {  return "MoreExecutors.directExecutor()";}

同步执行还是不太好,我们希望不光给我们解耦,还要异步执行,EventBus给我们提供了AsyncEventBusExecutor我们自己传入就好了。

public class AsyncEventBus extends EventBus {  public AsyncEventBus(String identifier, Executor executor) {    super(identifier, executor, Dispatcher.legacyAsync(), LoggingHandler.INSTANCE);  }   public AsyncEventBus(Executor executor, SubscriberExceptionHandler subscriberExceptionHandler) {    super("default", executor, Dispatcher.legacyAsync(), subscriberExceptionHandler);  }  public AsyncEventBus(Executor executor) {    super("default", executor, Dispatcher.legacyAsync(), LoggingHandler.INSTANCE);  }

上面的代码我们改成异步的,这样不就好起来了嘛,这样的话,实际上可以结合我们自己的线程池来处理了。

private static AsyncEventBus eventBus = new AsyncEventBus(Executors.newFixedThreadPool(3));

OK,这个说清楚了,我们可以顺便再看看事件分发的处理,看到DeadEvent了吧,没有当前事件的订阅者,就会发送一个DeadEvent事件,bingo!

public void post(Object event) {
  Iterator<Subscriber> eventSubscribers = subscribers.getSubscribers(event);
    if (eventSubscribers.hasNext()) {
    dispatcher.dispatch(event, eventSubscribers);
  } else if (!(event instanceof DeadEvent)) {
    // the event had no subscribers and was not itself a DeadEvent
    post(new DeadEvent(this, event));
  }
}

总结

OK,这个使用和源码还是比较简单的哈,有兴趣的同学可以自己去瞅瞅,花不了多少工夫。

总的来说,EventBus就是提供了我们一个更优雅的代码解耦的方式,实际工作中的业务你肯定能用上它!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容