Spring Bean的配置綁定

配置綁定的場景

最常見的配置綁定的場景华蜒,是在自定義的bean中通過@Value注解將某個屬性和對應的配置綁定:

application.yml

di-task:
  root-path: "/usr/lib/cluster001/..."

某個bean

@Component
public class TaskApplicationConfig {
    @Value("${di-task.root-path}")
    private String rootPath;
    ...
}

在創(chuàng)建定時任務的bean時,使用的@Scheduled注解也可以通過相同的方式綁定配置途样,本質(zhì)上和上面是相同的。

@Component
public class InitStatTask {

    @Scheduled(cron = "${di-task.task.init-stat.cron}")
    public void initTask() {
        // do business
    }
}

批量綁定

@ConfigurationProperties提供了更強大的功能徊哑,比如在我們需要批量綁定的時候硼啤,可以將多個配置項一起綁定到某個bean的多個屬性上,而不用每一個屬性都使用一個@Value注解:

@Component
@ConfigurationProperties(prefix = "hbm")
public class HbmConfig {
    /**
     * CM master 的IP
     */
    private String agentHostIp;
    /**
     * CM master 的帳號.
     */
    private String agentHostUser;
    /**
     * CM master 的密碼.
     */
    private String agentHostPassword;
    /**
     * CM master 的端口.
     */
    private Integer sshdPort;

    // getters and setters
}

這種定義横堡,暗示著應用中存在下面的配置:

hbm:
  agent-host-ip: ...
  agent-host-user: ...
  agent-host-password: ...
  sshd-port: ...

這樣埋市,四個屬性就可以一次性綁定到一個spring bean上。

為第三方依賴綁定配置

上面的示例命贴,都有一個共同的特點:在自定義的bean中綁定配置道宅。更直白一點,作為spring bean的代碼是項目內(nèi)的代碼胸蛛,不是第三方引入的依賴污茵。

其中一個特征,就是我們直接在類上使用諸如@Component葬项、@Service泞当、@RestController的注解來聲明為spring的bean。

如果是引入的第三方依賴民珍,要實例化為spring的bean襟士,需要我們自己進行配置。在當前項目環(huán)境中嚷量,這種類一般很少陋桂,直接寫配置類就可以了:

@Configuration
public class SomeConfig {
    @Bean
    public SomeType someType() {
        SomeType bean = new SomeType();
        // init bean
        return bean;
    }
}

只要這個配置類能被@SpringBootApplication@ComponentScan掃描到,對應的bean就會被創(chuàng)建蝶溶。

順便提一下嗜历,如果我們在配置bean的時候,需要注入其他的bean依賴抖所,可以將依賴作為參數(shù)梨州。spring會根據(jù)類型和參數(shù)名來尋找對應的bean,然后作為參數(shù)參入進來:

@Configuration
public class SomeConfig {
    @Bean
    public SomeType someType() {
        SomeType bean = new SomeType();
        // init bean
        return bean;
    }
    
    @Bean
    public OtherType otherType(SomeType someType) {
        OtherType bean = new OtherType();
        // init bean with some type
        return bean;
    }
}

一個新的問題:當我們需要為第三方依賴的bean綁定配置的時候部蛇,應該如何做摊唇。

手工綁定

基于已有的知識,我們可以將配置項先綁定到自己的代碼涯鲁,然后再調(diào)用第三方依賴的setter方法巷查,將對應的配置一一設置到bean中:

@Configuration
@ConfigurationProperties(prefix = "some-type")
public class SomeConfig {
    
    private String field1;
    private String field2;
    ...
    
    // getters and setters for field*
    
    @Bean
    public SomeType someType() {
        SomeType bean = new SomeType();
        // init bean
        bean.setField1(field1);
        bean.setField2(field2);
        ...
        return bean;
    }
}

對應的配置大概下面的樣子:

some-type:
  field1: ...
  field2: ...

自動綁定

@ConfigurationProperties簡化了這個工作,我們可以將它和@Bean一起使用抹腿,這種綁定的工作就會被自動完成:

@Configuration
public class SomeConfig {
    
