如何优化实现了ApplicationListener接口导致的onApplicationEvent方法多次调用问题?

背景:记录一次代码优化,CreateIndex中实现ApplicationListener接口导致onApplicationEvent方法多次调用,方法里重复加载该注解的类. this.applicationContext.getBeansWithAnnotation(ESMetaData.class).

排查过程:首先在服务启动run方法打断点,在springboot在加载的过程中,会发布多个事件(event),根据过往所学,实现ApplicationListener接口 onApplicationEvent方法对这些事件(event)去监听,多次加载会出现.

public ConfigurableApplicationContext run(String... args) {
        long startTime = System.nanoTime();
        DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
        ConfigurableApplicationContext context = null;
        this.configureHeadlessProperty();
        //1.0 第一次调用
        SpringApplicationRunListeners listeners = this.getRunListeners(args);
        listeners.starting(bootstrapContext, this.mainApplicationClass);

        Throwable ex;
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            //2.0 第二次调用
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
            this.configureIgnoreBeanInfo(environment);
            Banner printedBanner = this.printBanner(environment);
            context = this.createApplicationContext();
            context.setApplicationStartup(this.applicationStartup);
            //3.0 第三次调用
            this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
            this.refreshContext(context);
            this.afterRefresh(context, applicationArguments);
            Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
            if (this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), timeTakenToStartup);
            }
            //4.0 第四次调用
            listeners.started(context, timeTakenToStartup);
            this.callRunners(context, applicationArguments);
        } catch (Throwable var12) {
            ex = var12;
            this.handleRunFailure(context, ex, listeners);
            throw new IllegalStateException(ex);
        }

        try {
            Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
            //5.0 第五次调用
            listeners.ready(context, timeTakenToReady);
            return context;
        } catch (Throwable var11) {
            ex = var11;
            //6.0 第六次调用(optional)
            this.handleRunFailure(context, ex, (SpringApplicationRunListeners)null);
            throw new IllegalStateException(ex);
        }
    }

解决办法:也是在调试springboot启动的代码中,发现启动最后回调CommandLineRunner或者ApplicationRunner接口的方法,将原有的ApplicationListener接口 替换成这两个当中的一个,即可实现一次调用的效果.

接下来详细分析下这个优化过程,并提供完整的解决方案:

1.0 原始代码(存在问题的实现):
@Component
public class CreateIndex implements ApplicationListener<ApplicationEvent> {
    
    @Autowired
    private ApplicationContext applicationContext;
    
    @Autowired
    private RestHighLevelClient client;
    
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        // 问题代码:会被多次调用
        try {
            // 获取所有带有@ESMetaData注解的类
            Map<String, Object> beansWithAnnotation = 
                this.applicationContext.getBeansWithAnnotation(ESMetaData.class);
                
            for (Object bean : beansWithAnnotation.values()) {
                // 处理每个类的索引创建
                processIndexCreation(bean);
            }
        } catch (Exception e) {
            log.error("Create elasticsearch index error", e);
        }
    }
}

2.0 优化后的代码(使用CommandLineRunner):

@Component
@Slf4j
public class ESIndexInitializer implements CommandLineRunner {
    
    @Autowired
    private ApplicationContext applicationContext;
    
    @Autowired
    private RestHighLevelClient client;
    
    @Override
    public void run(String... args) throws Exception {
        initializeIndices();
    }
    
    private void initializeIndices() {
        try {
            log.info("Starting to initialize Elasticsearch indices...");
            
            // 获取所有带有@ESMetaData注解的类
            Map<String, Object> beansWithAnnotation = 
                this.applicationContext.getBeansWithAnnotation(ESMetaData.class);
                
            for (Object bean : beansWithAnnotation.values()) {
                Class<?> clazz = AopUtils.getTargetClass(bean);
                ESMetaData esMetaData = clazz.getAnnotation(ESMetaData.class);
                
                if (esMetaData != null) {
                    createOrUpdateIndex(clazz, esMetaData);
                }
            }
            
            log.info("Elasticsearch indices initialization completed.");
        } catch (Exception e) {
            log.error("Failed to initialize Elasticsearch indices", e);
            throw new RuntimeException("Index initialization failed", e);
        }
    }
    
    private void createOrUpdateIndex(Class<?> clazz, ESMetaData esMetaData) throws IOException {
        String indexName = esMetaData.indexName();
        
        // 检查索引是否存在
        boolean indexExists = client.indices()
            .exists(new GetIndexRequest(indexName), RequestOptions.DEFAULT);
            
        if (!indexExists) {
            // 创建新索引
            CreateIndexRequest createIndexRequest = buildCreateIndexRequest(clazz, esMetaData);
            CreateIndexResponse createIndexResponse = client.indices()
                .create(createIndexRequest, RequestOptions.DEFAULT);
                
            if (createIndexResponse.isAcknowledged()) {
                log.info("Successfully created index: {}", indexName);
            } else {
                log.warn("Failed to create index: {}", indexName);
            }
        } else {
            // 更新现有索引的映射
            updateIndexMapping(indexName, clazz);
        }
    }
    
