初識 Spring Boot<深度版>

文章較長松申,建議先收藏凿叠,在較空的時(shí)候來看。

Spring Boot 框架的初衷快速地啟動(dòng)Spring應(yīng)用

Spring Boot應(yīng)用本質(zhì)上就是一個(gè) 基于Spring框架 的應(yīng)用, 它是對 Spring" 約定優(yōu)先于配置 "理念的最佳實(shí)踐產(chǎn)物

Spring Boot最重要的四大核心

  • 自動(dòng)配置

  • 起步依賴

  • Actuator

  • 命令行界面

由此可見昆淡,開發(fā)人員對于Spring的理解直接決定了其對Spring Boot的理解锰瘸。故本文前面有四個(gè)小節(jié)關(guān)于Spring的基礎(chǔ)知識,分別是:

  • IoC容器

  • JavaConfig

  • 事件監(jiān)聽

  • SpringFactoriesLoader詳解

如果對于這部分比較熟悉了昂灵,可以直接跳過避凝。

探索Spring IoC容器

Spring IoC容器

BeanDefinition舞萄、BeanDefinitionRegistry & BeanFactory 概念和關(guān)系

<u>BeanDefinition</u>:Bean的具體定義

<u>BeanDefinitionRegistry和BeanFactory</u>存儲了BeanDefinition:

  • BeanDefinitionRegistry抽象出bean的注冊邏輯,
  • BeanFactory 抽象出bean的管理邏輯恕曲,

而各個(gè)BeanFactory的實(shí)現(xiàn)類就具體承擔(dān)了bean的注冊以及管理工作鹏氧。它們之間的關(guān)系就如下圖:

image

DefaultListableBeanFactory作為一個(gè)比較通用的BeanFactory實(shí)現(xiàn),它同時(shí)也實(shí)現(xiàn)了BeanDefinitionRegistry接口佩谣,因此它就承擔(dān)了Bean的注冊管理工作把还。

示例代碼

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

// 默認(rèn)容器實(shí)現(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);
// 如果有多個(gè)bean,還可以指定各個(gè)bean之間的依賴關(guān)系
// ........

// 然后可以從容器中獲取這個(gè)bean的實(shí)例
// 注意:這里的beanRegistry其實(shí)實(shí)現(xiàn)了BeanFactory接口调鬓,所以可以強(qiáng)轉(zhuǎn)艇炎,
// 單純的BeanDefinitionRegistry是無法強(qiáng)制轉(zhuǎn)換到BeanFactory類型的
BeanFactory container = (BeanFactory)beanRegistry;
Business business = (Business)container.getBean("beanName");

IoC容器二階段工作流程

Spring IoC容器的整個(gè)工作流程大致可以分為兩個(gè)階段。(類似于初始化和實(shí)例化)

容器啟動(dòng)階段----> 加載Configuration MetaData

BeanDefinitionReader會對加載的Configuration MetaData進(jìn)行解析和分析腾窝,并將分析后的信息組裝為相應(yīng)的BeanDefinition缀踪,最后把這些保存了bean定義的BeanDefinition健田,注冊到相應(yīng)的BeanDefinitionRegistry鹅龄,這樣容器的啟動(dòng)工作就完成了。

這個(gè)階段主要完成一些準(zhǔn)備性工作岩瘦,更側(cè)重于<u>bean對象管理信息的收集</u>循集。

當(dāng)然一些驗(yàn)證性或者輔助性的工作也在這一階段完成唇敞。

下面的代碼將模擬BeanFactory如何從xml配置文件中加載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)過第一階段疆柔,所有bean定義都通過BeanDefinition的方式注冊到BeanDefinitionRegistry中。

當(dāng)

  1. 某個(gè)請求通過容器的getBean方法請求某個(gè)對象镶柱,

  2. 或者因?yàn)橐蕾囮P(guān)系容器需要隱式的調(diào)用getBean旷档,

    就會觸發(fā)第二階段的活動(dòng)

    容器會首先檢查所請求的對象之前是否已經(jīng)實(shí)例化完成。如果沒有歇拆,則會根據(jù)注冊的BeanDefinition所提供的信息實(shí)例化被請求對象鞋屈,并為其注入依賴。當(dāng)該對象裝配完畢后查吊,容器會立即將其返回給請求方法使用谐区。

BeanFactory只是Spring IoC容器的一種實(shí)現(xiàn)湖蜕,如果沒有特殊指定逻卖,它采用采用延遲初始化策略:只有當(dāng)訪問容器中的某個(gè)對象時(shí),才對該對象進(jìn)行初始化和依賴注入操作昭抒。

另外一種使用更多的容器:ApplicationContext评也,它構(gòu)建在BeanFactory之上炼杖,屬于更高級的容器,除了具有BeanFactory的所有能力之外盗迟,還提供對事件監(jiān)聽機(jī)制以及國際化的支持等坤邪。它管理的bean,在容器啟動(dòng)時(shí)全部完成初始化和依賴注入操作罚缕。

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

IoC容器負(fù)責(zé)管理容器中所有bean的生命周期艇纺,而在bean生命周期的不同階段,Spring提供了不同的擴(kuò)展點(diǎn)來改變bean的命運(yùn)邮弹。

  • BeanFactoryPostProcessor

    在容器實(shí)例化相應(yīng)對象之前黔衡,對beandefinition做一些額外的操作

  • BeanPostProcessor

    在容器實(shí)例化相應(yīng)對象之后,對BeanDefinition做一些額外的操作

