Spring Boot 啟動優化實踐

本文系統性分析并優化了一個Spring Boot項目啟動耗時高達 280 秒的問題。通過識別瓶頸、優化分庫分表加載邏輯、異步初始化耗時任務等手段,最終將啟動耗時縮短至 159 秒,提升近 50%。文章涵蓋啟動流程分析、性能熱點識別、異步初始化設計等關鍵技術細節,適用于大型Spring Boot項目的性能優化參考。

文章太長?1分鐘看圖抓住核心觀點👇

圖片

一、前言

隨著業務的發展,筆者項目對應的Spring Boot工程的依賴越來越多。隨著依賴數量的增長,Spring 容器需要加載更多組件、解析復雜依賴并執行自動裝配,導致項目啟動時間顯著增長。在日常開發或測試過程中,一旦因為配置變更或者其他熱部署不生效的變更時,項目重啟就需要等待很長的時間影響代碼的交付。加快Spring項目的啟動可以更好的投入項目中,提升開發效率。

整體環境介紹:

  • Spring版本:4.3.22
  • Spring Boot版本:1.5.19
  • CPU:i5-9500
  • 內存:24GB
  • 優化前啟動耗時:280秒

二、Spring Boot項目啟動流程介紹

Spring Boot項目主要啟動流程都在org.spring-

framework.boot.SpringApplication#run(java.lang.String...)方法中:

public?ConfigurableApplicationContext?run(String... args)?{StopWatch?stopWatch?=?new?StopWatch();stopWatch.start();// Spring上下文ConfigurableApplicationContext?context?=?null;FailureAnalyzers?analyzers?=?null;configureHeadlessProperty();// 初始化SpringApplicationRunListener監聽器SpringApplicationRunListeners?listeners?=?getRunListeners(args);listeners.starting();try?{ApplicationArguments?applicationArguments?=?new?DefaultApplicationArguments(args);// 環境準備ConfigurableEnvironment?environment?=?prepareEnvironment(listeners,applicationArguments);// 打印bannerBanner?printedBanner?=?printBanner(environment);// 創建上下文context = createApplicationContext();analyzers =?new?FailureAnalyzers(context);// 容器初始化prepareContext(context, environment, listeners, applicationArguments,printedBanner);// 刷新容器內容refreshContext(context);afterRefresh(context, applicationArguments);// 結束監聽廣播listeners.finished(context,?null);stopWatch.stop();if?(this.logStartupInfo) {new?StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);}return?context;}?catch?(Throwable ex) {handleRunFailure(context, listeners, analyzers, ex);throw?new?IllegalStateException(ex);}
}

可以看到在啟動流程中,監聽器應用在了應用的多個生命周期中。并且Spring Boot中也預留了針對listener的擴展點。我們可以借此實現一個自己的擴展點去監聽Spring Boot的每個階段的啟動耗時,實現如下:

@Slf4j
public?class?MySpringApplicationRunListener?implements?SpringApplicationRunListener{private?Long?startTime;public?MySpringApplicationRunListener(SpringApplication?application,?String[] args){}@Overridepublic?void?starting(){startTime =?System.currentTimeMillis();log.info("MySpringListener啟動開始 {}",?LocalTime.now());}@Overridepublic?void?environmentPrepared(ConfigurableEnvironment environment){log.info("MySpringListener環境準備 準備耗時:{}毫秒", (System.currentTimeMillis() - startTime));startTime =?System.currentTimeMillis();}@Overridepublic?void?contextPrepared(ConfigurableApplicationContext context){log.info("MySpringListener上下文準備 耗時:{}毫秒", (System.currentTimeMillis() - startTime));startTime =?System.currentTimeMillis();}@Overridepublic?void?contextLoaded(ConfigurableApplicationContext context){log.info("MySpringListener上下文載入 耗時:{}毫秒", (System.currentTimeMillis() - startTime));startTime =?System.currentTimeMillis();}@Overridepublic?void?finished(ConfigurableApplicationContext context, Throwable exception){log.info("MySpringListener結束 耗時:{}毫秒", (System.currentTimeMillis() - startTime));startTime =?System.currentTimeMillis();}
}

