你好呀,我是歪歪。
前两天在看 SOFABoot 的时分,看到一个让我眼前一亮的东西,来给咱们盘一下。
SOFABoot,你可能不眼熟,可是没联系,本文也不是给你讲这个东西的,你就认为它是 SpringBoot 的变种就行了。
由于有蚂蚁金服背书,所以主要是一些金融类的公司在运用这个结构:
官方介绍是这样的:
SOFABoot 是蚂蚁金服开源的根据 Spring Boot 的研发结构,它在 Spring Boot 的基础上,供给了诸如 Readiness Check,类阻隔,日志空间阻隔等才能。在增强了 Spring Boot 的一起,SOFABoot 供给了让用户能够在 Spring Boot 中非常方便地运用 SOFA 中间件的才能。
上面这些功用都很强壮,可是我主要是分享一下它的这个小功用:
help.aliyun.com/document_de…
这个功用能够让 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 这样的一个注解:
你先甭管这个注解是哪里来的,从这个注解的称号你也知道它是干啥的:异步履行初始化。
这个时分我再发动项目:
从日志中能够看到:
- whyBean 和 maxBean 的 init 办法是由两个不同的线程并行履行的。
- 发动时刻缩短到了 6.049s。
所以 @SofaAsyncInit 这个注解完结了“指定 Bean 的初始化办法完结异步化”。
你想想,假如你有 10 个 Bean,每个 Bean 都需求 1s 的时刻做初始化,总计 10s。
可是这些 Bean 之间其实不需求串行初始化,那么用这个注解,并行只需求 1s,搞定。
到这儿,你算是看到了这样的东西存在,属于“我看到了”。
接下来,咱们进入到“我会用了”这个环节。
怎样来的。
在解读原理之前,我还得告知你这个注解到底是怎样来的。
它属于 SOFABoot 结构里边的注解,首要你得把你的 SpringBoot 修改为 SOFABoot。
这一步参照官方文档中的“快速开端”部分,非常的简略:
www.sofastack.tech/projects/so…
第一步便是把项目中 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,用于标示当前运用的称号
#ApplicationName
spring.application.name=SOFABootDemo
就搞定了,我就完结了一个从 SpringBoot 切换为 SOFABoot 这个大动作。
当然了,我这个是一个 Demo 项目,结构和 pom 依靠都非常简略,所以切换起来也非常容易。假如你的项目比较大的话,可能会遇到一些兼容性的问题。
可是,注意我要说可是了。
你是在学习摸索阶段,Demo 一定要简略,越小越好,越纯洁越好。所以这个切换的动刁难你建立的一个全新的 Demo 项目来说没啥难度,不会遇到任何问题。
这个时分,你就能够运用 @SofaAsyncInit 注解了:
到这儿,祝贺你,会用了。
拿来吧你
不知道你看到这儿是什么感触。
反正关于我来说,假如仅仅是为了让我能运用这个注解,到达异步初始化的意图,要让我从了解的 SpringBoot 修改为听都没听过的 SOFABoot,即使这个结构背面有阿里给它背书,我必定也是不会这么干的。
所以,关于这一类“人有我无”的东西,我都是采纳“拿来吧你”策略。
你想,最开端的我就说了,SOFABoot 是 SpringBoot 的变种,它的底层仍是 SpringBoot。
而 SOFABoot 又是开源的,整个项意图源码我都有了:
github.com/sofastack/s…
从其间剥离出一个根据 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 履行完结之后,容器才算发动成功,对外供给服务。
到这儿,原理部分我算是讲完了。
可是写到这儿的时分,我忽然冒出了一个写之前没有过的主意:在整个完结的进程中,最要害的有两个东西:
- 一个 Map:里边保护的是一切能够异步履行 init 办法的 Bean 和其对应的 init 办法。
- 一个线程池:异步履行 init 办法。
而这个 Map 是怎样来的?
不是经过扫描 @SofaAsyncInit 注解得到的吗?
那么扫描出来的 @SofaAsyncInit 怎样来的?
不便是我写代码的时分自动标示上去的吗?
所以,咱们是不是能够彻底不必 Map ,直接运用异步线程池:
剩去中间环节,直接一步到位,只需求留下两个类即可:
我这儿把这个两个类贴出来。
AsyncTaskExecutionListener:
publicclassAsyncTaskExecutionListenerimplementsPriorityOrdered,
ApplicationListener<ContextRefreshedEvent>,
ApplicationContextAware{
privateApplicationContextapplicationContext;
@Override
publicvoidonApplicationEvent(ContextRefreshedEventevent){
if(applicationContext.equals(event.getApplicationContext())){
AsyncTaskExecutor.ensureAsyncTasksFinish();
}
}
@Override
publicintgetOrder(){
returnOrdered.HIGHEST_PRECEDENCE+1;
}
@Override
publicvoidsetApplicationContext(ApplicationContextapplicationContext)throwsBeansException{
this.applicationContext=applicationContext;
}
}
AsyncTaskExecutor:
@Slf4j
publicclassAsyncTaskExecutor{
protectedstaticfinalintCPU_COUNT=Runtime.getRuntime().availableProcessors();
protectedstaticfinalAtomicReference<ThreadPoolExecutor>THREAD_POOL_REF=newAtomicReference<ThreadPoolExecutor>();
protectedstaticfinalList<Future>FUTURES=newArrayList<>();
publicstaticFuturesubmitTask(Runnablerunnable){
if(THREAD_POOL_REF.get()==null){
ThreadPoolExecutorthreadPoolExecutor=createThreadPoolExecutor();
booleansuccess=THREAD_POOL_REF.compareAndSet(null,threadPoolExecutor);
if(!success){
threadPoolExecutor.shutdown();
}
}
Futurefuture=THREAD_POOL_REF.get().submit(runnable);
FUTURES.add(future);
returnfuture;
}
privatestaticThreadPoolExecutorcreateThreadPoolExecutor(){
intthreadPoolCoreSize=CPU_COUNT+1;
intthreadPoolMaxSize=CPU_COUNT+1;
log.info(String.format(
"createwhy-async-init-beanthreadpool,corePoolSize:%d,maxPoolSize:%d.",
threadPoolCoreSize,threadPoolMaxSize));
returnnewThreadPoolExecutor(threadPoolCoreSize,threadPoolMaxSize,30,
TimeUnit.SECONDS,newLinkedBlockingQueue<>(),newThreadPoolExecutor.CallerRunsPolicy());
}
publicstaticvoidensureAsyncTasksFinish(){
for(Futurefuture:FUTURES){
try{
future.get();
}catch(Throwablee){
thrownewRuntimeException(e);
}
}
FUTURES.clear();
if(THREAD_POOL_REF.get()!=null){
THREAD_POOL_REF.get().shutdown();
THREAD_POOL_REF.set(null);
}
}
}
你只需求把这两个类,总共 68 行代码,粘到你的项目中,然后把 AsyncTaskExecutionListener 以 @Bean 的办法注入:
@Bean
publicAsyncTaskExecutionListenerasyncTaskExecutionListener(){
returnnewAsyncTaskExecutionListener();
}
祝贺你,你项目中的 Bean 也能够异步履行 init 办法了,运用办法就像这款式儿的:
可是,假如你要比照这两种写的法的话:
必定是选注解嘛,高雅的一比。
所以,我现在问你一个问题:清理聊聊异步初始化 Bean 的思路。
然后在诘问你一个问题:假如经过自定义注解的办法完结?需求用到 Spring 的那些扩展点?
还考虑个毛啊,不便是这个进程吗?
回想一下前面的内容,是不是品出点滋味了,是不是有点感觉了,是不是觉得自己又行了?
其实说真的,这个计划,当需求人来自动标识哪些 Bean 是能够异步初始化的时分,就现已“输了”,现已不行冷艳了。
可是,你想想本文只是想教你“异步初始化”这个点吗?
不是的,只是以“异步初始化”为抓手,试图教你一种源码解读的办法,找到扯开 Spring 结构的又一个口子,这才是重要的。
终究,前两天阿里开发者公众号也发布了一篇叫《Bean异步初始化,让你的运用发动飞起来》的文章,想要达成的意图一样,可是终究的落地计划能够说距离很大。这篇文章没有详细的源码,可是也能够比照着看一下,扬长避短,融会贯通。
行了,我就带你走到这了,我只是给你指个路,剩下的路就要你自己走了。
天黑路滑,灯火暗淡,抓住主干,及时回头。