為了控制Bean的載入我使出了這些殺手鐗
故事一: 絕代有佳人,幽居在空谷
美女同學小張,在工作中遇到了煩心事。心情那是破涼破涼的,無法言喻。
故事背景是最近由於需求變動,小張在專案中加入了MQ的整合,剛開始還沒什麼問題,後面慢慢問題的顯露出來了。
自己在本地Debug的時候總是能消費到訊息,由於歷史原因,公司的專案只區分了兩套環境,也就是測試和線上。本地啟動預設就是測試環境,所以會消費測試環境的訊息。
MQ的配置程式碼如下:
@Configuration public class MqConfig { @Bean(initMethod = "start", destroyMethod = "shutdown") public ConsumerBean consumerBean() { // .... } }
想要解決小張的問題,那麼就必須得有第三個環境的區分,也就是增加一個本地開發環境,然後通過環境來決定是否需要初始化MQ。
這個時候就可以用到Spring Boot為我們提供的Conditional家族的註解了,@Conditional註解會根據具體的條件決定是否建立 bean 到容器中, 如下圖:
通過@ConditionalOnProperty來決定MqConfig是否要載入,@ConditionalOnProperty的name就是配置項的名稱,havingValue就是匹配的值,也就是在application配置中存在env=dev才會初始化MqConfig。程式碼如下:
@Configuration @ConditionalOnProperty(name = "env", havingValue = "dev") public class MqConfig { @Bean(initMethod = "start", destroyMethod = "shutdown") public ConsumerBean consumerBean() { // .... } }
但這好像不符合小張同學的需求呀,需求是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。
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");
}
}
方案三:繼承AnyNestedCondition自定義條件
可以使用@Conditional(MqAvailableCondition.class)註解,自定義一個條件類,在類中可以使用其他的Conditional註解來進行判斷,比如使用@ConditionalOnProperty。
@Order(Ordered.LOWEST_PRECEDENCE)
public class MqAvailableCondition extends AnyNestedCondition {
public MqAvailableCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(name = "env", havingValue = "test")
static class EnvTest {
}
@ConditionalOnProperty(name = "env", havingValue = "prod")
static class EnvProd {
}
}
方案四:@ConditionalOnExpression
支援SpEL進行判斷,如果滿足SpEL表示式條件則載入這個bean。這個就相當靈活了,可以將需要滿足的條件都寫進來。
@ConditionalOnExpression("#{'test'.equals(environment['env']) || 'prod'.equals(environment['env'])}")
上面的表示式定義了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,讓使用者自己去引入依賴。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<optional>true</optional>
</dependency>
問題二
第二個問題是攔截器的初始化,如果不做任何處理的話兩個攔截器都會被初始化,如果使用者沒有依賴Feign,那麼就會報錯,所以我們需要對攔截器的初始化進行處理。
下面是預設的配置:
@Bean
public FeignRequestInterceptor feignRequestInterceptor() {
return new FeignRequestInterceptor();
}
@Bean
public RestTemplateRequestInterceptor restTemplateRequestInterceptor() {
return new RestTemplateRequestInterceptor();
}
兩個攔截器都是實現框架自帶的介面,所以我們可以在最外層使用@ConditionalOnClass來判斷如果專案中存在這個Class再裝置配置。
第二層可以通過@ConditionalOnProperty來決定是否要啟用,將控制權交給使用者。
@Configuration
@ConditionalOnClass(name = "feign.RequestInterceptor")
protected static class FeignRequestInterceptorConfiguration {
@Bean
@ConditionalOnProperty("feign.requestInterceptor.enabled")
public FeignRequestInterceptor feignRequestInterceptor() {
return new FeignRequestInterceptor();
}
}
@Configuration
@ConditionalOnClass(name = "org.springframework.http.client.ClientHttpRequestInterceptor")
protected static class RestTemplateRequestInterceptorConfiguration {
@Bean
@ConditionalOnProperty("restTemplate.requestInterceptor.enabled")
public RestTemplateRequestInterceptor restTemplateRequestInterceptor() {
return new RestTemplateRequestInterceptor();
}
}
故事三:自己去學習
文章裡只根據案例講了一個使用的方式,當然還有很多沒有講的,大家可以自己去嘗試瞭解一些作用以及在什麼場景可以使用,像@ConditionalOnBean,@ConditionalOnMissingBean等註解。
另一種學習的方式就是鼓勵大家去看一些框架的原始碼,特別在Spring Cloud這些框架中大量的自動配置,都有用到這些註解,我貼幾個圖給大家看看。