接著還需要在classpath/META-INF目錄下新建spring.factories文件,并添加如下文件內容:

org.springframework.boot.SpringApplicationRunListener=com.vivo.internet.gameactivity.api.web.MySpringApplicationRunListener

至此,借助Listener機制,我們能夠追蹤Spring Boot啟動各階段的耗時分布,為后續性能優化提供數據支撐。

圖片

contextLoaded事件是在run方法中的prepareContext()結束時調用的,因此contextLoaded事件和finished事件之間僅存在兩個語句:refreshContext(context)和afterRefresh

(context,applicationArguements)消耗了285秒的時間,調試一下就能發現主要耗時在refreshContext()中。

三、AbstractApplicationContext#refresh

refreshContext()最終調用到org.spring-framework.context.support.AbstractApplicationContext#refresh方法中,這個方法主要是beanFactory的預準備、對beanFactory完成創建并進行后置處理、向容器添加bean并且給bean添加屬性、實例化所有bean。通過調試發現,finishBeanFactoryInitialization(beanFactory) 方法耗時最久。該方法負責實例化容器中所有的單例 Bean,是啟動性能的關鍵影響點。

四、找出實例化耗時的Bean

Spring Boot也是利用的Spring的加載流程。在Spring中可以實現InstantiationAwareBeanPost-

Processor接口去在Bean的實例化和初始化的過程中加入擴展點。因此我們可以實現該接口并添加自己的擴展點找到處理耗時的Bean。

