【开源项目】SofaBoot实现Spring Bean 异步初始化的源码拆解

news2024/12/23 23:08:28

使用场景

在实际使用 Spring/Spring Boot 开发中,一些 Bean 在初始化过程中执行准备操作,如拉取远程配置、初始化数据源等等。在应用启动期间,这些 Bean 会增加 Spring 上下文刷新时间,导致应用启动耗时变长。

Demo展示

SpringBoot案例

MaxBean实例

@Slf4j
public class MaxBean {

    public void init() throws Exception {
        Thread.sleep(5000);
        log.info("maxBean init ok");
    }
}

WhyBean实例

@Slf4j
public class WhyBean {

    public void init() throws Exception {
        Thread.sleep(5000);
        log.info("whyBean init ok");
    }
}

系统启动

@SpringBootApplication
public class DemoSofabootApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoSofabootApplication.class, args);
    }

    @Bean(name = "maxBean", initMethod = "init")
    public MaxBean maxBean() {
        return new MaxBean();
    }

    @Bean(name = "whyBean", initMethod = "init")
    public WhyBean whyBean() {
        return new WhyBean();
    }

}

启动日志输出,花了11s钟

2023-06-06 13:45:51.219  INFO 21320 --- [           main] com.charles.entity.MaxBean               : maxBean init ok
2023-06-06 13:45:56.220  INFO 21320 --- [           main] com.charles.entity.WhyBean               : whyBean init ok
2023-06-06 13:45:56.483  INFO 21320 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2023-06-06 13:45:56.492  INFO 21320 --- [           main] com.charles.DemoSofabootApplication      : Started DemoSofabootApplication in 11.893 seconds (JVM running for 12.746)

Sofa优化

修改依赖,设置父pom为sofaboot-dependencies,引入runtime-sofa-boot-starter

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>com.alipay.sofa</groupId>
        <artifactId>sofaboot-dependencies</artifactId>
        <version>3.18.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.charles</groupId>
    <artifactId>demo-sofaboot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo-sofaboot</name>
    <description>demo-sofaboot</description>
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.6.13</spring-boot.version>
        <sofa.boot.version>3.18.0</sofa.boot.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.alipay.sofa</groupId>
            <artifactId>runtime-sofa-boot-starter</artifactId>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <mainClass>com.charles.DemoSofabootApplication</mainClass>
                    <skip>true</skip>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

添加配置项

spring:
  application:
    name: demo-sofaboot

修改系统启动,添加注解SofaAsyncInit

@SpringBootApplication
public class DemoSofabootApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoSofabootApplication.class, args);
    }

    @Bean(name = "maxBean", initMethod = "init")
    @SofaAsyncInit
    public MaxBean maxBean() {
        return new MaxBean();
    }

    @Bean(name = "whyBean", initMethod = "init")
    @SofaAsyncInit
    public WhyBean whyBean() {
        return new WhyBean();
    }

}

启动日志,启动时间花费了6.8s左右

2023-06-06 13:48:42.440  INFO 10528 --- [bean-1-thread-2] com.charles.entity.WhyBean               : whyBean init ok
2023-06-06 13:48:42.440  INFO 10528 --- [bean-1-thread-1] com.charles.entity.MaxBean               : maxBean init ok
2023-06-06 13:48:42.440  INFO 10528 --- [bean-1-thread-1] com.alipay.sofa                          : com.charles.entity.MaxBean(maxBean) init method execute 5013ms, moduleName: RootApplicationContext.
2023-06-06 13:48:42.440  INFO 10528 --- [bean-1-thread-2] com.alipay.sofa                          : com.charles.entity.WhyBean(whyBean) init method execute 5009ms, moduleName: RootApplicationContext.
2023-06-06 13:48:42.450  INFO 10528 --- [           main] com.charles.DemoSofabootApplication      : Started DemoSofabootApplication in 6.883 seconds (JVM running for 7.535)

源码理解

AsyncInitBeanFactoryPostProcessor

