Day608.Spring事件常见错误 -Spring编程常见错误

Posted 阿昌喜欢吃黄桃

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day608.Spring事件常见错误 -Spring编程常见错误相关的知识,希望对你有一定的参考价值。

Spring事件常见错误

针对Spring事件,它是一个相对比较独立的点。

或许你从没有在自己的项目中使用过 Spring 事件,但是你一定见过它的相关日志。

而且在未来的编程实践中,你会发现,一旦你用上了 Spring 事件,往往完成的都是一些有趣的、强大的功能,例如动态配置。


前言

Spring 事件就是,监听器设计模式在 Spring 中的一种实现,参考下图:

Spring 事件包含以下三大组件

  • 事件(Event):事件本身
    用来区分和定义不同的事件,在 Spring 中,常见的如 ApplicationEvent 和 AutoConfigurationImportEvent,它们都继承于 java.util.EventObject。

  • 事件广播器(Multicaster):触发发送事件
    负责发布上述定义的事件。例如,负责发布 ApplicationEvent 的 ApplicationEventMulticaster 就是 Spring 中一种常见的广播器。

  • 事件监听器(Listener):监听事件,并执行逻辑
    负责监听和处理广播器发出的事件,例如 ApplicationListener 就是用来处理 ApplicationEventMulticaster 发布的 ApplicationEvent,它继承于 JDK 的 EventListener,我们可以看下它的定义来验证这个结论:

    public interface ApplicationListener extends EventListener void onApplicationEvent(E event);
    

上述的组件中,任何一个都是缺一不可的,但是功能模块命名不见得完全贴合上述提及的关键字,例如发布 AutoConfigurationImportEvent 的广播器就不含有 Multicaster 字样。它的发布是由 AutoConfigurationImportSelector 来完成的。


一、未正确抓住事件

下面这段是基于 Spring Boot 的技术栈的代码:

@Slf4j
@Component
public class MyContextStartedEventListener implements ApplicationListener<ContextStartedEvent> 

  public void onApplicationEvent(final ContextStartedEvent event) 
    log.info(" received: ", this.toString(), event);
  
  

很明显,这段代码定义了一个监听器 MyContextStartedEventListener,试图拦截 ContextStartedEvent。

我们希望期待出现类似下面的日志的:

2021-03-07 07:08:21.197 INFO 2624 — [nio-8080-exec-1] c.s.p.l.e.MyContextStartedEventListener : com.spring.puzzle.class7.example1.MyContextStartedEventListener@d33d5a received: org.springframework.context.event.ContextStartedEvent[source=org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@19b56c0, started on Sun Mar 07 07:07:57 CST 2021]

但是当我们启动 Spring Boot 后,能没有拦截到这个事件,那是为什么呢???


在 Spring Boot 中,这个事件的抛出只发生在一处,即位于方法 AbstractApplicationContext#start 中。

@Override
public void start() 
   getLifecycleProcessor().start();
   publishEvent(new ContextStartedEvent(this));

也就是说,只有上述方法被调用,才会抛出 ContextStartedEvent,但是这个方法在 Spring Boot 启动时会被调用么?

我们可以查看 Spring 启动方法中围绕 Context 的关键方法调用,代码如下:

public ConfigurableApplicationContext run(String... args) 
      //省略非关键代码
      context = createApplicationContext();
      //省略非关键代码
      prepareContext(context, environment, listeners, applicationArguments, printedBanner);
      refreshContext(context);
      //省略非关键代码 
      return context;

以上代码最要做了两件事情:

  • 创建一个context
  • 然后通过context 去执行refresh
protected void refresh(ApplicationContext applicationContext) 
   Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
   ((AbstractApplicationContext) applicationContext).refresh();

最终会去在下面,去抛出一个ContextRefreshedEvent事件

protected void finishRefresh() 
   //省略非关键代码
   initLifecycleProcessor();
   // Propagate refresh to lifecycle processor first.
   getLifecycleProcessor().onRefresh();
   // Publish the final event.
   publishEvent(new ContextRefreshedEvent(this));
   //省略非关键代码

所以很明显,上面的代码,并没有抛出start事件,相反,他抛出了一个refresh事件

Spring 启动最终调用的是 AbstractApplicationContext#refresh,并不是 AbstractApplicationContext#start。


针对上面,那提供两种解决方案

  • 选择去监听ContextRefreshedEvent事件
@Component
public class MyContextRefreshedEventListener implements ApplicationListener<ContextRefreshedEvent> 
  public void onApplicationEvent(final ContextRefreshedEvent event) 
    log.info(" received: ", this.toString(), event);
  

  • 主动触发start事件
@RestController
public class HelloWorldController 

    @Autowired
    private AbstractApplicationContext applicationContext;

    @RequestMapping(path = "publishEvent", method = RequestMethod.GET)
    public String notifyEvent()
        applicationContext.start();       
        return "ok";
    ;

所以当一个事件拦截不了时,我们第一个要查的是拦截的事件类型对不对,执行的代码能不能抛出它。


二、监听事件的没有被Spring所收录问题

通过以上,我们可以保证事件的抛出,但是抛出的事件就一定能被我们监听到么?我们再来看这样一个案例,首先上代码:

@Slf4j
@Component
public class MyApplicationEnvironmentPreparedEventListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent > 
    public void onApplicationEvent(final ApplicationEnvironmentPreparedEvent event) 
        log.info(" received: ", this.toString(), event);
    

这里我们试图处理 ApplicationEnvironmentPreparedEvent。期待出现拦截事件的日志如下:

2021-03-07 09:12:08.886 INFO 27064 — [ restartedMain] licationEnvironmentPreparedEventListener : com.spring.puzzle.class7.example2.MyApplicationEnvironmentPreparedEventListener@2b093d received: org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent[source=org.springframework.boot.SpringApplication@122b9e6]

首先我们就确认这个事件是否会被抛出,并会不会存在问题。这个事件在 Spring 中是由 EventPublishingRunListener#environmentPrepared 方法抛出,代码如下:

@Override
public void environmentPrepared(ConfigurableEnvironment environment) 
   this.initialMulticaster
         .multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));

调试下代码,你会发现这个方法在 Spring 启动时一定经由 SpringApplication#prepareEnvironment 方法调用,调试截图如下:

表面上看,既然代码会被调用,事件就会抛出,那么我们在最开始定义的监听器就能处理,但是我们真正去运行程序时会发现,效果和上面第一个问题一样,都是监听器的处理并不执行,即拦截不了。那这又是为什么???


查看代码,我们会发现这个事件的监听器就存储在 SpringApplication#Listeners 中,调试下就可以找出所有的监听器,截图如下:

以上listeners中,我们发现并不存在我们自定义的 MyApplicationEnvironmentPreparedEventListener,那是为什么呢???


还是查看代码,当 Spring Boot 被构建时,会使用下面的方法去寻找上述监听器:

setListeners((Collection)
getSpringFactoriesInstances(ApplicationListener.class));

而上述代码最终寻找 Listeners 的候选者,参考代码 SpringFactoriesLoader#loadSpringFactories 中的关键行:

// 下面的 FACTORIES_RESOURCE_LOCATION 定义为 "META-INF/spring.factories"
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :


可以寻找下这样的文件(spring.factories),确实可以发现类似的定义:

org.springframework.context.ApplicationListener=\\
org.springframework.boot.ClearCachesApplicationListener,\\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\\
//省略其他监听器 

我们自定义的监听器并没有被放置在 META-INF/spring.factories 中。

监听器:由上述提及的 META-INF/spring.factories 中加载的监听器以及扫描到的 ApplicationListener 类型的 Bean 共同组成。


那提供两种解决方案

  • 手动实例化自定义监听器在构建 Spring Boot 时,添加 MyApplicationEnvironmentPreparedEventListener
@SpringBootApplication
public class Application 
    public static void main(String[] args) 
        MyApplicationEnvironmentPreparedEventListener myApplicationEnvironmentPreparedEventListener = new MyApplicationEnvironmentPreparedEventListener();
        SpringApplication springApplication = new SpringApplicationBuilder(Application.class).listeners(myApplicationEnvironmentPreparedEventListener).build();
        springApplication.run(args);
    

-使用 META-INF/spring.factories,即在 /src/main/resources 下面新建目录 META-INF,并在里面创建spring.factories,指定自定义监听器的全类路径

org.springframework.context.ApplicationListener=\\com.spring.puzzle.listener.example2.MyApplicationEnvironmentPreparedEventListener

三、部分事件监听器失效

以上可以确保事件在合适的时机被合适的监听器所捕获。

但是理想总是与现实有差距,有些时候,我们可能还会发现部分事件监听器一直失效或偶尔失效。

这里我们可以写一段代码来模拟偶尔失效的场景,首先我们完成一个自定义事件和两个监听器,代码如下:

public class MyEvent extends ApplicationEvent 
    public MyEvent(Object source) 
        super(source);
    


@Component
@Order(1)
public class MyFirstEventListener implements ApplicationListener<MyEvent> 

    Random random = new Random();

    @Override
    public void onApplicationEvent(MyEvent event) 
        log.info(" received: ", this.toString(), event);
        //模拟部分失效
        if(random.nextInt(10) % 2 == 1)
            throw new RuntimeException("exception happen on first listener");
    


@Component
@Order(2)
public class MySecondEventListener implements ApplicationListener<MyEvent> 
    @Override
    public void onApplicationEvent(MyEvent event) 
        log.info(" received: ", this.toString(), event);
    


这里监听器 MyFirstEventListener 的优先级稍高,且执行过程中会有 50% 的概率抛出异常。

然后我们再写一个 Controller 来触发事件的发送:

@RestController
@Slf4j
public class HelloWorldController 

    @Autowired
    private AbstractApplicationContext applicationContext;

    @RequestMapping(path = "publishEvent", method = RequestMethod.GET)
    public String notifyEvent()
        log.info("start to publish event");
        applicationContext.publishEvent(new MyEvent(UUID.randomUUID()));
        return "ok";
    ;

