如果你的系統啓動耗時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
@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 性能效果
作者|嚴熠彬(言益)
本文爲阿里雲原創內容,未經允許不得轉載。