AsyncInitBeanFactoryPostProcessor#postProcessBeanFactory,系统启动的时候,执行AbstractApplicationContext#invokeBeanFactoryPostProcessors,触发所有的BeanFactoryPostProcessor,执行BeanFactoryPostProcessor#postProcessBeanFactoryAsyncInitBeanFactoryPostProcessor实现了BeanFactoryPostProcessor,会扫描到带有SofaAsyncInit的Bean。

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        Arrays.stream(beanFactory.getBeanDefinitionNames())
                .collect(Collectors.toMap(Function.identity(), beanFactory::getBeanDefinition))
                .forEach((key, value) -> scanAsyncInitBeanDefinition(key, value, beanFactory));
    }

AsyncInitBeanFactoryPostProcessor#scanAsyncInitBeanDefinitionOnClass,扫描到带有SofaAsyncInit的Bean,收集起来。

    private void scanAsyncInitBeanDefinitionOnClass(String beanId, Class<?> beanClass,
                                                    BeanDefinition beanDefinition,
                                                    ConfigurableListableBeanFactory beanFactory) {
        // See issue: https://github.com/sofastack/sofa-boot/issues/835
        SofaAsyncInit sofaAsyncInitAnnotation = AnnotationUtils.findAnnotation(beanClass,
            SofaAsyncInit.class);
        registerAsyncInitBean(beanId, sofaAsyncInitAnnotation, beanDefinition);
    }

    private void registerAsyncInitBean(String beanId, SofaAsyncInit sofaAsyncInitAnnotation,
                                       BeanDefinition beanDefinition) {
        if (sofaAsyncInitAnnotation == null) {
            return;
        }
        PlaceHolderAnnotationInvocationHandler.AnnotationWrapperBuilder<SofaAsyncInit> wrapperBuilder = PlaceHolderAnnotationInvocationHandler.AnnotationWrapperBuilder
            .wrap(sofaAsyncInitAnnotation).withBinder(binder);
        sofaAsyncInitAnnotation = wrapperBuilder.build();

        if (sofaAsyncInitAnnotation.value()) {
            AsyncInitBeanHolder.registerAsyncInitBean(moduleName, beanId,
                beanDefinition.getInitMethodName());
        }
    }

AsyncInitBeanHolder对于收集到的Bean的处理,就是存放到Map集合中。

public class AsyncInitBeanHolder {
    private static final ConcurrentMap<String, Map<String, String>> asyncBeanInfos = new ConcurrentHashMap<String, Map<String, String>>();

    public static void registerAsyncInitBean(String moduleName, String beanId, String methodName) {
        if (moduleName == null || beanId == null || methodName == null) {
            return;
        }

        Map<String, String> asyncBeanInfosInModule = asyncBeanInfos.get(moduleName);
        if (asyncBeanInfosInModule == null) {
            asyncBeanInfos.putIfAbsent(moduleName, new ConcurrentHashMap<String, String>());
            asyncBeanInfosInModule = asyncBeanInfos.get(moduleName);
        }

        asyncBeanInfosInModule.put(beanId, methodName);
    }

    public static String getAsyncInitMethodName(String moduleName, String beanId) {
        Map<String, String> asyncBeanInfosInModule;
        asyncBeanInfosInModule = (moduleName == null) ? null : asyncBeanInfos.get(moduleName);
        return (beanId == null || asyncBeanInfosInModule == null) ? null : asyncBeanInfosInModule
            .get(beanId);
    }
}

AsyncProxyBeanPostProcessor

