Spring Boot自動裝配的原理

Spring Boot不得不說的一個特點就是自動裝配,它是starter的基礎,也是spring boot的核心,那到底什么是自動裝配呢?

簡單的說,就是自動將Bean裝配到IoC容器中。接下來,我們通過一個例子來了解下自動裝配。

  1. 添加starter
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 在application.properties中添加Redis的配置
spring.redis.host=localhost
spring.redis.port=6379
  1. 在Controller中使用redisTemplate對Redis進行操作
@RestController
public class RedisController {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @GetMapping("/test")
    public String test() {
        redisTemplate.opsForValue().set("test", "test demo");
        return "Test Demo";
    }
}

在上面例子中,我們并沒有通過XML形式或者注解形式把RedisTemplate注入到IoC容器中,但是在RedisController中卻可以直接使用@Autowired來注入redisTemplate實例,這就表明IoC容器中已經存在RedisTemplate實例了,這就是Spring Boot自動裝配機制。

自動裝配的實現

自動裝配在Spring boot中是通過@EnableAutoConfiguration注解來開啟的,這個注解是在啟動類注解@SpringBootApplication中聲明的。

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

查看@SpringBootApplication注解,可以看到@EnableAutoConfiguration注解的聲明

@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{

其實spring 3.1版本就已經開始支持@Enable注解了,它的主要作用是把相關組件的Bean裝配到IoC容器中。@Enable注解對JavaConfig的進一步完善,使開發者減少了配置代碼量,降低了使用難度,比較常見的Enable注解有@EnableWebMvc,@EnableScheduling等。

如果我們要基于JavaConfig的形式來完成Bean的裝載,則必須要使用@注解及@Bean注解。@Enable注解本質上就是對這兩種注解的封裝,在@Enable注解中,一般都會帶有一個@Import注解,比如@EnableScheduling注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import({SchedulingConfiguration.class})
@Documented
public @interface EnableScheduling {
}

所以,使用@Enable注解后,Spring會解析到@Import導入的配置類,并且根據這個配置類中的描述來實現Bean的裝配。

EnableAutoConfiguration注解

當我們查看@EnableAutoConfiguration這個注解的時候,可以看到除了@Import注解之外,還有一個@AutoConfigurationPackage注解,而且@Import注解導入的并不是一個Configuration的配置類,而是AutoConfigurationImportSelector類,那AutoConfigurationImportSelector里面包含什么東西呢?

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

AutoConfigurationImportSelector

AutoConfigurationImportSelector這個類實現了ImportSelector,它只有一個selectImports抽象方法,并且返回一個String數組,這個數組中的值就是需要裝配到IoC容器中的類,當在@Import中導入一個ImportSelector的實現類后,會把該實現類中返回的class名稱都裝載到IoC容器中。

public interface ImportSelector {
    String[] selectImports(AnnotationMetadata var1);
}

和@Confifguration不同的是,ImportSelector可以實現批量裝配,而且可以通過邏輯處理來實現Bean的選擇性裝配,也就是可以根據上下文來決定哪些類可以被裝配到IoC容器中,下面通過一個例子介紹下ImportSelector的使用:

  1. 首先創建兩個類,我們要把這兩個類裝配到IoC容器中
public class FirstClass {
...
}
public class SecondClass {
...
}
  1. 創建ImportSelector的實現類,在實現類中把上面定義的兩個類加入到數組中
public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[] {FirstClass.class.getName(), SecondClass.class.getName()}
    }
}
  1. 創建一個類似EnableAutoConfiguration的注解,通過@Import導入MyImportSelector
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({MyImportSelector.class})
public @interface MyAutoImport {
}
  1. 創建啟動類,在啟動類上使用MyAutoImport注解,然后通過ConfigurableApplicationContext獲取FirstClass
@SpringBootApplication
@MyAutoImport
public class ImportSelectorDemo {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringBootApplication.run(ImportSelectorDemo.class);
        FirstClass fc = context.getBean(FirstClass.class);
    }
}

這種方式相比于@Import(*Configuration.class)的好處在于裝配的靈活性,也可以實現批量裝配。在MyImportSelector的String數組中可以定義多個Configuration類,一個配置類代表的就是某一個技術組件中批量的Bean的聲明,所以在自動裝配這個過程中只需要掃描到指定路徑下對應的配置類即可。

自動裝配原理

