Bean异步初始化,让你的应用启动飞起来

应用启动速度主要的瓶颈在于 bean 的初始化过程,本文提供了启动速度的一个探索方向。

如果你的系统启动耗时 250s 以上,文章思路应该可以帮到你。

一、背景

近期,在做应用启动提速相关工作的过程中,我们发现,应用启动速度主要的瓶颈在于 bean 的初始化过程(init,afterPropertiesSet 方法的耗时)。很多中间件 bean 的初始化逻辑涉及到网络 io,且在没有相互依赖的情况下串行执行。将这一部分中间件 bean 进行异步加载,是提升启动速度的一个探索方向。

二、解决方案

  1. 自动扫描可批量异步的中间件 bean,而后,在 bean 的初始化阶段利用线程池并行执行其初始化逻辑。

  2. 允许使用方自行配置耗时 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 的替换动作!

四、效果

4.1 性能效果

猜你喜欢

转载自blog.csdn.net/agonie201218/article/details/131001661
今日推荐