问题描述
我们在系统中使用 Guava 的 EventBus 进行代码解耦,一直运行良好,但是在某一次发版后,出现了消息重复消费的情况,示例代码如下:
预期执行情况:
实际执行情况:
可以看到消费者对事件消费了两次,但是通过检查代码发现并没有相关的改动,机器环境也没有变动。
经过一番定位后发现是编译机器 JDK 版本升级到 JDK 8,新版 javac 编译后字节码有变动。
导致相同代码和环境下出现了不同的行为。
定位过程:
对代码调用进行跟踪,先从 EventBus.post 方法看起
debug 时发现在 JDK 7 环境下,subscribersByType.get(eventType) 只能获取到一个 EventSubscriber 对象,即:onEvent(CallBack event)。
而在 JDK 8 环境下,subscribersByType.get(eventType) 的确能够获取到两个 EventSubscriber 对象,分别是:onEvent(CallBack event)和onEvent(Object o),
导致 enqueueEvent 了两次
subscribersByType 对象是一个类似 Map 结构,用于存储 EventSubscriber 对象,初始化数据的位置在 EventBus.register中:
这里 findAllSubscribers 的实现是在 AnnotatedSubscriberFinder 类中,逻辑比较简单,先调用 getAnnotatedMethods(clazz) 获取类中有 @Subscribe 注解的方法列表,然后实例化成 EventSubscriber 对象,存进 methodsInListener 并返回。
JDK 7 环境下 getAnnotatedMethods(clazz) 返回了 onEvent(CallBack event) 方法
JDK 8 环境下 getAnnotatedMethods(clazz) 返回了 onEvent(CallBack event) 方法和 onEvent(Object o) 方法,但是 onEvent(Object o) 不是我们定义的方法,而是 JDK 实现泛型时生成的桥方法,可以看成是在父类中定义的 onEvent(T t) 擦除泛型转换而来,并没有带 @Subscribe
getAnnotatedMethods(clazz) 方法中使用了 LoadingCache 缓存,最后获取注解的方法是:getAnnotatedMethodsInternal
这里关键的一句判断是 superClazzMethod.isAnnotationPresent(Subscribe.class),isAnnotationPresent是 JDK reflect 包中的方法,经过调试,发现在 JDK 7 中,
而在 JDK 8 中,
导致返回了 onEvent(CallBack event) 和 onEvent(Object o),并初始化出两个 EventSubscriber。
原因
对比两个版本 javac 编译出来的字节码
JDK 7 版本
第一个方法 onEvent(com.acvrock.guavatest.CallBack) 有一个 #27
Subscribe 运行时注解
第二个方法 onEvent(java.lang.Object) 没有带 RuntimeVisibleAnnotations
JDK 8 版本
两个方法都有 运行时注解 #27()
,也就是 Subscribe
在网上 JDK-8029563 : Method.getAnnotations() changed its return value in JDK 8. 这个描述和我们发现的问题是一致的
导致这一变动的是这个调整
JDK-6695379 : Copy method annotations and parameter annotations to synthetic bridge methods,
javac 实现了在编译时,拷贝方法和参数上的注解到桥方法上,也就是 onEvent(java.lang.Object)上,在 JDK 7u80 和 JDK 8b94 后生效
guava 如何解决
在 guava 18 中就已经解决了这个问题,代码如下:
判断了方法有 Subscribe 注解并且不是桥方法才认为是 subscribe 方法,对应的代码提交记录:
Fix EventBus to not include bridge methods when registering subscribe
真是一个摸不着头脑的 bug
参考
关于桥方法可以参考
Effects of Type Erasure and Bridge Methods
示例代码Exercise-code/eventbusTest