一. 前言
前段时间体验了几个开源的开发框架 ,发现他们的亮点主要集中在启动快 ,内存低上面。
随之回想 SpringBoot ,发现自己并不能准确的说出 SpringBoot 启动慢的详细原因,所以才有了这篇文章。
来,让我们详细的理解一下 ,SpringBoot 启动这么慢 ,是做了什么?
二. 宏观路线
先来选择一个最简单的 MVC 项目 ,来看一下时间轴 :
- 其中耗时最多的是我标注的 :prepareEnvironment 和 refreshContext
- 这段代码是 Spring 启动类里面的代码 ,这里简单列一下 :
三. 看一下细节
3.1 各环节做了什么这么慢
prepareEnvironment 部分 :
这里我专门做了一些简单的配置 ,可以看到这些对启动的影响微乎其微 (这里先不考虑 Cloud 取配置)
其实可以理解 ,在不获取远程配置的情况下 ,整个过程中无非就是内存里面的处理
如果你近期准备面试跳槽,建议在ddkk.com在线刷题,涵盖 一万+ 道 Java 面试题,几乎覆盖了所有主流技术面试题,还有市面上最全的技术五百套,精品系列教程,免费提供。
来对着源码来看一下 :
refreshContext 部分 :
可以看到 ,refreshContext 部分才是大头 ,这里就是 Bean 加载创建最核心的流程 ,我们一般知道的 doGetBean 和 populateBean 就是在这个环节中进行的 :
最耗时的三个地方 :
BeanFactory后置处理器 : 这个是由于 Spring 做Bean管理的时候 ,大量用到了这类对象
初始化Web容器 : 这能理解 ,创建 Web 容器 ,Tomcat 等处理都是要耗时的
结束初始化单例Bean : 这就是 Bean 创建的主流程 ,当然也会很慢
3.2 空项目如此 ,如果是加上各种依赖的生产级项目呢 ?
生产级项目的分析和上面的是完全不一样的 ,生产级的复杂度远超单一的小项目 ,这也是导致大家认为 : Spring 启动太慢了
。
我贴了一个我这边启动中规中矩的一个项目的处理栈 ,整个大概花了 35s , 来分析一下 :
场景一 : 多容器环境 (反复创建容器)
- 就像 SpringApplication.run() 方法 ,在主应用启动 和 Cloud BootStrap 启动的时候都会分别加载一次
- 也就是说环境会被整个执行多次 ,那么创建不同环境的耗时是很大的
场景二 :配置侧的环境 Listener (资源文件的读取)
这里不能理解为我们通常用的某个Listener 的操作 ,监听个什么东西什么的
这里的环境 Listener 是为了加载 BootStrap 等复杂流程
或者你可以理解为这里是为了开启 Cloud 的配置加载 ,也就是初始 Bootstrap上下文 , 加载外部配置
ApplicationEnvironmentPreparedEvent 有很多很复杂的 Listener 在监听
ConfigFileApplicationListener
:
用于加载外部文件
SpringBootServletInitializer
:
用于 Servlet容器集成
BootstrapApplicationListener
:
初始化 SpringCloud 到的配置
场景三 : 大量的 doRefrsh() Bean 的加载 (重复读资源)
一个10万行代码的小项目 ,反反复复的触发了40多次 refresh() ,要知道这个环节要执行近10项大型处理流程
每一次循环都会触发大量的 Listner 和 PostProcessor 以及 Aware 操作
场景四 :各种 Bean 的加载 (大量 Bean)
这个是一大根源 ,(由于统计时间的逻辑不够严谨,所以Bean处理的时间被分摊到 prepareRefresh 中了)
场景五 : 第三方组件 Client 的创建 (连接第三方)
以 Redis 为例 ,创建 Client 的过程也消耗了大量的时间
同理 MySQL , 因为创建 Client 的时候就会创建连接 ,有的甚至于会创建连接池
四. 贴一下统计的源码
大部分时间都用来写这了 ,感觉还行, 在 IDEA 里面进行 DEBUG 就行 :
S1 : 在需要记录的位置运行断点 Log :StopWatchExpand.start("Bean","开始-创建RedisClient")
S2 : 结束后执行 : StopWatchExpand.start("主线","全部完成"); StopWatchExpand.stop();
总结一下
整个过程中 ,Client 端的连接是最耗时的 ,其次是配置读取 。 也就是外部资源的加载更耗时 。
所以后面看看新版本的时候 ,来看一下他们是怎么解决的 ,以及其他优秀的开源组件又是怎么解决的。
SpringBoot 本身是知道自己过于臃肿的 ,所以在后面的迭代中都有意识的为自己的代码进行瘦身。
先看懂了 SpringBoot2 的慢 ,后面会有一篇来感受一下SpringBoot3 干了什么 ,以及是否真的提升了加载的速度。