    @Bean
    @ConfigurationProperties(prefix = "some-type")
    public SomeType someType() {
        SomeType bean = new SomeType();
        // init bean
        return bean;
    }
}

spring會在someType()執(zhí)行完之后岛请,按照默認的綁定策略將配置綁定到返回的bean中。其效果和上文中我們逐個將配置綁定到bean中是一樣的警绩。

需要注意的是崇败,spring按照setter的規(guī)范去綁定變量,配置的key和bean的setter方法要相呼應。比如某個bean有屬性jdbcUrl后室,但對應的setter方法是setUrl缩膝,那么配置文件中應該配置的是url而不是jdbc-url

如果setter方法在bean對應的類型的父類中岸霹,綁定同樣有效疾层。

可選擇的綁定

我們可能會有不同的場景:允許用戶選擇使用哪一個數(shù)據(jù)源的實現(xiàn),比如druid或者hikari贡避。我們需要根據(jù)用戶選擇的數(shù)據(jù)源類型痛黎,來配置DataSource的實例化和參數(shù)綁定的動作。

換句話來說刮吧,上文中someType()方法可能返回的具體類型有多種湖饱,而使用@ConfigurationProperties的前提是我知道返回的具體是哪一種,以根據(jù)其setter方法設計對應的配置杀捻。所以我們所說的上述場景井厌,就無法直接使用了。

選擇并手工綁定

如果我們退一步水醋,依然可以通過顯示的逐個綁定來完成這項工作旗笔。首先,我們將所有的數(shù)據(jù)源類型的相關參數(shù)拄踪,都一一綁定到我們自己的類里面,然后在構造數(shù)據(jù)源Bean的時候拳魁,根據(jù)用戶配置的類型來構建具體的數(shù)據(jù)源Bean惶桐,并將對應的配置參數(shù)通過setter方法進行設置。代碼大意大概如下:

spring.datasource:
  type: druid
  druid:
    url: jdbc:hive2://10.3.70.118:10000
    driver-class-name: org.apache.hive.jdbc.HiveDriver
  hikari:
    jdbc-url: jdbc:hive2://10.3.70.118:10000
    driver-class-name: org.apache.hive.jdbc.HiveDriver
    connection-test-query: SELECT 'di-sql-engine'

你可能注意到druid和hikari的配置有些不同潘懊,這是與具體的實現(xiàn)類及其父類所提供的setter方法相關的姚糊。

@ConfigurationProperties(prefix = "spring.datasource.druid")
public class DruidConfig {
    
    private String url;
    private String driverClassName;
    ...
    
    // getters and setters for field*
}
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public class HikariConfig {
    
    private String jdbcUrl;
    private String driverClassName;
    private String connectionTestQuery;
    ...
    
    // getters and setters for field*
}
@Configuration
@ConfigurationProperties(prefix = "spring.datasource")
public class SomeConfig {
    @Autowired
    private DruidConfig druidConfig;
    @Autowired
    private HikariConfig hikariConfig;
    
    private String type;
    ...
    
    // getters and setters for field*
    
    @Bean
    public DataSource datasource() {
        DataSource ds = null;
        switch(type) {
            case "druid":
                DruidDataSource druid = ... // init druid datasource
                druid.setUrl(druidConfig.getUrl);
                druid.setDriverClassName(druidConfig.getDriverClassName);
                ds = druid;
                break;
            case "hikari":
                // ...
                break;
        }
        ...
        return ds;
    }
}

上面的代碼應該可以自己想得到,它只是組合了我們已知的知識授舟。

選擇并自動綁定

但是在我們接觸了自動綁定之后救恨,再手動逐個綁定看起來有點蠢。接下來介紹另一種更加可控的自動綁定方式释树,幫助我們消除那些無聊的代碼肠槽。

依然是基于同樣的配置,我們可以這樣編寫配置類:

@Configuration
@ConfigurationProperties(prefix = "spring.datasource")
public class SomeConfig {

    @Autowired
    private Environment environment;
    
    private String type;
    ...
    
    // getters and setters for field*
    
