你限号 發表於 2025-6-19 10:42:00

Spring Boot 启动优化实践

<blockquote data-pm-slice="0 0 []">
<p>作者:vivo 互联网服务器团队- Liu Di</p>
<p>&nbsp;</p>
<p>本文系统性分析并优化了一个Spring Boot项目启动耗时高达 280 秒的问题。通过识别瓶颈、优化分库分表加载逻辑、异步初始化耗时任务等手段,最终将启动耗时缩短至 159 秒,提升近 50%。文章涵盖启动流程分析、性能热点识别、异步初始化设计等关键技术细节,适用于大型Spring Boot项目的性能优化参考。</p>
</blockquote>
<p>&nbsp;</p>
<p>文章太长?1分钟看图抓住核心观点👇</p>
<p><img src="https://static001.geekbang.org/infoq/9b/9bf3ab2e240fe20c261e131e0a340164.gif"></p>
<p>&nbsp;</p>
<h1>一、前言</h1>
<p>随着业务的发展,笔者项目对应的Spring Boot工程的依赖越来越多。随着依赖数量的增长,Spring 容器需要加载更多组件、解析复杂依赖并执行自动装配,导致项目启动时间显著增长。在日常开发或测试过程中,一旦因为配置变更或者其他热部署不生效的变更时,项目重启就需要等待很长的时间影响代码的交付。加快Spring项目的启动可以更好的投入项目中,提升开发效率。</p>
<p>&nbsp;</p>
<p>整体环境介绍:</p>
<ul>
<li>
<p>Spring版本:4.3.22</p>
</li>
<li>
<p>Spring Boot版本:1.5.19</p>
</li>
<li>
<p>CPU:i5-9500</p>
</li>
<li>
<p>内存:24GB</p>
</li>
<li>
<p>优化前启动耗时:280秒</p>
</li>
</ul>
<p>&nbsp;</p>
<h1>二、Spring Boot项目启动流程介绍</h1>
<p>Spring Boot项目主要启动流程都在org.spring-</p>
<p>framework.boot.SpringApplication#run(java.lang.String...)方法中:</p>
<p>&nbsp;</p>
<pre class="highlighter-hljs"><code>public&nbsp;ConfigurableApplicationContext&nbsp;run(String... args)&nbsp;{
&nbsp; &nbsp;&nbsp;StopWatch&nbsp;stopWatch&nbsp;=&nbsp;new&nbsp;StopWatch();
&nbsp; &nbsp; stopWatch.start();
&nbsp; &nbsp;&nbsp;// Spring上下文
&nbsp; &nbsp;&nbsp;ConfigurableApplicationContext&nbsp;context&nbsp;=&nbsp;null;
&nbsp; &nbsp;&nbsp;FailureAnalyzers&nbsp;analyzers&nbsp;=&nbsp;null;
&nbsp; &nbsp; configureHeadlessProperty();
&nbsp; &nbsp;&nbsp;// 初始化SpringApplicationRunListener监听器
&nbsp; &nbsp;&nbsp;SpringApplicationRunListeners&nbsp;listeners&nbsp;=&nbsp;getRunListeners(args);
&nbsp; &nbsp; listeners.starting();
&nbsp; &nbsp;&nbsp;try&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ApplicationArguments&nbsp;applicationArguments&nbsp;=&nbsp;new&nbsp;DefaultApplicationArguments(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; args);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 环境准备
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ConfigurableEnvironment&nbsp;environment&nbsp;=&nbsp;prepareEnvironment(listeners,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; applicationArguments);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 打印banner
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Banner&nbsp;printedBanner&nbsp;=&nbsp;printBanner(environment);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 创建上下文
&nbsp; &nbsp; &nbsp; &nbsp; context = createApplicationContext();
&nbsp; &nbsp; &nbsp; &nbsp; analyzers =&nbsp;new&nbsp;FailureAnalyzers(context);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 容器初始化
&nbsp; &nbsp; &nbsp; &nbsp; prepareContext(context, environment, listeners, applicationArguments,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; printedBanner);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 刷新容器内容
&nbsp; &nbsp; &nbsp; &nbsp; refreshContext(context);
&nbsp; &nbsp; &nbsp; &nbsp; afterRefresh(context, applicationArguments);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 结束监听广播
&nbsp; &nbsp; &nbsp; &nbsp; listeners.finished(context,&nbsp;null);
&nbsp; &nbsp; &nbsp; &nbsp; stopWatch.stop();
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(this.logStartupInfo) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;new&nbsp;StartupInfoLogger(this.mainApplicationClass)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .logStarted(getApplicationLog(), stopWatch);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;context;
&nbsp; &nbsp; }&nbsp;catch&nbsp;(Throwable ex) {
&nbsp; &nbsp; &nbsp; &nbsp; handleRunFailure(context, listeners, analyzers, ex);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;throw&nbsp;new&nbsp;IllegalStateException(ex);
&nbsp; &nbsp; }
}</code></pre>
<p>可以看到在启动流程中,监听器应用在了应用的多个生命周期中。并且Spring Boot中也预留了针对listener的扩展点。我们可以借此实现一个自己的扩展点去监听Spring Boot的每个阶段的启动耗时,实现如下:</p>
<pre class="highlighter-hljs"><code>@Slf4j
public&nbsp;class&nbsp;MySpringApplicationRunListener&nbsp;implements&nbsp;SpringApplicationRunListener{
&nbsp; &nbsp;&nbsp;private&nbsp;Long&nbsp;startTime;
&nbsp; &nbsp;&nbsp;public&nbsp;MySpringApplicationRunListener(SpringApplication&nbsp;application,&nbsp;String[] args){
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;@Override
&nbsp; &nbsp;&nbsp;public&nbsp;void&nbsp;starting(){
&nbsp; &nbsp; &nbsp; &nbsp; startTime =&nbsp;System.currentTimeMillis();
&nbsp; &nbsp; &nbsp; &nbsp; log.info("MySpringListener启动开始 {}",&nbsp;LocalTime.now());
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;@Override
&nbsp; &nbsp;&nbsp;public&nbsp;void&nbsp;environmentPrepared(ConfigurableEnvironment environment){
&nbsp; &nbsp; &nbsp; &nbsp; log.info("MySpringListener环境准备 准备耗时:{}毫秒", (System.currentTimeMillis() - startTime));
&nbsp; &nbsp; &nbsp; &nbsp; startTime =&nbsp;System.currentTimeMillis();
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;@Override
&nbsp; &nbsp;&nbsp;public&nbsp;void&nbsp;contextPrepared(ConfigurableApplicationContext context){
&nbsp; &nbsp; &nbsp; &nbsp; log.info("MySpringListener上下文准备 耗时:{}毫秒", (System.currentTimeMillis() - startTime));
&nbsp; &nbsp; &nbsp; &nbsp; startTime =&nbsp;System.currentTimeMillis();
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;@Override
&nbsp; &nbsp;&nbsp;public&nbsp;void&nbsp;contextLoaded(ConfigurableApplicationContext context){
&nbsp; &nbsp; &nbsp; &nbsp; log.info("MySpringListener上下文载入 耗时:{}毫秒", (System.currentTimeMillis() - startTime));
&nbsp; &nbsp; &nbsp; &nbsp; startTime =&nbsp;System.currentTimeMillis();
&nbsp; &nbsp; }
&nbsp; &nbsp;@Override
&nbsp; &nbsp;public&nbsp;void&nbsp;finished(ConfigurableApplicationContext context, Throwable exception){
&nbsp; &nbsp; &nbsp; &nbsp; log.info("MySpringListener结束 耗时:{}毫秒", (System.currentTimeMillis() - startTime));
&nbsp; &nbsp; &nbsp; &nbsp; startTime =&nbsp;System.currentTimeMillis();
&nbsp; &nbsp; }
}</code></pre>
<p>接着还需要在classpath/META-INF目录下新建spring.factories文件,并添加如下文件内容:</p>
<pre class="highlighter-hljs"><code>org.springframework.boot.SpringApplicationRunListener=com.vivo.internet.gameactivity.api.web.MySpringApplicationRunListener</code></pre>
<p>至此,借助Listener机制,我们能够追踪Spring Boot启动各阶段的耗时分布,为后续性能优化提供数据支撑。</p>
<p><img src="https://static001.geekbang.org/infoq/09/094cf87fbe19b17dd88b51b795a33ade.png"></p>
<p>&nbsp;</p>
<p>contextLoaded事件是在run方法中的prepareContext()结束时调用的,因此contextLoaded事件和finished事件之间仅存在两个语句:refreshContext(context)和afterRefresh</p>
<p>(context,applicationArguements)消耗了285秒的时间,调试一下就能发现主要耗时在refreshContext()中。</p>
<p>&nbsp;</p>
<h1>三、AbstractApplicationContext#refresh</h1>
<p>refreshContext()最终调用到org.spring-framework.context.support.AbstractApplicationContext#refresh方法中,这个方法主要是beanFactory的预准备、对beanFactory完成创建并进行后置处理、向容器添加bean并且给bean添加属性、实例化所有bean。通过调试发现,finishBeanFactoryInitialization(beanFactory) 方法耗时最久。该方法负责实例化容器中所有的单例 Bean,是启动性能的关键影响点。</p>
<p>&nbsp;</p>
<h1>四、找出实例化耗时的Bean</h1>
<p>Spring Boot也是利用的Spring的加载流程。在Spring中可以实现InstantiationAwareBeanPost-</p>
<p>Processor接口去在Bean的实例化和初始化的过程中加入扩展点。因此我们可以实现该接口并添加自己的扩展点找到处理耗时的Bean。</p>
<p>&nbsp;</p>
<pre class="highlighter-hljs"><code>@Service
public&nbsp;class&nbsp;TimeCostCalBeanPostProcessor&nbsp;implements&nbsp;InstantiationAwareBeanPostProcessor&nbsp;{
&nbsp; &nbsp;&nbsp;private&nbsp;Map&lt;String,&nbsp;Long&gt; costMap =&nbsp;Maps.newConcurrentMap();

&nbsp; &nbsp;&nbsp;@Override
&nbsp; &nbsp;&nbsp;public&nbsp;Object&nbsp;postProcessBeforeInstantiation(Class&lt;?&gt; beanClass,&nbsp;String&nbsp;beanName) throws&nbsp;BeansException&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(!costMap.containsKey(beanName)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; costMap.put(beanName,&nbsp;System.currentTimeMillis());
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;null;
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;@Override
&nbsp; &nbsp;&nbsp;public&nbsp;boolean&nbsp;postProcessAfterInstantiation(Object&nbsp;bean,&nbsp;String&nbsp;beanName) throws&nbsp;BeansException&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;true;
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;@Override
&nbsp; &nbsp;&nbsp;public&nbsp;PropertyValues&nbsp;postProcessPropertyValues(PropertyValues&nbsp;pvs,&nbsp;PropertyDescriptor[] pds,&nbsp;Object&nbsp;bean,&nbsp;String&nbsp;beanName) throws&nbsp;BeansException&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;pvs;
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;@Override
&nbsp; &nbsp;&nbsp;public&nbsp;Object&nbsp;postProcessBeforeInitialization(Object&nbsp;bean,&nbsp;String&nbsp;beanName) throws&nbsp;BeansException&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;bean;
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;@Override
&nbsp; &nbsp;&nbsp;public&nbsp;Object&nbsp;postProcessAfterInitialization(Object&nbsp;bean,&nbsp;String&nbsp;beanName) throws&nbsp;BeansException&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if&nbsp;(costMap.containsKey(beanName)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Long&nbsp;start = costMap.get(beanName);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; long cost =&nbsp;System.currentTimeMillis() - start;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 只打印耗时长的bean
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if&nbsp;(cost &gt;&nbsp;5000) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;System.out.println("bean: "&nbsp;+ beanName +&nbsp;"\ttime: "&nbsp;+ cost +&nbsp;"ms");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return&nbsp;bean;
&nbsp; &nbsp; }
}</code></pre>
<p>具体原理就是在Bean开始实例化之前记录时间,在Bean初始化完成后记录结束时间,打印实例化到初始化的时间差获得Bean的加载总体耗时。结果如图:</p>
<p><img src="https://static001.geekbang.org/infoq/7a/7ae8b2f9e64ff3ee5b6af0028c939af3.png"></p>
<p>&nbsp;</p>
<p>可以看到有许多耗时在10秒以上的类,接下来可以针对性的做优化。值得注意的是,统计方式为单点耗时计算,未考虑依赖链上下文对整体加载顺序的影响,实际优化还需结合依赖关系分析。</p>
<p>&nbsp;</p>
<h1>五、singletonDataSource</h1>
<p>&nbsp;</p>
<pre class="highlighter-hljs"><code>@Bean(name = "singletonDataSource")
public&nbsp;DataSource&nbsp;singletonDataSource(DefaultDataSourceWrapper dataSourceWrapper)&nbsp;throws&nbsp;SQLException {
&nbsp; &nbsp;&nbsp;//先初始化连接
&nbsp; &nbsp; dataSourceWrapper.getMaster().init();
&nbsp; &nbsp;&nbsp;//构建分库分表数据源
&nbsp; &nbsp;&nbsp;String&nbsp;dataSource0&nbsp;=&nbsp;"ds0";
&nbsp; &nbsp; Map&lt;String, DataSource&gt; dataSourceMap =&nbsp;new&nbsp;HashMap&lt;&gt;();
&nbsp; &nbsp; dataSourceMap.put(dataSource0, dataSourceWrapper.getMaster());
&nbsp; &nbsp;&nbsp;//分库分表数据源
&nbsp; &nbsp;&nbsp;DataSource&nbsp;shardingDataSource&nbsp;=&nbsp;ShardingDataSourceFactory.createDataSource
&nbsp; &nbsp; (dataSourceMap,shardingRuleConfiguration, prop);
&nbsp; &nbsp;&nbsp;return&nbsp;shardingDataSource; &nbsp; &nbsp;
&nbsp; &nbsp; }</code></pre>
<p>singletonDataSource是一个分库分表的数据源,连接池采用的是Druid,分库分表组件采用的是公司内部优化后的中间件。通过简单调试代码发现,整个Bean耗时的过程发生在createDataSource方法,该方法中会调用createMetaData方法去获取数据表的元数据,最终运行到loadDefaultTables方法。该方法如下图,会遍历数据库中所有的表。因此数据库中表越多,整体就越耗时。</p>
<p><img src="https://static001.geekbang.org/infoq/93/93315cefdb24e84985d5b094d244874c.png"></p>
<p>笔者的测试环境数据库中有很多的分表,这些分表为了和线上保持一致,分表的数量都和线上是一样的。</p>
<p><img src="https://static001.geekbang.org/infoq/c6/c636b5862ecbd381e03cc6c5e56e18e7.png"></p>
<p>&nbsp;</p>
<p>因此在测试环境启动时,为了加载这些分表会更加的耗时。可通过将分表数量配置化,使测试环境在不影响功能验证的前提下减少分表数量,从而加快启动速度。</p>
<p>&nbsp;</p>
<h1>六、初始化异步</h1>
<p>activityServiceImpl启动中,主要会进行活动信息的查询初始化,这是一个耗时的操作。类似同样的操作在工程的其他类中也存在。</p>
<pre class="highlighter-hljs"><code>@Service
public&nbsp;class&nbsp;ActivityServiceImpl&nbsp;implements&nbsp;ActivityService, InitializingBean{
&nbsp; &nbsp; &nbsp;// 省略无关代码
&nbsp; &nbsp; &nbsp;@Override
&nbsp; &nbsp; &nbsp;public&nbsp;void&nbsp;afterPropertiesSet()&nbsp;throws&nbsp;Exception {
&nbsp; &nbsp; &nbsp; &nbsp; initActivity();
&nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp;// 省略无关代码
}</code></pre>
<p>可以通过将afterPropertiesSet()异步化的方式加速项目的启动。</p>
<p>观察Spring源码可以注意到afterPropertiesSet方法是在AbstractAutowireCapableBeanFactory#</p>
<p>invokeInitMethods中调用的。在这个方法中,不光处理了afterPropertiesSet方法,也处理了init-method。</p>
<p>因此我们可以写一个自己的BeanFactory继承AbstractAutowireCapableBeanFactory,将invokeInitMethods方法进行异步化重写。考虑到AbstractAutowireCapableBeanFactory是个抽象类,有额外的抽象方法需要实现,因此继承该抽象类的子类DefaultListableBeanFactory。具体实现代码如下:</p>
<pre class="highlighter-hljs"><code>public&nbsp;class&nbsp;AsyncInitListableBeanFactory&nbsp;extends&nbsp;DefaultListableBeanFactory{
&nbsp; &nbsp; &nbsp;public&nbsp;AsyncInitBeanFactory(ConfigurableListableBeanFactory beanFactory){
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;super(beanFactory);
&nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp;@Override
&nbsp; &nbsp; &nbsp;protected&nbsp;void&nbsp;invokeInitMethods(String beanName, Object bean, RootBeanDefinition mbd)throws&nbsp;Throwable {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(beanName.equals("activityServiceImpl")) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; AsyncTaskExecutor.submitTask(() -&gt; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;super.invokeInitMethods(beanName, bean, mbd);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;catch&nbsp;(Throwable throwable) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; throwable.printStackTrace();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; });
&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;super.invokeInitMethods(beanName, bean, mbd);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
}</code></pre>
<p>&nbsp;</p>
<p>又因为Spring在refreshContext()方法之前的prepareContext()发放中针对initialize方法提供了接口扩展(applyInitializers())。因此我们可以通过实现该接口并将我们的新的BeanFactory通过反射的方式更新到Spring的初始化流程之前。</p>
<pre class="highlighter-hljs"><code>public&nbsp;interface&nbsp;ApplicationContextInitializer&lt;C&nbsp;extends&nbsp;ConfigurableApplicationContext&gt; {
&nbsp; &nbsp; &nbsp;/**
&nbsp; &nbsp; &nbsp;* Initialize the given application context.
&nbsp; &nbsp; &nbsp;* @param applicationContext the application to configure
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp;&nbsp;void&nbsp;initialize(C applicationContext);

}</code></pre>
<p>&nbsp;</p>
<p>改造后的代码如下,新增AsyncAccelerate-</p>
<p>Initializer类实现ApplicationContextInitializer接口:</p>
<pre class="highlighter-hljs"><code>public&nbsp;class&nbsp;AsyncBeanFactoryInitializer&nbsp;implements&nbsp;ApplicationContextInitializer&lt;ConfigurableApplicationContext&gt; {
&nbsp; &nbsp; @SneakyThrows
&nbsp; &nbsp; @Override
&nbsp; &nbsp;&nbsp;public&nbsp;void&nbsp;initialize(ConfigurableApplicationContext applicationContext){
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(applicationContext instanceof GenericApplicationContext) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; AsyncInitListableBeanFactory beanFactory =&nbsp;new&nbsp;AsyncInitListableBeanFactory(applicationContext.getBeanFactory());
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Field field = GenericApplicationContext.class.getDeclaredField("beanFactory");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; field.setAccessible(true);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; field.set(applicationContext, beanFactory);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
}
public&nbsp;class&nbsp;AsyncBeanInitExecutor{
&nbsp; &nbsp;&nbsp;private&nbsp;static&nbsp;final&nbsp;int&nbsp;CPU_COUNT = Runtime.getRuntime().availableProcessors();
&nbsp; &nbsp;&nbsp;private&nbsp;static&nbsp;final AtomicReference&lt;ThreadPoolExecutor&gt; THREAD_POOL_REF =&nbsp;new&nbsp;AtomicReference&lt;&gt;();
&nbsp; &nbsp;&nbsp;private&nbsp;static&nbsp;final List&lt;Future&lt;?&gt;&gt; FUTURES =&nbsp;new&nbsp;ArrayList&lt;&gt;();
&nbsp; &nbsp; &nbsp;/**
&nbsp; &nbsp; &nbsp; * 创建线程池实例
&nbsp; &nbsp; &nbsp; */
&nbsp; &nbsp; &nbsp;private&nbsp;static&nbsp;ThreadPoolExecutor&nbsp;createThreadPoolExecutor(){
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;int&nbsp;poolSize = CPU_COUNT +&nbsp;1;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return&nbsp;new&nbsp;ThreadPoolExecutor(poolSize, poolSize,&nbsp;50L, TimeUnit.SECONDS,&nbsp;new&nbsp;LinkedBlockingQueue&lt;&gt;(),&nbsp;new&nbsp;ThreadPoolExecutor.CallerRunsPolicy()
&nbsp; &nbsp; &nbsp; &nbsp; );
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;/**
&nbsp; &nbsp; &nbsp;* 确保线程池已初始化(线程安全)
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp; &nbsp;private&nbsp;static&nbsp;void&nbsp;ensureThreadPoolExists(){
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if&nbsp;(THREAD_POOL_REF.get() !=&nbsp;null) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; ThreadPoolExecutor executor = createThreadPoolExecutor();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if&nbsp;(!THREAD_POOL_REF.compareAndSet(null, executor)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; executor.shutdown();&nbsp;// 另一线程已初始化成功
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;/**
&nbsp; &nbsp; &nbsp;* 提交异步初始化任务
&nbsp; &nbsp; &nbsp;*
&nbsp; &nbsp; &nbsp;* @param task 初始化任务
&nbsp; &nbsp; &nbsp;* @return 提交后的 Future 对象
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp;&nbsp;public&nbsp;static&nbsp;Future&lt;?&gt; submitInitTask(Runnable task) {
&nbsp; &nbsp; &nbsp; &nbsp; ensureThreadPoolExists();
&nbsp; &nbsp; &nbsp; &nbsp; Future&lt;?&gt; future = THREAD_POOL_REF.get().submit(task);
&nbsp; &nbsp; &nbsp; &nbsp; FUTURES.add(future);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;future;
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;/**
&nbsp; &nbsp; &nbsp;* 等待所有初始化任务完成并释放资源
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp;&nbsp;public&nbsp;static&nbsp;void&nbsp;waitForInitTasks(){
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;(Future&lt;?&gt; future : FUTURES) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; future.get();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;catch&nbsp;(Exception ex) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;throw&nbsp;new&nbsp;RuntimeException("Async init task failed", ex);
&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;finally&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; FUTURES.clear();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; shutdownThreadPool();
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp;/**
&nbsp; &nbsp; &nbsp;* 关闭线程池并重置引用
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp; &nbsp;private&nbsp;static&nbsp;void&nbsp;shutdownThreadPool(){
&nbsp; &nbsp; &nbsp; &nbsp; ThreadPoolExecutor executor = THREAD_POOL_REF.getAndSet(null);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if&nbsp;(executor !=&nbsp;null) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; executor.shutdown();
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
}</code></pre>
<p>&nbsp;</p>
<p>实现类后,还需要在META-INF/spring.factories下新增说明org.springframework.context.</p>
<p>ApplicationContextInitializer=com.xxx.AsyncAccelerateInitializer,这样这个类才能真正生效。</p>
<p>&nbsp;</p>
<p>这样异步化以后还有一个点需要注意,如果该初始化方法执行耗时很长,那么会存在Spring容器已经启动完成,但是异步初始化任务没执行完的情况,可能会导致空指针等异常。为了避免这种问题的发生,还要借助于Spring容器启动中finishRefresh()方法,监听对应事件,确保异步任务执行完成之后,再启动容器。</p>
<p>&nbsp;</p>
<pre class="highlighter-hljs"><code>public&nbsp;class&nbsp;AsyncInitCompletionListener&nbsp;implements&nbsp;ApplicationListener&lt;ContextRefreshedEvent&gt;, ApplicationContextAware, PriorityOrdered{
&nbsp; &nbsp;&nbsp;private&nbsp;ApplicationContext currentContext;
&nbsp; &nbsp;&nbsp;@Override
&nbsp; &nbsp;&nbsp;public&nbsp;void&nbsp;setApplicationContext(ApplicationContext applicationContext)throws&nbsp;BeansException {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;this.currentContext = applicationContext;
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;@Override
&nbsp; &nbsp;&nbsp;public&nbsp;void&nbsp;onApplicationEvent(ContextRefreshedEvent event){
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(event.getApplicationContext() == currentContext) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; AsyncBeanInitExecutor.waitForInitTasks();
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;@Override
&nbsp; &nbsp;&nbsp;public&nbsp;int&nbsp;getOrder(){
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;return&nbsp;Ordered.HIGHEST_PRECEDENCE;
&nbsp; &nbsp; }
}</code></pre>
<p>&nbsp;</p>
<h1>七、总结</h1>
<p>启动优化后的项目实际测试结果如下:</p>
<p><img src="https://static001.geekbang.org/infoq/31/311be6fbbe4abdc6780a0767129b649f.png"></p>
<p>通过异步化初始化和分库分表加载优化,项目启动时间从 280 秒缩短至 159 秒,提升约 50%。这对于提升日常开发效率、加快测试与联调流程具有重要意义。</p>

</div>
<div id="MySignature" role="contentinfo">
    分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。<br><br>
来源:https://www.cnblogs.com/vivotech/p/18936065
頁: [1]
查看完整版本: Spring Boot 启动优化实践