1. 前言
在開發Spring Boot應用時會用到根據條件來向Spring IoC容器注入Bean。比如配置檔案存在了某個配置屬性才注入Bean :
圖中紅色的部分是說,只有ali.pay.v1.app-id
存在於Spring的環境配置中時這個@Configuration
標記的類才能注入Spring IoC。
這裡面的@ConditionalOnProperty
就是條件註解系列的一種。它還有很多種來滿足各種場景的條件註解:
其實數量遠不止截圖中這幾個,在Spring 家族的其它框架中也有實現。
這裡扯得有點遠了,今天不是來講這些條件控制註解的用法的,只是我發現了一個使用條件註解@ConditionalOnProperty
無法解決的問題。
條件注入參考往期:Spring Boot 2 實戰:使用 @Condition 註解來根據條件注入 Bean
2. 配置檔案存在Map結構的場景
下面是一段配置檔案:
app:
v1:
foo:
name: felord.cn
description: 碼農小胖哥
bar:
name: ooxx.cn
description: xxxxxx
對應配置類:
@Data
@ConfigurationProperties("app")
public class AppProperties {
/**
*
*/
private Map<String, V1> v1 = new HashMap<>();
/**
*
*
* @author felord.cn
* @since 1.0.0.RELEASE
*/
@Data
public static class V1 {
/**
* name
*/
private String name;
/**
* description
*/
private String description;
}
}
特殊之處來了,yml
配置裡的 foo
、bar
其實是作為Map
中的key
來標識V1
的,和其它配置引數不同,這個key
使用者可以隨意定義一個String
來標識,可能是foo
,可能是bar
,完全根據開發者的喜好進行主觀定義。這個時候你想根據app.v1.*.name
(暫時用萬用字元*
)來進行@ConditionalOnProperty
判斷是行不通的,因為你不確定*
的值,該怎麼辦呢?
3. 解決方案
這裡我花了一天的時間去摸索,最開始我認為Spring提供萬用字元(app.v1.*.name
)甚至是SpringEL
表示式可以拿到,但是搞了半天無功而返。
突然我想到之前看Spring Security OAuth2原始碼中有類似的邏輯。用過Spring Security OAuth2相關的都知道Spring Security OAuth2也要求使用者自定義一個key
來標識自己的OAuth2客戶端。比如我用Gitee的:
spring:
security:
oauth2:
client:
registration:
gitee:
client-id: xxxxxx
client-secret: xxxxx
這裡的
key
就是gitee
,當然這根據開發者心情決定,甚至你用zhangshan
作為key
都可以。
Spring Security OAuth2 提供了相關的條件注入思路,下面是其條件注入判斷的核心類:
public class ClientsConfiguredCondition extends SpringBootCondition {
private static final Bindable<Map<String, OAuth2ClientProperties.Registration>> STRING_REGISTRATION_MAP = Bindable
.mapOf(String.class, OAuth2ClientProperties.Registration.class);
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("OAuth2 Clients Configured Condition");
Map<String, OAuth2ClientProperties.Registration> registrations = getRegistrations(context.getEnvironment());
if (!registrations.isEmpty()) {
return ConditionOutcome.match(message.foundExactly("registered clients " + registrations.values().stream() .map(OAuth2ClientProperties.Registration::getClientId).collect(Collectors.joining(", "))));
}
return ConditionOutcome.noMatch(message.notAvailable("registered clients"));
}
private Map<String, OAuth2ClientProperties.Registration> getRegistrations(Environment environment) {
return Binder.get(environment).bind("spring.security.oauth2.client.registration", STRING_REGISTRATION_MAP)
.orElse(Collections.emptyMap());
}
}
顯然OAuth2ClientProperties
的結構和我們要驗證的AppProperties
結構是一樣的。所以上面的邏輯是可以抄過來的,它可以將環境配置中的帶有不確定key
的配置繫結到我們的配置類AppProperties
中。核心的繫結邏輯是這一段:
Binder.get(environment).bind("spring.security.oauth2.client.registration", STRING_REGISTRATION_MAP)
首先通過Bindable
來宣告一個可繫結的資料結構,這裡呼叫了mapOf
方法宣告瞭一個Map
的資料繫結結構。然後通過繫結的具體操作物件Binder
從配置環境介面Environment
中提取了spring.security.oauth2.client.registration
開頭的配置屬性並注入到Map
中去。既然我們能夠獲取到了Map
,根據什麼策略判斷就完全掌握在我們手中了。
Bindable
為Spring Boot 2.0提供的資料繫結新特性,有興趣可從spring.io獲取更多資訊。
接下來不用我說了吧,照葫蘆畫瓢還有誰不會呢?配合@Conditional
註解就能實現根據app.v1
下引數的實際情況來動態的進行Bean注入。
4. 總結
今天利用Spring Boot 2.0的資料繫結特性解決了一個實際需求,花了不少時間。當我們解決問題陷入困境時,首先要去想想有沒有類似場景以及對應的解決方案。這同樣說明平時的積累很重要,很多粉絲的問題其實公眾號都有講過,所以處處留心解釋學問。多多留意:碼農小胖哥,共同學習,共同進步。
關注公眾號:Felordcn 獲取更多資訊