javascript
给你一份长长长的 Spring Boot 知识清单(下)
四、另一件武器:Spring容器的事件監聽機制
過去,事件監聽機制多用于圖形界面編程,比如:點擊按鈕、在文本框輸入內容等操作被稱為事件,而當事件觸發時,應用程序作出一定的響應則表示應用監聽了這個事件,而在服務器端,事件的監聽機制更多的用于異步通知以及監控和異常處理。Java提供了實現事件監聽機制的兩個基礎類:自定義事件類型擴展自java.util.EventObject、事件的監聽器擴展自java.util.EventListener。來看一個簡單的實例:簡單的監控一個方法的耗時。
首先定義事件類型,通常的做法是擴展EventObject,隨著事件的發生,相應的狀態通常都封裝在此類中:
public?class?MethodMonitorEvent?extends?EventObject?{//?時間戳,用于記錄方法開始執行的時間public?long?timestamp;public?MethodMonitorEvent(Object?source)?{super(source);} }事件發布之后,相應的監聽器即可對該類型的事件進行處理,我們可以在方法開始執行之前發布一個begin事件,在方法執行結束之后發布一個end事件,相應地,事件監聽器需要提供方法對這兩種情況下接收到的事件進行處理:
//?1、定義事件監聽接口 public?interface?MethodMonitorEventListener?extends?EventListener?{//?處理方法執行之前發布的事件public?void?onMethodBegin(MethodMonitorEvent?event);//?處理方法結束時發布的事件public?void?onMethodEnd(MethodMonitorEvent?event); } //?2、事件監聽接口的實現:如何處理 public?class?AbstractMethodMonitorEventListener?implements?MethodMonitorEventListener?{@Overridepublic?void?onMethodBegin(MethodMonitorEvent?event)?{//?記錄方法開始執行時的時間event.timestamp?=?System.currentTimeMillis();}@Overridepublic?void?onMethodEnd(MethodMonitorEvent?event)?{//?計算方法耗時long?duration?=?System.currentTimeMillis()?-?event.timestamp;System.out.println("耗時:"?+?duration);} }事件監聽器接口針對不同的事件發布實際提供相應的處理方法定義,最重要的是,其方法只接收MethodMonitorEvent參數,說明這個監聽器類只負責監聽器對應的事件并進行處理。有了事件和監聽器,剩下的就是發布事件,然后讓相應的監聽器監聽并處理。通常情況,我們會有一個事件發布者,它本身作為事件源,在合適的時機,將相應的事件發布給對應的事件監聽器:
public?class?MethodMonitorEventPublisher?{private?List<MethodMonitorEventListener>?listeners?=?new?ArrayList<MethodMonitorEventListener>();public?void?methodMonitor()?{MethodMonitorEvent?eventObject?=?new?MethodMonitorEvent(this);publishEvent("begin",eventObject);//?模擬方法執行:休眠5秒鐘TimeUnit.SECONDS.sleep(5);publishEvent("end",eventObject);}private?void?publishEvent(String?status,MethodMonitorEvent?event)?{//?避免在事件處理期間,監聽器被移除,這里為了安全做一個復制操作List<MethodMonitorEventListener>?copyListeners?=???new?ArrayList<MethodMonitorEventListener>(listeners);for?(MethodMonitorEventListener?listener?:?copyListeners)?{if?("begin".equals(status))?{listener.onMethodBegin(event);}?else?{listener.onMethodEnd(event);}}}public?static?void?main(String[]?args)?{MethodMonitorEventPublisher?publisher?=?new?MethodMonitorEventPublisher();publisher.addEventListener(new?AbstractMethodMonitorEventListener());publisher.methodMonitor();}//?省略實現public?void?addEventListener(MethodMonitorEventListener?listener)?{}public?void?removeEventListener(MethodMonitorEventListener?listener)?{}public?void?removeAllListeners()?{}對于事件發布者(事件源)通常需要關注兩點:
在合適的時機發布事件。此例中的methodMonitor()方法是事件發布的源頭,其在方法執行之前和結束之后兩個時間點發布MethodMonitorEvent事件,每個時間點發布的事件都會傳給相應的監聽器進行處理。在具體實現時需要注意的是,事件發布是順序執行,為了不影響處理性能,事件監聽器的處理邏輯應盡量簡單。
事件監聽器的管理。publisher類中提供了事件監聽器的注冊與移除方法,這樣客戶端可以根據實際情況決定是否需要注冊新的監聽器或者移除某個監聽器。如果這里沒有提供remove方法,那么注冊的監聽器示例將一直被MethodMonitorEventPublisher引用,即使已經廢棄不用了,也依然在發布者的監聽器列表中,這會導致隱性的內存泄漏。
Spring容器內的事件監聽機制
Spring的ApplicationContext容器內部中的所有事件類型均繼承自org.springframework.context.AppliationEvent,容器中的所有監聽器都實現org.springframework.context.ApplicationListener接口,并且以bean的形式注冊在容器中。一旦在容器內發布ApplicationEvent及其子類型的事件,注冊到容器的ApplicationListener就會對這些事件進行處理。
你應該已經猜到是怎么回事了。
ApplicationEvent繼承自EventObject,Spring提供了一些默認的實現,比如:ContextClosedEvent表示容器在即將關閉時發布的事件類型,ContextRefreshedEvent表示容器在初始化或者刷新的時候發布的事件類型……
容器內部使用ApplicationListener作為事件監聽器接口定義,它繼承自EventListener。ApplicationContext容器在啟動時,會自動識別并加載EventListener類型的bean,一旦容器內有事件發布,將通知這些注冊到容器的EventListener。
ApplicationContext接口繼承了ApplicationEventPublisher接口,該接口提供了void publishEvent(ApplicationEvent event)方法定義,不難看出,ApplicationContext容器擔當的就是事件發布者的角色。如果有興趣可以查看AbstractApplicationContext.publishEvent(ApplicationEvent event)方法的源碼:ApplicationContext將事件的發布以及監聽器的管理工作委托給ApplicationEventMulticaster接口的實現類。在容器啟動時,會檢查容器內是否存在名為applicationEventMulticaster的ApplicationEventMulticaster對象實例。如果有就使用其提供的實現,沒有就默認初始化一個SimpleApplicationEventMulticaster作為實現。
最后,如果我們業務需要在容器內部發布事件,只需要為其注入ApplicationEventPublisher依賴即可:實現ApplicationEventPublisherAware接口或者ApplicationContextAware接口(Aware接口相關內容請回顧上文)。
五、出神入化:揭秘自動配置原理
典型的Spring Boot應用的啟動類一般均位于src/main/java根路徑下,比如MoonApplication類:
@SpringBootApplication public?class?MoonApplication?{public?static?void?main(String[]?args)?{SpringApplication.run(MoonApplication.class,?args);} }其中@SpringBootApplication開啟組件掃描和自動配置,而SpringApplication.run則負責啟動引導應用程序。@SpringBootApplication是一個復合Annotation,它將三個有用的注解組合在一起:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters?=?{@Filter(type?=?FilterType.CUSTOM,?classes?=?TypeExcludeFilter.class),@Filter(type?=?FilterType.CUSTOM,?classes?=?AutoConfigurationExcludeFilter.class)?}) public?@interface?SpringBootApplication?{//?...... }@SpringBootConfiguration就是@Configuration,它是Spring框架的注解,標明該類是一個JavaConfig配置類。而@ComponentScan啟用組件掃描,前文已經詳細講解過,這里著重關注@EnableAutoConfiguration。
@EnableAutoConfiguration注解表示開啟Spring Boot自動配置功能,Spring Boot會根據應用的依賴、自定義的bean、classpath下有沒有某個類 等等因素來猜測你需要的bean,然后注冊到IOC容器中。那@EnableAutoConfiguration是如何推算出你的需求?首先看下它的定義:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import(EnableAutoConfigurationImportSelector.class) public?@interface?EnableAutoConfiguration?{//?...... }你的關注點應該在@Import(EnableAutoConfigurationImportSelector.class)上了,前文說過,@Import注解用于導入類,并將這個類作為一個bean的定義注冊到容器中,這里它將把EnableAutoConfigurationImportSelector作為bean注入到容器中,而這個類會將所有符合條件的@Configuration配置都加載到容器中,看看它的代碼:
public?String[]?selectImports(AnnotationMetadata?annotationMetadata)?{//?省略了大部分代碼,保留一句核心代碼//?注意:SpringBoot最近版本中,這句代碼被封裝在一個單獨的方法中//?SpringFactoriesLoader相關知識請參考前文List<String>?factories?=?new?ArrayList<String>(new?LinkedHashSet<String>(??SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class,?this.beanClassLoader))); }這個類會掃描所有的jar包,將所有符合條件的@Configuration配置類注入的容器中,何為符合條件,看看META-INF/spring.factories的文件內容:
//?來自?org.springframework.boot.autoconfigure下的META-INF/spring.factories //?配置的key?=?EnableAutoConfiguration,與代碼中一致 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\ org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\ org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration\ .....以DataSourceAutoConfiguration為例,看看Spring Boot是如何自動配置的:
@Configuration @ConditionalOnClass({?DataSource.class,?EmbeddedDatabaseType.class?}) @EnableConfigurationProperties(DataSourceProperties.class) @Import({?Registrar.class,?DataSourcePoolMetadataProvidersConfiguration.class?}) public?class?DataSourceAutoConfiguration?{ }分別說一說:
-
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }):當Classpath中存在DataSource或者EmbeddedDatabaseType類時才啟用這個配置,否則這個配置將被忽略。
-
@EnableConfigurationProperties(DataSourceProperties.class):將DataSource的默認配置類注入到IOC容器中,DataSourceproperties定義為:
-
@Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class }):導入其他額外的配置,就以DataSourcePoolMetadataProvidersConfiguration為例吧。
DataSourcePoolMetadataProvidersConfiguration是數據庫連接池提供者的一個配置類,即Classpath中存在org.apache.tomcat.jdbc.pool.DataSource.class,則使用tomcat-jdbc連接池,如果Classpath中存在HikariDataSource.class則使用Hikari連接池。
這里僅描述了DataSourceAutoConfiguration的冰山一角,但足以說明Spring Boot如何利用條件話配置來實現自動配置的。回顧一下,@EnableAutoConfiguration中導入了EnableAutoConfigurationImportSelector類,而這個類的selectImports()通過SpringFactoriesLoader得到了大量的配置類,而每一個配置類則根據條件化配置來做出決策,以實現自動配置。
整個流程很清晰,但漏了一個大問題:EnableAutoConfigurationImportSelector.selectImports()是何時執行的?其實這個方法會在容器啟動過程中執行:AbstractApplicationContext.refresh(),更多的細節在下一小節中說明。
六、啟動引導:Spring Boot應用啟動的秘密
6.1 SpringApplication初始化
SpringBoot整個啟動流程分為兩個步驟:初始化一個SpringApplication對象、執行該對象的run方法。看下SpringApplication的初始化流程,SpringApplication的構造方法中調用initialize(Object[] sources)方法,其代碼如下:
private?void?initialize(Object[]?sources)?{if?(sources?!=?null?&&?sources.length?>?0)?{this.sources.addAll(Arrays.asList(sources));}//?判斷是否是Web項目this.webEnvironment?=?deduceWebEnvironment();setInitializers((Collection)?getSpringFactoriesInstances(ApplicationContextInitializer.class));setListeners((Collection)?getSpringFactoriesInstances(ApplicationListener.class));//?找到入口類this.mainApplicationClass?=?deduceMainApplicationClass(); }初始化流程中最重要的就是通過SpringFactoriesLoader找到spring.factories文件中配置的ApplicationContextInitializer和ApplicationListener兩個接口的實現類名稱,以便后期構造相應的實例。ApplicationContextInitializer的主要目的是在ConfigurableApplicationContext做refresh之前,對ConfigurableApplicationContext實例做進一步的設置或處理。ConfigurableApplicationContext繼承自ApplicationContext,其主要提供了對ApplicationContext進行設置的能力。
實現一個ApplicationContextInitializer非常簡單,因為它只有一個方法,但大多數情況下我們沒有必要自定義一個ApplicationContextInitializer,即便是Spring Boot框架,它默認也只是注冊了兩個實現,畢竟Spring的容器已經非常成熟和穩定,你沒有必要來改變它。
而ApplicationListener的目的就沒什么好說的了,它是Spring框架對Java事件監聽機制的一種框架實現,具體內容在前文Spring事件監聽機制這個小節有詳細講解。這里主要說說,如果你想為Spring Boot應用添加監聽器,該如何實現?
Spring Boot提供兩種方式來添加自定義監聽器:
-
通過SpringApplication.addListeners(ApplicationListener… listeners)或者SpringApplication.setListeners(Collection> listeners)兩個方法來添加一個或者多個自定義監聽器
-
既然SpringApplication的初始化流程中已經從spring.factories中獲取到ApplicationListener的實現類,那么我們直接在自己的jar包的META-INF/spring.factories文件中新增配置即可:
關于SpringApplication的初始化,我們就說這么多。
6.2 Spring Boot啟動流程
Spring Boot應用的整個啟動流程都封裝在SpringApplication.run方法中,其整個流程真的是太長太長了,但本質上就是在Spring容器啟動的基礎上做了大量的擴展,按照這個思路來看看源碼:
public?ConfigurableApplicationContext?run(String...?args)?{StopWatch?stopWatch?=?new?StopWatch();stopWatch.start();ConfigurableApplicationContext?context?=?null;FailureAnalyzers?analyzers?=?null;configureHeadlessProperty();//?①SpringApplicationRunListeners?listeners?=?getRunListeners(args);listeners.starting();try?{//?②ApplicationArguments?applicationArguments?=?new?DefaultApplicationArguments(args);ConfigurableEnvironment?environment?=?prepareEnvironment(listeners,applicationArguments);//?③Banner?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();return?context;}catch?(Throwable?ex)?{handleRunFailure(context,?listeners,?analyzers,?ex);throw?new?IllegalStateException(ex);}}① 通過SpringFactoriesLoader查找并加載所有的SpringApplicationRunListeners,通過調用starting()方法通知所有的SpringApplicationRunListeners:應用開始啟動了。SpringApplicationRunListeners其本質上就是一個事件發布者,它在SpringBoot應用啟動的不同時間點發布不同應用事件類型(ApplicationEvent),如果有哪些事件監聽者(ApplicationListener)對這些事件感興趣,則可以接收并且處理。還記得初始化流程中,SpringApplication加載了一系列ApplicationListener嗎?這個啟動流程中沒有發現有發布事件的代碼,其實都已經在SpringApplicationRunListeners這兒實現了。
簡單的分析一下其實現流程,首先看下SpringApplicationRunListener的源碼:
public?interface?SpringApplicationRunListener?{//?運行run方法時立即調用此方法,可以用戶非常早期的初始化工作void?starting();//?Environment準備好后,并且ApplicationContext創建之前調用void?environmentPrepared(ConfigurableEnvironment?environment);//?ApplicationContext創建好后立即調用void?contextPrepared(ConfigurableApplicationContext?context);//?ApplicationContext加載完成,在refresh之前調用void?contextLoaded(ConfigurableApplicationContext?context);//?當run方法結束之前調用void?finished(ConfigurableApplicationContext?context,?Throwable?exception);}SpringApplicationRunListener只有一個實現類:EventPublishingRunListener。①處的代碼只會獲取到一個EventPublishingRunListener的實例,我們來看看starting()方法的內容:
public?void?starting()?{//?發布一個ApplicationStartedEventthis.initialMulticaster.multicastEvent(new?ApplicationStartedEvent(this.application,?this.args)); }順著這個邏輯,你可以在②處的prepareEnvironment()方法的源碼中找到listeners.environmentPrepared(environment);即SpringApplicationRunListener接口的第二個方法,那不出你所料,environmentPrepared()又發布了另外一個事件ApplicationEnvironmentPreparedEvent。接下來會發生什么,就不用我多說了吧。
② 創建并配置當前應用將要使用的Environment,Environment用于描述應用程序當前的運行環境,其抽象了兩個方面的內容:配置文件(profile)和屬性(properties),開發經驗豐富的同學對這兩個東西一定不會陌生:不同的環境(eg:生產環境、預發布環境)可以使用不同的配置文件,而屬性則可以從配置文件、環境變量、命令行參數等來源獲取。因此,當Environment準備好后,在整個應用的任何時候,都可以從Environment中獲取資源。
總結起來,②處的兩句代碼,主要完成以下幾件事:
-
判斷Environment是否存在,不存在就創建(如果是web項目就創建StandardServletEnvironment,否則創建StandardEnvironment)
-
配置Environment:配置profile以及properties
-
調用SpringApplicationRunListener的environmentPrepared()方法,通知事件監聽者:應用的Environment已經準備好
③、SpringBoot應用在啟動時會輸出這樣的東西:
??.???____??????????_????????????__?_?_/\\?/?___'_?__?_?_(_)_?__??__?_?\?\?\?\ (?(?)\___?|?'_?|?'_|?|?'_?\/?_`?|?\?\?\?\\\/??___)|?|_)|?|?|?|?|?||?(_|?|??)?)?)?)'??|____|?.__|_|?|_|_|?|_\__,?|?/?/?/?/=========|_|==============|___/=/_/_/_/::?Spring?Boot?::????????(v1.5.6.RELEASE)如果想把這個東西改成自己的涂鴉,你可以研究以下Banner的實現,這個任務就留給你們吧。
④、根據是否是web項目,來創建不同的ApplicationContext容器。
⑤、創建一系列FailureAnalyzer,創建流程依然是通過SpringFactoriesLoader獲取到所有實現FailureAnalyzer接口的class,然后在創建對應的實例。FailureAnalyzer用于分析故障并提供相關診斷信息。
⑥、初始化ApplicationContext,主要完成以下工作:
-
將準備好的Environment設置給ApplicationContext
-
遍歷調用所有的ApplicationContextInitializer的initialize()方法來對已經創建好的ApplicationContext進行進一步的處理
-
調用SpringApplicationRunListener的contextPrepared()方法,通知所有的監聽者:ApplicationContext已經準備完畢
-
將所有的bean加載到容器中
-
調用SpringApplicationRunListener的contextLoaded()方法,通知所有的監聽者:ApplicationContext已經裝載完畢
⑦、調用ApplicationContext的refresh()方法,完成IoC容器可用的最后一道工序。從名字上理解為刷新容器,那何為刷新?就是插手容器的啟動,聯系一下第一小節的內容。那如何刷新呢?且看下面代碼:
//?摘自refresh()方法中一句代碼 invokeBeanFactoryPostProcessors(beanFactory);看看這個方法的實現:
protected?void?invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory?beanFactory)?{PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory,?getBeanFactoryPostProcessors());...... }獲取到所有的BeanFactoryPostProcessor來對容器做一些額外的操作。BeanFactoryPostProcessor允許我們在容器實例化相應對象之前,對注冊到容器的BeanDefinition所保存的信息做一些額外的操作。這里的getBeanFactoryPostProcessors()方法可以獲取到3個Processor:
ConfigurationWarningsApplicationContextInitializer$ConfigurationWarningsPostProcessor SharedMetadataReaderFactoryContextInitializer$CachingMetadataReaderFactoryPostProcessor ConfigFileApplicationListener$PropertySourceOrderingPostProcessor不是有那么多BeanFactoryPostProcessor的實現類,為什么這兒只有這3個?因為在初始化流程獲取到的各種ApplicationContextInitializer和ApplicationListener中,只有上文3個做了類似于如下操作:
public?void?initialize(ConfigurableApplicationContext?context)?{context.addBeanFactoryPostProcessor(new?ConfigurationWarningsPostProcessor(getChecks())); }然后你就可以進入到PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors()方法了,這個方法除了會遍歷上面的3個BeanFactoryPostProcessor處理外,還會獲取類型為BeanDefinitionRegistryPostProcessor的bean:org.springframework.context.annotation.internalConfigurationAnnotationProcessor,對應的Class為ConfigurationClassPostProcessor。ConfigurationClassPostProcessor用于解析處理各種注解,包括:@Configuration、@ComponentScan、@Import、@PropertySource、@ImportResource、@Bean。當處理@import注解的時候,就會調用<自動配置>這一小節中的EnableAutoConfigurationImportSelector.selectImports()來完成自動配置功能。其他的這里不再多講,如果你有興趣,可以查閱參考資料6。
⑧、查找當前context中是否注冊有CommandLineRunner和ApplicationRunner,如果有則遍歷執行它們。
⑨、執行所有SpringApplicationRunListener的finished()方法。
這就是Spring Boot的整個啟動流程,其核心就是在Spring容器初始化并啟動的基礎上加入各種擴展點,這些擴展點包括:ApplicationContextInitializer、ApplicationListener以及各種BeanFactoryPostProcessor等等。你對整個流程的細節不必太過關注,甚至沒弄明白也沒有關系,你只要理解這些擴展點是在何時如何工作的,能讓它們為你所用即可。
整個啟動流程確實非常復雜,可以查詢參考資料中的部分章節和內容,對照著源碼,多看看,我想最終你都能弄清楚的。言而總之,Spring才是核心,理解清楚Spring容器的啟動流程,那Spring Boot啟動流程就不在話下了。
總結
以上是生活随笔為你收集整理的给你一份长长长的 Spring Boot 知识清单(下)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 转向AIOps之前,你应该做好哪些准备?
- 下一篇: 给你一份长长长的 Spring Boot