spring boot 中监听器下 onApplicationEvent 方法被执行两次问题分析及解决

本文探讨了SpringCloud项目中Kafka配置重复加载的问题。分析了项目启动时监听器执行两次的原因,并提供了三种不同的解决方案。

一、背景

  1. 项目中使用的技术栈是 spring cloud + spring boot 构建的分布式项目。

  2. 其中一个微服务下创建了一个监听者,用于项目启动时从 Apollo 中获取 kafka 的配置信息(地址、topic、等一些基本配置),然后利用加载的配置初始化 kafka。代码如下:
    代码

  3. 现在出现一个问题,项目启动的时候 onApplicationEvent() 方法每次都会执行两遍,kafka 也会初始化两遍,导致业务中发送 kafka 时会发送两遍同样的 kafka。
    启动时问题

二、分析

  1. 可以在 onApplicationEvent() 方法中添加图一中的各种参数,图二可以查看打印出来的内容。 注意:获取各种参数的语句可能会引起空指针异常比如 “event.getApplicationContext().getParent().getParent()”,可以根据自己的项目修改。
    图一:
    参数
    图二:
    打印参数
  2. 查看打印的两块内容中的“容器中参数 getSources:”内容,发现下面容器中的 “class com.example.demo.DemoApplication”是当前项目的启动类,也就是说它是通过启动类中 main() 方法传入的。而上面容器中的 “class org.springframework.cloud.stream.binder.rabbit.config.RabbitServiceAutoConfiguration” 不是当前项目中自定义的类,那么应该是第三方框架中的类。
  3. 于是用快捷键 Ctrl+N 搜索该类,发现它是因为在 pom 文件引入了 该依赖< artifactId >spring-cloud-starter-bus-amqp</ artifactId> 项目启动时这个依赖包就会启动一个新的容器。如果在 pom 文件中把该依赖去掉,再启动项目会发现 onApplicationEvent() 方法只加载一次了,即上面的容器没有被加载。但显然这不是解决问题的方法,因为项目中要用到这个功能,如果项目中没有用到这个依赖可以考虑去掉,也就解决了问题。
    依赖包
  4. 再分析图二中的另一参数 “容器中参数 getApplicationName: ”。上面容器打印的是空字符串,下面容器打印的是“/demo”,其实下面容器中的“/demo”就是配置文件即 yml 中 server.context-path: 的值,如果 yml 中没有配置则会同上面容器一样显示空字符串。
    配置
  5. 分析图二中的参数 “容器中参数 getParent:” 和 “容器中参数 getParent.getParent:” 可以看出,其实是三个容器进行了嵌套。上面那个容器有两个父容器,下面那个容器有一个父容器。至于为什么出现这种情况可以自己看看源码、找找资料,或者有大佬评论区分享一下。

三、解决

根据上面的分析可以提供以下几种解决方案。

  1. 根据分析 3,可以看一下引起 onApplicationEvent() 方法被执行两次的依赖包项目中有没有用到,没有的话删除依赖就能解决问题。如果用到了再考虑下面的方法。
  2. 根据分析 4,可以通过下面方法解决。前提是配置文件中要有该配置。
@Component
public class DataInitializerListener implements ApplicationListener<ApplicationReadyEvent> {
	@Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
	    //思路:只有加载配置文件的容器启动时才执行该方法,通过下面的判断排除掉别的容器。
       if("/demo".equals(event.getApplicationContext().getApplicationName())){
           //功能代码
           System.out.println("kafka 配置");
       }
    }
 }
  1. 根据分析 5,可以通过下面方法解决,这也是网上最常见的方法。
@Component
public class DataInitializerListener implements ApplicationListener<ApplicationReadyEvent> {
	@Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
	    //思路:既然是多个容器嵌套,那么只让最外层的容器即父容器启动时执行该方法,其他的容器排除掉
       if(event.getApplicationContext().getParent().getParent() == null){
       //本人的项目中是三个容器嵌套,需要找两层父容器,如果是两个容器嵌套只需要找一层。判断条件需要改为:
       // if(event.getApplicationContext().getParent() == null){
           //功能代码
           System.out.println("kafka 配置");
       }
    }
 }
  1. 测试一下就会发现,类只加载了一遍,方法执行了两次,因此还有一种方法。
@Component
public class DataInitializerListener implements ApplicationListener<ApplicationReadyEvent> {
    private boolean flag = false;
	@Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
	    //思路:加一个标记,第一个容器启动时不执行此方法,第二个容器启动时才执行。
       if(flag){
           //功能代码
           System.out.println("kafka 配置");
       }else {
           flag = true;
       }
    }
 }

四、最后

  1. 解决完启动问题后还要测试一下,项目运行过程中触发监听者中的事件 onApplicationEvent() 内的功能代码还能否执行到。
  2. 这几个解决的方案都是对 onApplicationEvent() 方法进行操作,如果还有别的方法欢迎讨论。
