说明:EventBus是google-guava提供的消息发布-订阅类库,3个最核心的方法如下:
发布:即post(Object),发布事件到所有注册的订阅者,当事件被发布到所有订阅者后,这个方法就会返回成功,这个方法会忽略掉订阅者抛出的任何异常;
注册:即register(Object);注册对象中所有订阅者方法,这些方法都能收到事件。
解除注册:即unregister(Object);取消已注册对象中所有订阅者方法的注册;
sharding-jdbc使用EventBus发布&订阅时,对EventBus稍微封装了一下,即把EventBus设计为单例,然后通过EventBusInstance.getInstance()方法获取EventBus实例。EventBus单例核心源码如下:
public final class EventBusInstance {
// 饿汉式实现单例模式
private static final EventBus INSTANCE = new EventBus();
public static EventBus getInstance() {
return INSTANCE;
}
}
EventBus全类名为
com.google.common.eventbus.EventBus
。
EventBus介绍
EventBus来自于google-guava包中。源码注释如下:
Dispatches events to listeners,
and provides ways for listeners to register themselves.
The EventBus allows publish-subscribe-style communication between components
without requiring the components to explicitly
register with one another (and thus be
aware of each other).
It is designed exclusively to replace traditional Java in-process event distribution using explicit registration.
It is not a general-purpose publish-subscribe
system, nor is it intended for interprocess communication.
翻译:将事件分派给监听器,并为监听器提供注册自己的方法。EventBus允许组件之间的发布 - 订阅式通信,而不需要组件彼此明确注册(并且因此彼此意识到)。 它专门用于使用显式注册替换传统的Java进程内事件分发。 它不是一个通用的发布 - 订阅系统,也不是用于进程间通信。
使用参考
关于EventBus的用例代码提取自sharding-jdbc源码,并结合lombok最大限度的简化;
-
DMLExecutionEvent
DMLExecutionEvent是发布&订阅事件的模型,并且有个父类BaseExecutionEvent,申明如下:
@Getter
@Setter
public class BaseExecutionEvent {
private String id;
}
@Getter
@Setter
public class DMLExecutionEvent extends BaseExecutionEvent{
private String dataSource;
private String sql;
}
-
DMLExecutionEventListener
即事件监听器,订阅者接收到发布的事件后,进行业务处理:
public final class DMLExecutionEventListener {
@Subscribe
@AllowConcurrentEvents
public void listener(final DMLExecutionEvent event) {
System.out.println("监听的DML执行事件: " + JSON.toJSONString(event));
// do something
}
}
-
发布&订阅
Main主方法中,注册订阅者监听事件,以及发布事件。
public class Main {
static{
// 注册监听器
EventBusInstance.getInstance().register(new DMLExecutionEventListener());
}
public static void main(String[] args) throws Exception {
// 循环发布10个事件
for (int i=0; i<10; i++) {
pub();
Thread.sleep(1000);
}
}
private static void pub(){
DMLExecutionEvent event = new DMLExecutionEvent();
event.setId(UUID.randomUUID().toString());
event.setDataSource("sj_db_1");
event.setSql("select * from t_order_0 where user_id=10");
System.out.println("发布的DML执行事件: " + JSON.toJSONString(event));
EventBusInstance.getInstance().post(event);
}
}
源码分析
主要分析发布事件以及注册监听器的核心源码;
注册源码分析
//注册Object上所有订阅方法,用来接收事件,上面的使用参考,DMLExecutionEventListener就是这里的object
public void register(Object object) {
// 根据注册对象,找到所有订阅者(一个注册对象里可以申明多个订阅者)
Multimap<Class<?>, EventSubscriber> methodsInListener =
finder.findAllSubscribers(object);
// 重入写锁保证线程安全
subscribersByTypeLock.writeLock().lock();
try {
// 把订阅者信息放到map中缓存起来(发布事件post()时就会用到)
subscribersByType.putAll(methodsInListener);
} finally {
// 重入琐写锁解锁
subscribersByTypeLock.writeLock().unlock();
}
}
说明:Multimap是guava自定义数据结构,类似Map<K, Collection<V>>
,key就是事件类型(@Subscribe注解方法的第一个入参:即事件DMLExecutionEvent),例如DMLExecutionEvent。value就是EventSubscriber即事件订阅者集合(其中包含target:为订阅者类,method为被@Subscribe注解的方法)。需要注意的是,这个的订阅者集合是指Object里符合订阅者条件的所有方法。例如DMLExecutionEventListener.listener(),DMLExecutionEventListener中可以有多个订阅者,加上注解@Subscribe即可。
-
注册总结
通过这段源码分析可知,注册的核心就是将注册对象中所有的订阅者信息缓存起来,方便接下来的发布过程找到订阅者。
发布源码分析:根据获取事件及事件的所有非Object父类,查找这些事件对应的所有订阅方法,将订阅方法加入订阅执行队列进行执行;
public void post(Object event) {
// 得到所有该类以及它的所有父类,父类的父类,直到Object(因为有些注册的监听器是监听其父类)
Set<Class<?>> dispatchTypes = flattenHierarchy(event.getClass());
boolean dispatched = false;
// 遍历类本身以及所有父类
for (Class<?> eventType : dispatchTypes) {
// 重入读锁先锁住
subscribersByTypeLock.readLock().lock();
try {
// 得到类的所有订阅者,例如DMLExecutionEvent的订阅者就是DMLExecutionEventListener(EventSubscriber有两个属性:重要的属性target和method,target就是监听器即DMLExecutionEventListener,method就是监听器方法即listener;从而知道DMLExecutionEvent这个事件由哪个类的哪个方法监听处理)
Set<EventSubscriber> wrappers = subscribersByType.get(eventType);
if (!wrappers.isEmpty()) {
// 如果有订阅者,那么dispatched = true,表示该事件可以分发
dispatched = true;
// 遍历所有的订阅者,往每个订阅者的队列中都增加该事件
for (EventSubscriber wrapper : wrappers) {
enqueueEvent(event, wrapper);
}
}
} finally {
// 解锁
subscribersByTypeLock.readLock().unlock();
}
}
// 如果没有订阅者,且发送的不是DeadEvent类型事件,那么强制发送一个DeadEvent类型事件。
if (!dispatched && !(event instanceof DeadEvent)) {
post(new DeadEvent(this, event));
}
// 分发进入队列的事件
dispatchQueuedEvents();
}
enqueueEvent()&dispatchQueuedEvents()方法源码分析:
/**
* 核心数据结构为LinkedList,保存的是EventBus.EventWithSubscriber类型数据
*/
private final ThreadLocal<Queue<EventBus.EventWithSubscriber>> eventsToDispatch =
new ThreadLocal<Queue<EventBus.EventWithSubscriber>>() {
@Override protected Queue<EventBus.EventWithSubscriber> initialValue() {
return new LinkedList<EventBus.EventWithSubscriber>();
}
};
void enqueueEvent(Object event, EventSubscriber subscriber) {
// 数据结构为new LinkedList<EventWithSubscriber>(),EventWithSubscriber就是对event和subscriber的封装,LinkedList数据结构保证进入队列和消费队列顺序一致
eventsToDispatch.get().offer(new EventBus.EventWithSubscriber(event, subscriber));
}
// 分发队列中的事件交给订阅者处理,这个过程称为drain,即排干。排干的过程中,可能有新的事件被追加到队列尾部
void dispatchQueuedEvents() {
// 如果当前正在分发,则不重复执行
if (isDispatching.get()) {
return;
}
// 如果没有正在分发,那么利用ThreadLocal设置正在分发即isDispatching为true
isDispatching.set(true);
try {
Queue<EventBus.EventWithSubscriber> events = eventsToDispatch.get();
EventBus.EventWithSubscriber eventWithSubscriber;
// 不断从Queue中取出任务处理(调用poll()方法)
while ((eventWithSubscriber = events.poll()) != null) {
// 调用订阅者处理事件(method.invoke(target, new Object[] { event });,method和target来自订阅者)
dispatch(eventWithSubscriber.event, eventWithSubscriber.subscriber);
}
} finally {
// ThreadLocal可能内存泄漏,用完需要remove
isDispatching.remove();
// 队列中的事件任务处理完,清空队列,即所谓的排干(Drain)
eventsToDispatch.remove();
}
}
-
发布总结
总结一下调用了EventBus的post()方法后的流程:
-
遍历发布对象本身以及所有父类,每个类都同等待遇;
-
得到类的所有订阅者(监听器的有效方法集合);
-
如果有订阅者,就往订阅者的队列中增加事件;若果没有订阅者,并且发送的不是DeadEvent,那么强制发送DeadEvent;
-
不断取出队列中的事件,交给订阅者处理。
实际操作案例
事件机制包括三个部分:事件、事件监听器、事件源。
在Spring cloud环境下,使用google公司开源的guava工具类EventBus。
一、引入guava的jar包
二、在config下新建一个类EventBusConfig.java
import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.Subscribe;
import org.reflections.Reflections;
import org.reflections.scanners.MethodAnnotationsScanner;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Set;
import java.util.concurrent.Executors;
@Component
public class EventBusConfig {
@Autowired
private ApplicationContext context;
@Bean
@ConditionalOnMissingBean(AsyncEventBus.class)
AsyncEventBus createEventBus() {
AsyncEventBus eventBus = new AsyncEventBus(Executors.newFixedThreadPool(5));
Reflections reflections = new Reflections("com.xxx", new MethodAnnotationsScanner());
Set<Method> methods = reflections.getMethodsAnnotatedWith(Subscribe.class);
if (null != methods ) {
for(Method method : methods) {
try {
eventBus.register(context.getBean(method.getDeclaringClass()));
}catch (Exception e ) {
//register subscribe class error
}
}
}
return eventBus;
}
}
三、利用接口封装事件发送
1、定义接口LocalEventBus.java
public interface LocalEventBus {
void post(Event event);
}
2、定义实现类LocalEventBusImpl.java
import com.google.common.eventbus.AsyncEventBus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class LocalEventBusImpl implements LocalEventBus {
@Autowired
private AsyncEventBus eventBus;
@Override
public void post(Event event) {
if (null != event) {
eventBus.post(event);
}
}
}
3、接口Event.class
public interface Event<T> {
T getContent();
}
四、在业务工程里使用:
需要定义事件、消息体、订阅者、发送者。
1、定义login事件
public class LoginEvent implements Event<LoginMsg> {
private LoginMsg loginMsg;
public LoginEvent(LoginMsg loginMsg) {
this.loginMsg = loginMsg;
}
@Override
public LoginMsg getContent() {
return this.loginMsg;
}
}
2、定义消息体
public class LoginMsg {
private Long uid;
private String mobile;
private String ip;
private String osVersion;
private String deviceModel;
private String deviceToken;
}
3、定义订阅者
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class LoginSubscriber {
@Subscribe
public void onLogin(LoginEvent event) throws BizException {
LoginMsg msg = event.getContent();
Long uid = msg.getUid();
// 具体业务
}
}
4、定义发送者,把消息发送到EventBus。
@Autowired
private LocalEventBus localEventBus;
LoginMsg msg = new LoginMsg(uid, mobile, ip, osVersion, deviceModel, deviceToken);
localEventBus.post(new LoginEvent(msg));
实际使用中问题
这个异步框架在使用中有类似切面的功用,同时异步处理为我们前端响应提供了更好的体验;
但是在我们的实际项目中遇到了一次难以发现问题的生产事故;
在频繁点击情况下前后端请求参数无法保障一致性;
事故起因:
我们将HttpServletRequest 取做内部框架中的内部使用参数;这就导致了事故的发现;
首先,有个核心概念 HttpServletRequest 的生命周期,其生命周期仅存在一次请求中,
因此,在异步的环境下,我们主线程的请求完成之后,可能我们容器就会把线程回收或者去处理下一个任务,而当我们线程在处理下一个任务的时候,上一个任务的HttpServletRequest 是还没有结束生命的,这个时候就会存在多线程的数据不一致情况;
解决方案: 直接获取请求中的参数出来,进行使用,而不对 HttpServletRequest 进行占用;
转载:
作者:天草二十六_
链接:https://www.jianshu.com/p/4efbfdc01cf6