簡單的對比腌乡,BeanFactoryPostProcessor處理bean的定義盟劫,而BeanPostProcessor則處理bean完成實(shí)例化后的對象

Bean的整個(gè)生命周期:


image

BeanFactoryPostProcessor

在容器的啟動(dòng)階段与纽,BeanFactoryPostProcessor允許我們在容器實(shí)例化相應(yīng)對象之前侣签,對注冊到容器的BeanDefinition所保存的信息做一些額外的操作,比如修改bean定義的某些屬性或者增加其他信息等急迂,這需要的步驟如下:

1影所、需要實(shí)現(xiàn)org.springframework.beans.factory.config.BeanFactoryPostProcessor接口,
2袋毙、需要實(shí)現(xiàn)org.springframework.core.Ordered接口型檀,以保證BeanFactoryPostProcessor按照順序執(zhí)行

BeanFactoryPostProcessor示例----PropertyPlaceholderConfigurer

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

在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中保存的對象的屬性還是以占位符方式存在的嫉称,比如${jdbc.mysql.url}。

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

BeanPostProcessor

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

BeanPostProcessor定義了兩個(gè)接口:

public interface BeanPostProcessor {
    // 前置處理
    Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
    // 后置處理
    Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}

這兩個(gè)方法中都傳入了bean對象實(shí)例的引用,為擴(kuò)展容器的對象實(shí)例化過程提供了很大便利店展,在這兒幾乎可以對傳入的實(shí)例執(zhí)行任何操作啊犬。

<u>** 注解、AOP ** 等功能的實(shí)現(xiàn)均大量使用了BeanPostProcessor壁查。</u>

BeanPostProcessor 示例一----自定義注解

比如有一個(gè)自定義注解觉至,你完全可以實(shí)現(xiàn)BeanPostProcessor的接口,在其中判斷bean對象的外部是否有該注解睡腿,如果有语御,你可以對這個(gè)bean實(shí)例執(zhí)行任何操作。

BeanPostProcessor 示例二----Aware接口

再來看一個(gè)更常見的例子席怪,在Spring中經(jīng)常能夠看到各種各樣的Aware接口应闯,其作用就是在對象實(shí)例化完成以后將Aware接口定義中規(guī)定的依賴注入到當(dāng)前實(shí)例中。比如最常見的ApplicationContextAware接口挂捻,實(shí)現(xiàn)了這個(gè)接口的類都可以獲取到一個(gè)ApplicationContext對象碉纺。

當(dāng)容器中每個(gè)對象的實(shí)例化過程走到BeanPostProcessor前置處理這一步時(shí),容器會檢測到之前注冊到容器的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);
    }
    // ......
}

JavaConfig & 常見Annotation

JavaConfig

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

<bean id="bookService" class="cn.moondev.service.BookServiceImpl"></bean>

而基于JavaConfig的配置形式是這樣的:

@Configuration
public class MoonBookConfiguration {

    // 任何標(biāo)志了@Bean的方法声怔,其返回值將作為一個(gè)bean注冊到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中則是這樣:

@Configuration
public class MoonBookConfiguration {

    // 如果一個(gè)bean依賴另一個(gè)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();
    }
}

@ComponentScan

@ComponentScan注解對應(yīng)XML配置形式中的<context:component-scan>元素醋火,表示啟用組件掃描悠汽,Spring會自動(dòng)掃描所有通過注解配置的bean,然后將其注冊到IOC容器中芥驳。

我們可以通過basePackages等屬性來指定@ComponentScan自動(dòng)掃描的范圍柿冲。如果不指定,默認(rèn)從聲明@ComponentScan所在類的package進(jìn)行掃描兆旬。正因?yàn)槿绱思俪琒pringBoot的啟動(dòng)類都默認(rèn)在src/main/java下。

@Import

現(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的定義注冊到IOC容器中碍舍。

@Conditional

@Conditional注解表示<u>在滿足某種條件后才初始化一個(gè)bean或者啟用某些配置</u>。

它一般用在由@Component邑雅、@Service片橡、@Configuration等注解標(biāo)識的類上面,或者由@Bean標(biāo)記的方法上淮野。

如果一個(gè)@Configuration類標(biāo)記了@Conditional捧书,則該類中所有標(biāo)識了@Bean的方法和@Import注解導(dǎo)入的相關(guān)類將遵從這些條件。

在Spring里可以很方便的編寫你自己的條件類骤星,所要做的就是實(shí)現(xiàn)Condition接口经瓷,并覆蓋它的matches()方法。舉個(gè)例子洞难,下面的簡單條件類表示只有在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來聲明bean的時(shí)候舆吮,可以使用這個(gè)自定義條件類:

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

這個(gè)例子中只有當(dāng)JdbcTemplateCondition類的條件成立時(shí)才會創(chuàng)建MyService這個(gè)bean。也就是說MyService這bean的創(chuàng)建條件是classpath里面包含JdbcTemplate队贱,否則這個(gè)bean的聲明就會被忽略掉色冀。

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

Spring Boot運(yùn)用條件化配置的方法是:定義多個(gè)特殊的條件化注解,并將它們用到配置類上慎式。下面列出了Spring Boot提供的部分條件化注解:

條件化注解 配置生效條件
@ConditionalOnBean 配置了某個(gè)特定bean
@ConditionalOnMissingBean 沒有配置特定的bean
@ConditionalOnClass Classpath里有指定的類
@ConditionalOnMissingClass Classpath里沒有指定的類
@ConditionalOnExpression 給定的Spring Expression Language表達(dá)式計(jì)算結(jié)果為true
@ConditionalOnJava Java的版本匹配特定指或者一個(gè)范圍值
@ConditionalOnProperty 指定的配置屬性要有一個(gè)明確的值
@ConditionalOnResource Classpath里有指定的資源
@ConditionalOnWebApplication 這是一個(gè)Web應(yīng)用程序
@ConditionalOnNotWebApplication 這不是一個(gè)Web應(yīng)用程序

@ConfigurationProperties與@EnableConfigurationProperties

當(dāng)某些屬性的值需要配置的時(shí)候:

我們一般會在application.properties文件中新建配置項(xiàng)伶氢,然后在bean中使用@Value注解來獲取配置的值

// 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);
    }
}

