使用场景
在实际使用 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#postProcessBeanFactory
。AsyncInitBeanFactoryPostProcessor
实现了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#postProcessBeforeInitialization
,AsyncProxyBeanPostProcessor
类实现了 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