国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频

一文講完 Spring Boot,3W 字超詳細(xì)總結(jié)

共 34523字,需瀏覽 70分鐘

 ·

2020-10-12 13:46

注意文末有最新Java實戰(zhàn)項目面試題


作者|CHEN川

http://www.jianshu.com/p/83693d3d0a65

說明:前面有 4 個小節(jié)關(guān)于Spring的基礎(chǔ)知識

分別是:IoC 容器、JavaConfig、事件監(jiān)聽、SpringFactoriesLoader 詳解

它們占據(jù)了本文的大部分內(nèi)容:

雖然它們之間可能沒有太多的聯(lián)系,但這些知識對于理解 Spring Boot 的核心原理至關(guān)重要,如果你對 Spring 框架爛熟于心,完全可以跳過這 4 個小節(jié)。正是因為這個系列的文章是由這些看似不相關(guān)的知識點組成,因此取名知識清單。

在過去兩三年的 Spring 生態(tài)圈,最讓人興奮的莫過于 Spring Boot 框架?;蛟S從命名上就能看出這個框架的設(shè)計初衷:快速的啟動 Spring 應(yīng)用。因而 Spring Boot 應(yīng)用本質(zhì)上就是一個基于 Spring 框架的應(yīng)用,它是 Spring 對“約定優(yōu)先于配置”理念的最佳實踐產(chǎn)物,它能夠幫助開發(fā)者更快速高效地構(gòu)建基于 Spring 生態(tài)圈的應(yīng)用。

那 Spring Boot 有何魔法?

自動配置、起步依賴、Actuator、命令行界面(CLI) 是 Spring Boot 最重要的4大核心特性,其中CLI是Spring Boot的可選特性,雖然它功能強(qiáng)大,但也引入了一套不太常規(guī)的開發(fā)模型,因而這個系列的文章僅關(guān)注其它3種特性。如文章標(biāo)題,本文是這個系列的第一部分,將為你打開Spring Boot的大門,重點為你剖析其啟動流程以及自動配置實現(xiàn)原理。要掌握這部分核心內(nèi)容,理解一些Spring框架的基礎(chǔ)知識,將會讓你事半功倍。

一、拋磚引玉:探索Spring IoC容器

如果有看過SpringApplication.run()方法的源碼,Spring Boot冗長無比的啟動流程一定

會讓你抓狂,透過現(xiàn)象看本質(zhì).

SpringApplication只是將一個典型的Spring應(yīng)用的啟動流程進(jìn)行了擴(kuò)展,因此,透徹理解, Spring容器是打開Spring Boot大門的一把鑰匙。

1.1、Spring IoC容器

可以把Spring IoC容器比作一間餐館,當(dāng)你來到餐館,通常會直接招呼服務(wù)員:點菜!至于菜的原料是什么?如何用原料把菜做出來?可能你根本就不關(guān)心。

IoC容器也是一樣,你只需要告訴它需要某個bean,它就把對應(yīng)的實例(instance)扔給你,至于這個bean是否依賴其他組件,怎樣完成它的初始化,根本就不需要你關(guān)心。

作為餐館,想要做出菜肴,得知道菜的原料和菜譜,同樣地,IoC容器想要管理各個業(yè)務(wù)對象以及它們之間的依賴關(guān)系,需要通過某種途徑來記錄和管理這些信息。

BeanDefinition對象就承擔(dān)了這個責(zé)任:容器中的每一個bean都會有一個對應(yīng)的BeanDefinition實例,該實例負(fù)責(zé)保存bean對象的所有必要信息,包括bean對象的class類型、是否是抽象類、構(gòu)造方法和參數(shù)、其它屬性等等。

當(dāng)客戶端向容器請求相應(yīng)對象時,容器就會通過這些信息為客戶端返回一個完整可用的bean實例。

原材料已經(jīng)準(zhǔn)備好(把BeanDefinition看著原料),開始做菜吧,等等,你還需要一份菜譜,BeanDefinitionRegistry和BeanFactory就是這份菜譜,BeanDefinitionRegistry抽象出bean的注冊邏輯,而BeanFactory則抽象出了bean的管理邏輯,而各個BeanFactory的實現(xiàn)類就具體承擔(dān)了bean的注冊以及管理工作。

它們之間的關(guān)系就如下圖:

BeanFactory、BeanDefinitionRegistry關(guān)系圖(來自:Spring揭秘)

DefaultListableBeanFactory作為一個比較通用的BeanFactory實現(xiàn),它同時也實現(xiàn)了BeanDefinitionRegistry接口,因此它就承擔(dān)了Bean的注冊管理工作。從圖中也可以看出,BeanFactory接口中主要包含getBean、containBean、getType、getAliases等管理bean的方法,而BeanDefinitionRegistry接口則包含registerBeanDefinition、removeBeanDefinition、getBeanDefinition等注冊管理BeanDefinition的方法。

下面通過一段簡單的代碼來模擬BeanFactory底層是如何工作的:

// 默認(rèn)容器實現(xiàn) DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory(); // 根據(jù)業(yè)務(wù)對象構(gòu)造相應(yīng)的BeanDefinition AbstractBeanDefinition definition = new RootBeanDefinition(Business.class,true); // 將bean定義注冊到容器中 beanRegistry.registerBeanDefinition(beanName,definition); // 如果有多個bean,還可以指定各個bean之間的依賴關(guān)系 // ........  // 然后可以從容器中獲取這個bean的實例 // 注意:這里的beanRegistry其實實現(xiàn)了BeanFactory接口,所以可以強(qiáng)轉(zhuǎn), // 單純的BeanDefinitionRegistry是無法強(qiáng)制轉(zhuǎn)換到BeanFactory類型的 BeanFactory container = (BeanFactory)beanRegistry; Business business = (Business)container.getBean(beanName);

這段代碼僅為了說明BeanFactory底層的大致工作流程.實際情況會更加復(fù)雜,比如bean之間的依賴關(guān)系可能定義在外部配置文件(XML/Properties)中、也可能是注解方式。

Spring IoC容器的整個工作流程大致可以分為兩個階段:

①、容器啟動階段

容器啟動時,會通過某種途徑加載Configuration MetaData。除了代碼方式比較直接外,在大部分情況下,容器需要依賴某些工具類,比如:BeanDefinitionReader,BeanDefinitionReader會對加載的Configuration MetaData進(jìn)行解析和分析,并將分析后的信息組裝為相應(yīng)的BeanDefinition,最后把這些保存了bean定義的BeanDefinition,注冊到相應(yīng)的BeanDefinitionRegistry,這樣容器的啟動工作就完成了。

這個階段主要完成一些準(zhǔn)備性工作,更側(cè)重于bean對象管理信息的收集,當(dāng)然一些驗證性或者輔助性的工作也在這一階段完成。

來看一個簡單的例子吧,過往,所有的bean都定義在XML配置文件中,下面的代碼將模擬

BeanFactory如何從配置文件中加載bean的定義以及依賴關(guān)系:

// 通常為BeanDefinitionRegistry的實現(xiàn)類,這里以DeFaultListabeBeanFactory為例 BeanDefinitionRegistry beanRegistry = new DefaultListableBeanFactory();  // XmlBeanDefinitionReader實現(xiàn)了BeanDefinitionReader接口,用于解析XML文件 XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReaderImpl(beanRegistry); // 加載配置文件 beanDefinitionReader.loadBeanDefinitions(classpath:spring-bean.xml);  // 從容器中獲取bean實例 BeanFactory container = (BeanFactory)beanRegistry; Business business = (Business)container.getBean(beanName);

②、Bean的實例化階段

經(jīng)過第一階段,所有bean定義都通過BeanDefinition的方式注冊到BeanDefinitionRegistry中當(dāng)某個請求通過容器的getBean方法請求某個對象,或者因為依賴關(guān)系容器需要隱式的調(diào)用getBean時,就會觸發(fā)第二階段的活動:容器會首先檢查所請求的對象之前是否已經(jīng)實例化完成。

如果沒有,則會根據(jù)注冊的BeanDefinition所提供的信息實例化被請求對象,并為其注入依賴。

當(dāng)該對象裝配完畢后,容器會立即將其返回給請求方法使用。BeanFactory只是Spring IoC容器的一種實現(xiàn),如果沒有特殊指定,它采用采用延遲初始化策略:只有當(dāng)訪問容器中的某個對象時,才對該對象進(jìn)行初始化和依賴注入操作。

而在實際場景下,我們更多的使用另外一種類型的容器:ApplicationContext,它構(gòu)建在BeanFactory之上,屬于更高級的容器,除了具有BeanFactory的所有能力之外,還提供對事件監(jiān)聽機(jī)制以及國際化的支持等。它管理的bean,在容器啟動時全部完成初始化和依賴注入操作。

1.2、Spring容器擴(kuò)展機(jī)制

IoC容器負(fù)責(zé)管理容器中所有bean的生命周期,而在bean生命周期的不同階段,Spring提供了不同的擴(kuò)展點來改變bean的命運。在容器的啟動階段,BeanFactoryPostProcessor允許我們在容器實例化相應(yīng)對象之前,對注冊到容器的BeanDefinition所保存的信息做一些額外的操作,比如修改bean定義的某些屬性或者增加其他信息等。

