一、前言
先来回顾下,Spring
事件包含以下三大组件:
-
事件(
Event
):用来区分和定义不同的事件,在Spring
中,常见的如ApplicationEvent
和AutoConfigurationImportEvent
,它们都继承于java.util.EventObject
。 -
事件广播器(
Multicaster
):负责发布上述定义的事件。例如,负责发布ApplicationEvent
的ApplicationEventMulticaster
就是Spring
中一种常见的广播器。 -
事件监听器(
Listener
):负责监听和处理广播器发出的事件,例如ApplicationListener
就是用来处理ApplicationEventMulticaster
发布的ApplicationEvent
,它继承于JDK
的EventListener
。
在使用事件监听时,容易出现问题,可归纳为如下三:
- 错误使用:误读事件本身含义
- 错误使用:监听错了事件的传播系统
- 异常处理:事件处理之间互相影响,导致部分事件处理无法完成
前面两个 错误使用 比较好理解,要么事件定义错,要么监听器监听错误对象。
而 异常处理 则相对复杂,事件是在这 pipeline
中顺序执行,途经其中一个监听器处理异常,导致后序监听器无法响应。
下面单独拎出 异常处理 来单独讨论。
二、常见错误:异常处理
这部分主要分为:
- 事故现场还原
- 问题出在哪?
- 问题解决
(1)事故现场还原
还原步骤:
- 定义事件
- 定义监听器(
@Order
数值越小,优先级越高) - 测试
定义事件
public class MyEvent extends ApplicationEvent {
public MyEvent(Object source) { super(source); }
}
定义监听器1:
@Slf4j
@Component
@Order(1)
public class MyFirstEventListener implements ApplicationListener<MyEvent> {
@Override
public void onApplicationEvent(MyEvent event) {
log.info("{} received: {}", this.toString(), event);
// 模拟失效
throw new RuntimeException("exception happen on first listener");
}
}
定义监听器2:
@Slf4j
@Component
@Order(2)
public class MySecondEventListener implements ApplicationListener<MyEvent> {
@Override
public void onApplicationEvent(MyEvent event) {
log.info("{} received: {}", this.toString(), event);
}
}
测试:
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class LearnApplicationTests {
@Autowired
private AbstractApplicationContext applicationContext;
@Test
public void contextLoads() {
log.info("start to publish event");
applicationContext.publishEvent(new MyEvent(UUID.randomUUID()));
log.info("end to publish event");
}
}
输出结果如下:
start to publish event
MyFirstEventListener received: MyEvent()
java.lang.RuntimeException: exception happen on first listener
at com.donaldy.config.event.MyFirstEventListener.onApplicationEvent(MyFirstEventListener.java:21)
at com.donaldy.config.event.MyFirstEventListener.onApplicationEvent(MyFirstEventListener.java:12)
... ...
发现并没有传递到第二个监听器中。
(2)问题出在哪?
究其原因:处理器的执行是顺序执行的,在执行过程中,如果一个监听器执行抛出了异常,则后续监听器就得不到被执行的机会了。
通过 Spring
源码看下事件是如何被执行的?
// 当广播一个事件,执行的方法参考
// SimpleApplicationEventMulticaster#multicastEvent(ApplicationEvent):
@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ?
eventType : resolveDefaultEventType(event));
Executor executor = getTaskExecutor();
// getApplicationListeners 获取了具有执行资格的所有监听器
//(即为 MyFirstEventListener 和 MySecondEventListener)
// 然后按顺序去执行
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
// 最终每个监听器的执行是通过 invokeListener() 来触发的
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
}
具体执行逻辑如下:
protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
ErrorHandler errorHandler = getErrorHandler();
if (errorHandler != null) {
try {
doInvokeListener(listener, event);
}
catch (Throwable err) {
errorHandler.handleError(err);
}
}
else {
doInvokeListener(listener, event);
}
}
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
try {
listener.onApplicationEvent(event);
}
catch (ClassCastException ex) {
//省略非关键代码
}
else {
throw ex;
}
}
结论:最终事件的执行是由同一个线程按顺序来完成的,任何一个报错,都会导致后续的监听器执行不了
(3)问题解决
可以从这两入手:
- 捕获异常:确保监听器的执行不会抛出异常
- 处理异常:保证不影响后续事件监听器。(使用
ErrorHandler
)
1)捕获异常
如果希望所有监听器均能执行,那么就得保障所有监听器不抛出错误即可。
修改监听器1代码,如下:
@Component
@Order(1)
public class MyFirstEventListener implements ApplicationListener<MyEvent> {
@Override
public void onApplicationEvent(MyEvent event) {
try {
// 省略事件处理相关代码
}catch(Exception e){
}
}
}
2)处理异常
设置一个 ErrorHandler
,可以利用这个 ErrorHandler
去处理掉异常,从而保证后续事件监听器处理不受影响。
可以利用 Spring
中的代码,如下:
SimpleApplicationEventMulticaster simpleApplicationEventMulticaster
= applicationContext.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME,
SimpleApplicationEventMulticaster.class);
simpleApplicationEventMulticaster.setErrorHandler(TaskUtils.LOG_AND_SUPPRESS_ERROR_HANDLER);
修改测试代码,如下:
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class LearnApplicationTests {
@Autowired
private AbstractApplicationContext applicationContext;
@Test
public void contextLoads() {
SimpleApplicationEventMulticaster simpleApplicationEventMulticaster
= applicationContext.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME,
SimpleApplicationEventMulticaster.class);
simpleApplicationEventMulticaster
.setErrorHandler(TaskUtils.LOG_AND_SUPPRESS_ERROR_HANDLER);
log.info("start to publish event");
applicationContext.publishEvent(new MyEvent(UUID.randomUUID()));
log.info("end to publish event");
}
}
测试结果,如下:
start to publish event
MyFirstEventListener received: MyEvent()
Unexpected error occurred in scheduled task.
java.lang.RuntimeException: exception happen on first listener
at com.donaldy.config.event.MyFirstEventListener.onApplicationEvent(MyFirstEventListener.java:21)
at com.donaldy.config.event.MyFirstEventListener.onApplicationEvent(MyFirstEventListener.java:12)
... ...
MySecondEventListener received: MyEvent()
end to publish event
这样就不用每次 try catch
了。