关于后端:可以很强68行代码实现Bean的异步初始化粘过去就能用

37次阅读

共计 10190 个字符,预计需要花费 26 分钟才能阅读完成。

你好呀,我是歪歪。

前两天在看 SOFABoot 的时候,看到一个让我眼前一亮的货色,来给大家盘一下。

SOFABoot,你可能不眼生,然而没关系,本文也不是给你讲这个货色的,你就认为它是 SpringBoot 的变种就行了。

因为有蚂蚁金服背书,所以次要是一些金融类的公司在应用这个框架:

官网介绍是这样的:

SOFABoot 是蚂蚁金服开源的基于 Spring Boot 的研发框架,它在 Spring Boot 的根底上,提供了诸如 Readiness Check,类隔离,日志空间隔离等能力。在加强了 Spring Boot 的同时,SOFABoot 提供了让用户能够在 Spring Boot 中十分不便地应用 SOFA 中间件的能力。

下面这些性能都很弱小,然而我次要是分享一下它的这个小性能:

https://help.aliyun.com/document_detail/133162.html

这个性能能够让 Bean 的初始化办法在异步线程外面执行,从而放慢 Spring 上下文加载过程,进步利用启动速度。

为什么看到性能的时候,我眼前一亮呢,因为我很久之前写过这篇文章《我是真没想到,这个面试题竟然从 11 年前就开始探讨了,而官网往年才表态。》

外面提到的面试题是这样的:

Spring 在启动期间会做类扫描,以单例模式放入 ioc。然而 spring 只是一个个类进行解决,如果为了减速,咱们勾销 spring 自带的类扫描性能,用写代码的多线程形式并行进行解决,这种计划可行吗?为什么?

过后通过 issue 找到了官网对于这个问题回复总结起来就是:应该是先找到启动慢的根本原因,而不是把问题甩锅给 Spring。这部分对于 Spring 来说,能不动,就别动。

仅从“启动减速 - 异步初始化办法”这个题目上来看,Spring 官网不反对的货色 SOFABoot 反对了。所以这玩意让我眼前一亮,我倒要看看你是怎么搞得。

先说论断:SOFABoot 的计划能从肯定水平上解决问题,然而它依赖于咱们编码的时候指定哪些 Bean 是能够异步初始化的,这样带来的益处是不用思考循环依赖、依赖注入等等各种简单的状况了,害处就是须要程序员本人去辨认哪些类是能够异步初始化的。

我倒是感觉,程序员原本就应该具备“辨认本人的我的项目中哪些类是能够异步初始化”的能力。

然而,一旦要求程序员来被动去辨认了,就曾经“输了”,曾经不够惊艳了,在实现难度上就不是一个级别的事件了。人家 Spring 想的可是框架给你全副搞定,顶多给你留一个开关,你开箱即用,啥都不必管。

然而总的来说,作为一次思路演变为源码的学习案例来说,还是很不错的。

咱们次要是看实现计划和具体逻辑代码,以 SOFABoot 为抓手,针对其“异步初始化办法”聚焦下钻,把源码当做纽带,协同 Spring,打出一套“我看到了 -> 我会用了 -> 我拿过去 -> 我看懂了 -> 是我的了 -> 写进简历”的组合拳。

Demo

先搞个 Demo 进去,演示一波成果,先让你直观的看到这是个啥玩意。

这个 Demo 十分之简略,几行代码就搞定。

先搞两个 java 类,外面有一个 init 办法:

而后把他们作为 Bean 交给 Spring 治理,Demo 就搭建好了:

间接启动我的项目,启动工夫只须要 1.152s,十分丝滑:

而后,留神,我要略微的变一下形。

在注入 Bean 的时候触发一下初始化办法,模仿理论我的项目中在 Bean 的初始化阶段,既在 Spring 我的项目启动过程中,做一些数据筹备、配置拉取等相干操作:

再次重启一下我的项目,因为须要执行两个 Bean 的初始化动作,各须要 5s 工夫,而且是串行执行,所以启动工夫间接来到了 11.188s:

那么接下来,就是见证奇观的时刻了。

我加上 @SofaAsyncInit 这样的一个注解:

你先别管这个注解是哪里来的,从这个注解的名称你也晓得它是干啥的:异步执行初始化。

这个时候我再启动我的项目:

从日志中能够看到:

  1. whyBean 和 maxBean 的 init 办法是由两个不同的线程并行执行的。
  2. 启动工夫缩短到了 6.049s。