以下方式更為得優(yōu)雅,

@Component
//  還可以通過@PropertySource("classpath:jdbc.properties")來指定配置文件
@ConfigurationProperties("jdbc.mysql")
// 前綴=jdbc.mysql瘪吏,會在配置文件中尋找jdbc.mysql.*的配置項(xiàng)
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<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;
    }
}

SpringFactoriesLoader詳解

JVM三種類加載器

  • BootStrapClassLoader ——> Java核心類庫
  • ExtClassLoader ——> Java擴(kuò)展類庫
  • AppClassLoader ——> 應(yīng)用的類路徑CLASSPATH下的類庫

雙親委派模型簡介

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

當(dāng)一個(gè)類加載器收到類加載任務(wù)時(shí),會先交給自己的父加載器去完成蓝丙,因此最終加載任務(wù)都會傳遞到最頂層的BootstrapClassLoader级遭,只有當(dāng)父加載器無法完成加載任務(wù)時(shí)望拖,才會嘗試自己來加載。

采用雙親委派模型的一個(gè)好處是保證使用不同類加載器最終得到的都是同一個(gè)對象挫鸽。

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ā)者自己實(shí)現(xiàn)的架诞,也就是說
            // 自定義類加載器時(shí),重寫此方法即可
           c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
    }
}

雙親委派弊端

雙親委派模型并不能解決所有的類加載器問題干茉。

比如谴忧,Java 提供了很多服務(wù)提供者接口(Service Provider Interface,SPI)角虫,允許第三方為這些接口提供實(shí)現(xiàn)俏蛮。常見的 SPI 有 JDBC、JNDI上遥、JAXP 等搏屑。

SPI的接口由核心類庫提供,卻由第三方實(shí)現(xiàn)粉楚,這樣就存在一個(gè)問題:SPI 的接口是 Java 核心庫的一部分辣恋,是由BootstrapClassLoader加載的;SPI實(shí)現(xiàn)的Java類一般是由AppClassLoader來加載的模软。BootstrapClassLoader是無法找到 SPI 的實(shí)現(xiàn)類的伟骨,因?yàn)樗患虞dJava的核心庫。它也不能代理給AppClassLoader燃异,因?yàn)樗亲铐攲拥念惣虞d器携狭。也就是說,雙親委派模型并不能解決這個(gè)問題回俐。

雙親委派弊端解決方案ContextClassLoader

線程上下文類加載器(ContextClassLoader)正好解決了這個(gè)問題逛腿。從名稱上看,可能會誤解為它是一種新的類加載器仅颇,實(shí)際上单默,它僅僅是Thread類的一個(gè)變量而已,可以通過setContextClassLoader(ClassLoader cl)和getContextClassLoader()來設(shè)置和獲取該對象忘瓦。

如果不做任何的設(shè)置搁廓,Java應(yīng)用的線程的上下文類加載器默認(rèn)就是AppClassLoader。在核心類庫使用SPI接口時(shí),傳遞的類加載器使用線程上下文類加載器境蜕,就可以成功的加載到SPI實(shí)現(xiàn)的類蝙场。線程上下文類加載器在很多SPI的實(shí)現(xiàn)中都會用到。但在JDBC中粱年,你可能會看到一種更直接的實(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í)不會一臉懵逼舟舒,這兩者除了在許多底層框架中取得的ClassLoader可能會有所不同外诅福,其他大多數(shù)業(yè)務(wù)場景下都是一樣的骡和。

類加載器的另外一個(gè)重要功能:加載資源

類加載器除了加載class外,還有一個(gè)非常重要功能,就是加載資源恬砂,它可以從jar包中讀取任何資源文件,比如憋槐,ClassLoader.getResources(String name)方法就是用于讀取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);
    }
    //findResources(name)方法會遍歷其負(fù)責(zé)加載的所有jar包,找到j(luò)ar包中名稱為name的資源文件币励,這里的資源可以是任何文件慷蠕,甚至是.class文件
    tmp[1] = findResources(name);
    return new CompoundEnumeration<>(tmp);
}

它的邏輯其實(shí)跟類加載的邏輯是一樣的,首先判斷父類加載器是否為空食呻,不為空則委托父類加載器執(zhí)行資源查找任務(wù)流炕,直到BootstrapClassLoader,最后才輪到自己查找仅胞。而不同的類加載器負(fù)責(zé)掃描不同路徑下的jar包每辟,就如同加載class一樣,最后會掃描所有的jar包干旧,找到符合條件的資源文件渠欺。

