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);    }}

這樣一來就實現了我們一開始的目標,讓@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 性能效果

作者|嚴熠彬(言益)

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章