我们使用了最简化的代码模拟出了部分事件监听器偶尔失效的情况。当然在实际项目中,抛出异常这个根本原因肯定不会如此明显,但还是可以借机举一反三的。那么如何理解这个问题呢?

就是当MyFirstEventListener 监听器出现了异常了后,MySecondEventListener 就不会再执行了!!!

我们猜测处理器的执行是顺序执行的,在执行过程中,如果一个监听器执行抛出了异常,则后续监听器就得不到被执行的机会了


当广播一个事件,执行的方法参考 SimpleApplicationEventMulticaster#multicastEvent(ApplicationEvent)

@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) 
   ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
   Executor executor = getTaskExecutor();
   for (ApplicationListener<?> listener : getApplicationListeners(event, type)) 
      if (executor != null) 
         executor.execute(() -> invokeListener(listener, event));
      
      else 
         invokeListener(listener, event);
      
   

最终每个监听器的执行是通过 invokeListener() 来触发的,调用的是接口方法 ApplicationListener#onApplicationEvent。执行逻辑可参考如下代码:

protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) 
   ErrorHandler errorHandler = getErrorHandler();
   //在这里,我们看到了如果有异常处理器errorHandler,那异常就会报异常处理器处理
   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);
   
   //在doInvokeListener中看到,其实他没有对异常进行处理,只是抓住了后再次抛出去
   catch (ClassCastException ex) 
        //省略非关键代码
      
      else 
         throw ex;
      
   

这里我们并没有去设置什么 org.springframework.util.ErrorHandler,也没有绑定什么 Executor 来执行任务,所以针对本案例的情况,我们可以看出:最终事件的执行是由同一个线程按顺序来完成的,任何一个报错,都会导致后续的监听器执行不了


那我再提供两个解决方案

  • 确保监听器的执行不会抛出异常:保证自己的代码中一定不会抛出异常,直接在我们自定义的监听器中try-catch捕获
@Component
@Order(1)
public class MyFirstEventListener implements ApplicationListener<MyEvent> 
    @Override
    public void onApplicationEvent(MyEvent event) 
        try 
          // 省略事件处理相关代码
        catch(Throwable throwable)
            //write error/metric to alert
        

    

  • 使用 org.springframework.util.ErrorHandler异常处理器

设置了一个 ErrorHandler,那么就可以用这个 ErrorHandler 去处理掉异常,从而保证后续事件监听器处理不受影响。我们可以使用下面的代码来修正问题:

SimpleApplicationEventMulticaster simpleApplicationEventMulticaster = applicationContext.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, SimpleApplicationEventMulticaster.class);
simpleApplicationEventMulticaster.setErrorHandler(TaskUtils.LOG_AND_SUPPRESS_ERROR_HANDLER);
public static final ErrorHandler LOG_AND_SUPPRESS_ERROR_HANDLER = new LoggingErrorHandler();

private static class LoggingErrorHandler implements ErrorHandler 
   private final Log logger = LogFactory.getLog(LoggingErrorHandler.class);
   
   @Override
   public void handleError(Throwable t) 
      logger.error("Unexpected error occurred in scheduled task", t);
   


四、总结

  • 事件的触发时机
  • 事件是否被Spring所录入
  • 监听器不能抛出异常,自己捕获或者使用异常处理器,不然导致监听器连锁,后置的处理器不会被执行

那在最后,我们看到打印的日志:

2021-03-09 09:10:33.052 INFO 18104 — [nio-8080-exec-1] c.s.p.listener.HelloWorldController : start to publish event2021-03-09 09:10:33.055 INFO 18104 — [nio-8080-exec-1] c.s.p.l.example3.MyFirstEventListener :
com.spring.puzzle.class7.example3.MyFirstEventListener@18faf0 received:
com.spring.puzzle.class7.example3.MyEvent[source=df42b08f-8ee2-44df-a957-d8464ff50c88]

发现他是单线程执行的,那我们如何可以将他改造成多线程异步执行呢?

事件异步处理有两种,

  • 发送方异步发送
@Bean
    public SimpleApplicationEventMulticaster testMulticaster(SimpleApplicationEventMulticaster caster) 
        caster.setErrorHandler(TaskUtils.LOG_AND_SUPPRESS_ERROR_HANDLER);
        caster.setTaskExecutor(Executors.newFixedThreadPool(10));
        return caster;
    
  • 监听方异步监听:用eventlisenner加上异步注解,记得在主启动类上去开启Spring异步@EnableAsync
@Slf4j
@Service
public class EventListenerDemo 
   @EventListener
   @Async
   public void eventListener(PushEvent event) 
       log.info(this.getClass().getSimpleName() + "监听到数据:" + event.getMsg());
   


以上是关于Day608.Spring事件常见错误 -Spring编程常见错误的主要内容,如果未能解决你的问题,请参考以下文章

Day615.SpringSecurity常见错误 -Spring编程常见错误

Day610.SpringWebHeader解析常见错误 -Spring编程常见错误

Day616.SpringException常见错误 -Spring常见编程错误

Day609.SpringWebURL解析常见错误 -Spring编程常见错误

Day621.Spring Test 常见错误 -Spring编程常见错误

Day614.SpringWebFilter常见错误② -Spring编程常见错误