    @Bean
    public DataSource datasource() {
        Class<? extends DataSource> classType = resolveFromType(type);
        
        DataSource dataSource = DataSourceBuilder.create()
                .type(
                    (Class<? extends DataSource>) Class.forName(classType)
                ).build();
        
        final Bindable<? extends DataSource> bindable = 
            Bindable.ofInstance(dataSource);
        
        final Binder binder = Binder.get(environment);
        
        switch(type) {
            case "druid":
                binder.bind("spring.datasource.druid", bindable);
                break;
            case "hikari":
                binder.bind("spring.datasource.hikari", bindable);
                break;
        }
        
        ...
        return dataSource;
    }
}

關鍵的代碼是:

binder.bind("spring.datasource.druid", bindable);
// or
binder.bind("spring.datasource.hikari", bindable);

只是為了要獲取關鍵代碼中的binder和bindable奢啥,需要學習關于Bindable和Binder的相關API秸仙。

簡單來說,Bindable包裝了待綁定的bean桩盲,它的構造也是基于已有的bean來構造的寂纪。Binder需要基于當前的Environment來構造,這樣它可以讀到相關的環(huán)境變量和配置。Environment是自動配置的捞蛋,我們可以直接注入孝冒。

這樣我們就實現(xiàn)了自動綁定,而且是根據(jù)場景去綁定不同的配置拟杉。

附錄

補充問題

為什么hikari要配置connection-test-query庄涡?

提出這個問題的同學很細心。這是因為我們連接的是hive數(shù)據(jù)源捣域,所使用的驅(qū)動包是hive-jdbc-1.2.1.spark2啼染。這個驅(qū)動包里的Connection實現(xiàn)類,即HiveConnection并沒有真正實現(xiàn)所有的抽象方法焕梅。而沒有實現(xiàn)的那些方法迹鹅,就直接拋出異常:

  @Override
  public boolean isValid(int timeout) throws SQLException {
    // TODO Auto-generated method stub
    throw new SQLException("Method not supported");
  }

而使用hikari數(shù)據(jù)源,在沒有配置connection-test-query的情況下贞言,它會調(diào)用Connection的isValid方法來驗證Connection的可用性斜棚,結(jié)果就是得到一個異常。

所以我們主動配置了connection-test-query该窗,讓它使用這個sql語句來驗證弟蚀。

參考資料

1、Property Binding in Spring Boot 2.0

2酗失、Guide to @ConfigurationProperties in Spring Boot

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末义钉,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子规肴,更是在濱河造成了極大的恐慌捶闸,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拖刃,死亡現(xiàn)場離奇詭異删壮,居然都是意外死亡,警方通過查閱死者的電腦和手機兑牡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門央碟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人均函,你說我怎么就攤上這事亿虽。” “怎么了边酒?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵经柴,是天一觀的道長。 經(jīng)常有香客問我墩朦,道長坯认,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮牛哺,結(jié)果婚禮上陋气,老公的妹妹穿的比我還像新娘。我一直安慰自己引润,他們只是感情好巩趁,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著淳附,像睡著了一般议慰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上奴曙,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天别凹,我揣著相機與錄音,去河邊找鬼洽糟。 笑死炉菲,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的坤溃。 我是一名探鬼主播拍霜,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼薪介!你這毒婦竟也來了祠饺?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤汁政,失蹤者是張志新(化名)和其女友劉穎吠裆,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體烂完,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年诵棵,在試婚紗的時候發(fā)現(xiàn)自己被綠了抠蚣。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡履澳,死狀恐怖嘶窄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情距贷,我是刑警寧澤柄冲,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站忠蝗,受9級特大地震影響现横,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一戒祠、第九天 我趴在偏房一處隱蔽的房頂上張望骇两。 院中可真熱鬧,春花似錦姜盈、人聲如沸低千。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽示血。三九已至,卻和暖如春救拉,著一層夾襖步出監(jiān)牢的瞬間难审,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工近上, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留剔宪,地道東北人。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓壹无,卻偏偏與公主長得像葱绒,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子斗锭,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345