了解Spring Boot AutoConfiguration

原文:http://sivalabs.in/2016/03/how-springboot-autoconfiguration-magic/
作者:Siva
譯者:http://oopsguy.com

在之前的博文《為什么是 Spring Boot》中兼贸,我們已經(jīng)介紹了如何創(chuàng)建一個 Spring Boot 應(yīng)用程序吃溅。但是,你可能還不了解它幕后究竟發(fā)生了什么很澄,你想了解 SpringBoot AutoConfiguration 背后的原理颜及。但在此之前蹂楣,你應(yīng)該先了解 Spring @Conditional 的特性,所有 Spring Boot AutoConfiguration 魔法都是基于它肄扎。

@Conditional 的魔力

在開發(fā)基于 Spring 的應(yīng)用程序時,我們可能需條件性地注冊 Bean犯祠。

例如,你希望在本地運(yùn)行應(yīng)用程序時注冊一個指向 DEV 數(shù)據(jù)庫的 DataSource bean搔耕,在生產(chǎn)中指向 PRODUCTION 數(shù)據(jù)庫痰娱。

你可以將數(shù)據(jù)庫連接參數(shù)外部化,放置在屬性(properties)文件中鲸睛,并在適當(dāng)?shù)沫h(huán)境中使用該個文件坡贺。但是,當(dāng)你需要指向不同的環(huán)境并構(gòu)建應(yīng)用程序時遍坟,你得更改配置。

為了解決這個問題风瘦,Spring 3.1 引入了 Profiles 概念公般。你可以注冊多個相同類型的 bean,并將它們與一個或多個配置文件相關(guān)聯(lián)瞬雹。在運(yùn)行應(yīng)用程序時,你可以激活所需的配置文件酗捌,只有與配置文件相關(guān)聯(lián)的 bean 才被注冊涌哲。

@Configuration
public class AppConfig
{
    @Bean
    @Profile("DEV")
    public DataSource devDataSource() {
        ...
    }
 
    @Bean
    @Profile("PROD")
    public DataSource prodDataSource() {
        ...
    }
}

然后,可以使用系統(tǒng)屬性(System Property)來指定要激活配置文件 - Dspring.profiles.active = DEV

這種方法適用于啟用了基于 profiles 來啟用或禁用 bean 注冊的簡單場景哪廓。如果要根據(jù)一些條件邏輯來注冊 bean初烘,profiles 方式是遠(yuǎn)遠(yuǎn)不夠的分俯。

為了讓有條件性注冊 Spring bean 變得更加靈活哆料,Spring 4 引入了 @Conditional 概念东亦。通過使用 @Conditional 方式,你可以根據(jù)任意條件注冊一個 bean讥此。

例如,你可能想在如下情況注冊一個 bean:

  • classpath 中某個類
  • 某種類型的 Spring bean卒稳,尚未在 ApplicationContext 中注冊
  • 存放在某個位置的某個文件
  • 一個存在于配置文件中的某個屬性值
  • 一個存在/不存在的某個系統(tǒng)屬性

這里只是列舉了幾個例子他巨,你可以根據(jù)自己的需要定義任何條件。

讓我們來看看 Spring 的 @Conditional 是如何工作的捻爷。

假設(shè)我們有一個 UserDAO 接口份企,其包含從數(shù)據(jù)存儲中獲取數(shù)據(jù)的方法。我們有兩個 UserDAO 接口的實現(xiàn)甜紫,即 JdbcUserDAO(與 MySQL 數(shù)據(jù)庫通信)和 MongoUserDAO(與 MongoDB 通信)骂远。

我們可能想通過基于系統(tǒng)屬性 dbType 來只啟用 JdbcUserDAOMongoUserDAO 其中一個激才。

如果應(yīng)用程序使用了 java -jar myapp.jar -DdbType = MySQL 啟動,則啟用 JdbcUserDAO瘸恼,如果啟動時使用了 java -jar myapp.jar -DdbType = MONGO钞脂,則啟用 MongoUserDAO

假設(shè)我們有 UserDAO 接口冰啃,以下是 JdbcUserDAOMongoUserDAO 實現(xiàn)類:

public interface UserDAO
{
    List<String> getAllUserNames();
}
 
