首页 > 其他分享 >如何优化实现了ApplicationListener接口导致的onApplicationEvent方法多次调用问题?

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

时间:2024-12-12 09:27:21浏览次数:10  
标签:ESMetaData indexName ApplicationListener builder onApplicationEvent private claz

背景:记录一次代码优化,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. 代码质量:
  • 更清晰的结构
  • 更好的可维护性
  • 更完善的日志

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

标签:ESMetaData,indexName,ApplicationListener,builder,onApplicationEvent,private,claz
From: https://blog.csdn.net/weixin_38804310/article/details/144415943

相关文章

  • nginx设置接口超时不生效问题
    nginx设置接口超时不生效问题只需在http,server,location,这三个任意一处地方设置proxy_read_timeout600s;即可参数值单位有s-秒,m-分钟,h-小时,不写单位默认为秒若不生效,可能是用restart命令重启nginx,配置文件未生效,此时可以先将nginx停止,再重新启动即可此时如果还不行,就不是ng......
  • 深入解析Java中的Set接口
    文章目录1.Set概念简介2.Set接口定义及关系3.Set接口常用的API核心方法代码示例4.Set的常见实现类特性HashSetLinkedHashSetTreeSetEnumSetConcurrentSkipListSet5.Set的实际案例扩展去重集合运算成员资格测试1.Set概念简介Set接口概述:Set是Java集合......
  • 【论文阅读】龙芯2号同时多线程处理器的软硬件接口设计
    学习体会:学习追踪龙芯系列“工程流”论文,看看如何做工程,完成→完美之前唐老师文章提到(simultaneousmultithreading,简称SMT)未来是大趋势,SMT技术巧妙地将线程级并行转化为指令级并行, 学习一下龙芯2号SMT处理器上软硬件接口及操作系统的设计方案摘录文章:本文的目的......
  • 浅谈Python+requests+pytest接口自动化测试框架的搭建
    框架的设计思路首先要明确进行接口自动化需要的步骤,如下图所示: 然后逐步拆解需要完成的工作:1)了解分析需求:了解接口要实现的功能2)数据准备:根据开发文档确定接口的基本情况,知晓接口的url、请求方式、入参等信息,然后根据业务逻辑以及入参来预期接口的输出需要有一个配置文件来......
  • ApiPost如何使用另一个接口的返回参数作为当前接口的Token发送Post请求
    1.全局参数-》全局header里面:Token:{{token_var}}2.登录接口预执行脚本:apt.sendRequest({"method":"post","url":"http://127.0.0.1:15555/user/login","content-type":"application/json","dat......
  • 接口请求时需要做哪些安全处理?怎么做?
    前端在进行接口请求时,需要考虑多种安全处理,以保护用户数据和应用安全。以下是一些常见的安全措施以及如何实现:1.HTTPS:作用:使用HTTPS协议加密传输数据,防止数据在传输过程中被窃听和篡改。做法:确保所有接口请求都使用https://协议。现在大部分浏览器都会强制或提示使......
  • Windows Image Acquisition (WIA) 服务是 Windows 操作系统中的一个关键服务,主要用于
    WindowsImageAcquisition(WIA)服务是Windows操作系统中的一个关键服务,主要用于扫描仪、数码相机等设备的图像采集和管理。它为这些设备提供必要的软件接口,使得用户可以通过标准应用程序(如Windows照片查看器、扫描仪应用等)来获取图像数据。1. WIA服务概述服务名称: St......
  • 关于《接口安全》的个人理解
     接口安全是所有系统中都会面对的问题。当然安全问题,不仅仅是接口安全,还有什么系统安全,数据安全,网络安全等等等。下图是16年画的,总结起来就是加密和拦截验证: 当时个我印象最深刻的还是当时系统接入支付,然后我就看网上的教程,说当初阿里为了解决这种场景,就出了一套这样的解决......
  • 驾驶证OCR识别API接口有哪些好处?
    在当今数字化快速发展的时代,各种先进的技术不断涌现,为我们的生活和工作带来了极大的便利。其中,驾驶证OCR识别API接口就是一项非常实用且具有广泛应用场景的技术创新。本文将详细介绍驾驶证OCR识别API接口的好处。驾驶证OCR识别API接口具有多方面的好处,主要包括以......
  • 驾驶证OCR识别API接口有哪些应用场景?
    在当今数字化快速发展的时代,各种先进的技术不断涌现,为我们的生活和工作带来了极大的便利。其中,驾驶证OCR识别API接口就是一项非常实用且具有广泛应用场景的技术创新。以下是驾驶证OCR识别API接口常见的一些应用场景:一、交通管理部门车辆管理业务:在车辆注册、年检、......