下面的示例,用于查找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)的文件來讀取資源內(nèi)容挠将。

詳解SpringFactoriesLoader源碼

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的知識编整,再來理解這段代碼:從CLASSPATH下的每個(gè)Jar包中搜尋所有META-INF/spring.factories配置文件捐名,然后將解析properties文件,找到指定名稱的配置后返回闹击。需要注意的是镶蹋,其實(shí)這里不僅僅是會去ClassPath路徑下查找,會掃描所有路徑下的Jar包,只不過這個(gè)文件只會在Classpath下的jar包中贺归。來簡單看下spring.factories文件的內(nèi)容吧:

// 來自 org.springframework.boot.autoconfigure下的META-INF/spring.factories
// EnableAutoConfiguration后文會講到淆两,它用于開啟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í)行l(wèi)oadFactoryNames(EnableAutoConfiguration.class, classLoader)后,得到對應(yīng)的一組@Configuration類拂酣,我們就可以通過反射實(shí)例化這些類然后注入到IOC容器中秋冰,最后容器里就有了一系列標(biāo)注了@Configuration的JavaConfig形式的配置類。

這就是SpringFactoriesLoader婶熬,它本質(zhì)上屬于Spring框架私有的一種擴(kuò)展方案剑勾,類似于SPI,Spring Boot在Spring基礎(chǔ)上的很多核心功能都是基于此赵颅。

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

在服務(wù)器端虽另,事件的監(jiān)聽機(jī)制更多的用于異步通知以及監(jiān)控和異常處理

Java提供了實(shí)現(xiàn)事件監(jiān)聽機(jī)制的兩個(gè)基礎(chǔ)類:

  • java.util.EventObject 自定義事件類型
  • java.util.EventListener 事件的監(jiān)聽器

同時(shí)還需要注意 EventPublisher 事件發(fā)布者

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

public class MethodMonitorEvent extends EventObject {
    // 時(shí)間戳募寨,用于記錄方法開始執(zhí)行的時(shí)間
    public long timestamp;

    public MethodMonitorEvent(Object source) {
        super(source);
    }
}

事件發(fā)布之后族展,相應(yīng)的監(jiān)聽器即可對該類型的事件進(jìn)行處理,我們可以在方法開始執(zhí)行之前發(fā)布一個(gè)begin事件拔鹰,在方法執(zhí)行結(jié)束之后發(fā)布一個(gè)end事件仪缸,相應(yīng)地,事件監(jiān)聽器需要提供方法對這兩種情況下接收到的事件進(jìn)行處理:

// 1列肢、定義事件監(jiān)聽接口
public interface MethodMonitorEventListener extends EventListener {
    // 處理方法執(zhí)行之前發(fā)布的事件
    public void onMethodBegin(MethodMonitorEvent event);
    // 處理方法結(jié)束時(shí)發(fā)布的事件
    public void onMethodEnd(MethodMonitorEvent event);
}
// 2腹殿、事件監(jiān)聽接口的實(shí)現(xiàn):如何處理
public class AbstractMethodMonitorEventListener implements MethodMonitorEventListener {

    @Override
    public void onMethodBegin(MethodMonitorEvent event) {
        // 記錄方法開始執(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)聽器接口針對不同的事件發(fā)布實(shí)際提供相應(yīng)的處理方法定義,最重要的是例书,其方法只接收MethodMonitorEvent參數(shù)锣尉,說明這個(gè)監(jiān)聽器類只負(fù)責(zé)監(jiān)聽器對應(yīng)的事件并進(jìn)行處理。

有了事件和監(jiān)聽器决采,剩下的就是發(fā)布事件自沧,然后讓相應(yīng)的監(jiān)聽器監(jiān)聽并處理。通常情況树瞭,我們會有一個(gè)事件發(fā)布者拇厢,它本身作為事件源,在合適的時(shí)機(jī)晒喷,將相應(yīng)的事件發(fā)布給對應(yīng)的事件監(jiān)聽器:

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)聽器被移除,這里為了安全做一個(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() {}

}

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

1凉敲、在合適的時(shí)機(jī)發(fā)布事件衣盾。此例中的methodMonitor()方法是事件發(fā)布的源頭寺旺,其在方法執(zhí)行之前和結(jié)束之后兩個(gè)時(shí)間點(diǎn)發(fā)布MethodMonitorEvent事件,每個(gè)時(shí)間點(diǎn)發(fā)布的事件都會傳給相應(yīng)的監(jiān)聽器進(jìn)行處理势决。在具體實(shí)現(xiàn)時(shí)需要注意的是阻塑,事件發(fā)布是順序執(zhí)行,為了不影響處理性能果复,事件監(jiān)聽器的處理邏輯應(yīng)盡量簡單陈莽。

2、事件監(jiān)聽器的管理虽抄。publisher類中提供了事件監(jiān)聽器的注冊與移除方法走搁,這樣客戶端可以根據(jù)實(shí)際情況決定是否需要注冊新的監(jiān)聽器或者移除某個(gè)監(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)部中(以下兩者皆以bean的形式注冊在容器中):

所有事件類型均繼承自org.springframework.context.AppliationEvent(繼承自EventObject)

所有監(jiān)聽器都實(shí)現(xiàn)org.springframework.context.ApplicationListener(繼承自EventListener)

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

Spring提供了ApplicationEvent接口的一些默認(rèn)的實(shí)現(xiàn),比如:

  • ContextClosedEvent 表示容器在即將關(guān)閉時(shí)發(fā)布的事件類型
  • ContextRefreshedEvent 表示容器在初始化或者刷新的時(shí)候發(fā)布的事件類型

ApplicationContext容器在啟動(dòng)時(shí)者春,會自動(dòng)識別并加載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接口的實(shí)現(xiàn)類。在容器啟動(dòng)時(shí)拥刻,會檢查容器內(nèi)是否存在名為applicationEventMulticaster的ApplicationEventMulticaster對象實(shí)例怜瞒。如果有就使用其提供的實(shí)現(xiàn),沒有就默認(rèn)初始化一個(gè)SimpleApplicationEventMulticaster作為實(shí)現(xiàn)般哼。

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

揭秘自動(dòng)配置原理

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

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

@SpringBootApplication是一個(gè)復(fù)合的注解漾橙,

@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 {
    // ......
}

以下三個(gè)注解起到關(guān)鍵作用:

  • @SpringBootConfiguration
    • 就是@Configuration,它是Spring框架的注解楞卡,標(biāo)明該類是一個(gè)JavaConfig配置類
  • @ComponentScan
    • 啟用組件掃描
  • @EnableAutoConfiguration
    • 就是開啟Spring Boot自動(dòng)配置功能霜运,Spring Boot會根據(jù)應(yīng)用的依賴脾歇、自定義的bean、classpath下有沒有某個(gè)類 等等因素來猜測你需要的bean觉渴,然后注冊到IoC容器中介劫。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
//@Import注解用于導(dǎo)入類,并將EnableAutoConfigurationImportSelector作為一個(gè)bean的定義注冊到容器中
//EnableAutoConfigurationImportSelector會將所有符合條件的@Configuration配置都加載到容器中
@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    // ......
}
//EnableAutoConfigurationImportSelector.class
public String[] selectImports(AnnotationMetadata annotationMetadata) {
    // 省略了大部分代碼案淋,保留一句核心代碼
    // 注意:SpringBoot最近版本中座韵,這句代碼被封裝在一個(gè)單獨(dú)的方法中
    List<String> factories = new ArrayList<String>(new LinkedHashSet<String>(  
        SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, this.beanClassLoader)));
}

這個(gè)類會掃描所有的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是如何自動(dòng)配置的:

@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類時(shí)才啟用這個(gè)配置黔帕,否則這個(gè)配置將被忽略。
  • @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為例吧逻杖。
@Configuration
public class DataSourcePoolMetadataProvidersConfiguration {

    @Configuration
    @ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class)
    static class TomcatDataSourcePoolMetadataProviderConfiguration {
        @Bean
        public DataSourcePoolMetadataProvider tomcatPoolDataSourceMetadataProvider() {
            .....
        }
    }
  ......
}

DataSourcePoolMetadataProvidersConfiguration是數(shù)據(jù)庫連接池提供者的一個(gè)配置類奋岁,即Classpath中存在org.apache.tomcat.jdbc.pool.DataSource.class,則使用tomcat-jdbc連接池荸百,如果Classpath中存在HikariDataSource.class則使用Hikari連接池闻伶。

以上足以說明Spring Boot如何利用條件話配置來實(shí)現(xiàn)自動(dòng)配置的。

@EnableAutoConfiguration中導(dǎo)入了EnableAutoConfigurationImportSelector類够话,而這個(gè)類的selectImports()通過SpringFactoriesLoader得到了大量的配置類蓝翰,而每一個(gè)配置類則根據(jù)條件化配置來做出決策,以實(shí)現(xiàn)自動(dòng)配置女嘲。

整個(gè)流程很清晰畜份,但EnableAutoConfigurationImportSelector.selectImports()是何時(shí)執(zhí)行的?

其實(shí)EnableAutoConfigurationImportSelector.selectImports()會在容器啟動(dòng)過程( AbstractApplicationContext.refresh() )中執(zhí)行欣尼。

Spring Boot 應(yīng)用啟動(dòng)的秘密

SpringBoot整個(gè)啟動(dòng)流程分為兩個(gè)步驟:

  • SpringApplication對象初始化
  • 執(zhí)行該對象的run方法
@SpringBootApplication
public class test {

    public static void main(String[] args) {
        //SpringApplication.run()方法所返回的Context爆雹,可以從中取出一些Bean 來進(jìn)行其它的一些操作
        //ConfigurableApplicationContext context = 
        //                              SpringApplication.run(DtsServerApp.class, args);
        //NettyServerController controller = context.getBean(NettyServerController.class);
        //controller.start();                  
         SpringApplication.run(test.class,args);
    }
}

SpringApplication初始化