public class JdbcUserDAO implements UserDAO
{
    @Override
    public List<String> getAllUserNames()
    {
        System.out.println("**** Getting usernames from RDBMS *****");
        return Arrays.asList("Siva","Prasad","Reddy");
    }
}
 
public class MongoUserDAO implements UserDAO
{
    @Override
    public List<String> getAllUserNames()
    {
        System.out.println("**** Getting usernames from MongoDB *****");
        return Arrays.asList("Bond","James","Bond");
    }
}

我們可以實現(xiàn)一個 MySQLDatabaseTypeCondition Condition 來檢查系統(tǒng)屬性是否是 MYSQL

public class MySQLDatabaseTypeCondition implements Condition
{
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
    {
        String enabledDBType = System.getProperty("dbType");
        return (enabledDBType != null && enabledDBType.equalsIgnoreCase("MYSQL"));
    }
}

現(xiàn)在我們可以使用 @Conditional 條件性地配置 JdbcUserDAOMongoUserDAO bean阎毅,如下所示:

@Configuration
public class AppConfig
{
    @Bean
    @Conditional(MySQLDatabaseTypeCondition.class)
    public UserDAO jdbcUserDAO(){
        return new JdbcUserDAO();
    }
 
    @Bean
    @Conditional(MongoDBDatabaseTypeCondition.class)
    public UserDAO mongoUserDAO(){
        return new MongoUserDAO();
    }
}

如果我們使用 java -jar myapp.jar -DdbType = MYSQL 來運(yùn)行應(yīng)用程序扇调,則只有 JdbcUserDAO bean 被注冊。

但如果將系統(tǒng)屬性設(shè)置為 -DdbType = MONGODB狼钮,則只會注冊 MongoUserDAO bean。

現(xiàn)在我們已經(jīng)了解了如何基于系統(tǒng)屬性條件性地注冊一個 bean莲镣。

假設(shè)我們想當(dāng) MongoDB java 驅(qū)動程序類 com.mongodb.Server 在 classpath 上可用時注冊 MongoUserDAO bean涎拉,否則,注冊 JdbcUserDAO bean半火。

為了實現(xiàn)這點(diǎn)季俩,我們可以創(chuàng)建 Condition 來檢查 MongoDB 的驅(qū)動程序類 com.mongodb.Server 是否存在:

public class MongoDriverPresentsCondition implements Condition
{
    @Override
    public boolean matches(ConditionContext conditionContext,AnnotatedTypeMetadata metadata)
    {
        try {
            Class.forName("com.mongodb.Server");
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }
}
 
public class MongoDriverNotPresentsCondition implements Condition
{
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
    {
        try {
            Class.forName("com.mongodb.Server");
            return false;
        } catch (ClassNotFoundException e) {
            return true;
        }
    }
}

我們剛剛看到了如何根據(jù) classpath 中的類是否存在來條件性地注冊 bean酌住。

如果我們只想在沒有其他 UserDAO 類型的 Spring bean 注冊時才注冊 MongoUserDAO bean,該怎么辦娱节?

我們同樣可以創(chuàng)建一個 Condition 來檢查是否存在現(xiàn)有的某個類型的 bean祭示,如下所示:

public class UserDAOBeanNotPresentsCondition implements Condition
{
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
    {
        UserDAO userDAO = conditionContext.getBeanFactory().getBean(UserDAO.class);
        return (userDAO == null);
    }
}

如果我們只想在 properties 配置文件中設(shè)置屬性 app.dbType = MONGO,那該怎么注冊 MongoUserDAO bean 呢稠歉?

我們可以實現(xiàn)如下 Condition:

public class MongoDbTypePropertyCondition implements Condition
{
    @Override
    public boolean matches(ConditionContext conditionContext,
    AnnotatedTypeMetadata metadata)
    {
        String dbType = conditionContext.getEnvironment()
                            .getProperty("app.dbType");
        return "MONGO".equalsIgnoreCase(dbType);
    }
}

我們剛剛已經(jīng)看到了如何實現(xiàn)各種 condition汇陆。

然而,使用注解實現(xiàn) Condition 的方式更為優(yōu)雅阅羹。而不是像為 MYSQL 和 MongoDB 創(chuàng)建 Condition 實現(xiàn)那樣做。我們可以創(chuàng)建一個 DatabaseType 注解执庐,如下所示:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(DatabaseTypeCondition.class)
public @interface DatabaseType
{
    String value();
}

然后可以通過編寫 DatabaseTypeCondition 使用 DatabaseType 的值來確定是否啟用或禁用 bean 注冊导梆,如下所示:

public class DatabaseTypeCondition implements Condition
{
    @Override
    public boolean matches(ConditionContext conditionContext,
    AnnotatedTypeMetadata metadata)
    {
        Map<String, Object> attributes = metadata.getAnnotationAttributes(DatabaseType.class.getName());
        String type = (String) attributes.get("value");
        String enabledDBType = System.getProperty("dbType","MYSQL");
        return (enabledDBType != null && type != null && enabledDBType.equalsIgnoreCase(type));
    }
}

現(xiàn)在我們可以將 @DatabaseType 注解放置在 bean 定義上:

@Configuration
@ComponentScan
public class AppConfig
{
    @DatabaseType("MYSQL")
    public UserDAO jdbcUserDAO(){
        return new JdbcUserDAO();
    }
 