@Service
public?class?TimeCostCalBeanPostProcessor?implements?InstantiationAwareBeanPostProcessor?{private?Map<String,?Long> costMap =?Maps.newConcurrentMap();@Overridepublic?Object?postProcessBeforeInstantiation(Class<?> beanClass,?String?beanName) throws?BeansException?{if?(!costMap.containsKey(beanName)) {costMap.put(beanName,?System.currentTimeMillis());}return?null;}@Overridepublic?boolean?postProcessAfterInstantiation(Object?bean,?String?beanName) throws?BeansException?{return?true;}@Overridepublic?PropertyValues?postProcessPropertyValues(PropertyValues?pvs,?PropertyDescriptor[] pds,?Object?bean,?String?beanName) throws?BeansException?{return?pvs;}@Overridepublic?Object?postProcessBeforeInitialization(Object?bean,?String?beanName) throws?BeansException?{return?bean;}@Overridepublic?Object?postProcessAfterInitialization(Object?bean,?String?beanName) throws?BeansException?{if?(costMap.containsKey(beanName)) {Long?start = costMap.get(beanName);long cost =?System.currentTimeMillis() - start;// 只打印耗時長的beanif?(cost >?5000) {System.out.println("bean: "?+ beanName +?"\ttime: "?+ cost +?"ms");}}return?bean;}
}

具體原理就是在Bean開始實例化之前記錄時間,在Bean初始化完成后記錄結束時間,打印實例化到初始化的時間差獲得Bean的加載總體耗時。結果如圖:

圖片

可以看到有許多耗時在10秒以上的類,接下來可以針對性的做優化。值得注意的是,統計方式為單點耗時計算,未考慮依賴鏈上下文對整體加載順序的影響,實際優化還需結合依賴關系分析。

五、singletonDataSource

@Bean(name = "singletonDataSource")
public?DataSource?singletonDataSource(DefaultDataSourceWrapper dataSourceWrapper)?throws?SQLException {//先初始化連接dataSourceWrapper.getMaster().init();//構建分庫分表數據源String?dataSource0?=?"ds0";Map<String, DataSource> dataSourceMap =?new?HashMap<>();dataSourceMap.put(dataSource0, dataSourceWrapper.getMaster());//分庫分表數據源DataSource?shardingDataSource?=?ShardingDataSourceFactory.createDataSource(dataSourceMap,shardingRuleConfiguration, prop);return?shardingDataSource; ? ?}

singletonDataSource是一個分庫分表的數據源,連接池采用的是Druid,分庫分表組件采用的是公司內部優化后的中間件。通過簡單調試代碼發現,整個Bean耗時的過程發生在createDataSource方法,該方法中會調用createMetaData方法去獲取數據表的元數據,最終運行到loadDefaultTables方法。該方法如下圖,會遍歷數據庫中所有的表。因此數據庫中表越多,整體就越耗時。

圖片

筆者的測試環境數據庫中有很多的分表,這些分表為了和線上保持一致,分表的數量都和線上是一樣的。

圖片

因此在測試環境啟動時,為了加載這些分表會更加的耗時。可通過將分表數量配置化,使測試環境在不影響功能驗證的前提下減少分表數量,從而加快啟動速度。

六、初始化異步

activityServiceImpl啟動中,主要會進行活動信息的查詢初始化,這是一個耗時的操作。類似同樣的操作在工程的其他類中也存在。

@Service
public?class?ActivityServiceImpl?implements?ActivityService, InitializingBean{// 省略無關代碼@Overridepublic?void?afterPropertiesSet()?throws?Exception {initActivity();}// 省略無關代碼
}

可以通過將afterPropertiesSet()異步化的方式加速項目的啟動。

觀察Spring源碼可以注意到afterPropertiesSet方法是在AbstractAutowireCapableBeanFactory#

invokeInitMethods中調用的。在這個方法中,不光處理了afterPropertiesSet方法,也處理了init-method。

因此我們可以寫一個自己的BeanFactory繼承AbstractAutowireCapableBeanFactory,將invokeInitMethods方法進行異步化重寫。考慮到AbstractAutowireCapableBeanFactory是個抽象類,有額外的抽象方法需要實現,因此繼承該抽象類的子類DefaultListableBeanFactory。具體實現代碼如下:

public?class?AsyncInitListableBeanFactory?extends?DefaultListableBeanFactory{public?AsyncInitBeanFactory(ConfigurableListableBeanFactory beanFactory){super(beanFactory);}@Overrideprotected?void?invokeInitMethods(String beanName, Object bean, RootBeanDefinition mbd)throws?Throwable {if?(beanName.equals("activityServiceImpl")) {AsyncTaskExecutor.submitTask(() -> {try?{super.invokeInitMethods(beanName, bean, mbd);}?catch?(Throwable throwable) {throwable.printStackTrace();}});}?else?{super.invokeInitMethods(beanName, bean, mbd);}}
}

又因為Spring在refreshContext()方法之前的prepareContext()發放中針對initialize方法提供了接口擴展(applyInitializers())。因此我們可以通過實現該接口并將我們的新的BeanFactory通過反射的方式更新到Spring的初始化流程之前。

public?interface?ApplicationContextInitializer<C?extends?ConfigurableApplicationContext> {/*** Initialize the given application context.* @param applicationContext the application to configure*/void?initialize(C applicationContext);}

改造后的代碼如下,新增AsyncAccelerate-

Initializer類實現ApplicationContextInitializer接口:

public?class?AsyncBeanFactoryInitializer?implements?ApplicationContextInitializer<ConfigurableApplicationContext> {@SneakyThrows@Overridepublic?void?initialize(ConfigurableApplicationContext applicationContext){if?(applicationContext instanceof GenericApplicationContext) {AsyncInitListableBeanFactory beanFactory =?new?AsyncInitListableBeanFactory(applicationContext.getBeanFactory());Field field = GenericApplicationContext.class.getDeclaredField("beanFactory");field.setAccessible(true);field.set(applicationContext, beanFactory);}}
}
public?class?AsyncBeanInitExecutor{private?static?final?int?CPU_COUNT = Runtime.getRuntime().availableProcessors();private?static?final AtomicReference<ThreadPoolExecutor> THREAD_POOL_REF =?new?AtomicReference<>();private?static?final List<Future<?>> FUTURES =?new?ArrayList<>();/*** 創建線程池實例*/private?static?ThreadPoolExecutor?createThreadPoolExecutor(){int?poolSize = CPU_COUNT +?1;return?new?ThreadPoolExecutor(poolSize, poolSize,?50L, TimeUnit.SECONDS,?new?LinkedBlockingQueue<>(),?new?ThreadPoolExecutor.CallerRunsPolicy());}/*** 確保線程池已初始化(線程安全)*/private?static?void?ensureThreadPoolExists(){if?(THREAD_POOL_REF.get() !=?null) {return;}ThreadPoolExecutor executor = createThreadPoolExecutor();if?(!THREAD_POOL_REF.compareAndSet(null, executor)) {executor.shutdown();?// 另一線程已初始化成功}}/*** 提交異步初始化任務** @param task 初始化任務* @return 提交后的 Future 對象*/public?static?Future<?> submitInitTask(Runnable task) {ensureThreadPoolExists();Future<?> future = THREAD_POOL_REF.get().submit(task);FUTURES.add(future);return?future;}/*** 等待所有初始化任務完成并釋放資源*/public?static?void?waitForInitTasks(){try?{for?(Future<?> future : FUTURES) {future.get();}}?catch?(Exception ex) {throw?new?RuntimeException("Async init task failed", ex);}?finally?{FUTURES.clear();shutdownThreadPool();}}/*** 關閉線程池并重置引用*/private?static?void?shutdownThreadPool(){ThreadPoolExecutor executor = THREAD_POOL_REF.getAndSet(null);if?(executor !=?null) {executor.shutdown();}}
}

實現類后,還需要在META-INF/spring.factories下新增說明org.springframework.context.

ApplicationContextInitializer=com.xxx.AsyncAccelerateInitializer,這樣這個類才能真正生效。

這樣異步化以后還有一個點需要注意,如果該初始化方法執行耗時很長,那么會存在Spring容器已經啟動完成,但是異步初始化任務沒執行完的情況,可能會導致空指針等異常。為了避免這種問題的發生,還要借助于Spring容器啟動中finishRefresh()方法,監聽對應事件,確保異步任務執行完成之后,再啟動容器。

public?class?AsyncInitCompletionListener?implements?ApplicationListener<ContextRefreshedEvent>, ApplicationContextAware, PriorityOrdered{private?ApplicationContext currentContext;@Overridepublic?void?setApplicationContext(ApplicationContext applicationContext)throws?BeansException {this.currentContext = applicationContext;}@Overridepublic?void?onApplicationEvent(ContextRefreshedEvent event){if?(event.getApplicationContext() == currentContext) {AsyncBeanInitExecutor.waitForInitTasks();}}@Overridepublic?int?getOrder(){return?Ordered.HIGHEST_PRECEDENCE;}
}

七、總結

啟動優化后的項目實際測試結果如下:

圖片

通過異步化初始化和分庫分表加載優化,項目啟動時間從 280 秒縮短至 159 秒,提升約 50%。這對于提升日常開發效率、加快測試與聯調流程具有重要意義。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/911940.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/911940.shtml
英文地址,請注明出處:http://en.pswp.cn/news/911940.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Jenkins執行Jenkinsfile報錯

遇到部署的步驟時傳輸文件到其他服務器&#xff0c;文件傳上去了&#xff0c;但是命令都沒有執行成功。 寫法&#xff1a; 報錯了&#xff1a;ERROR:Exception when publishing,exception message [Exec exit status not zero.Status [1]] 原因是因為&#xff1a;cd 引用了環…

Modbus TCP轉Profibus DP網接APM810/MCE安科瑞多功能電表通訊案例

Modbus TCP轉Profibus DP網接APM810/MCE安科瑞多功能電表通訊案例 在工業自動化和電力監控領域&#xff0c;Modbus TCP與Profibus DP是兩種常見的通訊協議&#xff0c;它們各自有著廣泛的應用場景和優勢。而當需要將這兩者進行連接&#xff0c;以實現不同設備間的數據傳輸和信…

MySQL常見問題概述

一、MySQL常見問題概述 MySQL是最常用的關系型數據庫&#xff0c;但使用中常會遇到 性能慢、數據丟失、主從不同步、鎖沖突 等問題。這些問題可能導致系統響應變慢、用戶操作失敗&#xff0c;甚至數據損壞。 核心解決思路&#xff1a;先定位問題類型&#xff08;是查詢慢&…

zlmediakit windows 編譯webrtc

1、環境準備 系統環境&#xff1a;Windows 10 專業版 序號名稱版本用途1Microsoft Visual Studio20222openssl3.0.53cmake3.24.04libsrtp2.4.0webrtc播放需要 2、安裝libsrtp https://github.com/cisco/libsrtp/releases/tag/v2.4.2 2.1、新建構建目錄 在libsrtp-2.4.2根目錄…

Redis Pipelining 是性能加速的秘密武器?

在高性能的現代應用中&#xff0c;Redis 因其閃電般的速度而備受青睞。而 Pipelining&#xff08;管道技術&#xff09; 則是 Redis 性能優化的核心功能之一。許多開發者都聽說過它能提升性能&#xff0c;但它究竟是如何做到的&#xff1f;是否會帶來負面影響&#xff1f;今天我…

系統性能優化-6 TCP 三次握手

系統性能優化-6 TCP 三次握手 TCP 三次握手 客戶端優化 客戶端發送 SYN 給服務器 此時客戶端連接狀態&#xff1a;SYN_SENT如果服務器繁忙或中間網絡不暢&#xff0c;客戶端會重發 SYN&#xff0c;重試的次數由 tcp_syn_retries 參數控制&#xff0c;默認是 6 次&#xff0c;第…

WPF 實現自定義彈窗輸入功能

1.前端實現 <Grid><Grid.RowDefinitions><RowDefinition Height"60" /><RowDefinition Height"*" /></Grid.RowDefinitions><BorderGrid.Row"0"BorderBrush"WhiteSmoke"BorderThickness"0.1&qu…

WPF中Converter基礎用法

IValueConverter 1.創建一個類集成接口IValueConverter,并實現 2在xaml中引入 舉例 性別用int來表示&#xff0c;1為男&#xff0c;2為女 核心代碼 創建GenderConverter繼承IValueConverter public class GenderConverter : IValueConverter {//model->view轉換public…

Postgresql的json充當字典應用

一般我們會將一些系統參數放到參數表中&#xff0c;有些參數的值是json結構&#xff0c;那么如何在查詢時引用這些參數&#xff1f;&#xff1f; 比如我在業務表的的xxx_type,或xxx_status記錄的是key,又想在查詢的時候顯示其描述。 先定義字典 如下圖如何應用 Postgresql對j…

Dify全面升級:打造極致智能應用開發體驗,攜手奇墨科技共拓AI新生態

智能應用開發平臺Dify以六大核心功能升級與深度性能優化&#xff0c;重新定義AI開發效率與體驗。本次更新不僅響應了開發者社區的迫切需求&#xff0c;更通過與云計算領域先鋒奇墨科技的戰略合作&#xff0c;為企業提供了從開發到部署的全鏈路智能化解決方案。 .技術領先&#…

關于uniapp開發阻止事件冒泡問題

背景。uniapp開發微信小程序。在使用兩個組件拼接嵌套使用后&#xff0c;發現問題&#xff0c;會誤操作跳轉到更多頁面。下圖中兩個事件若不使用stop修飾符&#xff0c;會相互影響。若點擊uni-list-item會串行觸發uni-card的handledoctorlist方法。 產生上面問題原因是組件之間…

箭頭函數和普通函數的區別?

箭頭函數&#xff08;Arrow Functions&#xff09;和普通函數&#xff08;傳統函數&#xff09;在 JavaScript 中有顯著的區別&#xff0c;主要體現在語法、this 的綁定、構造函數行為、參數處理等方面。以下是詳細對比&#xff1a; 1. 語法差異 普通函數&#xff1a; functio…

Linux系統日志與守護進程開發實戰指南

Linux系統日志與守護進程開發實戰指南 系統日志與守護進程 ├── 系統日志syslog │ ├── 日志路徑: /var/log/syslog │ └── 核心API │ ├── openlog │ ├── syslog │ └── closelog └── 守護進程daemon└── 創建步驟├── um…

Vue.js 過濾器詳解

Vue.js 過濾器詳解 下面我將詳細講解Vue.js中過濾器的語法和使用注意事項&#xff0c;并提供一個完整的演示頁面。 過濾器基本概念 在Vue.js中&#xff0c;過濾器&#xff08;Filters&#xff09; 是用于文本格式化的功能&#xff0c;可以在雙花括號插值和v-bind表達式中使用…

【iOS】iOS崩潰總結

【iOS】iOS崩潰總結 一、前言 之前寫了一篇博文《【Flutter】程序報錯導致的灰屏總結》&#xff0c;瀏覽量、收藏率和點贊量還挺高&#xff0c;還被收錄了&#xff0c;就想著總結一下iOS崩潰&#xff0c;這個也是在iOS面試中經常被問到的。 在 iOS 開發過程中&#xff0c;導致…

機器學習:特征向量與數據維數概念

特征向量與數據維數概念 一、特征向量與維數的定義 特征向量與特征類別 在機器學習和數據處理中&#xff0c;每個樣本通常由多個特征&#xff08;Feature&#xff09; 描述。例如&#xff0c;一張圖片的特征可能包括顏色、形狀、紋理等&#xff1b;一個客戶的特征可能包括年齡…

開發基于Jeston Orin Nx 開發版 16G的實現

一、基本配置 1.配置參數 密碼&#xff1a;yahboom Ubuntu 20.04版本、python3.8、CUDA11.4、cuDNN8.6、TensorRT8.5、Jetpack5.1.1、Opencv4.5.4版本 終端輸入命令&#xff1a;sudo jtop 其中Jetpack是英偉達提供的專門供它自己的嵌入式計算機平臺使用的人工智能包。 終…

【技術分享】XR技術體系淺析:VR、AR與MR的區別、聯系與應用實踐

XR技術體系淺析&#xff1a;VR、AR與MR的區別、聯系與應用實踐 作者&#xff1a;EQ 雪梨蛋花湯 本文是技術分享文檔&#xff0c;淺析VR&#xff08;虛擬現實&#xff09;、AR&#xff08;增強現實&#xff09;、MR&#xff08;混合現實&#xff09;的定義、特性、技術演進路線&…

R語言入門課| 05 一文掌握R語言常見數據類型

視頻教程 大家可以先做一做R語言基礎小測驗&#xff0c;看看自己是否需要跟我們5.5h入門R語言的課程。 先上教程視頻&#xff0c;B站同步播出&#xff1a; https://www.bilibili.com/video/BV1miNVeWEkw 完整視頻回放和答疑服務可見&#xff1a;5.5h入門R語言 本節課程視頻…

vRDMA 發布,助力云上 VPC 內高性能通信

資料來源&#xff1a;火山引擎-開發者社區 近日&#xff0c;火山引擎基于部分云服務器實例規格邀測發布 vRDMA 特性&#xff0c;提供云上 VPC 內大規模 RDMA 加速能力&#xff0c;可兼容傳統 HPC 應用、AI 應用以及傳統 TCP/IP 應用&#xff0c;降低大眾化場景的適配門檻&#…