• Spring常见问题解决 - 自定义ApplicationEnvironmentPreparedEvent监听器失效了?


    一. 自定义监听器失效

    我们先来看下Spring中事件相关的几个成员:

    • 事件(Event):用于区分和定义不同的事件。
    • 事件广播器(Multicaster):负责发布事件。
    • 事件监听器(Listener):负责监听和处理广播器发出的事件。

    关系结构图如下:
    在这里插入图片描述

    1.1 案例

    首先我们自定义一个监听器MyApplicationListener:希望感应应用处理环境变量相关的事件。

    @Component
    public class MyApplicationListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
    
        @Override
        public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
            System.out.println(this.toString() + " received: " + event);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    那么此时我们希望SpringBoot在启动过程中,凡是和ApplicationEnvironmentPreparedEvent相关的事件,我们都将其抛出来打个日志。那么程序启动后,结果如下:
    在这里插入图片描述
    可见并没有我们想要的结果。

    1.2 原理分析

    我们从SpringBoot项目启动的代码开始走起:

    SpringApplication.run(Main8080.class, args);
    ↓↓↓↓↓↓↓↓↓↓
    public class SpringApplication {
    	public ConfigurableApplicationContext run(String... args) {
    	 	// ...
    	    SpringApplicationRunListeners listeners = this.getRunListeners(args);
            // ...
            // 环境准备
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
            // ...
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    而我们希望监听环境准备时候的相关事件,可见,这部分逻辑必定发生在prepareEnvironment()函数中。那在这之前,Spring做了什么操作呢?即getRunListeners()

    1.2.1 EventPublishingRunListener到底干啥的?

    我们关注这行代码:

    SpringApplicationRunListeners listeners = this.getRunListeners(args);
    
    private SpringApplicationRunListeners getRunListeners(String[] args) {
        Class<?>[] types = new Class[]{SpringApplication.class, String[].class};
        return new SpringApplicationRunListeners(logger, this.getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里的意思就是寻找SpringApplicationRunListener类型的监听器。EventPublishingRunListener就是他的一个子类。
    在这里插入图片描述
    我们来看下它是如何获得的,代码继续往下走:

    private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
        ClassLoader classLoader = this.getClassLoader();
        Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
        List<T> instances = this.createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
        AnnotationAwareOrderComparator.sort(instances);
        return instances;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    重点关注:SpringFactoriesLoader.loadFactoryNames

    private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    	// ..
    	try {
    		Enumeration<URL> urls = (classLoader != null ?
    				classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
    				ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
    		// ..
    	}
    	// ..
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    我们可以看到,Spring通过类加载器,加载指定的资源,而这个FACTORIES_RESOURCE_LOCATION的值是啥呢?
    在这里插入图片描述
    而这个文件在哪?首先我来说下我的SpringBoot项目引入的包:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
    </parent>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    那么我们来看下它的相关资源文件:
    在这里插入图片描述

    SpringBoot容器启动的时候,相关的监听器都会放在SpringApplication.listeners属性中。我们来看下部分文件内容:

    # Run Listeners
    org.springframework.boot.SpringApplicationRunListener=\
    org.springframework.boot.context.event.EventPublishingRunListener
    
    • 1
    • 2
    • 3

    可以发现,SpringApplicationRunListener类型的监听器只有EventPublishingRunListener一个。那么说白了this.getRunListeners(args);这段代码实际上就是加载EventPublishingRunListener监听器的。

    那么我来说下EventPublishingRunListener是干啥的吧。它是SpringBoot中唯一一个实现了SpringApplicationRunListener接口的监听器,其主要作用是,根据SpringBoot程序启动过程的不同阶段发布对应的事件,进行对应阶段的事件广播操作。

    1.2.2 EventPublishingRunListener中的广播器

    现在我们知道,EventPublishingRunListener相当于一个枢纽,它下面有一个事件广播器,那么只有注册到这个广播器的监听器,才有可能对相关的事件进行推送。我们来看下代码:

    public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
        private final SpringApplication application;
        private final String[] args;
        private final SimpleApplicationEventMulticaster initialMulticaster;
    
        public EventPublishingRunListener(SpringApplication application, String[] args) {
            this.application = application;
            this.args = args;
            this.initialMulticaster = new SimpleApplicationEventMulticaster();
            Iterator var3 = application.getListeners().iterator();
    
            while(var3.hasNext()) {
                ApplicationListener<?> listener = (ApplicationListener)var3.next();
                this.initialMulticaster.addApplicationListener(listener);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    这里面initialMulticaster就扮演着事件广播器的角色,我们看下deubg:这里Spring就尝试着将下面的监听器都注册到initialMulticaster中。即this.initialMulticaster.addApplicationListener(listener);
    在这里插入图片描述
    可见,这里并没有我们自定义的MyApplicationListener监听器,即没有注册到initialMulticaster广播器中。因此再Spring的环境准备阶段,是不会有事件推送到我们自定义的监听器中的。

    那么上图中的这些监听器又从哪里加载的?我们依旧从run函数开始:

    SpringApplication.run(Main8080.class, args);
    ↓↓↓↓↓↓↓
    public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
        return (new SpringApplication(primarySources)).run(args);
    }
    ↓↓↓↓↓↓↓看SpringApplication构造↓↓↓↓↓↓↓
    public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    	// ...
        this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
        // ...
    }
    ↓↓↓↓↓↓↓getSpringFactoriesInstances↓↓↓↓↓↓↓
    private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
        ClassLoader classLoader = this.getClassLoader();
        Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
        List<T> instances = this.createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
        AnnotationAwareOrderComparator.sort(instances);
        return instances;
    }
    ↓↓↓↓↓↓↓SpringFactoriesLoader.loadFactoryNames↓↓↓↓↓↓↓
    private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    	// ..
    	try {
    		Enumeration<URL> urls = (classLoader != null ?
    				classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
    				ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
    		// ..
    	}
    	// ..
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    我们来看下它的部分文件内容(和监听器有关):
    在这里插入图片描述
    那么结合上面的listeners截图,我们可以看出这个内容是吻合的。

    1.2.3 总结

    总结下就是:

    1. 我们调用SpringApplication.run(Main8080.class,args);这个函数,我们可以分为两个步骤。一个是SpringApplication的构造。一个是run()函数的调用。
    2. SpringApplication构造的过程中,会去META-INF/spring.factories目录下寻找ApplicationListener类型的监听器。
    3. run()执行过程中。会通过this.getRunListeners(args);去找到一个SpringApplicationRunListener类型的监听器。同样是去META-INF/spring.factories目录下寻找。唯一实现为EventPublishingRunListener。它主要负责在启动过程的不同阶段发布对应的事件。
    4. EventPublishingRunListener中有个initialMulticaster广播器,将第二步中获得的所有监听器都注册进来。然后如果有对应的事件,就会推送给他们。
    5. 然后呢,我们自定义的MyApplicationListener监听器,并没有在第二步中获取到,因此在容器的环境准备阶段,并不会将相关的事件推送过来。

    1.3 解决

    方式一:在启动的时候,直接将我们自定义的监听器加入到广播器中:

    @SpringBootApplication
    public static void main(String[] args) {
        MyApplicationListener myApplicationListener = new MyApplicationListener();
        SpringApplication springApplication = new SpringApplicationBuilder(Main8080.class)
                .listeners(myApplicationListener).build();
        springApplication.run(args);
    
    //        SpringApplication.run(Main8080.class,args);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    debug验证下:这时候就多出来我们自定义的MyApplicationListener 监听器了。
    在这里插入图片描述
    输出结果如下:xxx received: xxx
    在这里插入图片描述


    方式二(推荐):创建一个目录META-INF/spring.factories,在里面添加自定义的监听器:

    org.springframework.context.ApplicationListener=\
    com.listener.MyApplicationListener
    
    • 1
    • 2

    注意改一下自定义监听器的全路径。

    二. 额外补充

    首先,本文案例对于事件类型的选择还是比较特殊的。我们选择监听了ApplicationEnvironmentPreparedEvent事件。那么这个事件就发生于SpringBoot的环境准备阶段,也就是:

    ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
    
    • 1

    但是请读者们注意,在这个阶段之前,Spring加载到的监听器都是通过META-INF/spring.factories目录下配置来的。那么我们自定义的监听器除了采取上述方式以外,啥时候可以加入到容器里呢?你可以复制一份MyApplicationListener2出来:
    在这里插入图片描述
    我们可以关注下SpringBoot启动的源码,即run()部分:

    public class SpringApplication {
    	 public ConfigurableApplicationContext run(String... args) {
    	 	// ..
            this.refreshContext(context);
            // ..
         }
    }
    ↓↓↓↓↓↓↓
    public abstract class AbstractApplicationContext extends DefaultResourceLoader
    		implements ConfigurableApplicationContext {
    	@Override
    	public void refresh() throws BeansException, IllegalStateException {
    		synchronized (this.startupShutdownMonitor) {
    			StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
    			// 1.刷新上下文环境,做一些准备工作,如对系统属性或者环境变量进行验证
    			prepareRefresh();
    			// 2.初始化BeanFactory,进行XML读取。此步骤结束后,ApplicationContext就拥有了BeanFactory的功能。
    			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
    			// 3.对BeanFactory进行填充,对 @Qualifier 和 @Autowired 注解进行支持
    			prepareBeanFactory(beanFactory);
    			try {
    				// 4.处理子类覆盖方法
    				postProcessBeanFactory(beanFactory);
    				StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
    				// 5.激活各种BeanFactory处理器
    				invokeBeanFactoryPostProcessors(beanFactory);
    				// 6.注册 [拦截bean的创建过程] 的处理器
    				registerBeanPostProcessors(beanFactory);
    				beanPostProcess.end();
    				// 7.为上下文初始化Message,国际化处理
    				initMessageSource();
    				// 8.初始化应用消息广播器
    				initApplicationEventMulticaster();
    				// 9.让子类来初始化其他的bean
    				onRefresh();
    				// 10.在已经注册的bean中寻找监听器,并注册到消息广播器中
    				registerListeners();
    				// 11.初始化非惰性单例bean
    				finishBeanFactoryInitialization(beanFactory);
    				// 12.完成刷新过程
    				finishRefresh();
    			}
    			catch (BeansException ex) {
    				// ...
    				// 若失败了,则销毁该bean,并且重置刷新状态
    				destroyBeans();
    				cancelRefresh(ex);
    				throw ex;
    			}
    			finally {
    				resetCommonCaches();
    				contextRefresh.end();
    			}
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    详细的可以看俺的这篇文章Spring源码系列 - ApplicationContext容器的功能扩展。本篇文章只讲和监听器有关的重点。我们看下registerListeners函数:

    protected void registerListeners() {
    	// 1.先把已经加载好的监听器加入到广播器。即`META-INF/spring.factories`下的
    	for (ApplicationListener<?> listener : getApplicationListeners()) {
    		getApplicationEventMulticaster().addApplicationListener(listener);
    	}
    
    	// 2.在Spring容器中的Bean中,寻找实现了ApplicationListener接口的Bean,即监听器。
    	String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
    	for (String listenerBeanName : listenerBeanNames) {
    		getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
    	}
    
    	// 3.推送事件
    	Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
    	this.earlyApplicationEvents = null;
    	if (!CollectionUtils.isEmpty(earlyEventsToProcess)) {
    		for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
    			getApplicationEventMulticaster().multicastEvent(earlyEvent);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    首先对于第一块内容,本文案例中,由于MyApplicationListener加在了我们自定义的spring.factories文件中,因此它能够第一时间被加载。
    在这里插入图片描述
    其次对于第二块内容,本文案例中,MyApplicationListener2实现了ApplicationListener接口,然后通过@Component注解注入到容器中。就是一个很简单的Bean而已。在第二步中注册到广播器中。

    在这里插入图片描述

    最后再总结下吧:

    • 如果我们自定义监听器,可以优先使用META-INF/spring.factories文件去进行配置。因为通过这种方式配置的监听器,能够更早的注册到广播器中
    • SpringBoot启动的时候,有个refresh()函数,主要是用于初始化和加载所有需要管理的Bean。其中也会将我们自定义的监听器加入到广播器中。
    • 因此如果项目中有特殊需要,比如需要自定义监听器去监听本文案例中的环境准备事件,就需要通过META-INF/spring.factories文件的方式去配置了。

    例如,如果我自定义一个监听器,监听ApplicationReadyEvent事件,即容器已经准备完毕,那么此时我onRefresh()函数肯定是执行完毕的。我们自定义的监听器在这一阶段也已经加入到广播器了,那么相关的事件是能够正常的接收的,证明:

    @Component
    public class MyApplicationListener2 implements ApplicationListener<ApplicationReadyEvent> {
    
        @Override
        public void onApplicationEvent(ApplicationReadyEvent event) {
            System.out.println("MyApplicationListener2: " + this.toString() + " received: " + event);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    结果:
    在这里插入图片描述

  • 相关阅读:
    【快应用】如何跳转应用市场app详情页?
    Go | 函数注意事项
    新式拥塞控制漫谈
    关于聚合函数的课后作业
    集群数据库系统的配置及安装过程
    STM32物联网项目-SPI FLASH编程
    java 八股文 基础 每天笔记随机刷
    基于MQTT机制的后台视频实时画面抛转到前台web显示
    Bearpi开发板HarmonyOS之GPIO中断
    CentOS 7 上安装 Oracle 11g 数据库
  • 原文地址:https://blog.csdn.net/Zong_0915/article/details/126525246