所以 @SofaAsyncInit 这个注解实现了“指定 Bean 的初始化办法实现异步化”。

你想想,如果你有 10 个 Bean,每个 Bean 都须要 1s 的工夫做初始化,总计 10s。

然而这些 Bean 之间其实不须要串行初始化,那么用这个注解,并行只须要 1s,搞定。

到这里,你算是看到了这样的货色存在,属于“我看到了”。

接下来,咱们进入到“我会用了”这个环节。

怎么来的。

在解读原理之前,我还得通知你这个注解到底是怎么来的。

它属于 SOFABoot 框架外面的注解,首先你得把你的 SpringBoot 批改为 SOFABoot。

这一步参照官网文档中的“疾速开始”局部,十分的简略:

https://www.sofastack.tech/projects/sofa-boot/quick-start/

第一步就是把我的项目中 pom.xml 中的:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>${spring.boot.version}</version>
    <relativePath/> 
</parent>

替换为:

<parent>
    <groupId>com.alipay.sofa</groupId>
    <artifactId>sofaboot-dependencies</artifactId>
    <version>${sofa.boot.version}</version>
</parent>

这里的 ${sofa.boot.version} 指定具体的 SOFABoot 版本,我这里应用的是最新的 3.18.0 版本。

而后咱们要应用 @SofaAsyncInit 注解,所以须要引入以下 maven:

<dependency>
    <groupId>com.alipay.sofa</groupId>
    <artifactId>runtime-sofa-boot-starter</artifactId>
</dependency>

对于 pom.xml 文件的变动,就只有这么一点:

最初,在工程的 application.properties 文件下增加 SOFABoot 工程一个必须的参数配置,spring.application.name,用于标示以后利用的名称

# Application Name
spring.application.name=SOFABoot Demo

就搞定了,我就实现了一个从 SpringBoot 切换为 SOFABoot 这个大动作。

当然了,我这个是一个 Demo 我的项目,构造和 pom 依赖都非常简单,所以切换起来也非常容易。如果你的我的项目比拟大的话,可能会遇到一些兼容性的问题。

然而,留神我要说然而了。

你是在学习摸索阶段,Demo 肯定要简略,越小越好,越污浊越好。所以这个切换的动作对你搭建的一个全新的 Demo 我的项目来说没啥难度,不会遇到任何问题。

这个时候,你就能够应用 @SofaAsyncInit 注解了:

到这里,祝贺你,会用了。

拿来吧你

不晓得你看到这里是什么感触。

反正对于我来说,如果仅仅是为了让我能应用这个注解,达到异步初始化的目标,要让我从相熟的 SpringBoot 批改为听都没听过的 SOFABoot,即便这个框架背地有阿里给它背书,我必定也是不会这么干的。

所以,对于这一类“人有我无”的货色,我都是采取“拿来吧你”策略。

你想,最开始的我就说了,SOFABoot 是 SpringBoot 的变种,它的底层还是 SpringBoot。

而 SOFABoot 又是开源的,整个我的项目的源码我都有了:

https://github.com/sofastack/sofa-boot/blob/master/README_ZH.md

从其中剥离出一个基于 SpringBoot 做的小性能,融入到我本人的 SpringBoot 我的项目中,还玩意难道不是手到擒来的事件?

不过就是略微高级一点的 cv 罢了。

首先,你得把 SOFABoot 的源码下载下来,或者在另外的一个我的项目中援用它,把本人的我的项目复原为一个 SpringBoot 我的项目。

我这边是间接把 SOFABoot 源码搞下来了,先把源码外面的 @SofaAsyncInit 注解粘到我的项目外面来,而后从 @SofaAsyncInit 注解动手,发现除了测试类只有一个 AsyncInitBeanFactoryPostProcessor 类在对其进行应用:

所以把这个类也搬运过去。

搬运过去之后你会发现有一些类找不到导致报错:

针对这部分类,你能够采取无脑搬运的形式,也能够稍加思考替换一些。

比方我就分为了两种类型:

标号为 ① 的局部,我是间接粘贴到本人的我的项目中,而后应用我的项目中的类。

标号为 ② 的局部,比方 BeanLoadCostBeanFactory 和 SofaBootConstants,他们的目标是为了获取一个 moduleName 变量:

我也不晓得这个 moduleName 是啥,所以我采取的策略是本人指定一个:

