共计 4810 个字符,预计需要花费 13 分钟才能阅读完成。
大家好,这期给大家盘一个面试题啊,就是上面的第二题。
这个面试题的图片都被弄的有一点“包浆”了。所以为了你的观感,我还是把第二道题目手打一遍。啧啧啧,这行为,暖男作者实锤了: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 外面,感兴趣,本人去翻,我就是指个路,就不细说了,有趣味本人去翻一翻。