看下SpringApplication的初始化流程,SpringApplication的構(gòu)造方法中調(diào)用initialize(Object[] sources)方法媒至,其代碼如下:

    public static ConfigurableApplicationContext run(Object source, String... args) {
        return run(new Object[]{source}, args);
    }
    
    public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
        return (new SpringApplication(sources)).run(args);
    }
    /**
     * Create a new {@link SpringApplication} instance. The application context will load
     * beans from the specified sources (see {@link SpringApplication class-level}
     * documentation for details. The instance can be customized before calling
     * {@link #run(String...)}.
     * @param sources the bean sources
     * @see #run(Object, String[])
     * @see #SpringApplication(ResourceLoader, Object...)
     */
    public SpringApplication(Object... sources) {
        initialize(sources);
    }
    private void initialize(Object[] sources) {
        // 上下文配置源保存在set中
        if (sources != null && sources.length > 0) {
            this.sources.addAll(Arrays.asList(sources));
        }
        // 推導(dǎo)是否是web應(yīng)用
        this.webEnvironment = deduceWebEnvironment();
        
        /*
        * TWO IMPORTANT STEPS:
        */
        // 從 META-INF/spring.factories 中加載應(yīng)用上下文初始化器類
        setInitializers((Collection) getSpringFactoriesInstances(
                ApplicationContextInitializer.class));
        // 從 META-INF/spring.factories 中加載事件監(jiān)聽器
        setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
        // 推導(dǎo)出主類顶别,
        this.mainApplicationClass = deduceMainApplicationClass();
    }

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

TWO IMPORTANT INTERFACE:

  • ApplicationContextInitializer
  • ApplicationListener

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

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

2屈尼、ApplicationListener是Spring框架對Java事件監(jiān)聽機(jī)制的一種框架實(shí)現(xiàn)。

如果想為Spring Boot應(yīng)用添加監(jiān)聽器拴孤,該如何實(shí)現(xiàn)脾歧?

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

  • 通過SpringApplication.addListeners(ApplicationListener<?>... listeners)
  • SpringApplication.setListeners(Collection<? extends ApplicationListener<?>> listeners)兩個(gè)方法來添加一個(gè)或者多個(gè)自定義監(jiān)聽器
  • @Listener(todo)
  • META-INF/spring.factories文件中新增配置

既然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的初始化演熟,我們就說這么多鞭执。

SpringApplication運(yùn)行

  • 運(yùn)行環(huán)境/參數(shù)設(shè)置 -> 打印Banner -> 創(chuàng)建ApplicationContext -> 上下文準(zhǔn)備 -> 上下文刷新
  • ------------------------------------------ 運(yùn)行監(jiān)視器 -----------------------------------

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

public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        FailureAnalyzers analyzers = null;
        configureHeadlessProperty();
        // ①  獲取SpringApplicationRunListeners
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting();
        try {
            // ②  運(yùn)行參數(shù)保存在ApplicationArguments中
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            //    配置環(huán)境屬性兄纺,同時(shí)會觸發(fā) ApplicationEnvironmentPreparedEvent 事件
            ConfigurableEnvironment environment =        
                                        prepareEnvironment(listeners,applicationArguments);
            // ③
            Banner printedBanner = printBanner(environment);
            // ④  創(chuàng)建具體的 ApplicationContext,
            //    根據(jù)是否為 web 環(huán)境分別為:AnnotationConfigEmbeddedWebApplicationContext化漆、
            //                             AnnotationConfigApplicationContext
            context = createApplicationContext();
            // ⑤  失敗分析估脆,錯(cuò)誤診斷
            analyzers = new FailureAnalyzers(context);
            // ⑥  上下文準(zhǔn)備
            prepareContext(context, environment, listeners, applicationArguments,printedBanner);
            // ⑦  刷新上下文
            refreshContext(context);
            // ⑧
            afterRefresh(context, applicationArguments);
            // ⑨  啟動(dòng)完成
            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)用開始啟動(dòng)了座云。