如果要自定義擴(kuò)展類,通常需要實現(xiàn).

org.springframework.beans.factory.config.BeanFactoryPostProcessor接口,與此同時,因為容器中可能有多個BeanFactoryPostProcessor,可能還需要實現(xiàn)org.springframework.core.Ordered接口,以保證BeanFactoryPostProcessor按照順序執(zhí)行。

Spring提供了為數(shù)不多的BeanFactoryPostProcessor實現(xiàn).我們以PropertyPlaceholderConfigurer來說明其大致的工作流程。

在Spring項目的XML配置文件中,經(jīng)常可以看到許多配置項的值使用占位符,而將占位符所代表的值單獨配置到獨立的properties文件,這樣可以將散落在不同XML文件中的配置集中管理,而且也方便運維根據(jù)不同的環(huán)境進(jìn)行配置不同的值。

這個非常實用的功能就是由PropertyPlaceholderConfigurer負(fù)責(zé)實現(xiàn)的。

根據(jù)前文,當(dāng)BeanFactory在第一階段加載完所有配置信息時,BeanFactory中保存的對象的屬性還是以占位符方式存在的,比如${jdbc.mysql.url}。

當(dāng)PropertyPlaceholderConfigurer作為BeanFactoryPostProcessor被應(yīng)用時,它會使用properties配置文件中的值來替換相應(yīng)的BeanDefinition中占位符所表示的屬性值。當(dāng)需要實例化bean時,bean定義中的屬性值就已經(jīng)被替換成我們配置的值。當(dāng)然其實現(xiàn)比上面描述的要復(fù)雜一些,這里僅說明其大致工作原理,更詳細(xì)的實現(xiàn)可以參考其源碼。

與之相似的,還有BeanPostProcessor,其存在于對象實例化階段。跟BeanFactoryPostProcessor類似,它會處理容器內(nèi)所有符合條件并且已經(jīng)實例化后的對象。

簡單的對比,BeanFactoryPostProcessor處理bean的定義,而BeanPostProcessor則處理bean完成實例化后的對象。

BeanPostProcessor定義了兩個接口:

// 通常為BeanDefinitionRegistry的實現(xiàn)類,這里以DeFaultListabeBeanFactory為例 BeanDefinitionRegistry beanRegistry = new DefaultListableBeanFactory();  // XmlBeanDefinitionReader實現(xiàn)了BeanDefinitionReader接口,用于解析XML文件 XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReaderImpl(beanRegistry); // 加載配置文件 beanDefinitionReader.loadBeanDefinitions(classpath:spring-bean.xml);  // 從容器中獲取bean實例 BeanFactory container = (BeanFactory)beanRegistry; Business business = (Business)container.getBean(beanName);

為了理解這兩個方法執(zhí)行的時機(jī),簡單的了解下bean的整個生命周期:

Bean的實例化過程(來自:Spring揭秘)

postProcessBeforeInitialization()方法與postProcessAfterInitialization()分別對應(yīng)圖中前置處理和后置處理兩個步驟將執(zhí)行的方法。這兩個方法中都傳入了bean對象實例的引用,為擴(kuò)展容器的對象實例化過程提供了很大便利,在這兒幾乎可以對傳入的實例執(zhí)行任何操作。

注解、AOP等功能的實現(xiàn)均大量使用了BeanPostProcessor,比如有一個自定義注解,你完全可以實現(xiàn)BeanPostProcessor的接口,在其中判斷bean對象的腦袋上是否有該注解,如果有,你可以對這個bean實例執(zhí)行任何操作,想想是不是非常的簡單?

再來看一個更常見的例子,在Spring中經(jīng)常能夠看到各種各樣的Aware接口,其作用就是在對象實例化完成以后將Aware接口定義中規(guī)定的依賴注入到當(dāng)前實例中。

比如最常見的ApplicationContextAware接口,實現(xiàn)了這個接口的類都可以獲取到一個ApplicationContext對象。當(dāng)容器中每個對象的實例化過程走到BeanPostProcessor前置處理這一步時,容器會檢測到之前注冊到容器的ApplicationContextAwareProcessor,然后就會調(diào)用其postProcessBeforeInitialization()方法,檢查并設(shè)置Aware相關(guān)依賴。

看看代碼吧,是不是很簡單:

// 代碼來自: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的啟動原理,如果在后續(xù)的學(xué)習(xí)過程中遇到一些晦澀難懂的知識,再回過頭來看看Spring的核心知識,也許有意想不到的效果。

也許Spring Boot的中文資料很少,但Spring的中文資料和書籍有太多太多,總有東西能給你啟發(fā)。

二、夯實基礎(chǔ):JavaConfig與常見Annotation

2.1、JavaConfig

我們知道bean是Spring IOC中非常核心的概念,Spring容器負(fù)責(zé)bean的生命周期的管理。

在最初,Spring使用XML配置文件的方式來描述bean的定義以及相互間的依賴關(guān)系,但隨著Spring的發(fā)展,越來越多的人對這種方式表示不滿,因為Spring項目的所有業(yè)務(wù)類均以bean的形式配置在XML文件中,造成了大量的XML文件,使項目變得復(fù)雜且難以管理。

后來,基于純Java Annotation依賴注入框架Guice出世,其性能明顯優(yōu)于采用XML方式的Spring,甚至有部分人認(rèn)為,Guice可以完全取代Spring(Guice僅是一個輕量級IOC框架,取代Spring還差的挺遠(yuǎn)).

正是這樣的危機(jī)感,促使Spring及社區(qū)推出并持續(xù)完善了JavaConfig子項目,它基于Java代碼和Annotation注解來描述bean之間的依賴綁定關(guān)系。

比如,下面是使用XML配置方式來描述bean的定義:

