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


@Overridepublic 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就会将实现了远程调用的代理对象注入成员变量。如下:



@Servicepublic 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 性能效果



相关文章
|
6月前
|
Java
【专栏】Java反射机制,该机制允许程序在运行时获取类信息、动态创建对象、调用方法和访问属性
【4月更文挑战第27天】本文探讨了Java反射机制,该机制允许程序在运行时获取类信息、动态创建对象、调用方法和访问属性。反射通过Class、Constructor、Method和Field类实现。文中列举了反射的应用场景,如动态创建对象、调用方法、访问属性和处理注解,并提供了相关实例代码演示。
78 4
|
6月前
|
存储 Java 程序员
【JVM】类的声明周期(加载、连接、初始化)
【JVM】类的声明周期(加载、连接、初始化)
36 1
|
6月前
|
Java
SpringBoot关闭过程中是如何销毁一个DisposableBean的?
SpringBoot关闭过程中是如何销毁一个DisposableBean的?
61 0
|
Java 编译器
04-面试:类的初始化做了什么?初始化的时机是?
类的初始化是指在首次使用类时,JVM对类进行的初始化操作。在类初始化阶段,JVM会执行一系列的步骤。
79 0
04-面试:类的初始化做了什么?初始化的时机是?
|
中间件 Java
Bean异步初始化
Bean异步初始化
|
监控 安全 Java
《SpringBoot启动流程三》:两万+字图文带你debug源码分析SpringApplication准备阶段(含配置文件加载时机、日志系统初始化时机)
《SpringBoot启动流程三》:两万+字图文带你debug源码分析SpringApplication准备阶段(含配置文件加载时机、日志系统初始化时机)
320 0
《SpringBoot启动流程三》:两万+字图文带你debug源码分析SpringApplication准备阶段(含配置文件加载时机、日志系统初始化时机)
|
SQL 存储 缓存
如何在SpringBoot启动时执行初始化操作,两个简单接口就可以实现
最近遇到一个功能点,数据库中一张很简单的表有一千多条数据,这里的数据主要做到了值域映射的作用,简单来讲就是我可以通过中文名拿到数据库中对应的code值。原本的实现方式是每次用到之后去查一次sql,虽然不会有什么问题,但是只要是走了网络io,都会消耗时间。所以这个方案需要想办法优化。 优化的方式其实很简单,数据量不多,一千多条数据放在内存里也占不了多少空间。因此完全可以把一次性把数据加载到内存中,后面只需要每次去内存里调用就可以了。
|
Java Spring 容器
SpringBoot (走读源码)静态方法中调用spring注入的对象,注入对象为null?
SpringBoot (走读源码)静态方法中调用spring注入的对象,注入对象为null?
437 0
SpringBoot (走读源码)静态方法中调用spring注入的对象,注入对象为null?
|
XML 缓存 Java
Spring源码解析之八finishBeanFactoryInitialization方法即初始化单例bean
Spring源码解析之八finishBeanFactoryInitialization方法即初始化单例bean 七千字长文深刻解读,Spirng中是如何初始化单例bean的,和面试中最常问的Spring是如何解决循环依赖?
|
Java 容器 Spring
Spring注解(六):Bean的生命周期中自定义初始化和销毁方法的四种方式
Bean的生命周期指的是Bean从被创建到初始化再被销毁的过程,IOC容器管理Bean的生命周期。在Bean的整个生命周期的过程中的初始化和销毁过程的方法可以被自定义,IOC容器当Bean进行到当前生命周期的时候调用自定义的初始化和销毁方法。在配置文件中可以通过添加init-method和destroy-method指定自定义的初始化和销毁方法
786 0
Spring注解(六):Bean的生命周期中自定义初始化和销毁方法的四种方式