javascript
springboot中接口实例化_疫情爆发在家闲出屁的我,梳理一下SpringBoot知识点
在過(guò)去兩三年的Spring生態(tài)圈,最讓人興奮的莫過(guò)于Spring Boot框架。或許從命名上就能看出這個(gè)框架的設(shè)計(jì)初衷:快速的啟動(dòng)Spring應(yīng)用。因而Spring Boot應(yīng)用本質(zhì)上就是一個(gè)基于Spring框架的應(yīng)用,它是Spring對(duì)“約定優(yōu)先于配置”理念的最佳實(shí)踐產(chǎn)物,它能夠幫助開(kāi)發(fā)者更快速高效地構(gòu)建基于Spring生態(tài)圈的應(yīng)用。
那Spring Boot有何魔法?自動(dòng)配置、起步依賴、Actuator、命令行界面(CLI) 是Spring Boot最重要的4大核心特性,其中CLI是Spring Boot的可選特性,雖然它功能強(qiáng)大,但也引入了一套不太常規(guī)的開(kāi)發(fā)模型,因而這個(gè)系列的文章僅關(guān)注其它3種特性。如文章標(biāo)題,本文是這個(gè)系列的第一部分,將為你打開(kāi)Spring Boot的大門,重點(diǎn)為你剖析其啟動(dòng)流程以及自動(dòng)配置實(shí)現(xiàn)原理。要掌握這部分核心內(nèi)容,理解一些Spring框架的基礎(chǔ)知識(shí),將會(huì)讓你事半功倍。
一、拋磚引玉:探索Spring IoC容器
如果有看過(guò) SpringApplication.run()方法的源碼,Spring Boot冗長(zhǎng)無(wú)比的啟動(dòng)流程一定會(huì)讓你抓狂,透過(guò)現(xiàn)象看本質(zhì),SpringApplication只是將一個(gè)典型的Spring應(yīng)用的啟動(dòng)流程進(jìn)行了擴(kuò)展,因此,透徹理解Spring容器是打開(kāi)Spring Boot大門的一把鑰匙。
1.1、Spring IoC容器
可以把Spring IoC容器比作一間餐館,當(dāng)你來(lái)到餐館,通常會(huì)直接招呼服務(wù)員:點(diǎn)菜!至于菜的原料是什么?如何用原料把菜做出來(lái)?可能你根本就不關(guān)心。IoC容器也是一樣,你只需要告訴它需要某個(gè)bean,它就把對(duì)應(yīng)的實(shí)例(instance)扔給你,至于這個(gè)bean是否依賴其他組件,怎樣完成它的初始化,根本就不需要你關(guān)心。
作為餐館,想要做出菜肴,得知道菜的原料和菜譜,同樣地,IoC容器想要管理各個(gè)業(yè)務(wù)對(duì)象以及它們之間的依賴關(guān)系,需要通過(guò)某種途徑來(lái)記錄和管理這些信息。BeanDefinition對(duì)象就承擔(dān)了這個(gè)責(zé)任:容器中的每一個(gè)bean都會(huì)有一個(gè)對(duì)應(yīng)的BeanDefinition實(shí)例,該實(shí)例負(fù)責(zé)保存bean對(duì)象的所有必要信息,包括bean對(duì)象的class類型、是否是抽象類、構(gòu)造方法和參數(shù)、其它屬性等等。當(dāng)客戶端向容器請(qǐng)求相應(yīng)對(duì)象時(shí),容器就會(huì)通過(guò)這些信息為客戶端返回一個(gè)完整可用的bean實(shí)例。
原材料已經(jīng)準(zhǔn)備好(把BeanDefinition看著原料),開(kāi)始做菜吧,等等,你還需要一份菜譜, BeanDefinitionRegistry和 BeanFactory就是這份菜譜,BeanDefinitionRegistry抽象出bean的注冊(cè)邏輯,而BeanFactory則抽象出了bean的管理邏輯,而各個(gè)BeanFactory的實(shí)現(xiàn)類就具體承擔(dān)了bean的注冊(cè)以及管理工作。它們之間的關(guān)系就如下圖:
BeanFactory、BeanDefinitionRegistry關(guān)系圖(來(lái)自:Spring揭秘)
DefaultListableBeanFactory作為一個(gè)比較通用的BeanFactory實(shí)現(xiàn),它同時(shí)也實(shí)現(xiàn)了BeanDefinitionRegistry接口,因此它就承擔(dān)了Bean的注冊(cè)管理工作。從圖中也可以看出,BeanFactory接口中主要包含getBean、containBean、getType、getAliases等管理bean的方法,而BeanDefinitionRegistry接口則包含registerBeanDefinition、removeBeanDefinition、getBeanDefinition等注冊(cè)管理BeanDefinition的方法。
下面通過(guò)一段簡(jiǎn)單的代碼來(lái)模擬BeanFactory底層是如何工作的:
// 默認(rèn)容器實(shí)現(xiàn)DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();// 根據(jù)業(yè)務(wù)對(duì)象構(gòu)造相應(yīng)的BeanDefinitionAbstractBeanDefinition definition = new RootBeanDefinition(Business.class,true);// 將bean定義注冊(cè)到容器中beanRegistry.registerBeanDefinition("beanName",definition);// 如果有多個(gè)bean,還可以指定各個(gè)bean之間的依賴關(guān)系// ........// 然后可以從容器中獲取這個(gè)bean的實(shí)例// 注意:這里的beanRegistry其實(shí)實(shí)現(xiàn)了BeanFactory接口,所以可以強(qiáng)轉(zhuǎn),// 單純的BeanDefinitionRegistry是無(wú)法強(qiáng)制轉(zhuǎn)換到BeanFactory類型的BeanFactory container = (BeanFactory)beanRegistry;Business business = (Business)container.getBean("beanName");
這段代碼僅為了說(shuō)明BeanFactory底層的大致工作流程,實(shí)際情況會(huì)更加復(fù)雜,比如bean之間的依賴關(guān)系可能定義在外部配置文件(XML/Properties)中、也可能是注解方式。Spring IoC容器的整個(gè)工作流程大致可以分為兩個(gè)階段:
①、容器啟動(dòng)階段
容器啟動(dòng)時(shí),會(huì)通過(guò)某種途徑加載 ConfigurationMetaData。除了代碼方式比較直接外,在大部分情況下,容器需要依賴某些工具類,比如:BeanDefinitionReader,BeanDefinitionReader會(huì)對(duì)加載的 ConfigurationMetaData進(jìn)行解析和分析,并將分析后的信息組裝為相應(yīng)的BeanDefinition,最后把這些保存了bean定義的BeanDefinition,注冊(cè)到相應(yīng)的BeanDefinitionRegistry,這樣容器的啟動(dòng)工作就完成了。這個(gè)階段主要完成一些準(zhǔn)備性工作,更側(cè)重于bean對(duì)象管理信息的收集,當(dāng)然一些驗(yàn)證性或者輔助性的工作也在這一階段完成。
來(lái)看一個(gè)簡(jiǎn)單的例子吧,過(guò)往,所有的bean都定義在XML配置文件中,下面的代碼將模擬BeanFactory如何從配置文件中加載bean的定義以及依賴關(guān)系:
// 通常為BeanDefinitionRegistry的實(shí)現(xiàn)類,這里以DeFaultListabeBeanFactory為例BeanDefinitionRegistry beanRegistry = new DefaultListableBeanFactory();// XmlBeanDefinitionReader實(shí)現(xiàn)了BeanDefinitionReader接口,用于解析XML文件XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReaderImpl(beanRegistry);// 加載配置文件beanDefinitionReader.loadBeanDefinitions("classpath:spring-bean.xml");// 從容器中獲取bean實(shí)例BeanFactory container = (BeanFactory)beanRegistry;Business business = (Business)container.getBean("beanName");
②、Bean的實(shí)例化階段
經(jīng)過(guò)第一階段,所有bean定義都通過(guò)BeanDefinition的方式注冊(cè)到BeanDefinitionRegistry中,當(dāng)某個(gè)請(qǐng)求通過(guò)容器的getBean方法請(qǐng)求某個(gè)對(duì)象,或者因?yàn)橐蕾囮P(guān)系容器需要隱式的調(diào)用getBean時(shí),就會(huì)觸發(fā)第二階段的活動(dòng):容器會(huì)首先檢查所請(qǐng)求的對(duì)象之前是否已經(jīng)實(shí)例化完成。如果沒(méi)有,則會(huì)根據(jù)注冊(cè)的BeanDefinition所提供的信息實(shí)例化被請(qǐng)求對(duì)象,并為其注入依賴。當(dāng)該對(duì)象裝配完畢后,容器會(huì)立即將其返回給請(qǐng)求方法使用。
BeanFactory只是Spring IoC容器的一種實(shí)現(xiàn),如果沒(méi)有特殊指定,它采用采用延遲初始化策略:只有當(dāng)訪問(wèn)容器中的某個(gè)對(duì)象時(shí),才對(duì)該對(duì)象進(jìn)行初始化和依賴注入操作。而在實(shí)際場(chǎng)景下,我們更多的使用另外一種類型的容器:ApplicationContext,它構(gòu)建在BeanFactory之上,屬于更高級(jí)的容器,除了具有BeanFactory的所有能力之外,還提供對(duì)事件監(jiān)聽(tīng)機(jī)制以及國(guó)際化的支持等。它管理的bean,在容器啟動(dòng)時(shí)全部完成初始化和依賴注入操作。
1.2、Spring容器擴(kuò)展機(jī)制
IoC容器負(fù)責(zé)管理容器中所有bean的生命周期,而在bean生命周期的不同階段,Spring提供了不同的擴(kuò)展點(diǎn)來(lái)改變bean的命運(yùn)。在容器的啟動(dòng)階段, BeanFactoryPostProcessor允許我們?cè)谌萜鲗?shí)例化相應(yīng)對(duì)象之前,對(duì)注冊(cè)到容器的BeanDefinition所保存的信息做一些額外的操作,比如修改bean定義的某些屬性或者增加其他信息等。
如果要自定義擴(kuò)展類,通常需要實(shí)現(xiàn) org.springframework.beans.factory.config.BeanFactoryPostProcessor接口,與此同時(shí),因?yàn)槿萜髦锌赡苡卸鄠€(gè)BeanFactoryPostProcessor,可能還需要實(shí)現(xiàn) org.springframework.core.Ordered接口,以保證BeanFactoryPostProcessor按照順序執(zhí)行。Spring提供了為數(shù)不多的BeanFactoryPostProcessor實(shí)現(xiàn),我們以 PropertyPlaceholderConfigurer來(lái)說(shuō)明其大致的工作流程。
在Spring項(xiàng)目的XML配置文件中,經(jīng)常可以看到許多配置項(xiàng)的值使用占位符,而將占位符所代表的值單獨(dú)配置到獨(dú)立的properties文件,這樣可以將散落在不同XML文件中的配置集中管理,而且也方便運(yùn)維根據(jù)不同的環(huán)境進(jìn)行配置不同的值。這個(gè)非常實(shí)用的功能就是由PropertyPlaceholderConfigurer負(fù)責(zé)實(shí)現(xiàn)的。
根據(jù)前文,當(dāng)BeanFactory在第一階段加載完所有配置信息時(shí),BeanFactory中保存的對(duì)象的屬性還是以占位符方式存在的,比如 ${jdbc.mysql.url}。當(dāng)PropertyPlaceholderConfigurer作為BeanFactoryPostProcessor被應(yīng)用時(shí),它會(huì)使用properties配置文件中的值來(lái)替換相應(yīng)的BeanDefinition中占位符所表示的屬性值。當(dāng)需要實(shí)例化bean時(shí),bean定義中的屬性值就已經(jīng)被替換成我們配置的值。當(dāng)然其實(shí)現(xiàn)比上面描述的要復(fù)雜一些,這里僅說(shuō)明其大致工作原理,更詳細(xì)的實(shí)現(xiàn)可以參考其源碼。
與之相似的,還有 BeanPostProcessor,其存在于對(duì)象實(shí)例化階段。跟BeanFactoryPostProcessor類似,它會(huì)處理容器內(nèi)所有符合條件并且已經(jīng)實(shí)例化后的對(duì)象。簡(jiǎn)單的對(duì)比,BeanFactoryPostProcessor處理bean的定義,而BeanPostProcessor則處理bean完成實(shí)例化后的對(duì)象。BeanPostProcessor定義了兩個(gè)接口:
public interface BeanPostProcessor { // 前置處理 Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException; // 后置處理 Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;}
為了理解這兩個(gè)方法執(zhí)行的時(shí)機(jī),簡(jiǎn)單的了解下bean的整個(gè)生命周期:
Bean的實(shí)例化過(guò)程(來(lái)自:Spring揭秘)
postProcessBeforeInitialization()方法與 postProcessAfterInitialization()分別對(duì)應(yīng)圖中前置處理和后置處理兩個(gè)步驟將執(zhí)行的方法。這兩個(gè)方法中都傳入了bean對(duì)象實(shí)例的引用,為擴(kuò)展容器的對(duì)象實(shí)例化過(guò)程提供了很大便利,在這兒幾乎可以對(duì)傳入的實(shí)例執(zhí)行任何操作。注解、AOP等功能的實(shí)現(xiàn)均大量使用了 BeanPostProcessor,比如有一個(gè)自定義注解,你完全可以實(shí)現(xiàn)BeanPostProcessor的接口,在其中判斷bean對(duì)象的腦袋上是否有該注解,如果有,你可以對(duì)這個(gè)bean實(shí)例執(zhí)行任何操作,想想是不是非常的簡(jiǎn)單?
再來(lái)看一個(gè)更常見(jiàn)的例子,在Spring中經(jīng)常能夠看到各種各樣的Aware接口,其作用就是在對(duì)象實(shí)例化完成以后將Aware接口定義中規(guī)定的依賴注入到當(dāng)前實(shí)例中。比如最常見(jiàn)的 ApplicationContextAware接口,實(shí)現(xiàn)了這個(gè)接口的類都可以獲取到一個(gè)ApplicationContext對(duì)象。當(dāng)容器中每個(gè)對(duì)象的實(shí)例化過(guò)程走到BeanPostProcessor前置處理這一步時(shí),容器會(huì)檢測(cè)到之前注冊(cè)到容器的ApplicationContextAwareProcessor,然后就會(huì)調(diào)用其postProcessBeforeInitialization()方法,檢查并設(shè)置Aware相關(guān)依賴。看看代碼吧,是不是很簡(jiǎn)單:
// 代碼來(lái)自:org.springframework.context.support.ApplicationContextAwareProcessor// 其postProcessBeforeInitialization方法調(diào)用了invokeAwareInterfaces方法private void invokeAwareInterfaces(Object bean) { if (bean instanceof EnvironmentAware) { ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment()); } if (bean instanceof ApplicationContextAware) { ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext); } // ......}
最后總結(jié)一下,本小節(jié)內(nèi)容和你一起回顧了Spring容器的部分核心內(nèi)容,限于篇幅不能寫更多,但理解這部分內(nèi)容,足以讓您輕松理解Spring Boot的啟動(dòng)原理,如果在后續(xù)的學(xué)習(xí)過(guò)程中遇到一些晦澀難懂的知識(shí),再回過(guò)頭來(lái)看看Spring的核心知識(shí),也許有意想不到的效果。也許Spring Boot的中文資料很少,但Spring的中文資料和書籍有太多太多,總有東西能給你啟發(fā)。
二、夯實(shí)基礎(chǔ):JavaConfig與常見(jiàn)Annotation
2.1、JavaConfig
我們知道 bean是Spring IOC中非常核心的概念,Spring容器負(fù)責(zé)bean的生命周期的管理。在最初,Spring使用XML配置文件的方式來(lái)描述bean的定義以及相互間的依賴關(guān)系,但隨著Spring的發(fā)展,越來(lái)越多的人對(duì)這種方式表示不滿,因?yàn)镾pring項(xiàng)目的所有業(yè)務(wù)類均以bean的形式配置在XML文件中,造成了大量的XML文件,使項(xiàng)目變得復(fù)雜且難以管理。
后來(lái),基于純Java Annotation依賴注入框架 Guice出世,其性能明顯優(yōu)于采用XML方式的Spring,甚至有部分人認(rèn)為, Guice可以完全取代Spring( Guice僅是一個(gè)輕量級(jí)IOC框架,取代Spring還差的挺遠(yuǎn))。正是這樣的危機(jī)感,促使Spring及社區(qū)推出并持續(xù)完善了 JavaConfig子項(xiàng)目,它基于Java代碼和Annotation注解來(lái)描述bean之間的依賴綁定關(guān)系。比如,下面是使用XML配置方式來(lái)描述bean的定義:
<bean id="bookService" class="cn.moondev.service.BookServiceImpl"></bean>
而基于JavaConfig的配置形式是這樣的:
@Configurationpublic class MoonBookConfiguration { // 任何標(biāo)志了@Bean的方法,其返回值將作為一個(gè)bean注冊(cè)到Spring的IOC容器中 // 方法名默認(rèn)成為該bean定義的id @Bean public BookService bookService() { return new BookServiceImpl(); }}
如果兩個(gè)bean之間有依賴關(guān)系的話,在XML配置中應(yīng)該是這樣:
<bean id="bookService" class="cn.moondev.service.BookServiceImpl"> <property name="dependencyService" ref="dependencyService"/></bean><bean id="otherService" class="cn.moondev.service.OtherServiceImpl"> <property name="dependencyService" ref="dependencyService"/></bean><bean id="dependencyService" class="DependencyServiceImpl"/>
而在JavaConfig中則是這樣:
@Configurationpublic class MoonBookConfiguration { // 如果一個(gè)bean依賴另一個(gè)bean,則直接調(diào)用對(duì)應(yīng)JavaConfig類中依賴bean的創(chuàng)建方法即可 // 這里直接調(diào)用dependencyService() @Bean public BookService bookService() { return new BookServiceImpl(dependencyService()); } @Bean public OtherService otherService() { return new OtherServiceImpl(dependencyService()); } @Bean public DependencyService dependencyService() { return new DependencyServiceImpl(); }}
你可能注意到這個(gè)示例中,有兩個(gè)bean都依賴于dependencyService,也就是說(shuō)當(dāng)初始化bookService時(shí)會(huì)調(diào)用 dependencyService(),在初始化otherService時(shí)也會(huì)調(diào)用 dependencyService(),那么問(wèn)題來(lái)了?這時(shí)候IOC容器中是有一個(gè)dependencyService實(shí)例還是兩個(gè)?這個(gè)問(wèn)題留著大家思考吧,這里不再贅述。
2.2、@ComponentScan
@ComponentScan注解對(duì)應(yīng)XML配置形式中的 <context:component-scan>元素,表示啟用組件掃描,Spring會(huì)自動(dòng)掃描所有通過(guò)注解配置的bean,然后將其注冊(cè)到IOC容器中。我們可以通過(guò) basePackages等屬性來(lái)指定 @ComponentScan自動(dòng)掃描的范圍,如果不指定,默認(rèn)從聲明 @ComponentScan所在類的 package進(jìn)行掃描。正因?yàn)槿绱?#xff0c;SpringBoot的啟動(dòng)類都默認(rèn)在 src/main/java下。
2.3、@Import
@Import注解用于導(dǎo)入配置類,舉個(gè)簡(jiǎn)單的例子:
@Configurationpublic class MoonBookConfiguration { @Bean public BookService bookService() { return new BookServiceImpl(); }}
現(xiàn)在有另外一個(gè)配置類,比如:MoonUserConfiguration,這個(gè)配置類中有一個(gè)bean依賴于 MoonBookConfiguration中的bookService,如何將這兩個(gè)bean組合在一起?借助 @Import即可:
@Configuration// 可以同時(shí)導(dǎo)入多個(gè)配置類,比如:@Import({A.class,B.class})@Import(MoonBookConfiguration.class)public class MoonUserConfiguration { @Bean public UserService userService(BookService bookService) { return new BookServiceImpl(bookService); }}
需要注意的是,在4.2之前, @Import注解只支持導(dǎo)入配置類,但是在4.2之后,它支持導(dǎo)入普通類,并將這個(gè)類作為一個(gè)bean的定義注冊(cè)到IOC容器中。
2.4、@Conditional
@Conditional注解表示在滿足某種條件后才初始化一個(gè)bean或者啟用某些配置。它一般用在由 @Component、 @Service、 @Configuration等注解標(biāo)識(shí)的類上面,或者由 @Bean標(biāo)記的方法上。如果一個(gè) @Configuration類標(biāo)記了 @Conditional,則該類中所有標(biāo)識(shí)了 @Bean的方法和 @Import注解導(dǎo)入的相關(guān)類將遵從這些條件。
在Spring里可以很方便的編寫你自己的條件類,所要做的就是實(shí)現(xiàn) Condition接口,并覆蓋它的 matches()方法。舉個(gè)例子,下面的簡(jiǎn)單條件類表示只有在 Classpath里存在 JdbcTemplate類時(shí)才生效:
public class JdbcTemplateCondition implements Condition { @Override public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { try { conditionContext.getClassLoader().loadClass("org.springframework.jdbc.core.JdbcTemplate"); return true; } catch (ClassNotFoundException e) { e.printStackTrace(); } return false; }}
當(dāng)你用Java來(lái)聲明bean的時(shí)候,可以使用這個(gè)自定義條件類:
@Conditional(JdbcTemplateCondition.class)@Servicepublic MyService service() { ......}
這個(gè)例子中只有當(dāng) JdbcTemplateCondition類的條件成立時(shí)才會(huì)創(chuàng)建MyService這個(gè)bean。也就是說(shuō)MyService這bean的創(chuàng)建條件是 classpath里面包含 JdbcTemplate,否則這個(gè)bean的聲明就會(huì)被忽略掉。
SpringBoot定義了很多有趣的條件,并把他們運(yùn)用到了配置類上,這些配置類構(gòu)成了 SpringBoot的自動(dòng)配置的基礎(chǔ)。SpringBoot運(yùn)用條件化配置的方法是:定義多個(gè)特殊的條件化注解,并將它們用到配置類上。下面列出了 SpringBoot提供的部分條件化注解:
條件化注解配置生效條件@ConditionalOnBean配置了某個(gè)特定bean@ConditionalOnMissingBean沒(méi)有配置特定的bean@ConditionalOnClassClasspath里有指定的類@ConditionalOnMissingClassClasspath里沒(méi)有指定的類@ConditionalOnExpression給定的Spring Expression Language表達(dá)式計(jì)算結(jié)果為true@ConditionalOnJavaJava的版本匹配特定指或者一個(gè)范圍值@ConditionalOnProperty指定的配置屬性要有一個(gè)明確的值@ConditionalOnResourceClasspath里有指定的資源@ConditionalOnWebApplication這是一個(gè)Web應(yīng)用程序@ConditionalOnNotWebApplication這不是一個(gè)Web應(yīng)用程序
2.5、@ConfigurationProperties與@EnableConfigurationProperties
當(dāng)某些屬性的值需要配置的時(shí)候,我們一般會(huì)在 application.properties文件中新建配置項(xiàng),然后在bean中使用 @Value注解來(lái)獲取配置的值,比如下面配置數(shù)據(jù)源的代碼。
// jdbc configjdbc.mysql.url=jdbc:mysql://localhost:3306/sampledbjdbc.mysql.username=rootjdbc.mysql.password=123456......// 配置數(shù)據(jù)源@Configurationpublic class HikariDataSourceConfiguration { @Value("jdbc.mysql.url") public String url; @Value("jdbc.mysql.username") public String user; @Value("jdbc.mysql.password") public String password; @Bean public HikariDataSource dataSource() { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setJdbcUrl(url); hikariConfig.setUsername(user); hikariConfig.setPassword(password); // 省略部分代碼 return new HikariDataSource(hikariConfig); }}
使用 @Value注解注入的屬性通常都比較簡(jiǎn)單,如果同一個(gè)配置在多個(gè)地方使用,也存在不方便維護(hù)的問(wèn)題(考慮下,如果有幾十個(gè)地方在使用某個(gè)配置,而現(xiàn)在你想改下名字,你改怎么做?)。對(duì)于更為復(fù)雜的配置,Spring Boot提供了更優(yōu)雅的實(shí)現(xiàn)方式,那就是 @ConfigurationProperties注解。我們可以通過(guò)下面的方式來(lái)改寫上面的代碼:
@Component// 還可以通過(guò)@PropertySource("classpath:jdbc.properties")來(lái)指定配置文件@ConfigurationProperties("jdbc.mysql")// 前綴=jdbc.mysql,會(huì)在配置文件中尋找jdbc.mysql.*的配置項(xiàng)pulic class JdbcConfig { public String url; public String username; public String password;}@Configurationpublic class HikariDataSourceConfiguration { @AutoWired public JdbcConfig config; @Bean public HikariDataSource dataSource() { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setJdbcUrl(config.url); hikariConfig.setUsername(config.username); hikariConfig.setPassword(config.password); // 省略部分代碼 return new HikariDataSource(hikariConfig); }}
@ConfigurationProperties對(duì)于更為復(fù)雜的配置,處理起來(lái)也是得心應(yīng)手,比如有如下配置文件:
#Appapp.menus[0].title=Homeapp.menus[0].name=Homeapp.menus[0].path=/app.menus[1].title=Loginapp.menus[1].name=Loginapp.menus[1].path=/loginapp.compiler.timeout=5app.compiler.output-folder=/temp/app.error=/error/
可以定義如下配置類來(lái)接收這些屬性
@Component@ConfigurationProperties("app")public class AppProperties { public String error; public List<Menu> menus = new ArrayList<>(); public Compiler compiler = new Compiler(); public static class Menu { public String name; public String path; public String title; } public static class Compiler { public String timeout; public String outputFolder; }}
@EnableConfigurationProperties注解表示對(duì) @ConfigurationProperties的內(nèi)嵌支持,默認(rèn)會(huì)將對(duì)應(yīng)Properties Class作為bean注入的IOC容器中,即在相應(yīng)的Properties類上不用加 @Component注解。
三、削鐵如泥:SpringFactoriesLoader詳解
JVM提供了3種類加載器:BootstrapClassLoader、 ExtClassLoader、 AppClassLoader分別加載Java核心類庫(kù)、擴(kuò)展類庫(kù)以及應(yīng)用的類路徑( CLASSPATH)下的類庫(kù)。JVM通過(guò)雙親委派模型進(jìn)行類的加載,我們也可以通過(guò)繼承 java.lang.classloader實(shí)現(xiàn)自己的類加載器。
何為雙親委派模型?當(dāng)一個(gè)類加載器收到類加載任務(wù)時(shí),會(huì)先交給自己的父加載器去完成,因此最終加載任務(wù)都會(huì)傳遞到最頂層的BootstrapClassLoader,只有當(dāng)父加載器無(wú)法完成加載任務(wù)時(shí),才會(huì)嘗試自己來(lái)加載。
采用雙親委派模型的一個(gè)好處是保證使用不同類加載器最終得到的都是同一個(gè)對(duì)象,這樣就可以保證Java 核心庫(kù)的類型安全,比如,加載位于rt.jar包中的 java.lang.Object類,不管是哪個(gè)加載器加載這個(gè)類,最終都是委托給頂層的BootstrapClassLoader來(lái)加載的,這樣就可以保證任何的類加載器最終得到的都是同樣一個(gè)Object對(duì)象。查看ClassLoader的源碼,對(duì)雙親委派模型會(huì)有更直觀的認(rèn)識(shí):
protected Class<?> loadClass(String name, boolean resolve) { synchronized (getClassLoadingLock(name)) { // 首先,檢查該類是否已經(jīng)被加載,如果從JVM緩存中找到該類,則直接返回 Class<?> c = findLoadedClass(name); if (c == null) { try { // 遵循雙親委派的模型,首先會(huì)通過(guò)遞歸從父加載器開(kāi)始找, // 直到父類加載器是BootstrapClassLoader為止 if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) {} if (c == null) { // 如果還找不到,嘗試通過(guò)findClass方法去尋找 // findClass是留給開(kāi)發(fā)者自己實(shí)現(xiàn)的,也就是說(shuō) // 自定義類加載器時(shí),重寫此方法即可 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }}
但雙親委派模型并不能解決所有的類加載器問(wèn)題,比如,Java 提供了很多服務(wù)提供者接口( ServiceProviderInterface,SPI),允許第三方為這些接口提供實(shí)現(xiàn)。常見(jiàn)的 SPI 有 JDBC、JNDI、JAXP 等,這些SPI的接口由核心類庫(kù)提供,卻由第三方實(shí)現(xiàn),這樣就存在一個(gè)問(wèn)題:SPI 的接口是 Java 核心庫(kù)的一部分,是由BootstrapClassLoader加載的;SPI實(shí)現(xiàn)的Java類一般是由AppClassLoader來(lái)加載的。BootstrapClassLoader是無(wú)法找到 SPI 的實(shí)現(xiàn)類的,因?yàn)樗患虞dJava的核心庫(kù)。它也不能代理給AppClassLoader,因?yàn)樗亲铐攲拥念惣虞d器。也就是說(shuō),雙親委派模型并不能解決這個(gè)問(wèn)題。
線程上下文類加載器( ContextClassLoader)正好解決了這個(gè)問(wèn)題。從名稱上看,可能會(huì)誤解為它是一種新的類加載器,實(shí)際上,它僅僅是Thread類的一個(gè)變量而已,可以通過(guò) setContextClassLoader(ClassLoadercl)和 getContextClassLoader()來(lái)設(shè)置和獲取該對(duì)象。如果不做任何的設(shè)置,Java應(yīng)用的線程的上下文類加載器默認(rèn)就是AppClassLoader。在核心類庫(kù)使用SPI接口時(shí),傳遞的類加載器使用線程上下文類加載器,就可以成功的加載到SPI實(shí)現(xiàn)的類。線程上下文類加載器在很多SPI的實(shí)現(xiàn)中都會(huì)用到。但在JDBC中,你可能會(huì)看到一種更直接的實(shí)現(xiàn)方式,比如,JDBC驅(qū)動(dòng)管理 java.sql.Driver中的 loadInitialDrivers()方法中,你可以直接看到JDK是如何加載驅(qū)動(dòng)的:
for (String aDriver : driversList) { try { // 直接使用AppClassLoader Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); }}
其實(shí)講解線程上下文類加載器,最主要是讓大家在看到 Thread.currentThread().getClassLoader()和 Thread.currentThread().getContextClassLoader()時(shí)不會(huì)一臉懵逼,這兩者除了在許多底層框架中取得的ClassLoader可能會(huì)有所不同外,其他大多數(shù)業(yè)務(wù)場(chǎng)景下都是一樣的,大家只要知道它是為了解決什么問(wèn)題而存在的即可。
類加載器除了加載class外,還有一個(gè)非常重要功能,就是加載資源,它可以從jar包中讀取任何資源文件,比如, ClassLoader.getResources(Stringname)方法就是用于讀取jar包中的資源文件,其代碼如下:
public Enumeration<URL> getResources(String name) throws IOException { Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2]; if (parent != null) { tmp[0] = parent.getResources(name); } else { tmp[0] = getBootstrapResources(name); } tmp[1] = findResources(name); return new CompoundEnumeration<>(tmp);}
是不是覺(jué)得有點(diǎn)眼熟,不錯(cuò),它的邏輯其實(shí)跟類加載的邏輯是一樣的,首先判斷父類加載器是否為空,不為空則委托父類加載器執(zhí)行資源查找任務(wù),直到BootstrapClassLoader,最后才輪到自己查找。而不同的類加載器負(fù)責(zé)掃描不同路徑下的jar包,就如同加載class一樣,最后會(huì)掃描所有的jar包,找到符合條件的資源文件。
類加載器的 findResources(name)方法會(huì)遍歷其負(fù)責(zé)加載的所有jar包,找到j(luò)ar包中名稱為name的資源文件,這里的資源可以是任何文件,甚至是.class文件,比如下面的示例,用于查找Array.class文件:
// 尋找Array.class文件public static void main(String[] args) throws Exception{ // Array.class的完整路徑 String name = "java/sql/Array.class"; Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(name); while (urls.hasMoreElements()) { URL url = urls.nextElement(); System.out.println(url.toString()); }}
運(yùn)行后可以得到如下結(jié)果:
$JAVA_HOME/jre/lib/rt.jar!/java/sql/Array.class
根據(jù)資源文件的URL,可以構(gòu)造相應(yīng)的文件來(lái)讀取資源內(nèi)容。
看到這里,你可能會(huì)感到挺奇怪的,你不是要詳解 SpringFactoriesLoader嗎?上來(lái)講了一堆ClassLoader是幾個(gè)意思?看下它的源碼你就知道了:
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";// spring.factories文件的格式為:key=value1,value2,value3// 從所有的jar包中找到META-INF/spring.factories文件// 然后從文件中解析出key=factoryClass類名稱的所有value值public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); // 取得資源文件的URL Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); List<String> result = new ArrayList<String>(); // 遍歷所有的URL while (urls.hasMoreElements()) { URL url = urls.nextElement(); // 根據(jù)資源文件URL解析properties文件 Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url)); String factoryClassNames = properties.getProperty(factoryClassName); // 組裝數(shù)據(jù),并返回 result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames))); } return result;}
有了前面關(guān)于ClassLoader的知識(shí),再來(lái)理解這段代碼,是不是感覺(jué)豁然開(kāi)朗:從 CLASSPATH下的每個(gè)Jar包中搜尋所有 META-INF/spring.factories配置文件,然后將解析properties文件,找到指定名稱的配置后返回。需要注意的是,其實(shí)這里不僅僅是會(huì)去ClassPath路徑下查找,會(huì)掃描所有路徑下的Jar包,只不過(guò)這個(gè)文件只會(huì)在Classpath下的jar包中。來(lái)簡(jiǎn)單看下 spring.factories文件的內(nèi)容吧:
// 來(lái)自 org.springframework.boot.autoconfigure下的META-INF/spring.factories// EnableAutoConfiguration后文會(huì)講到,它用于開(kāi)啟Spring Boot自動(dòng)配置功能org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
執(zhí)行 loadFactoryNames(EnableAutoConfiguration.class,classLoader)后,得到對(duì)應(yīng)的一組 @Configuration類,我們就可以通過(guò)反射實(shí)例化這些類然后注入到IOC容器中,最后容器里就有了一系列標(biāo)注了 @Configuration的JavaConfig形式的配置類。
這就是 SpringFactoriesLoader,它本質(zhì)上屬于Spring框架私有的一種擴(kuò)展方案,類似于SPI,Spring Boot在Spring基礎(chǔ)上的很多核心功能都是基于此,希望大家可以理解。
四、另一件武器:Spring容器的事件監(jiān)聽(tīng)機(jī)制
過(guò)去,事件監(jiān)聽(tīng)機(jī)制多用于圖形界面編程,比如:點(diǎn)擊按鈕、在文本框輸入內(nèi)容等操作被稱為事件,而當(dāng)事件觸發(fā)時(shí),應(yīng)用程序作出一定的響應(yīng)則表示應(yīng)用監(jiān)聽(tīng)了這個(gè)事件,而在服務(wù)器端,事件的監(jiān)聽(tīng)機(jī)制更多的用于異步通知以及監(jiān)控和異常處理。Java提供了實(shí)現(xiàn)事件監(jiān)聽(tīng)機(jī)制的兩個(gè)基礎(chǔ)類:自定義事件類型擴(kuò)展自 java.util.EventObject、事件的監(jiān)聽(tīng)器擴(kuò)展自 java.util.EventListener。來(lái)看一個(gè)簡(jiǎn)單的實(shí)例:簡(jiǎn)單的監(jiān)控一個(gè)方法的耗時(shí)。
首先定義事件類型,通常的做法是擴(kuò)展EventObject,隨著事件的發(fā)生,相應(yīng)的狀態(tài)通常都封裝在此類中:
public class MethodMonitorEvent extends EventObject { // 時(shí)間戳,用于記錄方法開(kāi)始執(zhí)行的時(shí)間 public long timestamp; public MethodMonitorEvent(Object source) { super(source); }}
事件發(fā)布之后,相應(yīng)的監(jiān)聽(tīng)器即可對(duì)該類型的事件進(jìn)行處理,我們可以在方法開(kāi)始執(zhí)行之前發(fā)布一個(gè)begin事件,在方法執(zhí)行結(jié)束之后發(fā)布一個(gè)end事件,相應(yīng)地,事件監(jiān)聽(tīng)器需要提供方法對(duì)這兩種情況下接收到的事件進(jìn)行處理:
// 1、定義事件監(jiān)聽(tīng)接口public interface MethodMonitorEventListener extends EventListener { // 處理方法執(zhí)行之前發(fā)布的事件 public void onMethodBegin(MethodMonitorEvent event); // 處理方法結(jié)束時(shí)發(fā)布的事件 public void onMethodEnd(MethodMonitorEvent event);}// 2、事件監(jiān)聽(tīng)接口的實(shí)現(xiàn):如何處理public class AbstractMethodMonitorEventListener implements MethodMonitorEventListener { @Override public void onMethodBegin(MethodMonitorEvent event) { // 記錄方法開(kāi)始執(zhí)行時(shí)的時(shí)間 event.timestamp = System.currentTimeMillis(); } @Override public void onMethodEnd(MethodMonitorEvent event) { // 計(jì)算方法耗時(shí) long duration = System.currentTimeMillis() - event.timestamp; System.out.println("耗時(shí):" + duration); }}
事件監(jiān)聽(tīng)器接口針對(duì)不同的事件發(fā)布實(shí)際提供相應(yīng)的處理方法定義,最重要的是,其方法只接收MethodMonitorEvent參數(shù),說(shuō)明這個(gè)監(jiān)聽(tīng)器類只負(fù)責(zé)監(jiān)聽(tīng)器對(duì)應(yīng)的事件并進(jìn)行處理。有了事件和監(jiān)聽(tīng)器,剩下的就是發(fā)布事件,然后讓相應(yīng)的監(jiān)聽(tīng)器監(jiān)聽(tīng)并處理。通常情況,我們會(huì)有一個(gè)事件發(fā)布者,它本身作為事件源,在合適的時(shí)機(jī),將相應(yīng)的事件發(fā)布給對(duì)應(yīng)的事件監(jiān)聽(tīng)器:
public class MethodMonitorEventPublisher { private List<MethodMonitorEventListener> listeners = new ArrayList<MethodMonitorEventListener>(); public void methodMonitor() { MethodMonitorEvent eventObject = new MethodMonitorEvent(this); publishEvent("begin",eventObject); // 模擬方法執(zhí)行:休眠5秒鐘 TimeUnit.SECONDS.sleep(5); publishEvent("end",eventObject); } private void publishEvent(String status,MethodMonitorEvent event) { // 避免在事件處理期間,監(jiān)聽(tīng)器被移除,這里為了安全做一個(gè)復(fù)制操作 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(); } // 省略實(shí)現(xiàn) public void addEventListener(MethodMonitorEventListener listener) {} public void removeEventListener(MethodMonitorEventListener listener) {} public void removeAllListeners() {}
對(duì)于事件發(fā)布者(事件源)通常需要關(guān)注兩點(diǎn):
Spring容器內(nèi)的事件監(jiān)聽(tīng)機(jī)制
Spring的ApplicationContext容器內(nèi)部中的所有事件類型均繼承自 org.springframework.context.AppliationEvent,容器中的所有監(jiān)聽(tīng)器都實(shí)現(xiàn) org.springframework.context.ApplicationListener接口,并且以bean的形式注冊(cè)在容器中。一旦在容器內(nèi)發(fā)布ApplicationEvent及其子類型的事件,注冊(cè)到容器的ApplicationListener就會(huì)對(duì)這些事件進(jìn)行處理。
你應(yīng)該已經(jīng)猜到是怎么回事了。
ApplicationEvent繼承自EventObject,Spring提供了一些默認(rèn)的實(shí)現(xiàn),比如:ContextClosedEvent表示容器在即將關(guān)閉時(shí)發(fā)布的事件類型, ContextRefreshedEvent表示容器在初始化或者刷新的時(shí)候發(fā)布的事件類型......
容器內(nèi)部使用ApplicationListener作為事件監(jiān)聽(tīng)器接口定義,它繼承自EventListener。ApplicationContext容器在啟動(dòng)時(shí),會(huì)自動(dòng)識(shí)別并加載EventListener類型的bean,一旦容器內(nèi)有事件發(fā)布,將通知這些注冊(cè)到容器的EventListener。
ApplicationContext接口繼承了ApplicationEventPublisher接口,該接口提供了 voidpublishEvent(ApplicationEventevent)方法定義,不難看出,ApplicationContext容器擔(dān)當(dāng)?shù)木褪鞘录l(fā)布者的角色。如果有興趣可以查看 AbstractApplicationContext.publishEvent(ApplicationEventevent)方法的源碼:ApplicationContext將事件的發(fā)布以及監(jiān)聽(tīng)器的管理工作委托給 ApplicationEventMulticaster接口的實(shí)現(xiàn)類。在容器啟動(dòng)時(shí),會(huì)檢查容器內(nèi)是否存在名為applicationEventMulticaster的ApplicationEventMulticaster對(duì)象實(shí)例。如果有就使用其提供的實(shí)現(xiàn),沒(méi)有就默認(rèn)初始化一個(gè)SimpleApplicationEventMulticaster作為實(shí)現(xiàn)。
最后,如果我們業(yè)務(wù)需要在容器內(nèi)部發(fā)布事件,只需要為其注入ApplicationEventPublisher依賴即可:實(shí)現(xiàn)ApplicationEventPublisherAware接口或者ApplicationContextAware接口(Aware接口相關(guān)內(nèi)容請(qǐng)回顧上文)。
五、出神入化:揭秘自動(dòng)配置原理
典型的Spring Boot應(yīng)用的啟動(dòng)類一般均位于 src/main/java根路徑下,比如 MoonApplication類:
@SpringBootApplicationpublic class MoonApplication { public static void main(String[] args) { SpringApplication.run(MoonApplication.class, args); }}
其中 @SpringBootApplication開(kāi)啟組件掃描和自動(dòng)配置,而 SpringApplication.run則負(fù)責(zé)啟動(dòng)引導(dǎo)應(yīng)用程序。@SpringBootApplication是一個(gè)復(fù)合 Annotation,它將三個(gè)有用的注解組合在一起:
@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框架的注解,標(biāo)明該類是一個(gè) JavaConfig配置類。而 @ComponentScan啟用組件掃描,前文已經(jīng)詳細(xì)講解過(guò),這里著重關(guān)注 @EnableAutoConfiguration。
@EnableAutoConfiguration注解表示開(kāi)啟Spring Boot自動(dòng)配置功能,Spring Boot會(huì)根據(jù)應(yīng)用的依賴、自定義的bean、classpath下有沒(méi)有某個(gè)類 等等因素來(lái)猜測(cè)你需要的bean,然后注冊(cè)到IOC容器中。那 @EnableAutoConfiguration是如何推算出你的需求?首先看下它的定義:
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import(EnableAutoConfigurationImportSelector.class)public @interface EnableAutoConfiguration { // ......}
你的關(guān)注點(diǎn)應(yīng)該在 @Import(EnableAutoConfigurationImportSelector.class)上了,前文說(shuō)過(guò), @Import注解用于導(dǎo)入類,并將這個(gè)類作為一個(gè)bean的定義注冊(cè)到容器中,這里它將把 EnableAutoConfigurationImportSelector作為bean注入到容器中,而這個(gè)類會(huì)將所有符合條件的@Configuration配置都加載到容器中,看看它的代碼:
public String[] selectImports(AnnotationMetadata annotationMetadata) { // 省略了大部分代碼,保留一句核心代碼 // 注意:SpringBoot最近版本中,這句代碼被封裝在一個(gè)單獨(dú)的方法中 // SpringFactoriesLoader相關(guān)知識(shí)請(qǐng)參考前文 List<String> factories = new ArrayList<String>(new LinkedHashSet<String>( SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, this.beanClassLoader)));}
這個(gè)類會(huì)掃描所有的jar包,將所有符合條件的@Configuration配置類注入的容器中,何為符合條件,看看 META-INF/spring.factories的文件內(nèi)容:
// 來(lái)自 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是如何自動(dòng)配置的:
@Configuration@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })@EnableConfigurationProperties(DataSourceProperties.class)@Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class })public class DataSourceAutoConfiguration {}
分別說(shuō)一說(shuō):
- @ConditionalOnClass({DataSource.class,EmbeddedDatabaseType.class}):當(dāng)Classpath中存在DataSource或者EmbeddedDatabaseType類時(shí)才啟用這個(gè)配置,否則這個(gè)配置將被忽略。
- @EnableConfigurationProperties(DataSourceProperties.class):將DataSource的默認(rèn)配置類注入到IOC容器中,DataSourceproperties定義為:
// 提供對(duì)datasource配置信息的支持,所有的配置前綴為:spring.datasource@ConfigurationProperties(prefix = "spring.datasource")public class DataSourceProperties { private ClassLoader classLoader; private Environment environment; private String name = "testdb"; ......}
- @Import({Registrar.class,DataSourcePoolMetadataProvidersConfiguration.class}):導(dǎo)入其他額外的配置,就以 DataSourcePoolMetadataProvidersConfiguration為例吧。
@Configurationpublic class DataSourcePoolMetadataProvidersConfiguration { @Configuration @ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class) static class TomcatDataSourcePoolMetadataProviderConfiguration { @Bean public DataSourcePoolMetadataProvider tomcatPoolDataSourceMetadataProvider() { ..... } } ......}
DataSourcePoolMetadataProvidersConfiguration是數(shù)據(jù)庫(kù)連接池提供者的一個(gè)配置類,即Classpath中存在 org.apache.tomcat.jdbc.pool.DataSource.class,則使用tomcat-jdbc連接池,如果Classpath中存在 HikariDataSource.class則使用Hikari連接池。
這里僅描述了DataSourceAutoConfiguration的冰山一角,但足以說(shuō)明Spring Boot如何利用條件話配置來(lái)實(shí)現(xiàn)自動(dòng)配置的。回顧一下, @EnableAutoConfiguration中導(dǎo)入了EnableAutoConfigurationImportSelector類,而這個(gè)類的 selectImports()通過(guò)SpringFactoriesLoader得到了大量的配置類,而每一個(gè)配置類則根據(jù)條件化配置來(lái)做出決策,以實(shí)現(xiàn)自動(dòng)配置。
整個(gè)流程很清晰,但漏了一個(gè)大問(wèn)題:EnableAutoConfigurationImportSelector.selectImports()是何時(shí)執(zhí)行的?其實(shí)這個(gè)方法會(huì)在容器啟動(dòng)過(guò)程中執(zhí)行:AbstractApplicationContext.refresh(),更多的細(xì)節(jié)在下一小節(jié)中說(shuō)明。
六、啟動(dòng)引導(dǎo):Spring Boot應(yīng)用啟動(dòng)的秘密
6.1 SpringApplication初始化
SpringBoot整個(gè)啟動(dòng)流程分為兩個(gè)步驟:初始化一個(gè)SpringApplication對(duì)象、執(zhí)行該對(duì)象的run方法。看下SpringApplication的初始化流程,SpringApplication的構(gòu)造方法中調(diào)用initialize(Object[] sources)方法,其代碼如下:
private void initialize(Object[] sources) { if (sources != null && sources.length > 0) { this.sources.addAll(Arrays.asList(sources)); } // 判斷是否是Web項(xiàng)目 this.webEnvironment = deduceWebEnvironment(); setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); // 找到入口類 this.mainApplicationClass = deduceMainApplicationClass();}
初始化流程中最重要的就是通過(guò)SpringFactoriesLoader找到 spring.factories文件中配置的ApplicationContextInitializer和 ApplicationListener兩個(gè)接口的實(shí)現(xiàn)類名稱,以便后期構(gòu)造相應(yīng)的實(shí)例。ApplicationContextInitializer的主要目的是在 ConfigurableApplicationContext做refresh之前,對(duì)ConfigurableApplicationContext實(shí)例做進(jìn)一步的設(shè)置或處理。ConfigurableApplicationContext繼承自ApplicationContext,其主要提供了對(duì)ApplicationContext進(jìn)行設(shè)置的能力。
實(shí)現(xiàn)一個(gè)ApplicationContextInitializer非常簡(jiǎn)單,因?yàn)樗挥幸粋€(gè)方法,但大多數(shù)情況下我們沒(méi)有必要自定義一個(gè)ApplicationContextInitializer,即便是Spring Boot框架,它默認(rèn)也只是注冊(cè)了兩個(gè)實(shí)現(xiàn),畢竟Spring的容器已經(jīng)非常成熟和穩(wěn)定,你沒(méi)有必要來(lái)改變它。
而 ApplicationListener的目的就沒(méi)什么好說(shuō)的了,它是Spring框架對(duì)Java事件監(jiān)聽(tīng)機(jī)制的一種框架實(shí)現(xiàn),具體內(nèi)容在前文Spring事件監(jiān)聽(tīng)機(jī)制這個(gè)小節(jié)有詳細(xì)講解。這里主要說(shuō)說(shuō),如果你想為Spring Boot應(yīng)用添加監(jiān)聽(tīng)器,該如何實(shí)現(xiàn)?
Spring Boot提供兩種方式來(lái)添加自定義監(jiān)聽(tīng)器:
- 通過(guò) SpringApplication.addListeners(ApplicationListener<?>...listeners)或者 SpringApplication.setListeners(Collection<?extendsApplicationListener<?>>listeners)兩個(gè)方法來(lái)添加一個(gè)或者多個(gè)自定義監(jiān)聽(tīng)器
- 既然SpringApplication的初始化流程中已經(jīng)從 spring.factories中獲取到 ApplicationListener的實(shí)現(xiàn)類,那么我們直接在自己的jar包的 META-INF/spring.factories文件中新增配置即可:
org.springframework.context.ApplicationListener=cn.moondev.listeners.xxxxListener
關(guān)于SpringApplication的初始化,我們就說(shuō)這么多。
6.2 Spring Boot啟動(dòng)流程
Spring Boot應(yīng)用的整個(gè)啟動(dòng)流程都封裝在SpringApplication.run方法中,其整個(gè)流程真的是太長(zhǎng)太長(zhǎng)了,但本質(zhì)上就是在Spring容器啟動(dòng)的基礎(chǔ)上做了大量的擴(kuò)展,按照這個(gè)思路來(lái)看看源碼:
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); } }
① 通過(guò)SpringFactoriesLoader查找并加載所有的 SpringApplicationRunListeners,通過(guò)調(diào)用starting()方法通知所有的SpringApplicationRunListeners:應(yīng)用開(kāi)始啟動(dòng)了。SpringApplicationRunListeners其本質(zhì)上就是一個(gè)事件發(fā)布者,它在SpringBoot應(yīng)用啟動(dòng)的不同時(shí)間點(diǎn)發(fā)布不同應(yīng)用事件類型(ApplicationEvent),如果有哪些事件監(jiān)聽(tīng)者(ApplicationListener)對(duì)這些事件感興趣,則可以接收并且處理。還記得初始化流程中,SpringApplication加載了一系列ApplicationListener嗎?這個(gè)啟動(dòng)流程中沒(méi)有發(fā)現(xiàn)有發(fā)布事件的代碼,其實(shí)都已經(jīng)在SpringApplicationRunListeners這兒實(shí)現(xiàn)了。
簡(jiǎn)單的分析一下其實(shí)現(xiàn)流程,首先看下SpringApplicationRunListener的源碼:
public interface SpringApplicationRunListener { // 運(yùn)行run方法時(shí)立即調(diào)用此方法,可以用戶非常早期的初始化工作 void starting(); // Environment準(zhǔn)備好后,并且ApplicationContext創(chuàng)建之前調(diào)用 void environmentPrepared(ConfigurableEnvironment environment); // ApplicationContext創(chuàng)建好后立即調(diào)用 void contextPrepared(ConfigurableApplicationContext context); // ApplicationContext加載完成,在refresh之前調(diào)用 void contextLoaded(ConfigurableApplicationContext context); // 當(dāng)run方法結(jié)束之前調(diào)用 void finished(ConfigurableApplicationContext context, Throwable exception);}
SpringApplicationRunListener只有一個(gè)實(shí)現(xiàn)類:EventPublishingRunListener。①處的代碼只會(huì)獲取到一個(gè)EventPublishingRunListener的實(shí)例,我們來(lái)看看starting()方法的內(nèi)容:
public void starting() { // 發(fā)布一個(gè)ApplicationStartedEvent this.initialMulticaster.multicastEvent(new ApplicationStartedEvent(this.application, this.args));}
順著這個(gè)邏輯,你可以在②處的 prepareEnvironment()方法的源碼中找到 listeners.environmentPrepared(environment);即SpringApplicationRunListener接口的第二個(gè)方法,那不出你所料, environmentPrepared()又發(fā)布了另外一個(gè)事件 ApplicationEnvironmentPreparedEvent。接下來(lái)會(huì)發(fā)生什么,就不用我多說(shuō)了吧。
② 創(chuàng)建并配置當(dāng)前應(yīng)用將要使用的 Environment,Environment用于描述應(yīng)用程序當(dāng)前的運(yùn)行環(huán)境,其抽象了兩個(gè)方面的內(nèi)容:配置文件(profile)和屬性(properties),開(kāi)發(fā)經(jīng)驗(yàn)豐富的同學(xué)對(duì)這兩個(gè)東西一定不會(huì)陌生:不同的環(huán)境(eg:生產(chǎn)環(huán)境、預(yù)發(fā)布環(huán)境)可以使用不同的配置文件,而屬性則可以從配置文件、環(huán)境變量、命令行參數(shù)等來(lái)源獲取。因此,當(dāng)Environment準(zhǔn)備好后,在整個(gè)應(yīng)用的任何時(shí)候,都可以從Environment中獲取資源。
總結(jié)起來(lái),②處的兩句代碼,主要完成以下幾件事:
- 判斷Environment是否存在,不存在就創(chuàng)建(如果是web項(xiàng)目就創(chuàng)建 StandardServletEnvironment,否則創(chuàng)建 StandardEnvironment)
- 配置Environment:配置profile以及properties
- 調(diào)用SpringApplicationRunListener的 environmentPrepared()方法,通知事件監(jiān)聽(tīng)者:應(yīng)用的Environment已經(jīng)準(zhǔn)備好
③、SpringBoot應(yīng)用在啟動(dòng)時(shí)會(huì)輸出這樣的東西:
. ____ _ __ _ _ / / ___'_ __ _ _(_)_ __ __ _ ( ( )___ | '_ | '_| | '_ / _` | / ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |___, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.5.6.RELEASE)
如果想把這個(gè)東西改成自己的涂鴉,你可以研究以下Banner的實(shí)現(xiàn),這個(gè)任務(wù)就留給你們吧。
④、根據(jù)是否是web項(xiàng)目,來(lái)創(chuàng)建不同的ApplicationContext容器。
⑤、創(chuàng)建一系列 FailureAnalyzer,創(chuàng)建流程依然是通過(guò)SpringFactoriesLoader獲取到所有實(shí)現(xiàn)FailureAnalyzer接口的class,然后在創(chuàng)建對(duì)應(yīng)的實(shí)例。FailureAnalyzer用于分析故障并提供相關(guān)診斷信息。
⑥、初始化ApplicationContext,主要完成以下工作:
- 將準(zhǔn)備好的Environment設(shè)置給ApplicationContext
- 遍歷調(diào)用所有的ApplicationContextInitializer的 initialize()方法來(lái)對(duì)已經(jīng)創(chuàng)建好的ApplicationContext進(jìn)行進(jìn)一步的處理
- 調(diào)用SpringApplicationRunListener的 contextPrepared()方法,通知所有的監(jiān)聽(tīng)者:ApplicationContext已經(jīng)準(zhǔn)備完畢
- 將所有的bean加載到容器中
- 調(diào)用SpringApplicationRunListener的 contextLoaded()方法,通知所有的監(jiān)聽(tīng)者:ApplicationContext已經(jīng)裝載完畢
⑦、調(diào)用ApplicationContext的 refresh()方法,完成IoC容器可用的最后一道工序。從名字上理解為刷新容器,那何為刷新?就是插手容器的啟動(dòng),聯(lián)系一下第一小節(jié)的內(nèi)容。那如何刷新呢?且看下面代碼:
// 摘自refresh()方法中一句代碼invokeBeanFactoryPostProcessors(beanFactory);
看看這個(gè)方法的實(shí)現(xiàn):
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) { PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors()); ......}
獲取到所有的 BeanFactoryPostProcessor來(lái)對(duì)容器做一些額外的操作。BeanFactoryPostProcessor允許我們?cè)谌萜鲗?shí)例化相應(yīng)對(duì)象之前,對(duì)注冊(cè)到容器的BeanDefinition所保存的信息做一些額外的操作。這里的getBeanFactoryPostProcessors()方法可以獲取到3個(gè)Processor:
ConfigurationWarningsApplicationContextInitializer$ConfigurationWarningsPostProcessorSharedMetadataReaderFactoryContextInitializer$CachingMetadataReaderFactoryPostProcessorConfigFileApplicationListener$PropertySourceOrderingPostProcessor
不是有那么多BeanFactoryPostProcessor的實(shí)現(xiàn)類,為什么這兒只有這3個(gè)?因?yàn)樵诔跏蓟鞒太@取到的各種ApplicationContextInitializer和ApplicationListener中,只有上文3個(gè)做了類似于如下操作:
public void initialize(ConfigurableApplicationContext context) { context.addBeanFactoryPostProcessor(new ConfigurationWarningsPostProcessor(getChecks()));}
然后你就可以進(jìn)入到 PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors()方法了,這個(gè)方法除了會(huì)遍歷上面的3個(gè)BeanFactoryPostProcessor處理外,還會(huì)獲取類型為 BeanDefinitionRegistryPostProcessor的bean:org.springframework.context.annotation.internalConfigurationAnnotationProcessor,對(duì)應(yīng)的Class為 ConfigurationClassPostProcessor。ConfigurationClassPostProcessor用于解析處理各種注解,包括:@Configuration、@ComponentScan、@Import、@PropertySource、@ImportResource、@Bean。當(dāng)處理 @import注解的時(shí)候,就會(huì)調(diào)用<自動(dòng)配置>這一小節(jié)中的 EnableAutoConfigurationImportSelector.selectImports()來(lái)完成自動(dòng)配置功能。其他的這里不再多講,如果你有興趣,可以查閱參考資料6。
⑧、查找當(dāng)前context中是否注冊(cè)有CommandLineRunner和ApplicationRunner,如果有則遍歷執(zhí)行它們。
⑨、執(zhí)行所有SpringApplicationRunListener的finished()方法。
這就是Spring Boot的整個(gè)啟動(dòng)流程,其核心就是在Spring容器初始化并啟動(dòng)的基礎(chǔ)上加入各種擴(kuò)展點(diǎn),這些擴(kuò)展點(diǎn)包括:ApplicationContextInitializer、ApplicationListener以及各種BeanFactoryPostProcessor等等。你對(duì)整個(gè)流程的細(xì)節(jié)不必太過(guò)關(guān)注,甚至沒(méi)弄明白也沒(méi)有關(guān)系,你只要理解這些擴(kuò)展點(diǎn)是在何時(shí)如何工作的,能讓它們?yōu)槟闼眉纯伞?br />整個(gè)啟動(dòng)流程確實(shí)非常復(fù)雜,可以查詢參考資料中的部分章節(jié)和內(nèi)容,對(duì)照著源碼,多看看,我想最終你都能弄清楚的。言而總之,Spring才是核心,理解清楚Spring容器的啟動(dòng)流程,那Spring Boot啟動(dòng)流程就不在話下了。
總結(jié)
以上是生活随笔為你收集整理的springboot中接口实例化_疫情爆发在家闲出屁的我,梳理一下SpringBoot知识点的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: android double转strin
- 下一篇: spring 5企业级开发实战pdf_S