应用启动速度主要的瓶颈在于 bean 的初始化过程,本文提供了启动速度的一个探索方向。
如果你的系统启动耗时 250s 以上,文章思路应该可以帮到你。
一、背景
近期,在做应用启动提速相关工作的过程中,我们发现,应用启动速度主要的瓶颈在于 bean 的初始化过程(init,afterPropertiesSet 方法的耗时)。很多中间件 bean 的初始化逻辑涉及到网络 io,且在没有相互依赖的情况下串行执行。将这一部分中间件 bean 进行异步加载,是提升启动速度的一个探索方向。
二、解决方案
-
自动扫描可批量异步的中间件 bean,而后,在 bean 的初始化阶段利用线程池并行执行其初始化逻辑。
-
允许使用方自行配置耗时 bean 以享受异步加速能力。(需使用方自行确认依赖关系满足异步条件)
三、原理
3.1 异步初始化原理
3.1.1 如何异步 init 和 afterPropertiesSet?
3.1.1.1 这俩初始化方法在哪里执行的?
在 AbstractAutowireCapableBeanFactory#invokeInitMethods 方法(以下代码省略异常处理以及日志打印)
protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd)
throws Throwable {
// 先看bean是不是实现了InitializingBean,如果是则执行afterPropertiesSet方法。
boolean isInitializingBean = (bean instanceof InitializingBean);
if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
((InitializingBean) bean).afterPropertiesSet();
return null;
}, getAccessControlContext());
} else {
((InitializingBean) bean).afterPropertiesSet();
}
}
// xml定义的init方法
if (mbd != null && bean.getClass() != NullBean.class) {
String initMethodName = mbd.getInitMethodName();
if (StringUtils.hasLength(initMethodName) &&
!(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
!mbd.isExternallyManagedInitMethod(initMethodName)) {
invokeCustomInitMethod(beanName, bean, mbd);
}
}
}
- 调用位置图
3.1.1.2 如何自定义该方法逻辑使其支持异步执行?
- 很简单的想法
有没有可能,我可以替换原有的 BeanFactory,换成我自定义的一个 BeanFactory,然后我继承他,只是重写 invokeInitMethods 方法逻辑使其支持异步?
像这样:
public class AsyncInitBeanFactory extends DefaultListableBeanFactory {
private static final Logger logger = LoggerFactory.getLogger(AsyncInitBeanFactory.class);
// 省略
@Override
protected void invokeInitMethods(String beanName, Object bean, RootBeanDefinition mbd) throws Throwable {
if (AsyncInitBeanNameContainer.MIDDLEWARE_ASYNC_HSF_BEAN_NAME.contains(beanName)) {
// hsf异步init
this.asyncCallInitMethods(TaskUtil.threadPool4HsfBean, beanName, bean, mbd);
} else if (AsyncInitBeanNameContainer.MIDDLEWARE_ASYNC_INIT_BEAN_NAME.contains(beanName)) {
// 其他bean异步init
this.asyncCallInitMethods(TaskUtil.threadPool4NormalMBean, beanName, bean, mbd);
} else {
// 同步init call父类原来的invokeInitMethods
try {
super.invokeInitMethods(beanName, bean, mbd);
} catch (Exception e) {
logger.error("middleware-bean-accelerator sync-init error: {}", e.getMessage(), e);
throw e;
}
}
}
// 省略
}
那现在已经有了自定义方法了,只要解决替换就行了呗?
- 怎么替换?
实现 ApplicationContextInitializer 接口,ApplicationContextInitializer 在 ApplicationContext 做 refresh 之前可以对 ConfigurableApplicationContext 的实例做进一步的设置或者处理。在这里可以用反射替换掉原 BeanFactory。
像这样:
public class AsyncAccelerateInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
@Override
public void initialize(ConfigurableApplicationContext context) {
// 是否开启异步初始化
if (ConfigUtil.isEnableAccelerate(context) && context instanceof GenericApplicationContext) {
AsyncInitBeanFactory beanFactory = new AsyncInitBeanFactory(context.getBeanFactory());
// 通过反射替换beanFactory
try {
Field field = GenericApplicationContext.class.getDeclaredField("beanFactory");
field.setAccessible(true);
field.set(context, beanFactory);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
}
之后我们只需要在 spring.factories 文件将其注册即可。
这样一来就实现了我们一开始的目标,让 init 方法和 afterPropertiesSet 支持异步执行。
3.1.2 如何异步 PostConstruct?
3.1.2.1 @PostConstruct 在哪执行的?
在 CommonAnnotationBeanPostProcessor#postProcessBeforeInitialization 方法
- 这是哪里?
CommonAnnotationBeanPostProcessor 实现了 BeanPostProcessor 接口,postProcessBeforeInitialization 方法是 BeanPostProcessor 的方法。
BeanPostProcessor 在初始化阶段被调用。
- org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsBeforeInitialization
@Override
public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)
throws BeansException {
Object result = existingBean;
// 把BeanPostProcesss都抓出来调用一下
for (BeanPostProcessor processor : getBeanPostProcessors()) {
Object current = processor.postProcessBeforeInitialization(result, beanName);
if (current == null) {
return result;
}
result = current;
}
return result;
}
- 调用位置图
3.2.1.2 如何自定义该方法逻辑使其支持异步执行?
- 很简单的想法
有没有可能,我可以去掉原有的 CommonAnnotationBeanPostProcessor,换成我自定义的一个 BeanPostProcessor,然后我继承他,只是重写 postProcessBeforeInitialization 方法逻辑使其支持可异步的 @PostConstruct 方法?
像这样:
public class AsyncCommonAnnotationBeanPostProcessor extends CommonAnnotationBeanPostProcessor {
private static final Logger logger = LoggerFactory.getLogger(AsyncCommonAnnotationBeanPostProcessor.class);
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// 如果是我指定的beanName 那么走异步初始化, 把super.postProcessBeforeInitialization(bean, beanName) 放进线程池里执行
if (AsyncInitBeanNameContainer.MIDDLEWARE_ASYNC_POST_CONSTRUCT_BEAN_NAME.contains(beanName)) {
// 异步初始化
this.asyncExecutePostConstruct(bean, beanName);
} else {
// 同步初始化
return super.postProcessBeforeInitialization(bean, beanName);
}
return bean;
}
// 略
}
那现在已经有了自定义方法了,只要解决替换就行了呗?
- 怎么替换?
实现 InstantiationAwareBeanPostProcessorAdapter 接口,其中有一个方法叫做 postProcessBeforeInstantiation。postProcessBeforeInstantiation 方法是对象实例化前最先执行的方法,它在目标对象实例化之前调用,该方法的返回值类型是 Object,我们可以返回任何类型的值。由于这个时候目标对象还未实例化,所以这个返回值可以用来代替原本该生成的目标对象的实例 (比如代理对象)。
像这样:
public class OverrideAwareBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter {
@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
// 替换掉原处理@PostConstruct注解的后置处理器
if ("org.springframework.context.annotation.internalCommonAnnotationProcessor".equals(beanName)) {
AsyncCommonAnnotationBeanPostProcessor asyncBeanPostProcessor = new AsyncCommonAnnotationBeanPostProcessor();
// 省略基础的设置
return asyncBeanPostProcessor;
}
return super.postProcessBeforeInstantiation(beanClass, beanName);
}
}
之后我们只需要把这个 BeanPostProcessor 添加到 BeanFactory,beanFactory.addBeanPostProcessor(new OverrideAwareBeanPostProcessor(beanFactory));
这样一来就实现了我们一开始的目标,让 @PostConstruct 方法支持异步执行。
3.2 批量扫描 & 异步加载中间件 Bean 原理
中间件 bean 批量异步实现案例以 RPC 为例
RPC 是后端日常开发中最常见的中间件之一,HSF 是阿里内部常见的 RPC 中间件,3.2 节的讲述我们以 HSF 为案例,实现 HSFConsumerBean 的批量异步初始化。
3.2.1 如何获取待异步的 Bean 信息?
3.2.1.1 HSF Consumer 是怎么样使用的?
与 Dubbo 相似,对于使用者而言,只需在成员变量上加上 @HSFConsumer 注解,服务启动过程中 HSF 就会将实现了远程调用的代理对象注入成员变量。如下:
@Service
public class XXXService {
@HSFConsumer(serviceVersion = "1.0.0", serviceGroup = "HSF")
private OrderService orderService;
// 省略
}
3.2.1.2 如何通过 Consumer 的注解获取 Bean 信息?
如 3.2.1.1 节所示,被注入代理对象的成员变量字段上带有 @HSFConsumer 注解,这样,我们是不是可以利用该注解在启动过程中找到这些 Bean,并对其实施异步初始化处理?
答案是肯定的
通过实现 BeanFactoryPostProcessor 接口,我们可以在 beanDefinition 被扫描 & 记录后,在 postProcessBeanFactory 方法中获取所有 bean 的定义信息,并找出其中带有 @HSFConsumer 注解的 bean 进行记录,以便在后续调用 init 方法时(见 3.1.1.2 节)进行异步初始化。
public class HsfBeanNameCollector implements BeanFactoryPostProcessor, BeanClassLoaderAware {
private ClassLoader classLoader;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// 省略
for (String beanName : beanFactory.getBeanDefinitionNames()) {
BeanDefinition definition = beanFactory.getBeanDefinition(beanName);
String beanClassName = definition.getBeanClassName();
Class<?> clazz = ClassUtils.resolveClassName(definition.getBeanClassName(), this.classLoader);
ReflectionUtils.doWithFields(clazz, field -> {
if (AnnotationUtils.getAnnotation(field, HSFConsumer.class) == null) {
return;
}
// 收集HsfConsumerBeanName方便后续异步化
AsyncInitStaticVariables.MIDDLEWARE_ASYNC_HSF_BEAN_NAME.add(field.getName());
});
}
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
}
3.3.2 如何安全异步 HSFSpringConsumerBean?
3.3.2.1 我们加 @HSFConsumer 注解的成员变量是如何被注入动态代理类的?
HSFSpringConsumerBean 实现了 FactoryBean 接口,其中的 getObject 方法会在属性注入时被调用,获取其返回值,注入成员变量。而真正接口的实现(也就是动态代理类)就是在这里被注入的。
public class HSFSpringConsumerBean implements FactoryBean, InitializingBean, ApplicationListener, ApplicationContextAware {
// 省略
@Override
public Object getObject() throws Exception {
return consumerBean.getObject();
}
// 省略
}
而该动态代理类是如何生成的呢?答案在 HSFApiConsumerBean 的 init 方法中
如下所示:metadata.setTarget(consume(metadata));
public class HSFApiConsumerBean {
// 省略
/**
* 初始化
*
* @throws Exception
*/
public void init() throws Exception {
// 省略
synchronized (metadata) {
// 省略
metadata.init();
try {
// 动态代理类的设置就在这里
metadata.setTarget(consume(metadata));
// 省略
} catch (Exception e) {
// 省略
} catch (Throwable t) {
// 省略
}
// 省略
}
}
// 省略
}
3.3.2.2 会存在什么问题?
- 动态代理对象的生成在 init 阶段意味着什么?
意味着 bean 初始化如果未完成,会为成员变量注入一个 null 值,导致 consumer 不可用,这是异步的巨大风险。
3.3.2.3 我们的解决方案
自定义一个 NewHsfSpringConsumerBean,继承 HSFSpringConsumerBean 并重写 getObject 方法,在父类的 getObject 方法执行前等待初始化任务完成。
像这样:
public class NewHsfSpringConsumerBean extends HSFSpringConsumerBean {
// 省略
private Future<?> initTaskFuture;
/**
* 重写NewHsfSpringConsumerBean的主要目的 在此加入卡点 防止hsfSpringConsumerBean未初始化完成导致的npe
*
* @return
* @throws Exception
*/
@Override
public Object getObject() throws Exception {
this.waitHsfInit();
return super.getObject();
}
private void waitHsfInit() {
if (this.initTaskFuture == null) {
logger.warn("middleware-bean-accelerator, hsf getObject wait future is null.");
return;
}
try {
this.initTaskFuture.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
// 省略
}
现在的问题就是我们如何将原有的 HSFSpringConsumerBean 替换成 NewHsfSpringConsumerBean?
答案还是 InstantiationAwareBeanPostProcessorAdapter 接口
如下所示:
public class OverrideAwareBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter {
private final AsyncInitBeanFactory beanFactory;
// 省略
@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
// 修改beanDefinition 使容器创建自定义的HsfSpringConsumerBean
if (beanClass == HSFSpringConsumerBean.class) {
this.reviseBeanDefinition(beanName, NewHsfSpringConsumerBean.class);
// 返回null可以让实例化的任务交由spring容器
return null;
}
return super.postProcessBeforeInstantiation(beanClass, beanName);
}
@Override
public boolean postProcessAfterInstantiation(Object bean, String beanName) {
if (bean.getClass() == NewHsfSpringConsumerBean.class) {
this.reviseBeanDefinition(beanName, HSFSpringConsumerBean.class);
}
return super.postProcessAfterInstantiation(bean, beanName);
}
/**
* 修改beanDefinition
* 设置NewHsfSpringConsumerBean使容器创建自定义的HsfSpringConsumerBean 实例化后设置回来
*
* @param beanName
* @return
*/
private void reviseBeanDefinition(String beanName, Class<?> clazz) {
try {
Method methodOfRootBeanDefinition = this.beanFactory.getClass().
getSuperclass().getSuperclass().getSuperclass().
getDeclaredMethod("getMergedLocalBeanDefinition", String.class);
methodOfRootBeanDefinition.setAccessible(true);
RootBeanDefinition beanDefinition = (RootBeanDefinition) methodOfRootBeanDefinition.invoke(this.beanFactory, beanName);
// 重点步骤: 修改beanDefinition 使容器创建自定义的HsfSpringConsumerBean, 并在实例化后设置回来
beanDefinition.setBeanClass(clazz);
} catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
我们在实例化之前,修改 beanDefinition,使容器创建自定义的 HsfSpringConsumerBean。然后在实例化后的阶段将 beanDefinition 改回,这样就非常优雅实现了对原有 HSFSpringConsumerBean 的替换动作!