最近在 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 使用的最简单例子,下面我们看看结合开头说的例子怎么处理。
结合实际
比如上面说的案例,举例来说比如注册和用户下单的场景,都需要发送消息和邮件给用户。
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
复制代码
去EmailMsgHandler
和SmsMsgHandler
都新增一个接收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 给我们提供了AsyncEventBus
,Executor
我们自己传入就好了。
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
就是提供了我们一个更优雅的代码解耦的方式,实际工作中的业务你肯定能用上它!
评论