自動裝配的核心是掃描約定目錄下的文件進行解析,解析完成之后把得到的Configuration配置類通過ImportSelector進行導入,進而完成Bean的自動裝配過程。

我們看一下AutoConfigurationImportSelector中的selectImports方法,它是ImportSelector接口的實現,該方法中主要做了兩件事:

  • AutoConfigurationMetadataLoader.loadMetadata方法從META-INF/spring-autoconfigure-metadata.properties文件中加載自動裝配的條件元數據,也就是只有滿足條件的Bean才會被裝配
  • autoConfigurationEntry.getConfigurations()方法收集所有符合條件的配置類,進行自動裝配
public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
            AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
            AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
            return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
        }
    }

接下來我們了解下getAutoConfigurationEntry這個方法,這個方法會掃描指定路徑下的文件進行解析,從而得到所需要裝配的配置類,它主要做了下面幾件事:

  • getAttributes方法獲得@EnableAutoConfiguration注解中的屬性exclude、excludeName等。
  • getCandidateConfiguration方法獲得所有自動裝配的配置類。
  • removeDuplicates方法去掉重復的配置項。
  • getExclusions方法根據@EnableAutoConfiguration注解中配置的exclude等屬性,把不需要自動裝配的配置類移除。
  • fireAutoConfigurationImportEvents廣播事件。
  • 最后返回經過多層判斷和過濾之后的配置類集合
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return EMPTY_ENTRY;
        } else {
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
            List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
            configurations = this.removeDuplicates(configurations);
            Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
            this.checkExcludedClasses(configurations, exclusions);
            configurations.removeAll(exclusions);
            configurations = this.filter(configurations, autoConfigurationMetadata);
            this.fireAutoConfigurationImportEvents(configurations, exclusions);
            return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
        }
    }

總的來說,它先獲得所有的配置類,通過去重、exclude排除等操作,得到最終需要實現自動裝配的配置類。其中getCandidateConfigurations方法是獲得配置類最核心的方法。

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
        return configurations;
    }

這個方法中用到了SpringFactoriesLoader,它是Spring內部提供的一種約定俗成的加載方式,和Java的SPI類似。它會掃描classpath下的META-INF/spring.factories文件,spring.factories文件中的數據以key=value的形式存儲,SpringFactoriesLoader.loadFactoryNames()會根據key的到對應的value值,因此,在自動裝配這個場景中,key對應為EnableAutoConfiguration,value是多個配置類,也就是getCandidateConfigurations方法的返回值。

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
......

如果我們打開RabbitAutoConfiguration,可以看到它就是一個基于JavaConfig形式的配置類:

@Configuration
@ConditionalOnClass({RabbitTemplate.class, Channel.class})
@EnableConfigurationProperties({RabbitProperties.class})
@Import({RabbitAnnotationDrivenConfiguration.class})
public class RabbitAutoConfiguration {
    public RabbitAutoConfiguration() {
    }

    @Configuration
    @ConditionalOnClass({RabbitMessagingTemplate.class})
    @ConditionalOnMissingBean({RabbitMessagingTemplate.class})
    @Import({RabbitAutoConfiguration.RabbitTemplateConfiguration.class})
    protected static class MessagingTemplateConfiguration {
        protected MessagingTemplateConfiguration() {
        }

        @Bean
        @ConditionalOnSingleCandidate(RabbitTemplate.class)
        public RabbitMessagingTemplate rabbitMessagingTemplate(RabbitTemplate rabbitTemplate) {
            return new RabbitMessagingTemplate(rabbitTemplate);
        }
   ......
 }

除了@Configuration注解,還有一個@ConditionalOnClass注解,這個條件控制機制在這里的用途是判斷classpath下是否存在RabbitTemplate和Channel這兩個類,如果有,則把當前配置類注冊到IoC容器中。@EnableConfigurationProperties是屬性配置,我們可以按照約定在application.properties中配置RabbitMQ的參數,這些配置會加載到RabbitProperties中。

到此處,自動裝配的工作流程就結束了,其實主要的核心過程是如下幾點:

  • 通過@Import(AutoConfigurationImportSelector)實現配置類的導入
  • AutoConfigurationImportSelector類實現了ImportSelector接口,重寫了方法selectImports,用于實現批量配置類的裝載。
  • 通過spring提供的SpringFactoriesLoader機制掃描classpath下META-INF/spring.factories文件,讀取需要實現自動裝配的配置類。
  • 通過條件篩選,把不符合條件的配置類移除,最終完成自動裝配。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372