Spring Boot 中的自動化配置確實夠吸引人,甚至有人說 Spring Boot 讓 Java 又一次煥發了生機,這話雖然聽着有點誇張,但是不可否認的是,曾經臃腫繁瑣的 Spring 配置確實讓人感到頭大,而 Spring Boot 帶來的全新自動化配置,又確實緩解了這個問題。
你要是問這個自動化配置是怎么實現的,很多人會說不就是 starter 嘛!那么 starter 的原理又是什么呢?松哥以前寫過一篇文章,介紹了自定義 starter:
這里邊有一個非常關鍵的點,那就是條件注解,甚至可以說條件注解是整個 Spring Boot 的基石。
條件注解並非一個新事物,這是一個存在於 Spring 中的東西,我們在 Spring 中常用的 profile 實際上就是條件注解的一個特殊化。
想要把 Spring Boot 的原理搞清,條件注解必須要會用,因此今天松哥就來和大家聊一聊條件注解。
定義
Spring4 中提供了更加通用的條件注解,讓我們可以在滿足不同條件時創建不同的 Bean,這種配置方式在 Spring Boot 中得到了廣泛的使用,大量的自動化配置都是通過條件注解來實現的,查看松哥之前的 Spring Boot 文章,凡是涉及到源碼解讀的文章,基本上都離不開條件注解:
有的小伙伴可能沒用過條件注解,但是開發環境、生產環境切換的 Profile 多多少少都有用過吧?實際上這就是條件注解的一個特例。
實踐
拋開 Spring Boot,我們來單純的看看在 Spring 中條件注解的用法。
首先我們來創建一個普通的 Maven 項目,然后引入 spring-context,如下:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
</dependencies>
然后定義一個 Food 接口:
public interface Food {
String showName();
}
Food 接口有一個 showName 方法和兩個實現類:
public class Rice implements Food {
public String showName() {
return "米飯";
}
}
public class Noodles implements Food {
public String showName() {
return "面條";
}
}
分別是 Rice 和 Noodles 兩個類,兩個類實現了 showName 方法,然后分別返回不同值。
接下來再分別創建 Rice 和 Noodles 的條件類,如下:
public class NoodlesCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getEnvironment().getProperty("people").equals("北方人");
}
}
public class RiceCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getEnvironment().getProperty("people").equals("南方人");
}
}
在 matches 方法中做條件屬性判斷,當系統屬性中的 people 屬性值為 '北方人' 的時候,NoodlesCondition 的條件得到滿足,當系統中 people 屬性值為 '南方人' 的時候,RiceCondition 的條件得到滿足,換句話說,哪個條件得到滿足,一會就會創建哪個 Bean 。
接下來我們來配置 Rice 和 Noodles :
@Configuration
public class JavaConfig {
@Bean("food")
@Conditional(RiceCondition.class)
Food rice() {
return new Rice();
}
@Bean("food")
@Conditional(NoodlesCondition.class)
Food noodles() {
return new Noodles();
}
}
這個配置類,大家重點注意兩個地方:
- 兩個 Bean 的名字都為 food,這不是巧合,而是有意取的。兩個 Bean 的返回值都為其父類對象 Food。
- 每個 Bean 上都多了 @Conditional 注解,當 @Conditional 注解中配置的條件類的 matches 方法返回值為 true 時,對應的 Bean 就會生效。
配置完成后,我們就可以在 main 方法中進行測試了:
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().getSystemProperties().put("people", "南方人");
ctx.register(JavaConfig.class);
ctx.refresh();
Food food = (Food) ctx.getBean("food");
System.out.println(food.showName());
}
}
首先我們創建一個 AnnotationConfigApplicationContext 實例用來加載 Java 配置類,然后我們添加一個 property 到 environment 中,添加完成后,再去注冊我們的配置類,然后刷新容器。容器刷新完成后,我們就可以從容器中去獲取 food 的實例了,這個實例會根據 people 屬性的不同,而創建出來不同的 Food 實例。
這個就是 Spring 中的條件注解。
進化
條件注解還有一個進化版,那就是 Profile。我們一般利用 Profile 來實現在開發環境和生產環境之間進行快速切換。其實 Profile 就是利用條件注解來實現的。
還是剛才的例子,我們用 Profile 來稍微改造一下:
首先 Food、Rice 以及 Noodles 的定義不用變,條件注解這次我們不需要了,我們直接在 Bean 定義時添加 @Profile 注解,如下:
@Configuration
public class JavaConfig {
@Bean("food")
@Profile("南方人")
Food rice() {
return new Rice();
}
@Bean("food")
@Profile("北方人")
Food noodles() {
return new Noodles();
}
}
這次不需要條件注解了,取而代之的是 @Profile 。然后在 Main 方法中,按照如下方式加載 Bean:
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("南方人");
ctx.register(JavaConfig.class);
ctx.refresh();
Food food = (Food) ctx.getBean("food");
System.out.println(food.showName());
}
}
效果和上面的案例一樣。
這樣看起來 @Profile 注解貌似比 @Conditional 注解還要方便,那么 @Profile 注解到底是什么實現的呢?
我們來看一下 @Profile 的定義:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
String[] value();
}
可以看到,它也是通過條件注解來實現的。條件類是 ProfileCondition ,我們來看看:
class ProfileCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
return true;
}
}
return false;
}
return true;
}
}
看到這里就明白了,其實還是我們在條件注解中寫的那一套東西,只不過 @Profile 注解自動幫我們實現了而已。
@Profile 雖然方便,但是不夠靈活,因為具體的判斷邏輯不是我們自己實現的。而 @Conditional 則比較靈活。
結語
兩個例子向大家展示了條件注解在 Spring 中的使用,它的一個核心思想就是當滿足某種條件的時候,某個 Bean 才會生效,而正是這一特性,支撐起了 Spring Boot 的自動化配置。
好了,本文就說到這里,有問題歡迎留言討論。
關注公眾號【江南一點雨】,專注於 Spring Boot+微服務以及前后端分離等全棧技術,定期視頻教程分享,關注后回復 Java ,領取松哥為你精心准備的 Java 干貨!