AsyncProxyBeanPostProcessor#postProcessBeforeInitializationAsyncProxyBeanPostProcessor 类实现了 BeanPostProcessor 接口,并重写了其 postProcessBeforeInitialization 方法。如果在AsyncInitBeanHolder找到对应的数据,就会生成相应的代理类。

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
                                                                               throws BeansException {
        String methodName = AsyncInitBeanHolder.getAsyncInitMethodName(moduleName, beanName);
        if (methodName == null || methodName.length() == 0) {
            return bean;
        }

        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.setTargetClass(bean.getClass());
        proxyFactory.setProxyTargetClass(true);
        AsyncInitializeBeanMethodInvoker asyncInitializeBeanMethodInvoker = new AsyncInitializeBeanMethodInvoker(
            bean, beanName, methodName);
        proxyFactory.addAdvice(asyncInitializeBeanMethodInvoker);
        return proxyFactory.getProxy();
    }

AsyncInitializeBeanMethodInvoker

AsyncProxyBeanPostProcessor.AsyncInitializeBeanMethodInvoker#invoke,代理的拦截器的核心就是把初始化方法扔到线程池中。

        @Override
        public Object invoke(final MethodInvocation invocation) throws Throwable {
            // if the spring refreshing is finished
            if (AsyncTaskExecutor.isStarted()) {
                return invocation.getMethod().invoke(targetObject, invocation.getArguments());
            }

            Method method = invocation.getMethod();
            final String methodName = method.getName();
            if (!isAsyncCalled && methodName.equals(asyncMethodName)) {
                isAsyncCalled = true;
                isAsyncCalling = true;
                AsyncTaskExecutor.submitTask(applicationContext.getEnvironment(), new Runnable() {
                    @Override
                    public void run() {
                        try {
                            long startTime = System.currentTimeMillis();
                            invocation.getMethod().invoke(targetObject, invocation.getArguments());
                            SofaLogger.info(String.format(
                                "%s(%s) %s method execute %dms, moduleName: %s.", targetObject
                                    .getClass().getName(), beanName, methodName, (System
                                    .currentTimeMillis() - startTime), moduleName));
                        } catch (Throwable e) {
                            throw new RuntimeException(e);
                        } finally {
                            initCountDownLatch.countDown();
                            isAsyncCalling = false;
                        }
                    }
                });
                return null;
            }

            if (isAsyncCalling) {
                long startTime = System.currentTimeMillis();
                initCountDownLatch.await();
                SofaLogger.info(String.format("%s(%s) %s method wait %dms, moduleName: %s.",
                    targetObject.getClass().getName(), beanName, methodName,
                    (System.currentTimeMillis() - startTime), moduleName));
            }
            return invocation.getMethod().invoke(targetObject, invocation.getArguments());
        }

AsyncTaskExecutionListener

AsyncTaskExecutionListener当监听到ContextRefreshedEvent,执行AsyncTaskExecutor.ensureAsyncTasksFinish();,保证线程池中的初始化任务都执行结束。

public class AsyncTaskExecutionListener implements PriorityOrdered,
                                       ApplicationListener<ContextRefreshedEvent>,
                                       ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        if (applicationContext.equals(event.getApplicationContext())) {
            AsyncTaskExecutor.ensureAsyncTasksFinish();
        }
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 1;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

AsyncTaskExecutor#ensureAsyncTasksFinish,保证启动完毕之前所有的异步线程执行结束

    public static void ensureAsyncTasksFinish() {
        for (Future future : FUTURES) {
            try {
                future.get();
            } catch (Throwable e) {
                throw new RuntimeException(e);
            }
        }

        STARTED.set(true);
        FUTURES.clear();
        if (THREAD_POOL_REF.get() != null) {
            THREAD_POOL_REF.get().shutdown();
            THREAD_POOL_REF.set(null);
        }
    }

相关参考

  • https://mp.weixin.qq.com/s/brEKfqWbaGt3FvpuMqSoGg

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/618344.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

苹果 Apple 发布的 AR 头显 Vision Pro 介绍

苹果今天凌晨的发布会&#xff0c;隆重推出了用了8 年时间研发的AR&#xff08;增强现实&#xff09;头戴显示器 Vision Pro。作为苹果 AR系列的最新成员&#xff0c;为用户带来了前所未有的沉浸式增强现实体验。 硬件 12个摄像头 &#xff0c;包括苹果首个 3D 相机&#xff0c…