至于 ErrorCode 和 SofaLogger,日志相干的,就用本人我的项目外面的日志就行了。

就是这个意思:

这样解决实现之后,AsyncInitBeanFactoryPostProcessor 类不报错了,接着看这个类在哪里应用到了。

就这样顺藤摸瓜,最初搬运实现之后,就是这些类移过来了:

除了这些类之外,你还会把这个 spring.factories 搬运过去,在我的项目启动时把这几个相干的类加载进去:

而后再次启动这个和 SOFABoot 没有一点关系的我的项目:

你会发现,你的我的项目也具备异步初始化 Bean 的性能了。

你要再进一步,把它间接封装为一个 spring-boot-starter-asyncinitbean,公布到你们公司的私服外面。

其余团队也能开箱即用的应用这个性能了。

别问,问就是你本人独立开发进去的,把握全副源码,技术危险可控:

啃原理

在开始啃原理之前,我先多比比两句。

我写文章的时候,为什么要把“拿来吧你”这一大节放在“啃原理”之前,我是有思考的。

当咱们把“异步初始化”这个性能点剥离进去之后,你会发现,要实现这个性能,一共也没波及到几个类。

聚焦点从一整个我的项目变成了几个类而已,至多从感官上不会感觉那么的难,对浏览其源码产生太大的抗拒心理。

而我之前很多对于源码浏览的文章,都强调过这一点:带着疑难去调试源码,要抓住骨干,谨防走偏。

后面这一大节,不过是把这一句话具化了而已。即便没有把这些类剥离进去,你间接基于 SOFABoot 来调试这个性能。在你搞清楚“异步初始化”这个性能的实现原理之前,实践上你的关注点和注意力不应该被下面这些类之外的任何一个类给吸引走。

接下来,咱们就带你啃一下原理。

对于原理局部,咱们的突破口必定是看 @SofaAsyncInit 这个注解的在哪个中央被解析的。

你认真看这个注解外面有一个 value 属性,默认为 true,下面的注解说:用来标注是否应该对 init 办法进行异步调用。

而应用到这个 value 值的中央,就只有上面这一个中央:

com.alipay.sofa.runtime.spring.AsyncInitBeanFactoryPostProcessor#registerAsyncInitBean

判断为 true 的时候,执行了一个 registerAsyncInitBean 办法。

从办法名称也晓得,它是把能够异步执行的 init 办法的 Bean 收集起来。

所以看源码能够看出,这外面是用 Map 来进行的存储,提供了一个 register 和 get 办法:

那么这个 Map 外面到底放的是啥呢?

我也不晓得,打个断点瞅一眼,不就行了:

通过断点调试,咱们晓得这个外面把我的项目中哪些 Bean 能够异步执行 init 办法通过 Map 寄存了起来。

那么问题就来了:它怎么晓得哪些 Bean 能够异步执行 init 呢?

很简略啊,因为我在对应的 Bean 上打上了 @SofaAsyncInit 注解。所以能够通过扫描注解的形式找到这些 Bean。

所以你说 AsyncInitBeanFactoryPostProcessor 这个类是在干啥?

必定外围逻辑就是在解析标注了 @SofaAsyncInit 注解的中央嘛。

到这里,咱们通过注解的 value 属性,找到了 AsyncInitBeanHolder 这个要害类。

晓得了这个类外面有一个 Map,外面保护的是所有能够异步执行 init 办法的 Bean 和其对应的 init 办法。

好,你思考一下,接下来应该干啥?

接下来必定是看哪个中央在从这个 Map 外面获取数据进去,获取数据的时候,就阐明是要异步执行这个 Bean 的 init 办法的时候。

不然它把数据放到 Map 外面干啥?玩吗?

调用 getAsyncInitMethodName 办法的中央,也在 AsyncProxyBeanPostProcessor 类外面:

com.alipay.sofa.runtime.spring.AsyncProxyBeanPostProcessor#postProcessBeforeInitialization

AsyncProxyBeanPostProcessor 类实现了 BeanPostProcessor 接口,并从新了其 postProcessBeforeInitialization 办法。

在这个 postProcessBeforeInitialization 办法外面,执行了从 Map 外面拿对象的动作。

如果获取到了则通过 AOP 编程,编织进一个 AsyncInitializeBeanMethodInvoker 办法。

把 bean, beanName, methodName 都传递了进去:

所以关键点,就在 AsyncInitializeBeanMethodInvoker 外面,因为这个外面有真正判断是否要进行异步初始化的逻辑,次要解读一下这个类。

首先,关注一下它的这三个参数:

  • initCountDownLatch:是 CountDownLatch 对象,其中 count 初始化为 1
  • isAsyncCalling:示意是否正在异步执行 init 办法。
  • isAsyncCalled:示意是否曾经异步执行过 init 办法。

通过这三个字段,就能够感知到一个 Bean 是否曾经或者正在异步执行其 init 办法。

这个类的外围逻辑就是把能够异步执行、然而还没有执行 init 办法的 bean,把它的 init 办法扔到线程池外面去执行:

看一下在下面的 invoke 办法中的 if 办法:

if (!isAsyncCalled && methodName.equals(asyncMethodName))

isAsyncCalled,首先判断是否曾经异步执行过这个 bean 的 init 办法了。

而后看看 methodName.equals(asyncMethodName),要反射调用的办法是否是之前在 map 中保护的 init 办法。

如果都满足,就扔到线程池外面去执行,这样就算是实现了异步 init。

如果不满足呢?

首先,你想想不满足的时候阐明什么状况?

是不是阐明一个 Bean 的 init 办法在我的项目启动过程中不只被调用一次。

就像是这样:

尽管,我不晓得为什么一个 Bean 要执行两次 init 办法,大概率是代码写的有问题。

然而我不说,我也不给你抛出异样,我反正就是给你兼容了。

所以,这段代码就是在解决这个状况:

如果发现有屡次调用,那么只有第一次异步初始化实现了,即 isAsyncCalling 为 false,你能够继续执行反射调用初始化办法的动作。

这个 invoke 办法的逻辑就是这样,次要是有一个线程池在外面。

那么这个线程池是哪里来的呢?

com.alipay.sofa.runtime.spring.async.AsyncTaskExecutor

在第一次 submit 工作的时候,框架会帮咱们初始化一个线程池进去。

而后通过这个线程池帮咱们实现异步初始化的指标。

所以你想想,整个过程是十分清晰的。首先找进去哪些 Bean 上标注了 @SofaAsyncInit 注解,找个 Map 保护起来,接着搞个 AOP 切面,看看哪些 Bean 能在 Map 外面找到,在线程池外面通过动静代理,调用其 init 办法。

就完了。

对不对?

好,那么问题就来了?

为什么我不间接在 init 办法外面搞个线程池呢,就像是这样。

先注入一个自定义线程池,同时正文掉 @SofaAsyncInit 注解:

在指定 Bean 的 init 办法中应用该线程池:

这也不也是能达到“异步初始化”的目标吗?

你说对不对?

不对啊,对个锤子对。

你看启动日志:

服务曾经启动实现了,然而 4s 之后,Bean 的 init 办法才执行结束。

在这期间,如果有申请要应用对应的 Bean 怎么办?

拿着一个还未执行实现 init 办法的 Bean 框框一顿用,这画面想想就很美。

所以怎么办?

我也不晓得,看一下 SOFABoot 外面是怎么解决这个问题的。

在咱们后面提到的线程池外面,有这样的一个办法:

com.example.asyncthreadpool.spring.AsyncTaskExecutor#ensureAsyncTasksFinish

在这个办法外面,调用了 future 的 get 办法进行阻塞期待。当所有的 future 执行实现之后,会敞开线程池。

这个 FUTURES 是什么玩意,怎么来的?

它就是执行 submitTask 办法时,保护进行去的,外面装的就是一个个异步执行的 init 办法:

所以它通过这个办法能够确保能感知到所有的通过这个线程池执行的 init 办法都执行结束。

当初,办法有了,你先思考一下,咱们什么时候触发这个办法的调用呢?

是不是应该在 Spring 容器通知你:小老弟,我这边所有的 Bean 都搞定了,你这边啥状况了?

这个时候你就须要调用一下这个办法。

而 Spring 容器加载实现之后,会公布这样的一个事件。也就是它:

所以,SOFABoot 的做法就是监听这个事件:

com.example.asyncthreadpool.spring.AsyncTaskExecutionListener

这样,即可确保在异步线程中执行的 init 办法的 Bean 执行实现之后,容器才算启动胜利,对外提供服务。

到这里,原理局部我算是讲完了。

然而写到这里的时候,我忽然冒出了一个写之前没有过的想法:在整个实现的过程中,最要害的有两个货色:

  1. 一个 Map:外面保护的是所有能够异步执行 init 办法的 Bean 和其对应的 init 办法。
  2. 一个线程池:异步执行 init 办法。