    private CreateIndexRequest buildCreateIndexRequest(Class<?> clazz, ESMetaData esMetaData) {
        CreateIndexRequest request = new CreateIndexRequest(esMetaData.indexName());
        
        // 设置索引配置
        request.settings(Settings.builder()
            .put("index.number_of_shards", esMetaData.shards())
            .put("index.number_of_replicas", esMetaData.replicas())
            .put("index.refresh_interval", "30s")
        );
        
        // 构建映射
        XContentBuilder mapping = buildMapping(clazz);
        request.mapping(mapping);
        
        return request;
    }
    
    private XContentBuilder buildMapping(Class<?> clazz) throws IOException {
        XContentBuilder builder = XContentFactory.jsonBuilder();
        builder.startObject();
        {
            builder.startObject("properties");
            
            // 处理所有字段
            for (Field field : clazz.getDeclaredFields()) {
                ESField esField = field.getAnnotation(ESField.class);
                if (esField != null) {
                    builder.startObject(field.getName());
                    builder.field("type", esField.type());
                    if (esField.analyzer().length() > 0) {
                        builder.field("analyzer", esField.analyzer());
                    }
                    builder.endObject();
                }
            }
            
            builder.endObject();
        }
        builder.endObject();
        return builder;
    }
    
    private void updateIndexMapping(String indexName, Class<?> clazz) throws IOException {
        PutMappingRequest request = new PutMappingRequest(indexName);
        request.source(buildMapping(clazz));
        
        AcknowledgedResponse putMappingResponse = client.indices()
            .putMapping(request, RequestOptions.DEFAULT);
            
        if (putMappingResponse.isAcknowledged()) {
            log.info("Successfully updated mapping for index: {}", indexName);
        } else {
            log.warn("Failed to update mapping for index: {}", indexName);
        }
    }
}

 3.0 相关注解定义:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ESMetaData {
    String indexName();
    int shards() default 3;
    int replicas() default 1;
}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ESField {
    String type();
    String analyzer() default "";
}

4.0 使用示例:

@ESMetaData(indexName = "users")
@Data
public class UserIndex {
    
    @ESField(type = "keyword")
    private String id;
    
    @ESField(type = "text", analyzer = "ik_max_word")
    private String name;
    
    @ESField(type = "integer")
    private Integer age;
    
    @ESField(type = "date")
    private LocalDateTime createTime;
}

5.0 配置类:

@Configuration
public class ElasticsearchConfig {
    
    @Bean
    public RestHighLevelClient elasticsearchClient(
            @Value("${elasticsearch.hosts}") String[] hosts) {
        HttpHost[] httpHosts = Arrays.stream(hosts)
            .map(HttpHost::create)
            .toArray(HttpHost[]::new);
            
        return new RestHighLevelClient(
            RestClient.builder(httpHosts)
                .setRequestConfigCallback(requestConfigBuilder ->
                    requestConfigBuilder
                        .setConnectTimeout(5000)
                        .setSocketTimeout(60000))
                .setMaxRetryTimeoutMillis(60000)
        );
    }
}

主要改进点:

  1. 初始化时机:
  • 从ApplicationListener改为CommandLineRunner
  • 确保只执行一次
  • 在应用完全启动前执行
  1. 错误处理:
  • 完善的异常处理
  • 详细的日志记录
  • 失败时快速失败
  1. 功能增强:
  • 支持更新现有索引
  • 灵活的映射配置
  • 索引设置可配置
  1. 代码质量:
  • 更清晰的结构
  • 更好的可维护性
  • 更完善的日志

启发:这个优化不仅解决了多次执行的问题,还提供了更完善的索引初始化功能,使代码更加健壮和可维护。