    @Bean
    @DatabaseType("MONGO")
    public UserDAO mongoUserDAO(){
        return new MongoUserDAO();
    }
}

此處看尼,我們從 DatabaseType 注解獲取元數(shù)據(jù),并檢查系統(tǒng)屬性 dbType 的值以確定是否啟用或禁用 bean 注冊藏斩。

我們已經(jīng)看了許多示例灾茁,了解了如何使用 @Conditional 注解條件性地注冊 bean。

SpringBoot 大量地使用了 @Conditional 特性北专,根據(jù)各種標(biāo)準(zhǔn)條件性地注冊 bean拓颓。

你可以在 Spring-boot-autoconfigure- {version} .jarorg.springframework.boot.autoconfigure 包中找到 SpringBoot 使用的各種 Condition 實現(xiàn)。

現(xiàn)在我們來了解一下 SpringBoot 如何使用 @Conditional 特性條件性地檢查是否注冊一個 bean驶睦。

究竟是什么觸發(fā)了自動配置(auto-configuration)機(jī)制场航?

這就是我們下一節(jié)的內(nèi)容。

SpringBoot 的 AutoConfiguration

SpringBoot 自動配置魔法的關(guān)鍵在于 @EnableAutoConfiguration 注解溉痢。通常,我們使用 @SpringBootApplication 來注解應(yīng)用程序入口類髓削,如果我們要自定義默認(rèn)值镀娶,我們可以使用以下注解:

Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application
{
 
}

@EnableAutoConfiguration 注解允許通過掃描 classpath 中的組件并注冊匹配各種條件的 bean 來自動配置 Spring ApplicationContext

SpringBoot 在 spring-boot-autoconfigure- {version} .jar 中提供了各種 AutoConfiguration 類宝泵,它們負(fù)責(zé)注冊各種組件。

通常坎怪,AutoConfiguration 類使用 @Configuration 注解將其標(biāo)記為一個 Spring 配置類廓握,并用 @EnableConfigurationProperties 注解來綁定自定義屬性和一個或多個條件的 bean 注冊方法嘁酿。

例如,org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration 類娱仔。

@Configuration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class })
public class DataSourceAutoConfiguration 
{
    ...
    ...
    @Conditional(DataSourceAutoConfiguration.EmbeddedDataSourceCondition.class)
    @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
    @Import(EmbeddedDataSourceConfiguration.class)
    protected static class EmbeddedConfiguration {
 
    }
 
    @Configuration
    @ConditionalOnMissingBean(DataSourceInitializer.class)
    protected static class DataSourceInitializerConfiguration {
        @Bean
        public DataSourceInitializer dataSourceInitializer() {
        return new DataSourceInitializer();
        }
    }
 
    @Conditional(DataSourceAutoConfiguration.NonEmbeddedDataSourceCondition.class)
    @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
    protected static class NonEmbeddedConfiguration {
        @Autowired
        private DataSourceProperties properties;
 