而这个 Map 是怎么来的?

不是通过扫描 @SofaAsyncInit 注解失去的吗?

那么扫描进去的 @SofaAsyncInit 怎么来的?

不就是我写代码的时候被动标注下来的吗?

所以,咱们是不是能够齐全不必 Map,间接应用异步线程池:

剩去中间环节,间接一步到位,只须要留下两个类即可:

我这里把这个两个类贴出来。

AsyncTaskExecutionListener:

public class AsyncTaskExecutionListener implements PriorityOrdered,
                                       ApplicationListener<ContextRefreshedEvent>,
                                       ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {if (applicationContext.equals(event.getApplicationContext())) {AsyncTaskExecutor.ensureAsyncTasksFinish();
        }
    }

    @Override
    public int getOrder() {return Ordered.HIGHEST_PRECEDENCE + 1;}

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;}
}

AsyncTaskExecutor:

@Slf4j
public class AsyncTaskExecutor {protected static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    protected static final AtomicReference<ThreadPoolExecutor> THREAD_POOL_REF = new AtomicReference<ThreadPoolExecutor>();

    protected static final List<Future> FUTURES = new ArrayList<>();

    public static Future submitTask(Runnable runnable) {if (THREAD_POOL_REF.get() == null) {ThreadPoolExecutor threadPoolExecutor = createThreadPoolExecutor();
            boolean success = THREAD_POOL_REF.compareAndSet(null, threadPoolExecutor);
            if (!success) {threadPoolExecutor.shutdown();
            }
        }
        Future future = THREAD_POOL_REF.get().submit(runnable);
        FUTURES.add(future);
        return future;
    }

    private static ThreadPoolExecutor createThreadPoolExecutor() {
        int threadPoolCoreSize = CPU_COUNT + 1;
        int threadPoolMaxSize = CPU_COUNT + 1;
        log.info(String.format(
                "create why-async-init-bean thread pool, corePoolSize: %d, maxPoolSize: %d.",
                threadPoolCoreSize, threadPoolMaxSize));
        return new ThreadPoolExecutor(threadPoolCoreSize, threadPoolMaxSize, 30,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new ThreadPoolExecutor.CallerRunsPolicy());
    }

    public static void ensureAsyncTasksFinish() {for (Future future : FUTURES) {
            try {future.get();
            } catch (Throwable e) {throw new RuntimeException(e);
            }
        }

        FUTURES.clear();
        if (THREAD_POOL_REF.get() != null) {THREAD_POOL_REF.get().shutdown();
            THREAD_POOL_REF.set(null);
        }
    }
}

你只须要把这两个类, 一共 68 行代码,粘到你的我的项目中,而后把 AsyncTaskExecutionListener 以 @Bean 的形式注入:

@Bean
public AsyncTaskExecutionListener asyncTaskExecutionListener() {return new AsyncTaskExecutionListener();
}

祝贺你,你我的项目中的 Bean 也能够异步执行 init 办法了,应用办法就像这样式儿的:

然而,如果你要比照这两种写的法的话:

必定是选注解嘛,优雅的一比。

所以,我当初问你一个问题:清理聊聊异步初始化 Bean 的思路。

而后在诘问你一个问题:如果通过自定义注解的形式实现?须要用到 Spring 的那些扩大点?

还思考个毛啊,不就是这个过程吗?

回忆一下后面的内容,是不是品出点滋味了,是不是有点感觉了,是不是感觉本人又行了?

其实说真的,这个计划,当须要人来被动标识哪些 Bean 是能够异步初始化的时候,就曾经“输了”,曾经不够惊艳了。

然而,你想想本文只是想教你“异步初始化”这个点吗?

不是的,只是以“异步初始化”为抓手,试图教你一种源码解读的办法,找到撕开 Spring 框架的又一个口子,这才是重要的。

最初,前两天阿里开发者公众号也公布了一篇叫《Bean 异步初始化,让你的利用启动飞起来》的文章,想要达成的目标一样,然而最终的落地计划能够说差距很大。这篇文章没有具体的源码,然而也能够比照着看一下,舍短取长,死记硬背。

行了,我就带你走到这了,我只是给你指个路,剩下的路就要你本人走了。

天黑路滑,灯火暗淡,抓住骨干,及时回头。

正文完
 0