SpringApplicationRunListeners其本質(zhì)上就是一個(gè)事件發(fā)布者疙赠,它在SpringBoot應(yīng)用啟動(dòng)的不同時(shí)間點(diǎn)發(fā)布不同應(yīng)用事件類型(ApplicationEvent),如果有哪些事件監(jiān)聽者(ApplicationListener)對這些事件感興趣朦拖,則可以接收并且處理圃阳。還記得初始化流程中,SpringApplication加載了一系列ApplicationListener嗎贞谓?這個(gè)啟動(dòng)流程中沒有發(fā)現(xiàn)有發(fā)布事件的代碼限佩,其實(shí)都已經(jīng)在SpringApplicationRunListeners這兒實(shí)現(xià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。①處的代碼只會獲取到一個(gè)EventPublishingRunListener的實(shí)例泞坦,我們來看看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赃梧。

② 創(chuàng)建并配置當(dāng)前應(yīng)用將要使用的Environment.

Environment用于描述應(yīng)用程序當(dāng)前的運(yùn)行環(huán)境,其抽象了兩個(gè)方面的內(nèi)容:

  • 配置文件(profile)
  • 屬性(properties)

不同的環(huán)境(eg:生產(chǎn)環(huán)境豌熄、預(yù)發(fā)布環(huán)境)可以使用不同的配置文件授嘀,而屬性則可以從<u>配置文件、環(huán)境變量锣险、命令行參數(shù)</u>等來源獲取蹄皱。

因此览闰,當(dāng)Environment準(zhǔn)備好后,在整個(gè)應(yīng)用的任何時(shí)候巷折,都可以從Environment中獲取資源压鉴。

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

  • 判斷Environment是否存在油吭,不存在就創(chuàng)建(如果是web項(xiàng)目就創(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)用在啟動(dòng)時(shí)會輸出這樣的東西:

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

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

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

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

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

    b) 遍歷調(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)裝載完畢

    private void prepareContext(ConfigurableApplicationContext context,
              ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
              ApplicationArguments applicationArguments, Banner printedBanner) {
          //a)    
          context.setEnvironment(environment);
          // ApplicationContext 創(chuàng)建后的處理擴(kuò)展點(diǎn):可以設(shè)置 BeanNameGenerator 和 ResourceLoader
          postProcessApplicationContext(context);
          //b) 
          applyInitializers(context);
          // “應(yīng)用上下文已準(zhǔn)備好” 事件,默認(rèn)實(shí)現(xiàn)空
          listeners.contextPrepared(context);
          if (this.logStartupInfo) {
              logStartupInfo(context.getParent() == null);
              logStartupProfileInfo(context);
          }
    
          // Add boot specific singleton beans
          //注冊Spring boot 特有的單例 bean
          context.getBeanFactory().registerSingleton("springApplicationArguments",
                  applicationArguments);
          if (printedBanner != null) {
              context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
          }
    
          // Load the sources
          Set<Object> sources = getSources();
          Assert.notEmpty(sources, "Sources must not be empty");
          load(context, sources.toArray(new Object[sources.size()]));
          
            // 上下文加載完成柿隙,觸發(fā) ApplicationPreparedEvent 事件
          listeners.contextLoaded(context);
      }
    
    /**
     * Apply any {@link ApplicationContextInitializer}s to the context before it is
     * refreshed.
     * @param context the configured ApplicationContext (not refreshed yet)
     * @see ConfigurableApplicationContext#refresh()
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    protected void applyInitializers(ConfigurableApplicationContext context) {
        for (ApplicationContextInitializer initializer : getInitializers()) {
            Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(
                    initializer.getClass(), ApplicationContextInitializer.class);
            Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
            initializer.initialize(context);
        }
    }

⑦叶洞、這個(gè)過程和普通 Spring 應(yīng)用是一樣的( org.springframework.context.support.AbstractApplicationContext#refresh),實(shí)現(xiàn)對 bean 的加載初始化禀崖。

    private void refreshContext(ConfigurableApplicationContext context) {
        refresh(context);
        if (this.registerShutdownHook) {
            try {
                context.registerShutdownHook();
            }
            catch (AccessControlException ex) {
                // Not allowed in some environments.
            }
        }
    }
    
    /**
     * Refresh the underlying {@link ApplicationContext}.
     * @param applicationContext the application context to refresh
     */
    protected void refresh(ApplicationContext applicationContext) {
        Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
        ((AbstractApplicationContext) applicationContext).refresh();
    }
    
    @Override
    public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            // Prepare this context for refreshing.
            prepareRefresh();

            // Tell the subclass to refresh the internal bean factory.
            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

            // Prepare the bean factory for use in this context.
            prepareBeanFactory(beanFactory);

            try {
                // Allows post-processing of the bean factory in context subclasses.
                postProcessBeanFactory(beanFactory);

                // Invoke factory processors registered as beans in the context.
                invokeBeanFactoryPostProcessors(beanFactory);

                // Register bean processors that intercept bean creation.
                registerBeanPostProcessors(beanFactory);

                // Initialize message source for this context.
                initMessageSource();

                // Initialize event multicaster for this context.
                initApplicationEventMulticaster();

                // Initialize other special beans in specific context subclasses.
                onRefresh();

                // Check for listener beans and register them.
                registerListeners();

                // Instantiate all remaining (non-lazy-init) singletons.
                finishBeanFactoryInitialization(beanFactory);

                // Last step: publish corresponding event.
                finishRefresh();
            }

            catch (BeansException ex) {
                if (logger.isWarnEnabled()) {
                    logger.warn("Exception encountered during context initialization - " +
                            "cancelling refresh attempt: " + ex);
                }

                // Destroy already created singletons to avoid dangling resources.
                destroyBeans();

                // Reset 'active' flag.
                cancelRefresh(ex);

                // Propagate exception to caller.
                throw ex;
            }

            finally {
                // Reset common introspection caches in Spring's core, since we
                // might not ever need metadata for singleton beans anymore...
                resetCommonCaches();
            }
        }
    }

調(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來對容器做一些額外的操作。BeanFactoryPostProcessor允許我們在容器實(shí)例化相應(yīng)對象之前仅财,對注冊到容器的BeanDefinition所保存的信息做一些額外的操作狈究。

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

  • ConfigurationWarningsApplicationContextInitializer$ConfigurationWarningsPostProcessor
  • SharedMetadataReaderFactoryContextInitializer$CachingMetadataReaderFactoryPostProcessor
  • ConfigFileApplicationListener$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è)方法除了會遍歷上面的3個(gè)BeanFactoryPostProcessor處理外贯莺,還會獲取類型為BeanDefinitionRegistryPostProcessor的bean:org.springframework.context.annotation.internalConfigurationAnnotationProcessor,對應(yīng)的Class為ConfigurationClassPostProcessor宁改。

ConfigurationClassPostProcessor用于解析處理各種注解缕探,包括:@Configuration、@ComponentScan还蹲、@Import爹耗、@PropertySource、@ImportResource谜喊、@Bean潭兽。當(dāng)處理@import注解的時(shí)候,就會調(diào)用<自動(dòng)配置>這一小節(jié)中的EnableAutoConfigurationImportSelector.selectImports()來完成自動(dòng)配置功能斗遏。

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