        @Bean
        @ConfigurationProperties(prefix = DataSourceProperties.PREFIX)
        public DataSource dataSource() {
            DataSourceBuilder factory = DataSourceBuilder
                    .create(this.properties.getClassLoader())
                    .driverClassName(this.properties.getDriverClassName())
                    .url(this.properties.getUrl()).username(this.properties.getUsername())
                    .password(this.properties.getPassword());
            if (this.properties.getType() != null) {
                factory.type(this.properties.getType());
            }
            return factory.build();
        }
    }
    ...
    ...
    @Configuration
    @ConditionalOnProperty(prefix = "spring.datasource", name = "jmx-enabled")
    @ConditionalOnClass(name = "org.apache.tomcat.jdbc.pool.DataSourceProxy")
    @Conditional(DataSourceAutoConfiguration.DataSourceAvailableCondition.class)
    @ConditionalOnMissingBean(name = "dataSourceMBean")
    protected static class TomcatDataSourceJmxConfiguration {
        @Bean
        public Object dataSourceMBean(DataSource dataSource) {
        ....
        ....
        }
    }
    ...
    ...
}

這里的 DataSourceAutoConfiguration 使用了 @ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class}) 注解,這意味著只有 DataSource.classEmbeddedDatabaseType.class 類在 classpath 上可用時才會考慮 DataSourceAutoConfiguration 中的 bean 的自動配置借卧。

該類也使用 @EnableConfigurationProperties(DataSourceProperties.class) 注解,它可以將 application.properties 中的屬性自動綁定到 DataSourceProperties 類的屬性上陪每。

@ConfigurationProperties(prefix = DataSourceProperties.PREFIX)
public class DataSourceProperties implements BeanClassLoaderAware, EnvironmentAware, InitializingBean {
 
    public static final String PREFIX = "spring.datasource";
    ...
    ...
    private String driverClassName;
    private String url;
    private String username;
    private String password;
    ...
    //setters and getters
}

由于使用了此配置镰吵,所有以 spring.datasource.* 開頭的屬性將自動綁定到 DataSourceProperties 對象。

spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=secret
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

你還可以看到一些使用了 SpringBoot 的 Condition 注解(如 @ConditionalOnMissingBean盼产、@ConditionalOnClass 和 @ConditionalOnProperty 等)的內(nèi)部類和 bean 定義方法勺馆。

只有當(dāng)這些條件匹配時,這些 bean 定義才被注冊到 ApplicationContext 中蜈项。

你可以在 spring-boot-autoconfigure-{version}.jar 中瀏覽更多其他 AutoConfiguration 類:

  • org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration
  • org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
  • org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration
  • org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration

等等

我希望現(xiàn)在你已經(jīng)對 “Spring Boot 自動配置如何通過 AutoConfiguration 類以及 @Conditional 工作”有了一定的認(rèn)識续挟。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末诗祸,一起剝皮案震驚了整個濱河市轴总,隨后出現(xiàn)的幾起案子博个,更是在濱河造成了極大的恐慌,老刑警劉巖往堡,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件共耍,死亡現(xiàn)場離奇詭異,居然都是意外死亡穆咐,警方通過查閱死者的電腦和手機(jī)字旭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門遗淳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人洲脂,你說我怎么就攤上這事⊥” “怎么了一铅?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長肮之。 經(jīng)常有香客問我卜录,道長,這世上最難降的妖魔是什么筐高? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮蜀肘,結(jié)果婚禮上稽屏,老公的妹妹穿的比我還像新娘。我一直安慰自己狐榔,他們只是感情好荒叼,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般萝玷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蜓斧,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天挎春,我揣著相機(jī)與錄音豆拨,去河邊找鬼。 笑死施禾,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的邮绿。 我是一名探鬼主播攀例,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼匕累,長吁一口氣:“原來是場噩夢啊……” “哼惶我!你這毒婦竟也來了冠骄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤抱既,失蹤者是張志新(化名)和其女友劉穎扁誓,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體捷泞,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡寿谴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年讶泰,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片痪署。...
    茶點(diǎn)故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡狼犯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出悯森,到底是詐尸還是另有隱情,我是刑警寧澤肥缔,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布汹来,位于F島的核電站,受9級特大地震影響坟岔,放射性物質(zhì)發(fā)生泄漏摔桦。R本人自食惡果不足惜承疲,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一鸥咖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧啊研,春花似錦鸥拧、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至媳握,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背赵誓。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工俩功, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人诡蜓。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓蔓罚,卻偏偏與公主長得像,于是被迫代替她去往敵國和親豺谈。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評論 2 353

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