<think>在Spring框架中,应用初始化后置处理器(如`ApplicationListener`或使用`@EventListener`注解)可能会被多次触发,尤其是在某些特定的上下文层次结构(如父子上下文)或测试环境中。为了避免Spring事件被多次执行,我们可以采取一些措施。 ### 问题原因 1. **上下文层次结构**:当存在父子上下文时,同一个事件可能会被父上下文和子上下文分别发布,导致监听器被多次触发。 2. **重复的监听器定义**:如果监听器被定义多次(例如,在父上下文和子上下文都扫描到了同一个监听器),那么每个实例都会响应事件。 3. **测试环境**:在测试中,可能会创建多个应用上下文,导致事件被多次触发。 ### 解决方法 #### 1. 确保监听器只被加载一次 确保事件监听器(如实现了`ApplicationListener`的Bean或带有`@EventListener`的方法)只在同一个上下文中被加载一次。避免在父子上下文中重复定义相同的监听器。 #### 2. 使用条件判断 在监听器内部,我们可以根据事件源或上下文环境来决定是否处理事件。例如,我们可以检查事件的来源上下文,只处理特定上下文的事件。 **示例代码:** ```java import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.stereotype.Component; @Component public class MyEventListener implements ApplicationListener<ContextRefreshedEvent> { @Override public void onApplicationEvent(ContextRefreshedEvent event) { // 获取触发事件的上下文 ApplicationContext context = event.getApplicationContext(); // 如果是根上下文(没有父上下文)才处理,避免子上下文重复处理 if (context.getParent() == null) { // 执行你的业务逻辑 doSomething(); } } private void doSomething() { // 业务逻辑 } } ``` #### 3. 使用`@EventListener`的条件属性 如果你使用的是基于注解的监听器,可以使用`condition`属性来添加条件表达式,只满足条件时才执行。 **示例代码:** ```java import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @Component public class MyAnnotatedEventListener { @EventListener(condition = "#event.applicationContext.parent == null") public void handleContextRefreshed(ContextRefreshedEvent event) { // 只在根上下文刷新时执行 doSomething(); } private void doSomething() { // 业务逻辑 } } ``` #### 4. 避免在父子上下文中重复扫描 确保组件扫描的路径不会导致同一个监听器在父子上下文中都被创建。例如,如果父上下文已经扫描了某个包,子上下文就不应再扫描同一个包。 ### 解释 上述方法的核心在于识别事件的来源上下文,并只处理来自根上下文(或指定上下文)的事件。这样,即使存在父子上下文,我们也只处理一次事件。 ### 注意事项 - 在Spring Boot应用中,通常只有一个应用上下文(没有父子上下文),所以可能不需要这样的处理。但在复杂的应用或集成测试中,可能会有多个上下文。 - 使用条件判断时,确保条件表达式正确,避免误过滤。 ### 相关问题 §§相关问题§§: 1. 在Spring中,如何创建父子应用上下文? 2. 如何防止在Spring测试中事件监听器被多次触发? 3. 除了`ContextRefreshedEvent`,还有哪些常见的Spring事件? 4. 如何在Spring中自定义事件,并确保它不会被多次处理?</think>### 防止 Spring 事件被多次执行的解决方案 #### 核心方法:使用初始化标志位 通过静态标志位确保初始化逻辑仅执行一次,特别适用于 `ContextRefreshedEvent` 等可能被多次触发的事件。 ```java import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.stereotype.Component; @Component public class StartupInitializer implements ApplicationListener<ContextRefreshedEvent> { // 静态标志位确保全局唯一性 private static boolean initialized = false; @Override public void onApplicationEvent(ContextRefreshedEvent event) { // 确保仅执行一次初始化 if (!initialized) { synchronized (StartupInitializer.class) { if (!initialized) { performInitialization(); // 实际初始化逻辑 initialized = true; // 更新标志位 } } } } private void performInitialization() { // 这里放置需要仅执行一次的初始化代码 System.out.println("应用初始化完成,此逻辑仅执行一次"); } } ``` #### 关键解释: 1. **双重检查锁定 (Double-Check Locking)**: - 外层 `if (!initialized)` 避免不必要的同步开销 - 内层 `synchronized` 块确保多线程环境下的原子性 - `volatile` 在此场景非必需(静态变量初始化保证可见性) 2. **问题根源**: - **父子容器问题**:Spring MVC + Spring Boot 组合时,Web 应用通常存在: - 父容器:`Root WebApplicationContext` - 子容器:`Servlet WebApplicationContext` - 每个容器都会独立触发 `ContextRefreshedEvent` - **测试环境问题**:测试套件多次加载上下文 - **事件广播机制**:默认事件会传播到所有监听器 3. **替代方案比较**: | 方法 | 优点 | 缺点 | |------|------|------| | 标志位 | 简单高效,明确控制 | 需手动管理状态 | | `@PostConstruct` | 容器自动管理 | 无法解决父子容器多次调用问题 | | `SmartApplicationListener` | 可控制事件处理顺序 | 仍需处理多次执行问题 | #### 其他注意事项: 1. 对于 **Spring Boot** 应用,优先使用: ```java @Component public class AppRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) { // 此方法在应用就绪后仅执行一次 } } ``` 2. 需要处理 **特定事件** 时: ```java @EventListener(condition = "#root.event.applicationContext.parent == null") public void handleRootContextEvent(ContextRefreshedEvent event) { // 仅当根容器触发时执行 } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值