SpringApplication啟動(dòng)后账蓉,如果我們想實(shí)現(xiàn)自己的邏輯,便可以通過實(shí)現(xiàn) ApplicationRunner 或 CommandLineRunner 來實(shí)現(xiàn)(二者的唯一區(qū)別是參數(shù)形態(tài))逾一。這兩種 Runner 的生效點(diǎn)便是上下文 refresh 完成后铸本。

    /**
     * Called after the context has been refreshed.
     * @param context the application context
     * @param args the application arguments
     */
    protected void afterRefresh(ConfigurableApplicationContext context,
            ApplicationArguments args) {
        callRunners(context, args);
    }

    private void callRunners(ApplicationContext context, ApplicationArguments args) {
        List<Object> runners = new ArrayList<Object>();
        // 二者的順序
        runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
        runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
        //根據(jù)order排序
        AnnotationAwareOrderComparator.sort(runners);
        for (Object runner : new LinkedHashSet<Object>(runners)) {
            if (runner instanceof ApplicationRunner) {
                callRunner((ApplicationRunner) runner, args);
            }
            if (runner instanceof CommandLineRunner) {
                callRunner((CommandLineRunner) runner, args);
            }
        }
    }

⑨、執(zhí)行所有SpringApplicationRunListener的finished()方法遵堵。

    public void finished(ConfigurableApplicationContext context, Throwable exception) {
        for (SpringApplicationRunListener listener : this.listeners) {
            callFinishedListener(listener, context, exception);
        }
    }

    private void callFinishedListener(SpringApplicationRunListener listener,
            ConfigurableApplicationContext context, Throwable exception) {
        try {
            listener.finished(context, exception);
        }
        catch (Throwable ex) {
            if (exception == null) {
                ReflectionUtils.rethrowRuntimeException(ex);
            }
            if (this.log.isDebugEnabled()) {
                this.log.error("Error handling failed", ex);
            }
            else {
                String message = ex.getMessage();
                message = (message == null ? "no error message" : message);
                this.log.warn("Error handling failed (" + message + ")");
            }
        }
    }

這就是Spring Boot的整個(gè)啟動(dòng)流程箱玷,其核心就是<u>在Spring容器初始化并啟動(dòng)的基礎(chǔ)上加入各種擴(kuò)展點(diǎn)</u>,這些擴(kuò)展點(diǎn)包括:

  • ApplicationContextInitializer
  • ApplicationListener
  • 各種BeanFactoryPostProcessor

只要理解這些擴(kuò)展點(diǎn)是在何時(shí)如何工作的陌宿,能讓它們?yōu)槟闼眉纯伞?/p>

言而總之锡足,Spring才是核心,理解清楚Spring容器的啟動(dòng)流程限番,那Spring Boot啟動(dòng)流程就不在話下了舱污。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末呀舔,一起剝皮案震驚了整個(gè)濱河市弥虐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌媚赖,老刑警劉巖霜瘪,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異惧磺,居然都是意外死亡颖对,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門磨隘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缤底,“玉大人顾患,你說我怎么就攤上這事「鲞螅” “怎么了江解?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長徙歼。 經(jīng)常有香客問我犁河,道長,這世上最難降的妖魔是什么魄梯? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任桨螺,我火速辦了婚禮,結(jié)果婚禮上酿秸,老公的妹妹穿的比我還像新娘灭翔。我一直安慰自己,他們只是感情好辣苏,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布缠局。 她就那樣靜靜地躺著,像睡著了一般考润。 火紅的嫁衣襯著肌膚如雪狭园。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天糊治,我揣著相機(jī)與錄音唱矛,去河邊找鬼。 笑死井辜,一個(gè)胖子當(dāng)著我的面吹牛绎谦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播粥脚,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼窃肠,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了刷允?” 一聲冷哼從身側(cè)響起冤留,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎树灶,沒想到半個(gè)月后纤怒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡天通,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年泊窘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡烘豹,死狀恐怖瓜贾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情携悯,我是刑警寧澤阐虚,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站蚌卤,受9級特大地震影響实束,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜逊彭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一咸灿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧侮叮,春花似錦避矢、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至卸勺,卻和暖如春砂沛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背曙求。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工碍庵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人悟狱。 一個(gè)月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓静浴,卻偏偏與公主長得像,于是被迫代替她去往敵國和親挤渐。 傳聞我的和親對象是個(gè)殘疾皇子苹享,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)浴麻,斷路器得问,智...
    卡卡羅2017閱讀 134,652評論 18 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,806評論 6 342
  • 什么是Spring Spring是一個(gè)開源的Java EE開發(fā)框架。Spring框架的核心功能可以應(yīng)用在任何Jav...
    jemmm閱讀 16,461評論 1 133
  • 表姐是縣里銀行的收銀員,因?yàn)楣ぷ鞯年P(guān)系宣蔚,前幾年都有很嚴(yán)重的痔瘡向抢,懷孕后一直都很注意飲食认境,就怕孕期便秘影響胎寶寶,就...
    群美育兒閱讀 845評論 0 0
  • 李商隱的詩句:“夕陽無限好挟鸠,只是近黃昏叉信。”向來我只贊同前一句艘希。因?yàn)樵谖已劾锱鹕恚﹃栆恢倍际欠浅C篮玫拇嬖冢辉诤跛?..
    素簡152閱讀 860評論 0 1