【容器云架构】Calico 组件架构

Calico 组件 下图显示了 Kubernetes 的必需和可选 Calico 组件&#xff0c;具有网络和网络策略的本地部署。 Calico 组件 Calico API serverFelixBIRDconfdDikastesCNI pluginDatastore pluginIPAM pluginkube-controllersTyphacalicoctl 云编排器的插件 Plugins for cloud orc…

【vulnhub靶场】node 1

文章目录 前言开启靶机信息收集二层发现三层信息收集 攻击利用web信息收集权限提升后渗透 前言 描述&#xff1a;Node是一个中等级别的boot2root挑战&#xff0c;最初是为HackTheBox创建的。有两个标志可供查找&#xff08;用户和根标志&#xff09;和多种不同的技术可供使用。…

IDEA启动图片更改替换(2021.1/2022及其之后的版本)

目录 先说2022.1及其之后的版本: 2022.1之前的版本: 2022其他版本修改方法 最近一直在整理接口数据&#xff0c;盯屏幕太久了&#xff0c;然后打开IDEA突然感觉这个启动页面好刺眼&#xff0c;正好整理工作做完了&#xff0c;中午有空就找了下方法,发现了不少坑&#xff0c;…

项目管理中,如何减少项目风险?

我们公司&#xff0c;有一个项目已经做了一年多了&#xff0c;并且与客户进行了多次沟通&#xff0c;项目需求变更&#xff0c;范围扩大等&#xff0c;项目一直不能完成&#xff0c;客户生气&#xff0c;领导一直催&#xff0c;决定换一个项目经理把&#xff0c;领导让一个同事…

《vue 实践之 three.js 学习》

目录 three.js 学习包安装导包基础API学习Three.js 三要素【图文展示】 透视相机three.js 渲染器 之 WebGLRendererWebGLRenderer 实例化 three.js 学习 个人博客地址&#xff1a; 包安装 "three": "^0.153.0"命令&#xff1a;npm install --save three –…

模板初阶(C++)

目录 泛型编程 引入 模板 函数模板 函数模板的概念 函数模板格式 函数模板的原理 函数模板的实例化 隐式实例化 显式实例化 模板参数的匹配原则 类模板 类模板的定义格式 类模板的实例化 泛型编程 引入 我们在实际编写代码中&#xff0c;经常会遇到不同的类型需要实现同一种功…

使用纯C#语言实现4K图像平移显示性能的潜能测试

在介绍“熊猫视图.Net图形控件”系列文章中&#xff0c; 【“熊猫视图.Net图形控件”介绍链接】https://blog.csdn.net/mosangbike/article/details/126026801有对显示图像文件的测试结果&#xff0c;当时测试的不太严谨。今天抽时间详细测试了一下。 从网上找了一张Jpg图像作…

windows下安装配置 elasticsearch | kibana | analysis-ik

简介 elasticsearch是一款非常强大的开源搜索引擎&#xff0c;支持Restful风格&#xff0c;可以帮助我们从海量的数据中快速找到用户所需要的内容。是当前最流的开源企业级搜索引擎&#xff0c;能够达到近实时搜索、稳定、可靠、快速、安装使用方便。 elasticsearch结合kibana、…

wireshark使用-(2)运行时自动分包

现如今的网络通信分析基本都逃不开wireshark这个工具&#xff0c;而运行长时间的网络包往往过大&#xff0c;我们只想关注指定时间段的网络包&#xff0c;所以分包就显得尤为重要&#xff0c;好在wireshark工具自带这个功能&#xff0c;方便运行时能自动按时间&#xff0c;按大…

05.JavaWeb-Servlet(上)

目录 1.Servlet基础 1.1 Servlet概述 1.2 Servlet开发入门 1.2.1 Servlet接口及实现类 1.2.2实现Servlet程序 2.Servlet的请求与响应 2.1HttpServletRequest 2.1.1获取请求参数&#xff08;常用&#xff09; 2.1.2 获取请求方法 2.1.3获取与请求关联的会话对象 2.1.…

Vue总结

这里写自定义目录标题 一、Vue基本结介绍1、Vue 项目示例2、Vue 开发工具3、HTML 基本结构4、HTML 常用标签二、vue常用指令1、v-model 双向绑定a、v-model 修饰符2、插值3、条件渲染1、v-if 和 v-show 区别4、v-on 简介5、属性绑定6、 v-for 简介三、vue环境安装1、Vue 脚手架…

node基础与fs模块学习笔记

了解Node.js与内置模块 什么是Node.js? Node.js is an open-source, cross-platform JavaScript runtime environment. node.js是一个开源跨平台的js运行环境。 前端的运行环境就是浏览器。 注意&#xff1a;Node.js中无法调用DOM和BOM等浏览器内置API。 Node.js中的顶级对象…

【大数据工具】Zookeeper 分布式集群和伪分布式安装

Zookeeper 安装 zookeeper 安装包下载地址&#xff1a;https://archive.apache.org/dist/zookeeper/ 1. 伪分布式部署 说明&#xff1a;伪分布即在一台服务器上通过不同端口模拟出分布式集群的效果&#xff0c;分布式一般 3 台起&#xff0c;一主两从。 说明&#xff1a;伪…

华为OD机试真题 Java 实现【斗地主之顺子】【2023 B卷 100分】,附详细解题思路

一、题目描述 在斗地主扑克牌游戏中&#xff0c;扑克牌由小到大的顺序为: 3.4.5.6.7.8.9.10.J.Q.K A.2&#xff0c;玩家可以出的扑克牌阵型有: 单张、对子、顺子、飞机、炸弹等。 其中顺子的出牌规则为: 由至少 5 张由小到大连续递增的扑克牌组成&#xff0c;且不能包含 2。 …

RK1126 NPU yolov5 6.2

基于 rk npu &#xff0c; 实现 yolov5 6.2 模型推理 实现过程 ⚡️​ 编译 opencv 需根据自己路径修改. cmake -D CMAKE_BUILD_TYPERELEASE \-D CMAKE_C_COMPILER./gcc-arm-8.3-2019.02-x86_64-arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc \-D CMAKE_CXX_COMPILER./gc…

VMware虚拟机Ubuntu磁盘空间扩充详细教程

文章目录 一、写在前面二、具体步骤三、最后总结 一、写在前面 最近在做Linux内核相关实验的时候&#xff0c;发现有时候我们编译出来的内核太大&#xff0c;如果VMware虚拟机空间分配不足会导致编译Linux内核失败&#xff0c;经过摸索&#xff0c;发现可以扩充Ubuntu的磁盘空间…

【Rust日报】2023-06-05 Effective Rust: 35种提升 Rust 代码的方法

Effective Rust: 35种提升Rust代码的方法 这是一本关于 Rust 的最佳实践指南的电子书&#xff0c;其中包含了很多 Rust 编程的技巧和建议。 这篇指南分为多个章节&#xff0c;涵盖了 Rust 编程的各个方面, 他分别从 类型, 概念, 依赖, 工具 等几大类上阐述了 35 种提高 Rust 代…

记录--你真的能区分JavaScript的各种导入导出方式吗?

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 前言 &#x1f6f0;&#x1f6f0; 我们在无论是在查阅别人的代码&#xff0c;还是在实际项目开发的过程中&#xff0c;肯定都会使用导入导出的功能&#xff0c;有时候我们会搞混这几种方式到底有什么区…

有哪些比较好的游戏图标推荐

游戏图标设计在游戏UI中占有非常重要的地位。例如&#xff0c;当我们看到一个游戏的启动图标时&#xff0c;很容易区分它是哪个游戏。设计游戏图标不仅是一个图形&#xff0c;也是一个标志。 本文将通过各种游戏图标设计素材分享游戏图标的类别和设计游戏图标的思考。 1. 游戏…