學習方法之少廢話:吹牛、裝逼、叫大哥。
作者:A哥(YourBatman)
公眾號:BAT的烏托邦(ID:BAT-utopia)
文末是否有彩蛋:有
前言
各位小夥伴大家好,我是A哥。本文屬總結性文章,對總覽Spring Boot
生命週期很是重要,建議點在看、轉發“造福”更多小夥伴。
我最近不是在寫Spring Cloud深度剖析的相關專欄麼,最近有收到小夥伴發過來一些問題,通過這段時間收集到的反饋,總結了一下有一個問題非常集中:那便是對Spring Boot應用SpringApplication
的生命週期、事件的理解。有句話我不是經常掛嘴邊說的麼,你對Spring Framework有多瞭解決定了你對Spring Boot有多瞭解,你對Spring Boot的瞭解深度又會制約你去了解Spring Cloud,一環扣一環。因此此問題反饋比較集中是在清理之中的~
為何在Spring Boot中生命週期事件機制如此重要?緣由很簡單:Spring Cloud父容器是由該生命週期事件機制來驅動的,而它僅僅是一個典型代表。Spring Cloud構建在Spring Boot之上,它在此基礎上構建並新增了一些“Cloud”功能。應用程式事件ApplicationEvent
以及監聽ApplicationListener
是Spring Framework提供的擴充套件點,Spring Boot對此擴充套件點利用得非常充分和深入,並且還衍生出了非常多“子”事件型別,甚至自成體系。從ApplicationEvent
衍生出來的子事件型別非常多,例如JobExecutionEvent、RSocketServerInitializedEvent、AuditApplicationEvent...
本文並不會對每個子事件分別介紹(也並無必要),而是集中火力主攻Spring Boot最為重要的一套事件機制:SpringApplication生命週期的事件體系。
正文
本文將以SpringApplication
的啟動流程/生命週期各時期發出的Event事件為主線,結合每個生命週期內完成的大事記介紹,真正實現一文讓你總覽Spring Boot的全貌,這對你深入理解Spring Boot,以及整合進Spring Cloud都將非常重要。
為表誠意,本文一開始便把SpringApplication
生命週期事件流程圖附上,然後再精細化講解各個事件的詳情。
話外音:趕時間的小夥伴可以拿圖走人?,但不建議白嫖喲
生命週期事件流程圖
版本說明:
由於不同版本、類路徑下存在不同包時結果會存在差異,不指明版本的文章都是不夠負責任的。因此對導包/版本情況作出如下說明:
Spring Boot
:2.2.2.RELEASE。有且僅匯入spring-boot-starter-web
和spring-boot-starter-actuator
Spring Cloud
:Hoxton.SR1。有且僅匯入spring-cloud-context
(注意:並非spring-cloud-starter,並不含有spring-cloud-commons哦)
總的來說:本例導包是非常非常“乾淨”的,這樣在流程上才更有說服力嘛~
SpringApplicationEvent
它是和SpringApplication
生命週期有關的所有事件的父類,@since 1.0.0。
public abstract class SpringApplicationEvent extends ApplicationEvent {
private final String[] args;
public SpringApplicationEvent(SpringApplication application, String[] args) {
super(application);
this.args = args;
}
public SpringApplication getSpringApplication() {
return (SpringApplication) getSource();
}
public final String[] getArgs() {
return this.args;
}
}
它是抽象類,擴充套件自Spring Framwork的ApplicationEvent
,確保了事件和應用實體SpringApplication
產生關聯(當然還有String[] args
)。它有如下實現子類(7個):
每個事件都代表著SpringApplication不同生命週期所處的位置,下面分別進行講解。
ApplicationStartingEvent:開始啟動中
@since 1.5.0,並非1.0.0就有的哦。不過現在幾乎沒有人用1.5以下的版本了,所以可當它是標準事件。
完成的大事記
SpringApplication
例項已例項化:new SpringApplication(primarySources)
-
它在例項化階段完成了如下幾件“大”事:
- 推斷出應用型別
webApplicationType
、main方法所在類 - 給欄位initializers賦值:拿到SPI方式配置的
ApplicationContextInitializer
上下文初始化器
- 給欄位listeners賦值:拿到SPI方式配置的
ApplicationListener
應用監聽器
- 推斷出應用型別
-
注意:在此階段(早期階段)不要過多地使用它的內部狀態,因為它可能在生命週期的後期被修改(話外音:使用時需謹慎)
-
- 此時,
SpringApplicationRunListener
已例項化:它通過SPI方式指定org.springframework.boot.SpringApplicationRunListener=org.springframework.boot.context.event.EventPublishingRunListener
。- 若你有自己的執行時應用監聽器,使用相同方式配置上即可,均會生效
- 由於
EventPublishingRunListener
已經例項化了,因此在後續的事件傳送中,均能夠觸發對應的監聽器的執行 - 傳送ApplicationStartingEvent事件,觸發對應的監聽器的執行
監聽此事件的監聽器們
預設情況下,有4個監聽器監聽ApplicationStartingEvent
事件:
LoggingApplicationListener
:@since 2.0.0。對日誌系統抽象LoggingSystem
執行例項化以及初始化之前的操作,預設使用的是基於Logback的LogbackLoggingSystem
BackgroundPreinitializer
:啟動一個後臺進行對一些類進行預熱。如ValidationInitializer、JacksonInitializer...
,因為這些元件有第一次懲罰的特點(並且首次初始化均還比較耗時),所以使用後臺執行緒先預熱效果更佳DelegatingApplicationListener
:它監聽的是ApplicationEvent
,而實際上只會ApplicationEnvironmentPreparedEvent
到達時生效,所以此處忽略LiquibaseServiceLocatorApplicationListener
:略
總結:此事件節點結束時,SpringApplication
完成了一些例項化相關的動作:本例項例項化、本例項屬性賦值、日誌系統例項化等。
ApplicationEnvironmentPreparedEvent:環境已準備好
@since 1.0.0。該事件節點是最為重要的一個節點之一,因為對於Spring應用來說,環境抽象Enviroment
簡直太重要了,它是最為基礎的後設資料,決定著程式的構建和走向,所以構建的時機是比較早的。
完成的大事記
- 封裝命令列引數(main方法的args)到
ApplicationArguments
裡面 - 建立出一個環境抽象例項
ConfigurableEnvironment
的實現類,並且填入值:Profiles配置和Properties屬性,預設內容如下(注意,這只是初始狀態,後面還會改變、新增屬性源,實際見最後的截圖):
- 傳送ApplicationEnvironmentPreparedEvent事件,觸發對應的監聽器的執行
- 對環境抽象Enviroment的填值,均是由監聽此事件的監聽器去完成,見下面的監聽器詳解
bindToSpringApplication(environment)
:把環境屬性中spring.main.xxx = xxx
繫結到當前的SpringApplication例項屬性上,如常用的spring.main.allow-bean-definition-overriding=true
會被繫結到當前SpringApplication
例項的對應屬性上
監聽此事件的監聽器們
預設情況下,有9個監聽器監聽ApplicationEnvironmentPreparedEvent
事件:
BootstrapApplicationListener
:來自SC。優先順序最高,用於啟動/建立Spring Cloud的應用上下文。需要注意的是:到此時SB的上下文ApplicationContext
還並沒有建立哦。這個流程“巢狀”特別像Bean初始化流程:初始化Bean A時,遇到了Bean B,就需要先去完成Bean B的初始化,再回頭來繼續完成Bean A的步驟。- 說明:在建立SC的應用的時候,使用的也是
SpringApplication#run()
完成的(非web),因此也會走下一整套SpringApplication的生命週期邏輯,所以請你務必區分。- 特別是這種case會讓“絕大多數”初始化器、監聽器等執行多次,若你有那種只需要執行一次的需求(比如只想讓SB容器生命週期內執行,SC生命週期不執行),請務必自行處理,否則會被執行多次而帶來不可預知的結果
- SC應用上下文讀取的外部化配置檔名預設是:
bootstrap
,使用的也是ConfigFileApplicationListener
完成的載入/解析
- 說明:在建立SC的應用的時候,使用的也是
LoggingSystemShutdownListener
:來自SC。對LogbackLoggingSystem
先清理,再重新初始化一次,效果同上個事件,相當於重新來了一次,畢竟現在有Enviroment環境裡嘛ConfigFileApplicationListener
:@since 1.0.0。它也許是最重要的一個監聽器。做了如下事情:- 載入SPI配置的所有的
EnvironmentPostProcessor
例項,並且排好序。需要注意的是:ConfigFileApplicationListener
也是個EnvironmentPostProcessor
,會參與排序哦
- 排好序後,分別一個個的執行
EnvironmentPostProcessor
(@since 1.3.0,並非一開始就有),介紹如下:SystemEnvironmentPropertySourceEnvironmentPostProcessor
:@since 2.0.0。把SystemEnvironmentPropertySource
替換為其子類OriginAwareSystemEnvironmentPropertySource
(屬性值帶有Origin來源),僅此而已SpringApplicationJsonEnvironmentPostProcessor
:@since 1.3.0。把環境中spring.application.json=xxx
值解析成為一個MapPropertySource
屬性源,然後放進環境裡面去(屬性源的位置是做了處理的,一般不用太關心)- 可以看到,SB是直接支援JSON串配置的哦。Json解析參見:
JsonParser
- 可以看到,SB是直接支援JSON串配置的哦。Json解析參見:
CloudFoundryVcapEnvironmentPostProcessor
:@since 1.3.0。略ConfigFileApplicationListener
:@since 1.0.0(它比EnvironmentPostProcessor先出現的哦)。載入application.properties/yaml
等外部化配置,解析好後放進環境裡(這應該是最為重要的)。- 外部化配置預設的優先順序為:
"classpath:/,classpath:/config/,file:./,file:./config/"
。當前工程下的config目錄裡的application.properties優先順序最高,當前工程類路徑下的application.properties優先順序最低 - 值得強調的是:bootstrap.xxx也是由它負責載入的,處理規則一樣
- 外部化配置預設的優先順序為:
DebugAgentEnvironmentPostProcessor
:@since 2.2.0。處理和reactor
測試相關,略
- 載入SPI配置的所有的
AnsiOutputApplicationListener
:@since 1.2.0。讓你的終端(可以是控制檯、可以是日誌檔案)支援Ansi彩色輸出,使其更具可讀性。當然前提是你的終端支援ANSI顯示。參考類:AnsiOutput
。你可通過spring.output.ansi.enabled = xxx
配置,可選值是:DETECT/ALWAYS/NEVER
,一般不動即可。另外,針對控制檯可以單獨配置:spring.output.ansi.console-available = true/false
LoggingApplicationListener
:@since 2.0.0。根據Enviroment環境完成initialize()
初始化動作:日誌等級、日誌格式模版等- 值得注意的是:它這步相當於在ApplicationStartingEvent事件基礎上進一步完成了初始化(上一步只是例項化)
ClasspathLoggingApplicationListener
:@since 2.0.0。用於把classpath路徑以log.debug()
輸出,略- 值得注意的是:classpath類路徑是有N多個的
Arrays.toString(((URLClassLoader) classLoader).getURLs())
,也就是說每個.jar裡都屬於classpath的範疇 - 當然嘍,你需要注意Spring在處理類路徑時:classpath和classpath*的區別~,這屬於基礎知識
- 值得注意的是:classpath類路徑是有N多個的
BackgroundPreinitializer
:本事件達到時無動作DelegatingApplicationListener
:執行通過外部化配置context.listener.classes = xxx,xxx
的監聽器們,然後把該事件廣播給他們,關心此事件的監聽器執行- 這麼做的好處:可以通過屬性檔案外部化配置監聽器,而不一定必須寫在
spring.factories
裡,更具彈性 - 外部化配置的執行優先順序,還是相對較低的,到這裡才給與執行嘛
- 這麼做的好處:可以通過屬性檔案外部化配置監聽器,而不一定必須寫在
FileEncodingApplicationListener
:檢測當前系統環境的file.encoding和spring.mandatory-file-encoding設定的值是否一樣,如果不一樣則丟擲異常如果不配置spring.mandatory-file-encoding則不檢查
總結:此事件節點結束時,Spring Boot的環境抽象Enviroment已經準備完畢,但此時其上下文ApplicationContext
還沒有建立,但是Spring Cloud的應用上下文(引導上下文)已經全部初始化完畢哦,所以SC管理的外部化配置也應該都進入到了SB裡面。如下圖所示(這是基本上算是Enviroment的最終態了):
小提示:SC配置的優先順序是高於SB管理的外部化配置的。例如針對spring.application.name
這個屬性,若bootstrap裡已配置了值,再在application.yaml裡配置其實就無效了,因此生產上建議不要寫兩處。
ApplicationContextInitializedEvent:上下文已例項化
@since 2.1.0,非常新的一個事件。當SpringApplication的上下文ApplicationContext
準備好後,對單例Bean們例項化之前,傳送此事件。所以此事件又可稱為:contextPrepared
事件。
完成的大事記
-
printBanner(environment)
:列印Banner圖,預設列印的是Spring Boot字樣spring.main.banner-mode = xxx
來控制Banner的輸出,可選值為CONSOLE/LOG/OFF
,一般預設就好- 預設在類路徑下放置一個
banner.txt
檔案,可實現自定義Banner。關於更多自定義方式,如使用圖片、gif等,本處不做過多介紹- 小建議:別花裡胡哨搞個佛祖在那。讓它能自動列印輸出當前應用名,這樣才是最為實用,最高階的(但需要你定製化開發,並且支援可配置,最好對使用者無感,屬於一個common元件)
-
根據是否是web環境、是否是REACTIVE等,用空構造器建立出一個
ConfigurableApplicationContext
上下文例項(因為使用的是空構造器,所以不會立馬“啟動”上下文)- SERVLET -> AnnotationConfigServletWebServerApplicationContext
- REACTIVE -> AnnotationConfigReactiveWebServerApplicationContext
- 非web環境 -> AnnotationConfigApplicationContext(SC應用的容器就是使用的它)
-
既然上下文例項已經有了,那麼就開始對它進行一些引數的設定:
- 首先最重要的便是把已經準備好的環境Enviroment環境設定給它
- 設定些beanNameGenerator、resourceLoader、ConversionService等元件
- 例項化所有的
ApplicationContextInitializer
上下文初始化器,並且排序好後挨個執行它(這個很重要),預設有如下截圖這些初始化器此時要執行:
下面對這些初始化器分別做出簡單介紹:BootstrapApplicationListener.AncestorInitializer
:來自SC。用於把SC容器設定為SB容器的父容器。當然實際操作委託給了此方法:new ParentContextApplicationContextInitializer(this.parent).initialize(context)
去完成BootstrapApplicationListener.DelegatingEnvironmentDecryptApplicationInitializer
:來自SC。代理了下面會提到的EnvironmentDecryptApplicationInitializer
,也就是說在此處就會先執行,用於提前解密Enviroment環境裡面的屬性,如相關URL等PropertySourceBootstrapConfiguration
:來自SC。重要,和配置中心相關,若想自定義配置中心必須瞭解它。主要作用是PropertySourceLocator
屬性源定位器,我會把它放在配置中心章節詳解EnvironmentDecryptApplicationInitializer
:來自SC。屬性源頭通過上面載入回來了,通過它來實現解密- 值得注意的是:它被執行了兩次哦~
DelegatingApplicationContextInitializer
:和上面的DelegatingApplicationListener
功能類似,支援外部化配置context.initializer.classes = xxx,xxx
SharedMetadataReaderFactoryContextInitializer
:略ContextIdApplicationContextInitializer
:@since 1.0.0。設定應用ID ->applicationContext.setId()
。預設取值為spring.application.name
,再為application,再為自動生成ConfigurationWarningsApplicationContextInitializer
:@since 1.2.0。對錯誤的配置進行警告(不會終止程式),以warn()日誌輸出在控制檯。預設內建的只有對包名的檢查:若你掃包含有"org.springframework"/"org"
這種包名就警告- 若你想自定義檢查規則,請實現
Check
介面,然後...
- 若你想自定義檢查規則,請實現
RSocketPortInfoApplicationContextInitializer
:@since 2.2.0。暫略ServerPortInfoApplicationContextInitializer
:@since 2.0.0。將自己作為一個監聽器註冊到上下文ConfigurableApplicationContext
裡,專門用於監聽WebServerInitializedEvent
事件(非SpringApplication的生命週期事件)- 該事件有兩個實現類:
ServletWebServerInitializedEvent
和ReactiveWebServerInitializedEvent
。傳送此事件的時機是WebServer
已啟動完成,所以已經有了監聽的埠號 - 該監聽器做的事有兩個:
"local." + getName(context.getServerNamespace()) + ".port"
作為key(預設值是local.server.port
),value是埠值。這樣可以通過@Value來獲取到本機埠了(但貌似埠寫0的時候,SB在顯示上有個小bug)- 作為一個屬性源
MapPropertySource
放進環境裡,屬性源名稱為:server.ports
(因為一個server是可以監聽多個埠的,所以這裡用複數)
- 該事件有兩個實現類:
ConditionEvaluationReportLoggingListener
:將ConditionEvaluationReport
報告(自動配置中哪些匹配了,哪些沒匹配上)寫入日誌,當然只有LogLevel#DEBUG
時才會輸出(注意:這不是日誌級別哦,應該叫報告級別)。如你配置debug=true
就開啟了此自動配置類報告- 槽點:它明明是個初始化器,為毛命名為Listener?
-
傳送ApplicationContextInitializedEvent事件,觸發對應的監聽器的執行
監聽此事件的監聽器們
預設情況下,有2個監聽器監聽ApplicationContextInitializedEvent
事件:
BackgroundPreinitializer
:本事件達到時無動作DelegatingApplicationListener
:本事件達到時無動作
總結:此事件節點結束時,完成了應用上下文ApplicationContext的準備工作,並且執行所有註冊的上下文初始化器ApplicationContextInitializer
。但是此時,單例Bean是仍舊還沒有初始化,並且WebServer也還沒有啟動
ApplicationPreparedEvent:上下文已準備好
@since 1.0.0。截止到上個事件ApplicationContextInitializedEvent
,應用上下文ApplicationContext充其量叫例項化好了,但是還剩下很重要的事沒做,這便是本週期的內容。
完成的大事記
- 把applicationArguments、printedBanner等都作為一個Bean放進Bean工廠裡(因此你就可以@Autowired注入的哦)
- 比如:有了Banner這個Bean,你可以在你任何想要輸出的地方輸出一個Banner,而不僅僅是啟動時只會輸出一次了
- 若
lazyInitialization = true
延遲初始化,那就向Bean工廠放一個:new LazyInitializationBeanFactoryPostProcessor()
- 該處理器@since 2.2.0。該處理器的作用是:對所有的Bean(通過
LazyInitializationExcludeFilter
介面指定的排除在外)全部.setLazyInit(true);
延遲初始化
- 該處理器@since 2.2.0。該處理器的作用是:對所有的Bean(通過
- 根據primarySources和allSources,交給
BeanDefinitionLoader
(SB提供的實現)實現載入Bean的定義資訊,它支援4種載入方式(4種源):AnnotatedBeanDefinitionReader
-> 基於註解XmlBeanDefinitionReader
-> 基於xml配置GroovyBeanDefinitionReader
-> Groovy檔案ClassPathBeanDefinitionScanner
-> classpath中載入- (不同的源使用了不同的load載入方式)
- 傳送ApplicationPreparedEvent事件,觸發對應的監聽器的執行
監聽此事件的監聽器們
預設情況下,有6個監聽器監聽ApplicationContextInitializedEvent
事件:
CloudFoundryVcapEnvironmentPostProcessor
:略ConfigFileApplicationListener
:向上下文註冊一個new PropertySourceOrderingPostProcessor(context)
。它的作用是:Bean工廠結束後對環境裡的屬性源進行重排序 -> 把名字叫defaultProperties
的屬性源放在最末位- 該屬性源是通過
SpringApplication#setDefaultProperties
API方式放進來的,一般不會使用到,留個印象即可
- 該屬性源是通過
LoggingApplicationListener
:因為這時已經有Bean工廠了嘛,所以它做的事是:向工廠內放入Bean- "springBootLoggingSystem" -> loggingSystem
- "springBootLogFile" -> logFile
- "springBootLoggerGroups" -> loggerGroups
BackgroundPreinitializer
:本事件達到時無動作RestartListener
:SC提供。把當前最新的上下文快取起來而已,目前並未發現有實質性作用,可忽略DelegatingApplicationListener
:本事件達到時無動作
總結:此事件節點結束時,應用上下文ApplicationContext初始化完成,該賦值的賦值了,Bean定義資訊也已全部載入完成。但是,單例Bean還沒有被例項化,web容器依舊還沒啟動。
ApplicationStartedEvent:應用成功啟動
@since 2.0.0。截止到此,應用已經準備就緒,並且通過監聽器、初始化器等完成了非常多的工作了,但仍舊剩下被認為最為重要的初始化單例Bean動作還沒做、web容器(如Tomcat)還沒啟動,這便是這個週期所要做的事。
完成的大事記
- 啟動Spring容器:
AbstractApplicationContext#refresh()
,這個步驟會做很多事,比如會例項化單例Bean- 該步驟屬於Spring Framework的核心內容範疇,做了很多事,請參考Spring核心技術內容章節
- 在Spring容器refresh()啟動完成後,
WebServer
也隨之完成啟動,成功監聽到對應埠(們)
- 輸出啟動成功的日誌:
Started Application in xxx seconds (JVM running for xxx)
- 傳送ApplicationStartedEvent事件,觸發對應的監聽器的執行
callRunners()
:依次執行容器內配置的ApplicationRunner/CommandLineRunner
的Bean實現類,支援sort排序ApplicationRunner
:@since 1.3.0,入參是ApplicationArguments
,先執行(推薦使用)CommandLineRunner
:@since 1.0.0,入參是String... args
,後執行(不推薦使用)
監聽此事件的監聽器們
預設情況下,有3個監聽器監聽ApplicationStartedEvent
事件:
- 前兩個不用再解釋了吧:本事件達到時無動作
TomcatMetricsBinder
:@since 2.1.0。和監控相關:將你的tomcat指標資訊TomcatMetrics
繫結到MeterRegistry
,從而就能收集到相關指標了
總結:此事件節點結束時,SpringApplication
的生命週期到這一步,正常的啟動流程就全部完成了。也就說Spring Boot應用可以正常對對外提供服務了。
ApplicationReadyEvent:應用已準備好
@since 1.3.0。該事件所處的生命週期可認為基本同ApplicationStartedEvent
,僅是在其後執行而已,兩者中間並無其它特別的動作,但是監聽此事件的監聽器們還是蠻重要的。
完成的大事記
同上。
監聽此事件的監聽器們
預設情況下,有4個監聽器監聽ApplicationStartedEvent
事件:
SpringApplicationAdminMXBeanRegistrar
:當此事件到達時,告訴Admin Spring應用已經ready,可以使用啦。- 中間這兩個不用再解釋了吧:本事件達到時無動作
RefreshEventListener
:當此事件到達時,告訴Spring應用已經ready了,接下來便可以執行ContextRefresher.refresh()
嘍
總結:此事件節點結束時,應用已經完完全全的準備好了,並且也已經完成了相關元件的周知工作。
異常情況
SpringApplication
是有可能在啟動的時候失敗(如埠號已被佔用),當然任何一步驟遇到異常時交給SpringApplication#handleRunFailure()
方法來處理,這時候也會有對應的事件發出。
ApplicationFailedEvent:應用啟動失敗
當SpringApplication
在啟動時丟擲異常:可能是埠繫結、也可能是你自定義的監聽器你寫了個bug等,就會“可能”傳送此事件。
完成的大事記
- 得到異常的退出碼ExitCode,然後傳送
ExitCodeEvent
事件(非生命週期事件) - 傳送
ApplicationFailedEvent
事件,觸發對應的監聽器的執行
監聽此事件的監聽器們
預設情況下,有6個監聽器監聽ApplicationStartedEvent
事件:
LoggingApplicationListener
:執行loggingSystem.cleanUp()
清理資源ClasspathLoggingApplicationListener
:輸出一句debug日誌:Application failed to start with classpath: ...
- 中間這兩個不用再解釋了吧:本事件達到時無動作
ConditionEvaluationReportLoggingListener
:自動配置輸出報告,輸出錯誤日誌唄:特別方便你檢視和錯誤定位- 不得不誇:SB對錯誤定位這塊才真叫智慧,比Spring Framework好用太多了
BootstrapApplicationListener.CloseContextOnFailureApplicationListener
:執行context.close()
總結:此事件節點結束時,會做一些釋放資源的操作。一般情況下:我們並不需要監聽到此事件
總結
關於SpringApplication
的生命週期體系的介紹就到這了,相信通過此“萬字長文”你能體會到A哥的用心。翻了翻市面上的相關文章,本文Almost可以保證是總結得最到位的,讓你通過一文便可從大的方面基本掌握Spring Boot,這不管是你使用SB,還是後續自行擴充套件、精雕細琢SB,以及去深入瞭解Spring Cloud均由非常重要的意義,希望對你有幫助,謝謝你的三連。