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

使用场景

在实际使用 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

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值