<think>我们正在讨论Spring框架中`onApplicationEvent(ContextRefreshedEvent event)`方法执行两次问题。根据提供的引用,我们可以总结原因和解决方案。 **原因分析:** 1. **多个Spring容器存在**:在Web应用中,通常存在两个Spring容器:一个是父容器(由`ContextLoaderListener`创建,负责加载service、dao等bean),另一个是子容器(由`DispatcherServlet`创建,负责加载controller等bean)。当两个容器都配置了`ApplicationListener<ContextRefreshedEvent>`时,事件会被触发两次。 2. **事件传播机制**:在Spring中,当一个容器触发`ContextRefreshedEvent`事件时,如果该容器有父容器,事件也会传播到父容器。但反过来,父容器的事件不会传播到子容器。然而,由于存在两个容器,每个容器在初始化完成后都会触发自己的`ContextRefreshedEvent`事件,导致监听器被调用两次。 **解决方案:** 1. **判断父容器**:在监听器的实现中,检查事件的`ApplicationContext`是否没有父容器(即根容器),这样确保只在根容器初始化完成时执行一次逻辑。代码示例: ```java @Override public void onApplicationEvent(ContextRefreshedEvent event) { if (event.getApplicationContext().getParent() == null) { // 执行你的逻辑 } } ``` 或者,如果你想在子容器初始化完成后执行(但注意避免重复),也可以反过来判断,但通常我们选择在根容器执行。 2. **确保监听器只被一个容器加载**:将监听器只配置在根容器(例如通过`ContextLoaderListener`加载的applicationContext.xml)或只配置在子容器(例如通过DispatcherServlet加载的spring-mvc.xml)中。但这种方法需要根据业务需求谨慎选择,因为可能有些逻辑需要在特定的容器中执行。 3. **使用其他初始化方式**:如果逻辑只需要执行一次,可以考虑使用其他方式,如: - `@PostConstruct`注解:在Bean初始化完成后执行一次(每个Bean都会执行自己的初始化方法,但注意每个Bean只执行一次)。 - `InitializingBean`接口:实现`afterPropertiesSet()`方法。 但是这些方法是在Bean初始化时执行,而不是在整个容器完全初始化完成后执行,所以使用场景不同。 4. **避免重复配置**:检查是否在多个配置文件中重复配置了同一个监听器,或者是否通过注解和XML同时配置了监听器,导致监听器被注册多次。 **代码示例:** ```java @Component public class MyApplicationListener implements ApplicationListener<ContextRefreshedEvent> { @Override public void onApplicationEvent(ContextRefreshedEvent event) { // 确保只在根容器初始化完成后执行 if (event.getApplicationContext().getParent() == null) { // 执行初始化逻辑 } } } ``` **注意事项:** - 在Web应用中,根容器(父容器)通常是由`ContextLoaderListener`创建的,而子容器是由`DispatcherServlet`创建的。根容器没有父容器,所以用`getParent() == null`判断。 - 在Spring Boot应用中,通常只有一个容器,所以不会出现两次执行的情况。但如果使用Spring Boot内嵌多个Web应用,或者手动创建多个容器,同样需要注意。 通过以上方法,可以避免`onApplicationEvent`方法被重复执行。 §§相关问题§§ 1. Spring容器初始化过程中事件触发的顺序是怎样的? 2. 除了`ContextRefreshedEvent`,Spring中还有哪些常用的事件? 3. 如何在Spring Boot中实现容器初始化完成后的操作? </think> 在Spring框架中,`onApplicationEvent(ContextRefreshedEvent event)`方法执行两次的主要原因及解决方案如下: ### 原因分析 1. **多容器初始化**(核心原因) 在Web应用中通常存在**父子容器结构**: - 父容器:由`ContextLoaderListener`创建,管理Service/Dao等bean - 子容器:由`DispatcherServlet`创建,管理Controller等bean $$ \text{容器结构} : \begin{cases} \text{父容器} \\ \text{子容器} \subset \text{父容器} \end{cases} $$ 当两个容器都配置了`ApplicationListener`时,会分别触发事件[^1][^3]。 2. **事件传播机制** Spring的事件机制会**广播到所有监听器**,包括父子容器的监听器: ```mermaid graph LR A[ContextRefreshedEvent] --> B[父容器监听器] A --> C[子容器监听器] ``` 3. **配置重复** 若通过XML和注解同时注册监听器,会导致**重复注册问题**[^4]。 ### 解决方案 1. **根容器过滤法**(推荐) 在方法中判断是否为根容器: ```java @Override public void onApplicationEvent(ContextRefreshedEvent event) { if (event.getApplicationContext().getParent() == null) { // 这里写初始化逻辑(仅根容器执行) } } ``` 原理:根容器(父容器)的`getParent()`返回`null`[^2] 2. **单容器注册法** 只在**父容器**中配置监听器(删除子容器中的相关配置): ```xml <!-- 在applicationContext.xml中配置 --> <bean class="com.example.MyListener"/> ``` 3. **注解优化法** 使用`@Component`但避免重复扫描: ```java @Component public class MyListener implements ApplicationListener<ContextRefreshedEvent> { // 方法内仍需判断根容器 } ``` 确保该组件**不被子容器重复扫描**(检查`<context:component-scan>`范围)[^4]。 ### 其他注意事项 - **Spring Boot场景**:通常只有单容器,无需特殊处理 - **执行时机**:该方法在**所有Bean初始化完成**后触发,适合做数据预热等操作 - **替代方案**:考虑使用`@PostConstruct`,但注意它会在**每个Bean初始化**时触发 > 最佳实践:方案1 + 方案2组合使用,既能明确执行边界,又能避免配置冲突[^3]。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

栗然

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值