我们好,我是歪歪。
这期给我们盘一个面试题啊,便是下面的第二题。
这个面试题的图片都被弄的有一点“包浆”了。
所以为了你的观感,我仍是把第二道标题手打一遍。
啧啧啧,这行为,暖男作者实锤了:
spring 在发动期间会做类扫描,以单例形式放入 ioc。可是 spring 仅仅一个个类进行处理,假如为了加快,咱们取消 spring 自带的类扫描功用,用写代码的多线程办法并行进行处理,这种方案可行吗?为什么?
老实说,我榜首次看到这个面试题的时分,人是懵的。
我知道 Spring 在发动期间会把 bean 放到 ioc 容器中,可是到底是单线程仍是多线程放,我还真不清楚。
所以我做的榜首件工作是去验证标题中这句话:可是 spring 仅仅一个个类进行处理。
怎样去验证呢?
必定是找源码啊,源码之下无秘密啊。
怎样去找呢?
这个就需求你个人的经历积累了,抽丝剥茧的去翻 Spring 源码,这个就不是本文要点了,所以我就不细说了。
可是我能够教你一个我一般用的比较多的奇技淫巧。
首要你必定要搞个 Bean 在项目里边,比如我这儿的 Person:
然后把项目日志等级调整为 debug:
logging.level.root=debug
接着发动项目,在项目里边找 Person 的要害字。
原理便是这是一个 Bean,Spring 在操作它的时分一定会打印相关日志,从日志反向去查找代码,要快的多。
所以通过 Debug 日志,咱们能定位到这样一行要害日志:
Identified candidate component class: xxxx.Person.class]
然后大局查找要害字,就能找到这个当地:
这个当地,便是打榜首个断点的当地。
然后发动项目,从调用仓库往前找,能找到这个当地:
这个类便是我要找的类:
org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan
从源码上看,里边确实没有并发相关的操作,看起来确实是在 for 循环里边单线程一个个处理的 Bean 的。
那么从理论上讲,假如是两个没有任何相关关系的 Bean,比如我下面 Person 和 Student 这两个 Bean,它们在交给 Spring 托管,往 ioc 容器里边放的时分,彻底能够用两个不同的线程处理嘛:
所以问题就来了:
假如为了加快,咱们取消 spring 自带的类扫描功用,用写代码的多线程办法并行进行处理,这样能够吗?
能够吗?
我也不知道啊。
可是我知道去哪里找答案。
可是在找答案之前,我先斗胆的猜一个答案:不能够。
为什么?
由于我看的是 Spring 5.x 版别的源码,在这个版别里边仍是单线程处理 Bean。
关于 Spring 这种运用规划如此之大的开源结构来说,假如能支撑多线程加载的话,必定老早就支撑了。
所以我先盲猜一个:不能够。
找答案
这个问题的答案必定就藏在 Spring 的 issues 里边。
不要问我为什么知道。这是来自老程序员的直觉。
所以我直接便是来到了这儿:
1.2k 个 issue,怎样找到我想要找的呢?
必定是用要害词查找一波。依据现在把握的信息,你说要害词是什么?
必定是咱们前面找到的这个办法、这个类啊,这也是你唯一把握到的信息:
org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan
话不多说,先拿着类名搜一搜,看看啥状况。
从查找结果上看,真的是一搜就中:
我带你看看这个 issue 的具体内容:
github.com/spring-proj…
有个叫做 kyangcmXF 的同学…
呃,我榜首眼看到他的姓名的时分,看到有 F,K 还有 C,榜首瞬间想起的是“疯狂周四”。
那我就叫他“周四”同学吧。
“周四”同学说:我的项目有数以万计的 Bean 要被 Spring 初始化。所以每次项目发动的时分需求好几分钟才干完结工作。
然后他发现 doScan 的代码是单线程,一个一个的去处理 Bean 的。
所以他提出了一个问题:我是不是能够用 ConcurrentHashMap 来替代 Set 数据结果,然后并发加载。
他的问题和咱们文章开头提出的面试题能够说是如出一辙。而他乃至还给出了完成的代码:
然后这个 issue 下只要一个回复,是这样的:
首要,咱们先看看这条回复的人是谁:
他便是 Spring 的 Contributors,他的答复能够说便是官方答复了。
他给“周四”同学说:thanks 老铁,but not possible。
but post-processing bean definitions asynchronously is not possible at the moment.
现在不可能异步的对 bean 进行后置处理。
到这儿,咱们至少知道了,想用异步加载的办法确实是在完成上有困难,不仅仅是简略的单线程改多线程。
然后,这个老哥给“周四”同学指了条路,说假如你想要进一步了解的话,能够看看编号为 13410 的 issue。
虽然咱们现在已经有一个答案了,可是已然大佬指路了,那我必定高低得带你去瞅上一眼。
还得从11年前说起
依据大佬指路的方向,我点开这个 issue 的时分都震动了:
github.com/spring-proj…
标题翻译过来是“在发动期间并行的处理 Bean 的初始化”,紧扣咱们的面试题。
让我震动的主要是这个 issue 的创建时刻:2011 年 10 月 12 号。
好家伙,本来 11 年前我们就提出了这个问题并进行了讨论。
可是依据我多年在 github 上冲浪的经历,遇到这种“年久失修”的 issue 不能自始至终的看,得反着来,得先看最终一个回复是什么时分。
所以我直接便是一个拉到最终,没想到最终一个回复还挺新鲜,是三个月前:
答复的这个哥们,也是 Spring 的官方人员,所以能够了解针对这个问题的官方答复:
这个哥们说了很长一段,我简略的翻译一下:
他说这个问题在最新的 6.0 版别中也不会被处理,由于它现在的优先级并不是特别高。
在处理真正的发动事例时,咱们经常发现,时刻都花在少量几个相互依靠的特定 bean 上。在那里引进并行化,在很多状况下并不能节约多少,由于这并不能加快要害途径。这一般与 ORM 设置和数据库迁移有关。
你也能够运用“运用程序发动跟踪功用”(application startup tracking)为自己的运用程序搜集更多这方面的信息:能够看到发动时刻花在哪里以及是怎样花的,以及并行化是否会改善这种状况。
关于 Spring Framework 6.0,咱们正专心于本地用例的 Ahead Of Time 功用,以及发动时刻的改善。
到这儿,就再次证明了官方关于并行化处理 bean 的情绪:
可是这个哥们的答复中倒没有说“这个功用做不了”,他说的是“经过调研,这个功用完成后的收益并不大”。
并且他还透露了一个要害的信息,针对 Spring 发动速度,在 6.0 里边的方向是 AOT。
其这也不算透露,早在 2020 年,乃至更早,我记住 Spring 就说过今后的努力方向是 AOT,提早编译(Ahead-of-Time Compilation)。
假如你关于 AOT 很生疏的话,能够去了解一下,不是本文要点,提一下就行。
接下来,关于这个 11 年前的帖子,里边的内容仍是比较多,我只能带你简略阅读一下帖子,假如你想要了解细节的话,还得自己去看看。
首要,提出这个问题的人其实已经提出了自己的处理之道:
中心想法仍是在 Bean 初始化的时分引进线程池,然后并发初始化 Bean。仅仅需求特别考虑的是存在循环依靠的 Bean。
然后官方立马就站出来对线了:
小老弟,虽然从代码上看,在 Spring 容器中引进并发的 Bean 初始化看起来是直截了当的办法,但在完成起来并非看起来这么简略。重要的是咱们需求看到更多的反应和需求,当我们都在说“Spring 容器的初始化从根本上说太慢了”,咱们才会认真考虑这种改变。
接着有个老哥跳出来说:我这边有个运用发动花了 2 小时 30 分…
官方针对这个时长也表示很震动:
可是他们的中心观念仍是:在 Spring 容器中并行化 Bean 初始化的好处关于少量运用 Spring 的运用程序来说是非常重要的,而害处是不可避免的 Bug、添加的复杂性和意想不到的副作用,这些可能会影响一切运用 Spring 的运用程序,恐怕这不是一个有吸引力的远景。
官方仍是把这个问题定义为”不会修复”,由于假如没有强有力的理由,官方确实不太可能在中心结构中引进这么大的变化。
这个观念也和他的榜首句话很匹配:more pragmatic approach.
more 我们都认识。
approach,也应该是一个比较了解的单词:
那么 pragmatic 是什么意思呢?
这个单词不认识很正常,属于冷僻词,可是你知道的,我写技术文的时分顺便教单词。
pragmatic,翻译过来是“务实的”的意思:
所以“more pragmatic approach”,是啥意思,来跟我大声的读一遍:更务实的办法。
官方的意思是,更务实的办法,便是先找到发动慢的根本原因,而不是把问题甩锅给 Spring,要害是这是中心逻辑,没有强有力的理由,能不动,就别动。
然后期间便是运用者和官方之间的相互扯皮,一向扯到 5 年后,也便是 2016 年 6 月 30 日:
官方重要决定:好吧,把这个问题的优先级提升一下,提升为”Major”任务,保留在 5.0 的积压项目中。
可是…
如同官方这波放了鸽子。
直到 2018 年,网友又不由得了,这个啥进展了呀?
没有回应。
又到了 2019 年,啥进展了啊,我很等待啊:
仍是没有回应。
然后,时刻来到了 2020 年。
三年之后又三年,现在都 9 年了,大佬,啥进展了啊?
斗转星移,白驹过隙,白云苍狗,换了人间。时刻很快,来到了 2021 年。
让咱们共同恭喜这个 issue 已经悬而未决 10 周年了:
最终,便是今年了,7 月 15 日,网友发问:有什么好消息了吗?
官方答:别问了,我鸽了,咋滴吧?
怎样才干快?
在寻觅答案的进程中,我找到了这样的一个项目:
github.com/dsyer/sprin…
这个项目是关于不同版别的 Spring Boot 做了发动时刻上的基准测验。
测验的定论最终都被官方选用了,所以仍是很有权威性的。
整个测验办法和测验进程以及火焰图什么都在链接里边贴了,我就不赘述了。
仅仅把最终的定论搬出来,给我们看看:
我依照自己的了解翻译一下。
首要,假如你要选用下面的办法,你就要抛弃一些功用,所以不是一切的主张都能适用于一切的运用程序。
- 从 Spring Boot web starters 中排除下面这些 Classpath:Hibernate Validator;Jackson(但Spring Boot actuators 依靠于它)。假如你需求JSON渲染,请运用 Gson;Logback:运用slf4j-jdk14替代
- 运用 spring-context-indexer,它不会有很大的帮助,可是有一点点,算一点点。
- 假如能够,别运用 actuators。
- 运用 Spring Boot 2.1 和Spring 5.1 版别。当 2.2 和 5.2 可用时,升级到 2.2 和 5.2 版别
- 用 spring.config.location(命令行参数或 System 属性等)固定 Spring Boot 配置文件的位置。
- 假如你不需求 JMX,就用 spring.jmx.enabled=false 来封闭它(这是 Spring Boot 2.2 的默认值)。
- 把 Bean 设置为 lazy,也便是懒加载。在 Spring Boot 2.2 中有一个配置项 spring.main.lazy-initialization=true 能够用。
- 解压 fat jar 并以明确的 classpath 运行。
- 用 -noverify 运行JVM。也能够考虑 -XX:TieredStopAtLevel=1 。目的是封闭分层编译。
至于每个点背面的原因,答案就藏在前面提到的 issue 里边,感爱好,自己去翻,我便是指个路,就不细说了,有爱好自己去翻一翻。