為了控制 Bean 的加載我使出了這些殺手鐗

迎關注我的頭條號:@Wooola,10 年 Java 軟件開發及架構設計經驗,專注於 Java、Go 語言、微服務架構,致力於每天分享原創文章、快樂編碼和開源技術。

故事一:絕代有佳人,幽居在空谷

美女同學小張,在工作中遇到了煩心事。心情那是破涼破涼的,無法言喻。

故事背景是最近由於需求變動,小張在項目中加入了 MQ 的集成,剛開始還沒什麼問題,後面慢慢問題的顯露出來了。

為了控制 Bean 的加載我使出了這些殺手鐧

自己在本地 Debug 的時候總是能消費到消息,由於歷史原因,公司的項目只區分了兩套環境,也就是測試和線上。本地啟動默認就是測試環境,所以會消費測試環境的消息。

MQ 的配置代碼如下:

<code>@Configuration
public class MqConfig {
    @Bean(initMethod = "start", destroyMethod = "shutdown")
    public ConsumerBean consumerBean() {
        // ....
    }
}/<code>

想要解決小張的問題,那麼就必須得有第三個環境的區分,也就是增加一個本地開發環境,然後通過環境來決定是否需要初始化 MQ。

這個時候就可以用到 Spring Boot 為我們提供的 Conditional 家族的註解了,@Conditional 註解會根據具體的條件決定是否創建 bean 到容器中, 如下圖:

為了控制 Bean 的加載我使出了這些殺手鐧

通過@ConditionalOnProperty 來決定 MqConfig 是否要加載,@ConditionalOnProperty 的 name 就是配置項的名稱,havingValue 就是匹配的值,也就是在 application 配置中存在 env=dev 才會初始化 MqConfig。代碼如下:

<code>@Configuration
@ConditionalOnProperty(name = "env", havingValue = "dev")
public class MqConfig {
    @Bean(initMethod = "start", destroyMethod = "shutdown")
    public ConsumerBean consumerBean() {
        // ....
    }
}/<code>

但這好像不符合小張同學的需求呀,需求是 dev 環境不加載才對。還有一個就是歷史原因,增加一個環境有風險,因為對應的環境加載的內容什麼的,都需要有變動,所以還是保留歷史情況,環境不變,看能不能從其他的點解決這個問題。

現在面臨的問題是不能增加新的環境,保留之前的 test 和 prod。只需要在 test 和 prod 初始化 Mq。

方案一:@ConditionalOnProperty

還是堅持使用@ConditionalOnProperty,既然不能通過環境來,我們可以單獨增加一個屬性來決定是否要啟用 Mq, 比如定義為:mq.enabled=true 表示開啟,mq.enabled=false 表示不開啟。

然後在 test 和 prod 啟動的時候增加-Dmq.enabled=true 或者在對應的配置文件中增加也可以,本地開發的時候-Dmq.enabled=false 就可以了。

雖然能夠解決問題,但是不是最佳的方案,因為已有的環境和開發人員本地都得增加啟動參數。

方案二:繼承 SpringBootCondition 自定義條件

可以使用@Conditional(MqConditional.class)註解,自定義一個條件類,在類中去判斷是否要加載 bean。

<code>public class MqConditional extends SpringBootCondition {
    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment environment = context.getEnvironment();
        String env = environment.getProperty("env");
        if (StringUtils.isBlank(env)) {
            return ConditionOutcome.noMatch("no match");
        }
        if (env.equals("test") || env.equals("prod")) {
            return ConditionOutcome.match();
        }
        return ConditionOutcome.noMatch("no match");
    }
}/<code>

方案三:繼承 AnyNestedCondition 自定義條件

可以使用@Conditional(MqAvailableCondition.class)註解,自定義一個條件類,在類中可以使用其他的 Conditional 註解來進行判斷,比如使用@ConditionalOnProperty。

<code>/<code>

方案四:@ConditionalOnExpression

支持 SpEL 進行判斷,如果滿足 SpEL 表達式條件則加載這個 bean。這個就相當靈活了,可以將需要滿足的條件都寫進來。

<code>/<code>

上面的表達式定義了 Spring Environment 中只要有 env 為 test 或者 prod 的時候就會初始化 MqConfig。這樣一來老的啟動命令都不用改變,本地開發的時候也不用增加參數,可以說是最佳的方案,因為改動的點變少了,出錯的幾率小,使用難度低。

故事二:北方有佳人,絕世而獨立

美女小楊同學最近也遇到了煩心事,雖然是女生,但是也工作了幾年了。最近受到領導重用,讓她搭一套 Spring Cloud 的框架給同事們分享一下。

她有個想法是將某些信息可以通過 Feign 或者 RestTemplate 進行傳遞,天然友好的方式就是在攔截器中統一實現。

如果在每個服務中都寫一份一樣的代碼,就顯得很低級了,所以她將這兩個攔截器統一寫在一個模塊中,作為 Spring Boot Starter 的方式引入。

問題一

遇到的第一個問題是這個模塊引入了 Feign 和 spring-web 兩個依賴,想做的通用一點,就是使用者可能會用 Feign 來調用接口,也可能會用 RestTemplate 來調用接口,如果使用者不用 Feign, 但是引入了這個 Starter 也會依賴 Feign。

所以需要在依賴的時候設置 Feign 的 Maven 依賴 optional=true,讓使用者自己去引入依賴。

<code>
  org.springframework.cloud
  spring-cloud-starter-openfeign
  true
/<code>

問題二

第二個問題是攔截器的初始化,如果不做任何處理的話兩個攔截器都會被初始化,如果使用者沒有依賴 Feign,那麼就會報錯,所以我們需要對攔截器的初始化進行處理。

下面是默認的配置:

<code>@Bean
public FeignRequestInterceptor feignRequestInterceptor() {
  return new FeignRequestInterceptor();
}
@Bean
public RestTemplateRequestInterceptor restTemplateRequestInterceptor() {
  return new RestTemplateRequestInterceptor();
}/<code>

兩個攔截器都是實現框架自帶的接口,所以我們可以在最外層使用@ConditionalOnClass 來判斷如果項目中存在這個 Class 再裝置配置。

第二層可以通過@ConditionalOnProperty 來決定是否要啟用,將控制權交給使用者。

<code>/<code>

故事三:自己去學習

文章裡只根據案例講了一個使用的方式,當然還有很多沒有講的,大家可以自己去嘗試瞭解一些作用以及在什麼場景可以使用,像@ConditionalOnBean,@ConditionalOnMissingBean 等註解。

另一種學習的方式就是鼓勵大家去看一些框架的源碼,特別在 Spring Cloud 這些框架中大量的自動配置,都有用到這些註解,我貼幾個圖給大家看看。

為了控制 Bean 的加載我使出了這些殺手鐧


為了控制 Bean 的加載我使出了這些殺手鐧


來源:

https://mp.weixin.qq.com/s/nyRgwiPrzGClacnGkgplkA

侵刪


分享到:


相關文章: