【Spring】事件监听器(常见错误)

1,305 阅读4分钟

一、前言

先来回顾下,Spring 事件包含以下三大组件: 2022-01-2723-50-49.png

  1. 事件(Event:用来区分和定义不同的事件,在 Spring 中,常见的如 ApplicationEventAutoConfigurationImportEvent,它们都继承于 java.util.EventObject

  2. 事件广播器(Multicaster:负责发布上述定义的事件。例如,负责发布 ApplicationEventApplicationEventMulticaster 就是 Spring 中一种常见的广播器。

  3. 事件监听器(Listener:负责监听和处理广播器发出的事件,例如 ApplicationListener 就是用来处理 ApplicationEventMulticaster 发布的 ApplicationEvent,它继承于 JDKEventListener

在使用事件监听时,容易出现问题,可归纳为如下三:

  1. 错误使用:误读事件本身含义
  2. 错误使用:监听错了事件的传播系统
  3. 异常处理:事件处理之间互相影响,导致部分事件处理无法完成

前面两个 错误使用 比较好理解,要么事件定义错,要么监听器监听错误对象。

异常处理 则相对复杂,事件是在这 pipeline 中顺序执行,途经其中一个监听器处理异常,导致后序监听器无法响应。

2022-01-2820-20-07.png

下面单独拎出 异常处理 来单独讨论。



二、常见错误:异常处理

这部分主要分为:

  1. 事故现场还原
  2. 问题出在哪?
  3. 问题解决

(1)事故现场还原

还原步骤:

  1. 定义事件
  2. 定义监听器(@Order 数值越小,优先级越高)
  3. 测试

2022-01-2821-22-20.png

定义事件

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)问题解决

可以从这两入手:

  1. 捕获异常:确保监听器的执行不会抛出异常
  2. 处理异常:保证不影响后续事件监听器。(使用 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 了。