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

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

科技

阿里妹导读


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

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

一、背景

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

二、解决方案

  1. 自动扫描可批量异步的中间件bean,而后,在bean的初始化阶段利用线程池并行执行其初始化逻辑。
  1. 允许使用方自行配置耗时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 性能效果

阿里云开发者社区,千万开发者的选择


阿里云开发者社区,百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
吉利的低空出行时代,从100辆电动飞行器开始𝐂𝐚𝐥𝐧𝐢𝐊𝐞𝐚𝐧双皮奶内衣裤,软弹有度,上身0束缚~电动飞机「绿」了航空业,一个真敢造,一个真敢投6件让你在床上舒服到飞起的东西微软贺韵:Azure OpenAI在游戏NPC和制作场景中的应用Agustín Hernández:中美洲建筑背景下的未来主义巨构母亲说 六再见32位应用!金标联盟7月起清理仅支持32位的应用破局之作:首部开源 AIGC 软件工程应用电子书《构筑大语言模型应用:应用开发与架构设计》探索|Spring并行初始化加速的思路和实践「AI+笔记」让办公效能飞起来!异步IO框架 io_uring电动飞机要来了!9000亿巨头大动作如何高效实现文件传输:小文件用零拷贝、大文件用异步io+直接io再接再厉一下雷声大雨点小?马斯克的“史上最强火箭”飞起来了,但炸了......明明拍到的苍蝇怎么又飞起来了?因为它们是“苍蝇侠”湾区|2026年旧金山机场将推出首个电动飞机!以后接送机可以“打飞的”?在法国犯人都圈(juān)不进去了还不同意多建点监狱?为啥呀?Flutter异步编程指南如何高效实现文件传输:小文件采用零拷贝、大文件采用异步io+直接io周末大事!阿里清仓AI龙头!俞敏洪再创业,看上了"它"!锂电巨头大动作,"电动飞机"要来了?“回到希腊去!”,也叫文艺复兴。“非秦汉文不读”,也叫古文运动。“不读或少读中国书”,也叫五四新文化运动。在赵谦书札中找字样,找MGR主备集群实现异步连接故障转移电影照进现实,电动飞行汽车现可预定,售价$30万空中讲坛|CRISPR基因编辑在肿瘤研究中的应用进展清华五道口:ChatGPT在金融领域的应用和前景美元霸权,天下苦之久也国产电动飞机E20发布!“空中的士”即将起飞!极其惨烈!悉尼华人区6车猛撞,有人当场死亡,直升机出动!华人亲历惊魂一幕!“SUV飞起来了…我捡回一条命!”飞起来了!但炸了!马斯克的最大火箭,第一次轨道飞行4分钟结束EB-5成功案例 │ 缺少官方文件证明初始资金来源?补件后顺利通过【𝐂𝐚𝐥𝐧𝐢𝐊𝐞𝐚𝐧双皮奶无痕内裤】49元三条!巨巨巨好穿 !!简直就是辣妹顶配,食品级冰箱收纳盒【一日团】解密服务性能利器:Pyroscope让你的应用飞起来登渣甸山
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。