而基于JavaConfig的配置形式是這樣的:
@Configuration public class MoonBookConfiguration{      // 任何標(biāo)志了@Bean的方法,其返回值將作為一個bean注冊到Spring的IOC容器中     // 方法名默認(rèn)成為該bean定義的id     @Bean     public BookService bookService() {         return new BookServiceImpl();     } }

如果兩個bean之間有依賴關(guān)系的話,在XML配置中應(yīng)該是這樣:

      



而在JavaConfig中則是這樣:

@Configuration public class MoonBookConfiguration {      // 如果一個bean依賴另一個bean,則直接調(diào)用對應(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();     } }

你可能注意到這個示例中,有兩個bean都依賴于dependencyService,也就是說當(dāng)初始化bookService時會調(diào)用dependencyService(),在初始化otherService時也會調(diào)用dependencyService(),那么問題來了?

這時候IOC容器中是有一個dependencyService實例還是兩個?這個問題留著大家思考吧,這里不再贅述。

2.2、@ComponentScan

@ComponentScan注解對應(yīng)XML配置形式中的元素表示啟用組件掃描,Spring會自動掃描所有通過注解配置的bean,然后將其注冊到IOC容器中。

我們可以通過basePackages等屬性來指定@ComponentScan自動掃描的范圍,如果不指定,默認(rèn)從聲明@ComponentScan所在類的package進(jìn)行掃描。正因為如此,SpringBoot的啟動類都默認(rèn)在src/main/java下。

2.3、@Import

@Import注解用于導(dǎo)入配置類,舉個簡單的例子:

@Configuration public class MoonBookConfiguration{     @Bean     public BookService bookService() {         return new BookServiceImpl();     } }

現(xiàn)在有另外一個配置類,比如:MoonUserConfiguration,這個配置類中有一個bean依賴于MoonBookConfiguration中的bookService,如何將這兩個bean組合在一起?

借助@Import即可:

@Configuration // 可以同時導(dǎo)入多個配置類,比如:@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)入普通類,并將這個類作為一個bean的定義注冊到IOC容器中。

2.4、@Conditional

@Conditional注解表示在滿足某種條件后才初始化一個bean或者啟用某些配置。

它一般用在由@Component、@Service、@Configuration等注解標(biāo)識的類上面,或者由@Bean標(biāo)記的方法上。如果一個@Configuration類標(biāo)記了@Conditional,則該類中所有標(biāo)識了@Bean的方法和@Import注解導(dǎo)入的相關(guān)類將遵從這些條件。

在Spring里可以很方便的編寫你自己的條件類,所要做的就是實現(xiàn)Condition接口,并覆蓋它的matches()方法。

舉個例子,下面的簡單條件類表示只有在Classpath里存在JdbcTemplate類時才生效:

public?class?JdbcTemplateCondition?implements?Condition?{
@Overridepublic?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來聲明bean的時候,可以使用這個自定義條件類:

@Conditional(JdbcTemplateCondition.class)?@Service?public?MyService?service()?{?......?}

這個例子中只有當(dāng)JdbcTemplateCondition類的條件成立時才會創(chuàng)建MyService這個bean。

也就是說MyService這bean的創(chuàng)建條件是classpath里面包含JdbcTemplate,否則這個bean的聲明就會被忽略掉。

Spring Boot定義了很多有趣的條件,并把他們運用到了配置類上,這些配置類構(gòu)成了Spring Boot的自動配置的基礎(chǔ)。

Spring Boot運用條件化配置的方法是:定義多個特殊的條件化注解,并將它們用到配置類上。

下面列出了Spring Boot提供的部分條件化注解:

2.5、@ConfigurationProperties與@EnableConfigurationProperties

當(dāng)某些屬性的值需要配置的時候,我們一般會在application.properties文件中新建配置項,然后在bean中使用@Value注解來獲取配置的值,比如下面配置數(shù)據(jù)源的代碼。

// jdbc config jdbc.mysql.url=jdbc:mysql://localhost:3306/sampledb jdbc.mysql.username=root jdbc.mysql.password=123456?......
// 配置數(shù)據(jù)源 @Configuration?public?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注解注入的屬性通常都比較簡單,如果同一個配置在多個地方使用,也存在不方便維護(hù)的問題(考慮下,如果有幾十個地方在使用某個配置,而現(xiàn)在你想改下名字,你改怎么做?)

對于更為復(fù)雜的配置,Spring Boot提供了更優(yōu)雅的實現(xiàn)方式,那就是@ConfigurationProperties注解。

我們可以通過下面的方式來改寫上面的代碼:

@Component // 還可以通過@PropertySource(classpath:jdbc.properties)來指定配置文件 @ConfigurationProperties(jdbc.mysql) // 前綴=jdbc.mysql,會在配置文件中尋找jdbc.mysql.*的配置項 pulic class JdbcConfig {     public String url;     public String username;     public String password; }
?@Configuration?public?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對于更為復(fù)雜的配置,處理起來也是得心應(yīng)手,比如有如下配置文件:

#App app.menus[0].title=Home app.menus[0].name=Home app.menus[0].path=/ app.menus[1].title=Login app.menus[1].name=Login app.menus[1].path=/login
app.compiler.timeout=5 app.compiler.output-folder=/temp/
?app.error=/error/

可以定義如下配置類來接收這些屬性:

@Component@ConfigurationProperties(app)public?class?AppProperties?{
?????public?String?error; public List 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注解表示對@ConfigurationProperties的內(nèi)嵌支持默認(rèn)會將對應(yīng)Properties Class作為bean注入的IOC容器中,即在相應(yīng)的Properties類上不用加@Component注解。

三、削鐵如泥:SpringFactoriesLoader詳解

JVM提供了3種類加載器:

BootstrapClassLoader、ExtClassLoader、AppClassLoader分別加載Java核心類庫、擴(kuò)展類庫以及應(yīng)用的類路徑(CLASSPATH)下的類庫。

JVM通過雙親委派模型進(jìn)行類的加載,我們也可以通過繼承java.lang.classloader實現(xiàn)自己的類加載器。

何為雙親委派模型?當(dāng)一個類加載器收到類加載任務(wù)時,會先交給自己的父加載器去完成,因此最終加載任務(wù)都會傳遞到最頂層的BootstrapClassLoader,只有當(dāng)父加載器無法完成加載任務(wù)時,才會嘗試自己來加載。

采用雙親委派模型的一個好處是保證使用不同類加載器最終得到的都是同一個對象,這樣就可以保證Java 核心庫的類型安全,比如,加載位于rt.jar包中的java.lang.Object類,不管是哪個加載器加載這個類,最終都是委托給頂層的BootstrapClassLoader來加載的,這樣就可以保證任何的類加載器最終得到的都是同樣一個Object對象。

查看ClassLoader的源碼,對雙親委派模型會有更直觀的認(rèn)識:

protected Class loadClass(String name, boolean resolve) {     synchronized (getClassLoadingLock(name)) {     // 首先,檢查該類是否已經(jīng)被加載,如果從JVM緩存中找到該類,則直接返回     Class c = findLoadedClass(name);     if (c == null) {         try {             // 遵循雙親委派的模型,首先會通過遞歸從父加載器開始找,             // 直到父類加載器是BootstrapClassLoader為止             if (parent != null) {                 c = parent.loadClass(name, false);             } else {                 c = findBootstrapClassOrNull(name);             }        } catch (ClassNotFoundException e) {}         if (c == null) {             // 如果還找不到,嘗試通過findClass方法去尋找             // findClass是留給開發(fā)者自己實現(xiàn)的,也就是說             // 自定義類加載器時,重寫此方法即可            c = findClass(name);         }     }     if (resolve) {        resolveClass(c);     }     return c;     } }

但雙親委派模型并不能解決所有的類加載器問題,比如,Java 提供了很多服務(wù)提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實現(xiàn)。

常見的 SPI 有 JDBC、JNDI、JAXP 等,這些SPI的接口由核心類庫提供,卻由第三方實現(xiàn)這樣就存在一個問題:SPI 的接口是 Java 核心庫的一部分,是由BootstrapClassLoader加載的;SPI實現(xiàn)的Java類一般是由AppClassLoader來加載的。BootstrapClassLoader是無法找到 SPI 的實現(xiàn)類的,因為它只加載Java的核心庫。它也不能代理給AppClassLoader,因為它是最頂層的類加載器。也就是說,雙親委派模型并不能解決這個問題。

線程上下文類加載器(ContextClassLoader)正好解決了這個問題。

從名稱上看,可能會誤解為它是一種新的類加載器,實際上,它僅僅是Thread類的一個變量而已,可以通過setContextClassLoader(ClassLoader cl)和getContextClassLoader()來設(shè)置和獲取該對象。

如果不做任何的設(shè)置,Java應(yīng)用的線程的上下文類加載器默認(rèn)就是AppClassLoader。

在核心類庫使用SPI接口時,傳遞的類加載器使用線程上下文類加載器,就可以成功的加載到SPI實現(xiàn)的類。

線程上下文類加載器在很多SPI的實現(xiàn)中都會用到。但在JDBC中,你可能會看到一種更直接的實現(xiàn)方式,比如,JDBC驅(qū)動管理java.sql.Driver中的loadInitialDrivers()方法中

你可以直接看到JDK是如何加載驅(qū)動的:

for (String aDriver : driversList) {     try {         // 直接使用AppClassLoader         Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());     } catch (Exception ex) {         println(DriverManager.Initialize: load failed:  + ex);      }   }

其實講解線程上下文類加載器,最主要是讓大家在看到:

Thread.currentThread().getClassLoader()Thread.currentThread().getContextClassLoader()

時不會一臉懵逼.

這兩者除了在許多底層框架中取得的ClassLoader可能會有所不同外,其他大多數(shù)業(yè)務(wù)場景下都是一樣的,大家只要知道它是為了解決什么問題而存在的即可。

類加載器除了加載class外,還有一個非常重要功能,就是加載資源,它可以從jar包中讀取任何資源文件,比如,ClassLoader.getResources(String name)方法就是用于讀取jar包中的資源文件,其代碼如下:

public Enumeration getResources(String name) throws IOException {     Enumeration[] tmp = (Enumeration[]) new Enumeration[2];     if (parent != null) {         tmp[0] = parent.getResources(name);     } else {         tmp[0] = getBootstrapResources(name);     }     tmp[1] = findResources(name);     return new CompoundEnumeration<>(tmp); }

是不是覺得有點眼熟,不錯,它的邏輯其實跟類加載的邏輯是一樣的,首先判斷父類加載器是否為空,不為空則委托父類加載器執(zhí)行資源查找任務(wù), 直到BootstrapClassLoader,最后才輪到自己查找。

而不同的類加載器負(fù)責(zé)掃描不同路徑下的jar包,就如同加載class一樣,最后會掃描所有的jar包,找到符合條件的資源文件。

類加載器的findResources(name)方法會遍歷其負(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 urls = Thread.currentThread().getContextClassLoader().getResources(name);     while (urls.hasMoreElements()) {         URL url = urls.nextElement();         System.out.println(url.toString());     } }

運行后可以得到如下結(jié)果:

$JAVA_HOME/jre/lib/rt.jar!/java/sql/Array.class
根據(jù)資源文件的URL,可以構(gòu)造相應(yīng)的文件來讀取資源內(nèi)容。

看到這里,你可能會感到挺奇怪的,你不是要詳解SpringFactoriesLoader嗎?

上來講了一ClassLoader是幾個意思?看下它的源碼你就知道了:

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 loadFactoryNames(Class factoryClass, ClassLoader classLoader) {     String factoryClassName = factoryClass.getName();     // 取得資源文件的URL     Enumeration urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));     List result = new ArrayList();     // 遍歷所有的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的知識,再來理解這段代碼,是不是感覺豁然開朗:

從CLASSPATH下的每個Jar包中搜尋所有META-INF/spring.factories配置文件,然后將解析properties文件,找到指定名稱的配置后返回。

需要注意的是,其實這里不僅僅是會去ClassPath路徑下查找,會掃描所有路徑下的Jar包,只不過這個文件只會在Classpath下的jar包中。

來簡單看下spring.factories文件的內(nèi)容吧:

// 來自 org.springframework.boot.autoconfigure下的META-INF/spring.factories // EnableAutoConfiguration后文會講到,它用于開啟Spring Boot自動配置功能 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\ org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration\

執(zhí)行l(wèi)oadFactoryNames(EnableAutoConfiguration.class, classLoader)后,得到對應(yīng)的一組@Configuration類,我們就可以通過反射實例化這些類然后注入到IOC容器中,最后容器里就有了一系列標(biāo)注了@Configuration的JavaConfig形式的配置類。

這就是SpringFactoriesLoader,它本質(zhì)上屬于Spring框架私有的一種擴(kuò)展方案,類似于SPI,Spring Boot在Spring基礎(chǔ)上的很多核心功能都是基于此,希望大家可以理解。

四、另一件武器:Spring容器的事件監(jiān)聽機(jī)制

過去,事件監(jiān)聽機(jī)制多用于圖形界面編程,比如:點擊按鈕、在文本框輸入內(nèi)容等操作被稱為事件,而當(dāng)事件觸發(fā)時,應(yīng)用程序作出一定的響應(yīng)則表示應(yīng)用監(jiān)聽了這個事件,而在服務(wù)器端,事件的監(jiān)聽機(jī)制更多的用于異步通知以及監(jiān)控和異常處理。

Java提供了實現(xiàn)事件監(jiān)聽機(jī)制的兩個基礎(chǔ)類:自定義事件類型擴(kuò)展自java.util.EventObject、事件的監(jiān)聽器擴(kuò)展自java.util.EventListener。

來看一個簡單的實例:簡單的監(jiān)控一個方法的耗時。

首先定義事件類型,通常的做法是擴(kuò)展EventObject,隨著事件的發(fā)生,相應(yīng)的狀態(tài)通常都封裝在此類中:

public class MethodMonitorEvent extends EventObject {     // 時間戳,用于記錄方法開始執(zhí)行的時間?????public?long?timestamp;
public MethodMonitorEvent(Object source) { super(source); } }

事件發(fā)布之后,相應(yīng)的監(jiān)聽器即可對該類型的事件進(jìn)行處理,我們可以在方法開始執(zhí)行之前發(fā)布一個begin事件.

在方法執(zhí)行結(jié)束之后發(fā)布一個end事件,相應(yīng)地,事件監(jiān)聽器需要提供方法對這兩種情況下接收到的事件進(jìn)行處理:

// 1、定義事件監(jiān)聽接口 public interface MethodMonitorEventListener extends EventListener {     // 處理方法執(zhí)行之前發(fā)布的事件     public void onMethodBegin(MethodMonitorEvent event);     // 處理方法結(jié)束時發(fā)布的事件     public void onMethodEnd(MethodMonitorEvent event); } // 2、事件監(jiān)聽接口的實現(xiàn):如何處理 public class AbstractMethodMonitorEventListener implements MethodMonitorEventListener {      @Override     public void onMethodBegin(MethodMonitorEvent event) {         // 記錄方法開始執(zhí)行時的時間         event.timestamp = System.currentTimeMillis();     }      @Override     public void onMethodEnd(MethodMonitorEvent event) {         // 計算方法耗時         long duration = System.currentTimeMillis() - event.timestamp;         System.out.println(耗時:+ duration);     } }

事件監(jiān)聽器接口針對不同的事件發(fā)布實際提供相應(yīng)的處理方法定義,最重要的是,其方法只接收MethodMonitorEvent參數(shù),說明這個監(jiān)聽器類只負(fù)責(zé)監(jiān)聽器對應(yīng)的事件并進(jìn)行處理。

有了事件和監(jiān)聽器,剩下的就是發(fā)布事件,然后讓相應(yīng)的監(jiān)聽器監(jiān)聽并處理。

通常情況,我們會有一個事件發(fā)布者,它本身作為事件源,在合適的時機(jī),將相應(yīng)的事件發(fā)布給對應(yīng)的事件監(jiān)聽器:

public?class?MethodMonitorEventPublisher?{
?????private?List?listeners?=?new?ArrayList();??????????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)聽器被移除,這里為了安全做一個復(fù)制操作 List copyListeners = ? new ArrayList(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(); } // 省略實現(xiàn) public void addEventListener(MethodMonitorEventListener listener) {} public void removeEventListener(MethodMonitorEventListener listener) {} public void removeAllListeners() {}

對于事件發(fā)布者(事件源)通常需要關(guān)注兩點:

1. 在合適的時機(jī)發(fā)布事件。此例中的methodMonitor()方法是事件發(fā)布的源頭,其在方法執(zhí)行之前和結(jié)束之后兩個時間點發(fā)布MethodMonitorEvent事件,每個時間點發(fā)布的事件都會傳給相應(yīng)的監(jiān)聽器進(jìn)行處理。

在具體實現(xiàn)時需要注意的是,事件發(fā)布是順序執(zhí)行,為了不影響處理性能,事件監(jiān)聽器的處理邏輯應(yīng)盡量簡單。

2. 事件監(jiān)聽器的管理。publisher類中提供了事件監(jiān)聽器的注冊與移除方法,這樣客戶端可以根據(jù)實際情況決定是否需要注冊新的監(jiān)聽器或者移除某個監(jiān)聽器。

如果這里沒有提供remove方法,那么注冊的監(jiān)聽器示例將一直MethodMonitorEventPublisher引用,即使已經(jīng)廢棄不用了,也依然在發(fā)布者的監(jiān)聽器列表中,這會導(dǎo)致隱性的內(nèi)存泄漏。

Spring容器內(nèi)的事件監(jiān)聽機(jī)制

Spring的ApplicationContext容器內(nèi)部中的所有事件類型均繼承自org.springframework.context.AppliationEvent,容器中的所有監(jiān)聽器都實現(xiàn)org.springframework.context.ApplicationListener接口,并且以bean的形式注冊在容器中。

一旦在容器內(nèi)發(fā)布ApplicationEvent及其子類型的事件,注冊到容器的ApplicationListener就會對這些事件進(jìn)行處理。

你應(yīng)該已經(jīng)猜到是怎么回事了。

ApplicationEvent繼承自EventObject,Spring提供了一些默認(rèn)的實現(xiàn),比如:

ContextClosedEvent表示容器在即將關(guān)閉時發(fā)布的事件類型,ContextRefreshedEvent

表示容器在初始化或者刷新的時候發(fā)布的事件類型......容器內(nèi)部使用ApplicationListener作為事件監(jiān)聽器接口定義,它繼承自EventListener。

ApplicationContext容器在啟動時,會自動識別并加載EventListener類型的bean一旦容器內(nèi)有事件發(fā)布,將通知這些注冊到容器的EventListener。

ApplicationContext接口繼承了ApplicationEventPublisher接口,該接口提供了void publishEvent(ApplicationEvent event)方法定義,不難看出,ApplicationContext容器擔(dān)當(dāng)?shù)木褪鞘录l(fā)布者的角色。

如果有興趣可以查看AbstractApplicationContext.publishEvent(ApplicationEvent event)方法的源碼:ApplicationContext將事件的發(fā)布以及監(jiān)聽器的管理工作委托給 ApplicationEventMulticaster接口的實現(xiàn)類。

在容器啟動時,會檢查容器內(nèi)是否存在名為applicationEventMulticaster的 ApplicationEventMulticaster對象實例。

如果有就使用其提供的實現(xiàn),沒有就默認(rèn)初始化一個SimpleApplicationEventMulticaster作為實現(xiàn)。

最后,如果我們業(yè)務(wù)需要在容器內(nèi)部發(fā)布事件,只需要為其注入ApplicationEventPublisher 依賴即可:實現(xiàn)ApplicationEventPublisherAware接口或者ApplicationContextAware接口.

五、出神入化:揭秘自動配置原理

典型的Spring Boot應(yīng)用的啟動類一般均位于src/main/java根路徑下

比如MoonApplication類:

@SpringBootApplication public class MoonApplication {      public static void main(String[] args) {         SpringApplication.run(MoonApplication.class, args);     } }

其中@SpringBootApplication開啟組件掃描和自動配置,而SpringApplication.run則負(fù)責(zé)啟動引導(dǎo)應(yīng)用程序。

@SpringBootApplication是一個復(fù)合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框架的注解,標(biāo)明該類是一個JavaConfig配置類。

而@ComponentScan啟用組件掃描,前文已經(jīng)詳細(xì)講解過,這里著重關(guān)注@EnableAutoConfiguration。@EnableAutoConfiguration注解表示開啟Spring Boot自動配置功能,Spring Boot會根據(jù)應(yīng)用的依賴、自定義的bean、classpath下有沒有某個類 等等因素來猜測你需要的bean,

然后注冊到IOC容器中。

那@EnableAutoConfiguration是如何推算出你的需求?

首先看下它的定義:

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import(EnableAutoConfigurationImportSelector.class)public?@interface?EnableAutoConfiguration?{     // ...... }

你的關(guān)注點應(yīng)該在@Import(EnableAutoConfigurationImportSelector.class)上了,前文說過,@Import注解用于導(dǎo)入類,并將這個類作為一個bean的定義注冊到容器中,這里將把EnableAutoConfigurationImportSelector作為bean注入到容器中,而這個類會將所有符合條件的@Configuration配置都加載到容器中,看看它的代碼:

public String[] selectImports(AnnotationMetadata annotationMetadata) {     // 省略了大部分代碼,保留一句核心代碼     // 注意:SpringBoot最近版本中,這句代碼被封裝在一個單獨的方法中     // SpringFactoriesLoader相關(guān)知識請參考前文     List factories = new ArrayList(new LinkedHashSet(           SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, this.beanClassLoader))); }

這個類會掃描所有的jar包,將所有符合條件的@Configuration配置類注入的容器中何為符合條件,看看META-INF/spring.factories的文件內(nè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是如何自動配置的:

@Configuration@ConditionalOnClass({?DataSource.class,?EmbeddedDatabaseType.class?})@EnableConfigurationProperties(DataSourceProperties.class)@Import({?Registrar.class,?DataSourcePoolMetadataProvidersConfiguration.class?})public?class?DataSourceAutoConfiguration?{ }

分別說一說:

@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }):當(dāng)Classpath中存在DataSource或者EmbeddedDatabaseType類時才啟用這個配置,否則這個配置將被忽略。

@EnableConfigurationProperties(DataSourceProperties.class):將DataSource的默認(rèn)配置類注入到IOC容器中,DataSourceproperties定義為:

// 提供對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ù)庫連接池提供者的一個配置類,即Classpath中存在org.apache.tomcat.jdbc.pool.DataSource.class,則使用tomcat-jdbc連接池,如果Classpath中存在HikariDataSource.class則使用Hikari連接池。

這里僅描述了DataSourceAutoConfiguration的冰山一角,但足以說明Spring Boot如何利用條件話配置來實現(xiàn)自動配置的。

回顧一下,@EnableAutoConfiguration中導(dǎo)入了EnableAutoConfigurationImportSelector類,而這個類的selectImports()通過SpringFactoriesLoader得到了大量的配置類,而每一個配置類則根據(jù)條件化配置來做出決策,以實現(xiàn)自動配置。

整個流程很清晰,但漏了一個大問題:

EnableAutoConfigurationImportSelector.selectImports()是何時執(zhí)行的?其實這個方法會在容器啟動過程中執(zhí)行:AbstractApplicationContext.refresh(),更多的細(xì)節(jié)在下一小節(jié)中說明。

六、啟動引導(dǎo):Spring Boot應(yīng)用啟動的秘密

6.1 SpringApplication初始化

SpringBoot整個啟動流程分為兩個步驟:初始化一個SpringApplication對象、執(zhí)行該對象的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項目      this.webEnvironment = deduceWebEnvironment();      setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));      setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));      // 找到入口類      this.mainApplicationClass = deduceMainApplicationClass(); }

初始化流程中最重要的就是通過SpringFactoriesLoader找到spring.factories文件中配置的ApplicationContextInitializer和ApplicationListener兩個接口的實現(xiàn)類名稱,以便后期構(gòu)造相應(yīng)的實例。

ApplicationContextInitializer的主要目的是在ConfigurableApplicationContext做refresh之前,對ConfigurableApplicationContext實例做進(jìn)一步的設(shè)置或處理。

ConfigurableApplicationContext繼承自ApplicationContext,其主要提供了對ApplicationContext進(jìn)行設(shè)置的能力。

實現(xiàn)一個ApplicationContextInitializer非常簡單,因為它只有一個方法,但大多數(shù)情況下我們沒有必要自定義一個ApplicationContextInitializer,即便是Spring Boot框架,它默認(rèn)也只是注冊了兩個實現(xiàn),畢竟Spring的容器已經(jīng)非常成熟和穩(wěn)定,你沒有必要來改變它。

而ApplicationListener的目的就沒什么好說的了,它是Spring框架對Java事件監(jiān)聽機(jī)制的一種框架實現(xiàn),具體內(nèi)容在前文Spring事件監(jiān)聽機(jī)制這個小節(jié)有詳細(xì)講解。這里主要說說,如果你想為Spring Boot應(yīng)用添加監(jiān)聽器,該如何實現(xiàn)?

Spring Boot提供兩種方式來添加自定義監(jiān)聽器:

通過SpringApplication.addListeners(ApplicationListener... listeners)或者SpringApplication.setListeners(Collection> listeners)兩個方法來添加一個或者多個自定義監(jiān)聽器

既然SpringApplication的初始化流程中已經(jīng)從spring.factories中獲取到ApplicationListener的實現(xiàn)類,那么我們直接在自己的jar包的META-INF/spring.factories文件中新增配置即可:

org.springframework.context.ApplicationListener=\ cn.moondev.listeners.xxxxListener\

關(guān)于SpringApplication的初始化,我們就說這么多。

6.2 Spring Boot啟動流程.

Spring Boot應(yīng)用的整個啟動流程都封裝在SpringApplication.run方法中,其整個流程真的是太長太長了,但本質(zhì)上就是在Spring容器啟動的基礎(chǔ)上做了大量的擴(kuò)展,按照這個思路來看看

源碼:

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通過調(diào)用starting()方法通知所有的SpringApplicationRunListeners:應(yīng)用開始啟動了。

SpringApplicationRunListeners其本質(zhì)上就是一個事件發(fā)布者,它在SpringBoot應(yīng)用啟動的不同時間點發(fā)布不同應(yīng)用事件類型(ApplicationEvent),如果有哪些事件監(jiān)聽者(ApplicationListener)對這些事件感興趣,則可以接收并且處理。

還記得初始化流程中,SpringApplication加載了一系列ApplicationListener嗎?這個啟動流程中沒有發(fā)現(xiàn)有發(fā)布事件的代碼,其實都已經(jīng)在SpringApplicationRunListeners這兒實現(xiàn)了。

簡單的分析一下其實現(xiàn)流程,首先看下SpringApplicationRunListener的源碼:

public interface SpringApplicationRunListener {?????//?運行run方法時立即調(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只有一個實現(xiàn)類:EventPublishingRunListener。

①處的代碼只會獲取到一個EventPublishingRunListener的實例

我們來看看starting()方法的內(nèi)容:

public void starting() {     // 發(fā)布一個ApplicationStartedEvent     this.initialMulticaster.multicastEvent(new ApplicationStartedEvent(this.application, this.args)); }

順著這個邏輯,你可以在②處的prepareEnvironment()方法的源碼中找到

listeners.environmentPrepared(environment);

即SpringApplicationRunListener接口的第二個方法,那不出你所料,environmentPrepared()又發(fā)布了另外一個事件ApplicationEnvironmentPreparedEvent。

接下來會發(fā)生什么,就不用我多說了吧。

② 創(chuàng)建并配置當(dāng)前應(yīng)用將要使用的Environment,Environment用于描述應(yīng)用程序當(dāng)前的運行環(huán)境,其抽象了兩個方面的內(nèi)容:

配置文件(profile)和屬性(properties),開發(fā)經(jīng)驗豐富的同學(xué)對這兩個東西一定不會陌生:不同的環(huán)境(eg:生產(chǎn)環(huán)境、預(yù)發(fā)布環(huán)境)可以使用不同的配置文件,而屬性則可以從配置文件、環(huán)境變量、命令行參數(shù)等來源獲取。

因此,當(dāng)Environment準(zhǔn)備好后,在整個應(yīng)用的任何時候,都可以從Environment中獲取資源。

總結(jié)起來,②處的兩句代碼,主要完成以下幾件事:

判斷Environment是否存在,不存在就創(chuàng)建(如果是web項目就創(chuàng)建StandardServletEnvironment,否則創(chuàng)建StandardEnvironment)

配置Environment:配置profile以及properties

調(diào)用SpringApplicationRunListener的environmentPrepared()方法,通知事件監(jiān)聽者:應(yīng)用的Environment已經(jīng)準(zhǔn)備好

③、SpringBoot應(yīng)用在啟動時會輸出這樣的東西:

. ____          _            __ _ _  /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \  \\/ ___)| |_)| | | | | || (_| | ) ) ) )   ' |____| .__|_| |_|_| |_\__, | / / / /  =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.5.6.RELEASE)

如果想把這個東西改成自己的涂鴉,你可以研究以下Banner的實現(xiàn),這個任務(wù)就留給你們吧。

④、根據(jù)是否是web項目,來創(chuàng)建不同的ApplicationContext容器。

⑤、創(chuàng)建一系列FailureAnalyzer,創(chuàng)建流程依然是通過SpringFactoriesLoader獲取到所有實現(xiàn)FailureAnalyzer接口的class,然后在創(chuàng)建對應(yīng)的實例。FailureAnalyzer用于分析故障并提供相關(guān)診斷信息。

⑥、初始化ApplicationContext,主要完成以下工作:

將準(zhǔn)備好的Environment設(shè)置給ApplicationContext

遍歷調(diào)用所有的ApplicationContextInitializer的initialize()方法來對已經(jīng)創(chuàng)建好的ApplicationContext進(jìn)行進(jìn)一步的處理

調(diào)用SpringApplicationRunListener的contextPrepared()方法,通知所有的監(jiān)聽者:ApplicationContext已經(jīng)準(zhǔn)備完畢

將所有的bean加載到容器中

調(diào)用SpringApplicationRunListener的contextLoaded()方法,通知所有的監(jiān)聽者:ApplicationContext已經(jīng)裝載完畢

⑦、調(diào)用ApplicationContext的refresh()方法,完成IoC容器可用的最后一道工序。

從名字上理解為刷新容器,那何為刷新?就是插手容器的啟動,聯(lián)系一下第一小節(jié)的內(nèi)容。

獲取到所有的BeanFactoryPostProcessor來對容器做一些額外的操作。

BeanFactoryPostProcessor允許我們在容器實例化相應(yīng)對象之前,對注冊到容器的BeanDefinition所保存的信息做一些額外的操作。

這里的getBeanFactoryPostProcessors()方法可以獲取到3個Processor:

ConfigurationWarningsApplicationContextInitializer$ConfigurationWarningsPostProcessorSharedMetadataReaderFactoryContextInitializer$CachingMetadataReaderFactoryPostProcessorConfigFileApplicationListener$PropertySourceOrderingPostProcessor

不是有那么多BeanFactoryPostProcessor的實現(xiàn)類,為什么這兒只有這3個?

因為在初始化流程獲取到的各種ApplicationContextInitializer和ApplicationListener中,只有上文3個做了類似于如下操作:

public void initialize(ConfigurableApplicationContext context) {     context.addBeanFactoryPostProcessor(new ConfigurationWarningsPostProcessor(getChecks())); }

然后你就可以進(jìn)入到PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors()方法了,這個方法除了會遍歷上面的3個BeanFactoryPostProcessor處理外,還會獲取類型為BeanDefinitionRegistryPostProcessor的bean:org.springframework.context.annotation.internalConfigurationAnnotationProcessor,對應(yīng)的Class為ConfigurationClassPostProcessor。ConfigurationClassPostProcessor用于解析處理各種注解,包括:@Configuration、@ComponentScan、@Import、@PropertySource、@ImportResource、@Bean。當(dāng)處理@import注解的時候,就會調(diào)用這一小節(jié)中的EnableAutoConfigurationImportSelector.selectImports()來完成自動配置功能。其他的這里不再多講,如果你有興趣,可以查閱參考資料6。

⑧、查找當(dāng)前context中是否注冊有CommandLineRunner和ApplicationRunner,如果有則遍歷執(zhí)行它們。

⑨、執(zhí)行所有SpringApplicationRunListener的finished()方法。這就是Spring Boot的整個啟動流程,其核心就是在Spring容器初始化并啟動的基礎(chǔ)上加入各種擴(kuò)展點,這些擴(kuò)展點包括:ApplicationContextInitializer、ApplicationListener以及各種BeanFactoryPostProcessor等等。

你對整個流程的細(xì)節(jié)不必太過關(guān)注,甚至沒弄明白也沒有關(guān)系,你只要理解這些擴(kuò)展點是在何時如何工作的,能讓它們?yōu)槟闼眉纯伞?/span>

整個啟動流程確實非常復(fù)雜,可以查詢參考資料中的部分章節(jié)和內(nèi)容,對照著源碼,多看看,我想最終你都能弄清楚的。言而總之,Spring才是核心,理解清楚Spring容器的啟動流程,那Spring Boot啟動流程就不在話下了。

---END---
文末福利



瀏覽 74
點贊
評論
收藏
分享

手機(jī)掃一掃分享

分享
舉報
評論
圖片
表情
推薦
點贊
評論
收藏
分享

手機(jī)掃一掃分享

分享
舉報

感谢您访问我们的网站,您可能还对以下资源感兴趣:

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 北条麻妃人妻中文字幕91影视| 欧美性精品| 中文子幕免费毛片| 亚洲国产视频一区| 中文原创麻豆传媒md0052| 操操操操操操| 操人妻| 7799综合| 欧美大屌网站| 亚洲性爱片| 无码人妻一区二区| 成人亚洲AV日韩AV无码| 国产视频一区二区在线观看| 国产精品1区2区3区| 欧洲成人无码| 四川少扫搡BBw搡BBBB| 天堂俺去俺来也www久久婷婷| 97超碰碰碰| 欧美乱欲视频| 欧美A级视频在线观看| 国产高清在线观看| 黄色激情视频网站| 欧美亚洲日韩一区二区三区| 182在线视频| 在线三级av| 欧美成人不卡| 亚洲美眉综合网| 五月天婷婷在线观看| 精品四区| 日韩在线视频一区二区三区| 操逼逼片| 欧美一级生活片| 亚洲AV一二三| 视频一区乳奴| 樱桃码一区二区三区| 波多野结衣亚洲| 暖暖在线视频| 91福利视频在线观看| 国产精品理论片| 亚洲av高清| 精品国产乱子伦一区二区三区,小小扐| 成人无码区免费A片久久鸭| 大香蕉在线精品视频| h片在线看| 久久99国产乱子伦...| 久久精品苍井空免费一区| a片在线免费| 欧美东京热视频| 无码a区天堂| 综合久久久| 暗呦网一区二区三区| 亚洲AV秘无码苍井空| 婷婷综合素质二区| 免费观看在线无码视频| 国产一道本| 国产a片视频| 蜜臀AV成人| 黄色内射在线播放| 尤物视频在线播放| 激情91| h片在线观看免费| 操片| 在线无码一区二区三区| 亚洲激情五月| 欧美色视频一区二区三区在线观看| 日本黄色免费在线观看| 北条麻妃中文字幕旡码| 午夜理论在线| 五月色婷婷综合| 很色很黄的A片一| 懂色成人Av| 人人妻人人爱| 91欧美日韩| 加勒比精品| 五月婷婷色色| 天天添天天干| 欧美老熟妇乱大交XXXXX| 欧美精品第一页| 麻豆射区| 免费做a爰片77777| 欧美亚洲综合在线| 日韩综合在线视频| 亚洲小视频在线播放| 青青草成人在线观看| 国产人妖网站| 国内精品久久久久久久久久变脸| 欧美AAA大片| 一级片网址| 日韩极品在线观看| 国产又粗又长又硬又大毛苴茸图片 | 嫩草视频| 四川w搡BBB搡wBBB搡| 久久久久久AV| 激情乱伦五月天| 亚洲AV无码电影| 亚洲AV永久无码精品| 日韩午夜成人电影| 干干日日| 日韩a电影| 日韩成人无码人妻| 精品乱子伦一区二区三区免费播放 | 91精品老司机| 亚洲精品成人片在线观看精品字幕 | 亚洲图片欧美色图| 操B在线| 中文字幕精品视频| 91狠狠综合久久| 蜜桃免费视频| 青青国产视频| 一级AA片| 免费操逼| 丁香婷婷男人天堂| 懂色成人av影院| 国产精品HongKong麻豆| 一区二区三区国产视频| 欧美国产成人在线| 美女啪啪网站| 亚洲中文字幕播放| 欧美XXXXBBBB| 先锋资源男人站| 婷婷激情六月| 日日干网| 蜜桃影院| 亚洲AV成人一区二区三区不卡 | 亚洲国产精品尤物yw在线观看| 日日摸日日添日日躁AV| 日本中文字幕视频| 91三级片在线观看| 中文字幕在线观看有码| 中文字幕黄色| 青青在线免费视频| 在线观看日韩av| 中文有码在线观看| 丰满人妻一区二区| 嗯啊av| 日韩免费视频一区二区| 伊人久久大香| 日本无码一区二区三三| 天天视频色版免费观看视频| 日韩黄色电影在线免费观看 | 一本到在线观看午夜剧场| 国产和日韩中文字幕| 欧美区在线观看| 西西444WWW无码视频软件| 欧美性爱在线网站| 色片免费| 91亚洲免费视频| 日本免费无码| AV大全在线观看| www天天干| 日韩人妻精品无码制服| 伊人大香蕉在线观看| 大香蕉久久视频| 国产探花视频在线免费观看| 手机在线成人视频| 亚洲无码你懂的| 国产日逼片| 亚洲男同Gay一区二区| 天天干妹子| 密臀av在线| 内射一区| 成人做爰A片AAA毛真人| 国产性爱网站| 91av天堂| 精品国产一二三区| 国产精品同| 欧美婬乱片A片AAA毛片地址| 在线观看免费国产| 黄色成人网站免费在线观看| 最新中文字幕在线| 北条麻妃一区二区三区-免费免费高清观看 | 久草国产视频| 极品人妻疯狂3p超刺激| 苍井空无码| 国产中文| 欧美A一| 黄色小说在线播放| 91精品网| 97人妻天天摸天天爽天天| 免费高潮视频| 黄色片网站在线观看| 一级黄片免费看| 69午夜| 欧美一区二区三区成人| 无码乱码在线观看| 男女做爱网站| 婷婷久久久久| 国产第二页| 日韩人妻电影| 成人视频毛片| 永久AV免费网站| 欧美嗯啊| 操人人| 午夜精品视频| 日本黄色精品| 国产老女人农村HD| 天天艹夜夜| 91人人妻人人澡| 欧一美一婬一伦一区二区三区自慰 | 老女人网站| 羞羞视频com.入口| 中文字幕永久免费| 亚洲免费MV| 欧美大鸡巴视频| 中文熟妇| 人妻体内射精一区二区三区| 成人无码区免费AV毛片| 久久精品秘一区二区三免费| 天天综合7799| 日本爱爱网站| 成人做爰100片免费-百度| 九月婷婷综合| 蜜臀久久99精品久久久久久婷婷| 中文字幕乱码中文字幕| 中文字幕AV在线观看| 超碰91在线| 久久久久久黄片| 久操网在线| 无码AV中文字幕| 性少妇| 先锋影音一区二区三区| 欧美视频在线观看一区| 强奸乱伦五月天| 永久免费视频| 国产欧美综合视频一区二区在线| AV婷婷在线| 少妇搡BBBB搡BBB搡造水多/ | 中文字幕乱妇无码Av在线| 国产婷婷色一区二区| 亚洲人妖在线| 密臀久久| 久热精品视频在线观看| 国产欧美日本视频| 骚逼日本| 色婷婷六月天| 日韩欧美三级| free性欧美| 国产成人中文字幕| 91无码在线视频| 欧美一级日韩一级| 国产高清无码免费视频| 精品黄色视频| 日本A片免费看| 黑人操逼| 亚洲videos| 欧美自拍视频在线| 久久久久免费视频| 国产成人a亚洲精品www| 99久久国产精| 天天综合天天做天天综合| 狠狠干综合| 美女十八禁| www.五月丁香| 久草手机在线视频| 日日骚中文字幕| av网站免费在线观看| av岛国免费| 激情五月综合| 青青草视频免费在线观看| 久草视频免费看| 一级片无码| 国产精品外围| 日本黄色视频网| 中文在线字幕高清电视剧| 波多野结衣网站| 在线观看中文字幕无码| 91jiujiu| 日韩在线视频观看| 免费欧美成人网站| 亚洲性天堂| 国产成人三级| 欧美精品在线免费观看| 中文在线字幕免费观| 日韩成人无码视频| 日本家庭乱伦视频| 日韩无码高清一区| 免费福利在线观看| av色欲| 亚洲欧美第一页| 夜夜国自一区| 青草五月天| 激情爱爱网站| 亚洲毛片网| 麻豆艾秋MD0056在线| 成人美女视频| 久久五月天婷婷| 亚洲AV无码成人精品区东京热| 99精品国自产在线| 熟妇槡BBBB槡BBBB| 人人爱人人看| 91大香蕉视频| 69超碰| 天天毛片| 四川婬妇BBw搡BBBB搡| 在线观看免费黄色视频| 色色网的五月天| 日韩无码免费播放| 国产黄色免费网站| 97精品人妻一区二区三区香蕉农| 加勒比无码在线播放| 麻豆一区二区| 中国一级片| 亚洲一区二区三区在线| 黄色一级aa片| 国产精品91在线| 天天日综合| 久久99视频免费观看| 精品一区二区久久久久久久网站 | 99久操| 日本成人网址| 成人无码视频| 亚洲有码在线视频| 日韩人妻精品无码制服| 色哟哟视频在线观看| 中文成人无字幕乱码精品区| 91精东传媒果冻传媒| 台湾精品一区二区三区| 日韩免费无码视频| 日本无码片| 九九九九国产| 黄色免费网站| 免费无码婬片AAAA片在线蜜芽 | 97午夜福利视频| 极品AV| 日本黄色电影网站| 天天干免费视频| 99在线精品视频免费观看软件| 五月花在线视频| 俺来也在线视频| 蜜桃在线视频| 国产91探花精品一区二区| 天天日天天干天天操| 日韩成人无码精品| 成人国产精品视频| 中文字幕成人av| 成年人视频在线免费观看| 黄色操逼网站?| 亚洲精品18禁| 欧美激情在线| 婷婷五月丁香在线| 青青草精品| 免费的a片| 爱搞搞视频| 成人蜜臀AV| 不卡不在线中文| 就去色色五月丁香婷婷久久久| 尤物网站在线播放| 97福利视频| 天天干天天操天天爽| 九九视频免费在线观看| 干老女人逼| 翔田千里一区二区| www.色中色| 伊人九九热| 午夜成人福利视频在线观看| 亚洲成人视频在线免费观看| 操逼视频免费看| 欧美熟妇BBB搡BBB| 日本黄色视频免费看| 好吊妞视频在线| 黄色片A| 蜜桃视频com.www| 日韩电影免费在线观看中文字幕| 久久久久久久9999| 亚洲欧美动漫| 日韩中文字幕在线高清| 日本免费黄色片| 91免费观看网站| 香蕉久久a毛片| 天天日天天日天天操| 日逼综合网| 99久久人妻无码中文字幕系列| 国产A片视频| 国产精品国产三级国产AⅤ| 免费一区| 日韩欧美三级在线| 天天操天| 天天综合天天| 蜜桃av秘无码一区三区四| 国产网友自拍| 77Q视频| 婷婷色五月激情| 欧美mv日韩mv国产网站| 北京熟妇搡BBBB搡BBBB| 女人久久| xxxxxbbbbb| 国产一级a毛一级a| 亚洲无码人妻视频| 一本道不卡色色| 成人午夜福利视频| 日韩精品一区二区三区四区| 激情无码一区二区| 体内射精免费视频| av无码中文| 欧美日韩亚洲一区二区三区| 老司机av| 思思热免费视频| 人妻视频网站| 学生妹一级片内射视频| 无码视频播放| 亚洲AV无码精品久久一区二区| 欧美精品A级片| 在线看片A| 亚洲性爱一区二区三区| 三级久久久| 亚洲AV无码成人精品区www| 久久久婷婷婷| 99黄网| eeuss| 中文字幕在线免费播放| 影音先锋AV资源网站| 亚洲图片一区| 99热官网| 欧美色道| 少妇搡BBBB搡BBB搡AA| 欧美精品A级片| 国产A片网站| 黄色电影免费网站| 日韩无码免费看| 国产成人免费观看视频| 亚洲无码网| 91ccc| 韩国高清无码| 亚洲黄色免费| 国产精品色呦呦| 91久久国产综合| 精品国产区一区二| 91在线视频| 爱爱视频h| 免费看黄色大全| 99在线视频免费观看| 久久久久久久伊人| 亚洲色播放| 国产成人精品123区免费视频| 午夜福利h| 91乱子伦国产乱子伦!| 国产精品视频在线免费观看 | a在线| 亚洲欧美日韩激情| 91在线免费看| 日本激情网| 亚洲激情自拍| 国产人国产视频成人免费观看…| 亚洲中文字幕高清| 欧美成人精品激情在线观看| 成人视频网站在线观看| 欧美一级黄色大片| www.黄色大片| 69国产精品成人无码| 超碰九九热| 操逼视频免费| 免费操逼视频在线观看| 欧美色图在线观看视频| 国产精品一级| 老熟妇一区二区三区啪啪| 高清无码一区二区在线| 亚洲精品久久久久久| 超碰免费91| 蜜桃视频免费网站| 久久精品视频在线观看| 最新黄色av| 中文字幕无码精品三级在线欧美| 插菊花综合网2| 青草青草视频| 日本在线www| 欧美成人猛片AAAAAAA| 欧美操逼在线| 日韩综合不卡| 婷婷五月天激情小说| 久久婷婷六月| 国产一区二区三区视频在线| 大地二中文在线观看免费鲁大师| 91无码成人| 国产午夜福利免费视频在线观看 | 国产成人片色情AAAA片| 亚洲AV毛片| 国产精品一色哟哟哟| 中文字幕日日| 日韩人妻一区| 久久国产精品免费视频| 久草a视频| 欧美日韩精品在线观看| 亚洲日韩欧美一区二区天天天 | 69成人| 天堂A片电影网站在线观看| 91网站免费看| 高清无码视频免费在线观看| 黄色带亚州| 91在线无码精品秘入口动作| 黄色无码av| 黄色一级片在线看| 18禁免费网站| 久久久久逼| 天天操夜夜干| 成人AV在线一区二区| 国产在线成人| 亚洲最大福利视频| AV在线资源观看| 详情:绿帽夫妻多人运动开淫啪-91n| 亚洲中文字幕视频在线观看| 中文字幕AV第一页| 欧美一级AAA大片免费观看 | 91国产精品视频在线| 操b在线观看| 欧美精品在线观看视频| 天天艹天天| 韩国色情中文字幕| 午夜在线观看视频18| 狠狠干狠狠爱| 亚洲字幕AV| 特黄AAAAAAAA片视频| 怡红院男人天堂| 亚洲性爱小说| av天堂中文| 久久精品禁一区二区三区四区五区 | 亚洲一区二区成人| 青青草国产在线视频| 亚洲黄色视频网站| 中文字幕+乱码+中文乱码电影| 蜜臀久久99精品久久久| 亚洲天堂视频网| 人人爽人人操| 黑人无码AV黑人天堂无码AV| 69人妻人人澡人人爽久久| 天天爱夜夜爱| 日本中文字幕在线| 国产毛片一区二区三区| 亚洲精品69| 国产久久性爱| 亚洲一级av无码毛片精品| 乱伦91| 久久久久黄色| 国产免费无码视频| 天堂中文资源在线观看| 看一级黄色毛片| 日韩黄色小说| 国产一级婬片A片免费无成人黑豆| 国产资源AV| 日韩性无码| 无码天堂| 淫香淫色综合网| 久久婷婷网| 成人做爰黄片视频免费| 成人黄色视频网| 无码a区天堂| 精精品人妻一区二区三区| 国产嫩BBwBBw高潮| AV四虎| 囯产精品久久| 天天舔| 日韩操逼网| 麻豆国产一区二区三区四区| 午夜福利国产| 日韩精品免费一区二区在线观看| 亚洲在线视频| 国产精品操逼视频| 男女www视频| 丝袜一区二区三区| 在线播放高清无码| 精品乱子伦一区二区三区下载| 国产黄色视频网站| 99无码视频| 安徽妇搡BBBB搡BBBB| 狠狠色五月亚洲91| 国产女人十八水真多| av国产精品| 无码免费观看| 国产精品性爱| 日韩日屄视频| 亚洲精品69| 日本韩国欧美18| 四川性BBB搡BBB爽爽爽小说| 欧美自拍视频| 操碰在线视频| 麻豆精品久久久久久久99蜜桃| 国产综合久久久7777777| 懂色AV一区二区三区国产中文在线 | 欧美不卡| 老熟女-ThePorn| 黄色国产免费| 日韩十八禁网站| 特级西西人体www高清大胆| 91麻豆精品在线| av干在线| 日日干日日操| 怡春院成人| 久热国产精品| 亚洲免费观看高清完整版在va线观看 | 欧美激情一级| 91AV一区二区三区| 欧美第一网站| 婷婷五月天激情俺来也| 天堂中文在线观看| 中文字幕日韩在线观看| 日韩性爱AV| 九九九九色| 一级a免一级a做片免费| 日韩在线女优天天干| 一本大道久久久久| 亚洲va欧美va天堂v国产综合| 婷婷丁香五月激情一区综合网 | 操欧美女人| 国产清纯可爱美女自卫裸贷偷情 | 日韩视频免费| 亚洲视频中文字幕| 麻豆亚洲AV成人无码久久精品 | 午夜成人小视频| 亚洲综合区| 亚洲操操操| 色婷婷一区| 亚洲午夜无码| 台湾成人综合网| 五月婷婷六月香| 国产精品成人免费视频| 西西人体视频| 国产在线播放av| 国产一级婬乱片免费| 欧美一级操逼视频| 噜噜噜噜射| 成人AV在线一区二区| 按摩忍不住BD中文字幕| 国产xxxx| a片在线免费观看| 欧美色婷婷| 男人AV在线| AV无码在线免费观看| 欧美三级推荐| 一级欧美黑人大战白妞| 国产中文字幕波多| 少妇人妻av| 伊人久久五月| 人人妻人人躁人人DVD| 无码在线视频播放| 欧美大鸡巴视频| 99AV| 三级国产网站| 一级国产黄色视频| 中国无码专区| 亚洲一区中文字幕成人在线| 伊人网站| 操逼逼视频| 亚洲三级无码| av无码在线观看| 五月天婷婷丁香| 露脸丨91丨九色露脸| 在线观看国产免费视频| 特级无码| 亚洲天堂免费| 岛国精品在线播放| 无码AV在线观看| 免费在线看a| 午夜男女福利| 日本十八禁网站| 亚洲永久视频| 精品午夜福利| 亚洲AV无码免费| 一区二区三区免费看| 免费乱伦视频| 无码在线免费视频| 丰满人妻一区二区三区不卡二| 丁香五月激情小说| 成人黄片免费| 99久在线精品99re8热| 久久精品成人| 99在线观看精品视频| 北条麻妃无码精品AV| 少妇黄色视频| 自拍偷拍一区| 爱爱黄色视频| 国产又爽又黄A片| 在线免费看黄色视频| 先锋影音成人| 麻豆亚洲AV成人无码久久精品 | 亚洲日韩精品在线视频| 爱爱免费视频| 黄色激情av| 亚洲久爱| 欧美a片在线看| www日韩欧美| 久久国产精品电影| 久久婷婷五月丁香| 日本免费黄色小视频| 人与禽一级A片一区二区三区| 色婷婷18正码国产| 免费成人高清视频| 国产l精品久久久久久久久久| 久99久视频| 欧洲精品视频在线观看| 喷潮视频| 日本伊人大香蕉| 波多野结衣av在线播放| 亚洲av在线免费观看| 在线看色| 亚洲无码不卡| www.操逼| 韩日美女性爱| 国产色秘乱码一区二区三区| 欧美日韩四区| 欧美操大逼| 五月丁香婷中文字幕| 久久先锋| 韩国成人无码| 国产精品久久久久久久久久久久久久久| 国产做爱视频| 亚洲成人第一页| 美女久久久| 蜜桃AV一区二区三区| 婷婷五月综合网| 亚洲视频一区二区三区| 国产婷婷| www.天天射视频| 老太奶性BBwBBw侧所| 最好看2019中文在线播放电影| 国产精品视频导航| 亚洲AV无码乱码国产精品黑人| 午夜视频在线看| 超碰免费91| 蝌蚪窝免费视频| 奇米97| 国产熟妇毛多久久久久一区 | 国产AV资源| 亚洲搞清视频日本| 91亚洲在线观看| 婷婷日韩在线| 欧美多人| 亚洲精品视频免费观看| 欧美一级黄色大片| 久草视频观看| 成人性爱AV| 亚洲AV无码乱码| 99视频色| 人人操人人看人人干| 国产农村乱婬片A片AAA图片| 男人的天堂青青草| 国产裸体网站| 你懂得在线| 无码视频网| 午夜国产在线| 国产欧美一区在线看| 国产精品啪啪啪| 日本黄色大片网站| 日韩精品小电影| 色视频在线观看免费| 久草中文网| 国产黃色AAA片| 99在线精品视频| 全部免费黄色视频| 七区九区一区在线| 国产粗大| 在线国产91| 亚洲欧洲成人在线| 免费在线观看毛片| 亚洲小电影| 伊人色爱| 影音先锋三级资源| 欧美操逼在线| 国产精品日韩无码| 国产毛片在线看| 国产精品内射| 日韩做爱| 国产一区二区波多野结衣| 青青草大香蕉伊人| 91精品电影| 日本少妇无码| 日韩免费在线| 人妻无码蜜桃视频| 欧美视频自拍| 韩国人妻无码| 五月婷亚洲精品AV天堂| 人人摸人人艹| 中文字幕精品在线免费视频观看视频 | 日韩无码www| 国产福利网站| 精品蜜桃一区内容| 国产性受XXXXXYX性爽| 色噜噜人妻av中文字幕| 国产成人免费做爰视频| 欧美sese| 无码视频在线| 插入综合网| 国产一道本| 翔田千里无码播放| 亚洲无码专区在线| 日韩精品在线播放| www黄片视频| 国产色拍| 欧美性爱天天操| 久久久久久精| 无码视频在线| 射射AV| 日韩在线观看网址| 亚洲精品色| 日韩成人无码视频| 蜜桃免费AV| 777米奇视频| 日本中文字幕亚洲| 美日韩中文字幕| 黄色无码视频| 中文字幕高清视频| 日无码视频| 国产情侣在线视频| 97乱伦| 亚洲AV免费在线| 玖玖爱资源站| 一本色道久久综合无码人妻四虎 | 一级日逼视频| 免费无码毛片| 成人国产精品秘在线看| 国产一级AA片| 日本色电影在线观看| 国产精品一区二区毛片A片婊下载| 91在线免费视频| 亚洲国产成人精品女人久久| 骚逼综合网| 一级黄色视频在线观看| 免费观看色情视频| 天天日天天草天天干| 国产第一页在线播放| 人人操人人插| 老太奶性BBwBBw侧所| 九一国产在线| 午夜无码影院| 久久九九99| 久久成人久久爱| 18禁无码永久免费网站大全| 特级西西444www高清| 日韩大码无码| 无码砖区| 九九热超碰| 久久久久久久| 欧美午夜影院| 亚洲电影无码| 国产69精品久久久久久久久久久久| 色五月婷婷五月天激情| 伊人网av| 华女与黑人91A∨| 精品福利一区二区三区| 青青操视频在线| 青春草在线视频观看| 亚洲AV在线人妻| 国产黄片在线播放| 肏屄视频免费| 国产啊啊啊| 日韩免费性爱视频| 人妻黄色视频| 神马午夜精品96| 炮友五月天| 婷婷久久综合久| 99热伊人| 成人尤物网站| 肏屄视频网站| 久久综合中文字幕| 精品乱子伦一区二区三区下载| 亚洲无码人妻视频| 在线无码人妻| 婷婷欧美色图| 无码伦理| 黄色一级录像| 先锋影音亚洲AV每日资源网站| 亚洲人成人无码一区二区三区| 99er热精品视频| 91人兽| 无码精品人妻| 国产成人一区二区三区| 国产传媒av| 婷婷精品免费久久| 成人三级片网站| 国产又粗又大又黄视频| 天天干天天色| 国产精品揄拍一区二区| 免费看黄色录像| 18岁成人毛片| 手机AV在线| 草久在线观看| 一本大道DVD中文字幕| 中文字幕av高清片,中文在线观看| 97一区| 99re在线观看视频| A级片黄色片| 日韩成人影视| 久久久久久亚洲Av无码精品专口| 亲子乱AV一区二区| 俺来也在线视频| 大香蕉视频在线观看| 中文免费高清在线| 俺也去也| 2025AV中文字幕| 国产精品av在线播放| 操人人| 白丝在线观看| 91视频中文字幕| 国产欧美一区二区精品性色超碰| 综合久久视频| 少妇视频一区| 亚洲丁香五月激情| 欧美三p| 亚洲理论片| 成人性生交片无码免费看人| 亚洲无码一二区| 伊人久久综合|