SpringBoot 條件註解

一、引言

當我們使用 SpringBoot 進行開發時,會引入許多的 starter 包,引入後就會爲我們做一些自動化配置,省去了大量配置的時間,快速開發。

以 Redis 爲例,當我們引入 Redis 的 starter 包時:

 <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

spring-boot-autoconfigure 包中有一個類叫做 RedisAutoConfiguration ,該類頭上有三個註解:

  • @Configuration:標明該類是一個配置類,並注入容器
  • @ConditionalOnClass:當需求類均存在時,滿足啓用要求
  • @EnableConfigurationProperties:開啓 @ConfigurationProperties 註解

spring-boot-starter-data-redis 包被引入後,@ConditionalOnClass 的要求被滿足,RedisAutoConfiguration 這個類就會被啓用。在圖中可以看到最後注入了一個名爲 redisTemplate 的 Bean,這個 Bean 我們很熟悉,用它就可以來操縱 Redis。

@ConditionalOnClass註解是上面流程中最關鍵的註解,你可以認爲它是一個開關,只有滿足這個註解的條件,這個開關纔會打開,這個類就會啓用。這就是 SpringBoot 自動配置的關鍵,只有引入相關的 starter,@ConditionalOnClass 的條件纔會滿足,自動配置纔會生效。

二、簡單實踐

@ConditionalOnClass是 SpringBoot 二次封裝的註解,它的底層依賴於 Spring 的 @Conditional 註解。

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {
	...
}

@Conditional 註解又名 條件註解,下面就來跟着我一探究竟吧!

2.1 引入依賴

讓我們脫離 SpringBoot 框架,建立一個 Maven 項目,只引入 Spring 的依賴:

<dependencies>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.1.5.RELEASE</version>
  </dependency>
</dependencies>

2.2 接口與實現類

定義一個名爲 Animal 的接口,這個接口只有一個方法,即 say()

public interface Animal {
    void say();
}

然後分別定義 Dog 和 Cat 的實現,衆所周知,狗和貓的叫聲是不一樣的~

public class Dog implements Animal {
    public void say() {
        System.out.println("汪汪~");
    }
}
public class Cat implements Animal {
    public void say() {
        System.out.println("喵喵~");
    }
}

2.3 條件類

有了 Dog 和 Cat 這兩個實現類,下面需要配合 @Conditional 註解來決定什麼時候加載。首先我們需要定義加載的規則,簡單起見,以讀取環境變量的 animal 屬性爲例。

@Conditional 屬性的條件規則需要實現 Conditional 接口,該接口的 matches() 方法決定了是否加載。分別定義兩個 Condition,用於判斷是否加載 Dog 或者 Cat。

public class DogCondition implements Condition {
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        return "dog".equals(conditionContext.getEnvironment().getProperty("animal"));
    }
}
public class CatCondition implements Condition {
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        return "cat".equals(conditionContext.getEnvironment().getProperty("animal"));
    }
}

2.4 配置類

下面就是關鍵的配置類的信息了:

@Configuration
public class AnimalConfig {
    @Bean("animal")
    @Conditional(DogCondition.class)
    public Animal dog() {
        return new Dog();
    }

    @Bean("animal")
    @Conditional(CatCondition.class)
    public Animal cat() {
        return new Cat();
    }
}

你可以看到這裏注入了兩個都叫 animal 的 Bean,我們知道默認情況下同名會被覆蓋,因此我們通過 @Conditional 註解決定了到底應該如何加載。

  • @Conditional(DogCondition.class) 滿足時,加載 Dog;

  • @Conditional(CatCondition.class) 滿足時,加載 Cat。

2.5 啓動類

讓我們寫入一個啓動類來實現下,首先設置了環境變量中 animal 的值,然後將配置類注入容器並刷新 context,最後從容器中取出 Bean,並調用 Bean 的 say() 方法。

public class Application {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.getEnvironment().getSystemProperties().put("animal", "cat");

        context.register(AnimalConfig.class);
        context.refresh();

        Animal animal = (Animal) context.getBean("animal");
        animal.say();
    }
}

當變量值爲 cat 時,輸出:

喵喵~

當變量值爲 dog 時,輸出:

汪汪~

三、@Profile

在實際開發中,由於存在開發、測試、預發、生產等多套環境,程序中相應的也會準備多套配置文件,根據當前環境選擇對應的配置文件。傳統情況下我們會建立多個 application.yaml,命名符合 application-{profie}.yaml規範,然後通過 spring.profiles.active={profile} 來指定環境。

我們可以利用 @Profile 註解來決定在什麼環境下加載配置。例如在 dev 環境下加載 Dog,在 test 環境下加載 Cat。

3.1 配置類

修改配置類如下,不再需要 Condition 註解,使用 @Profile 註解決定是否加載:

@Configuration
public class AnimalConfig {
    @Bean("animal")
    @Profile("dev")
    public Animal dog() {
        return new Dog();
    }

    @Bean("animal")
    @Profile("test")
    public Animal cat() {
        return new Cat();
    }
}

3.2 啓動類

修改啓動類如下:

public class Application {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.getEnvironment().setActiveProfiles("dev");
        context.register(AnimalConfig.class);
        context.refresh();

        Animal animal = (Animal) context.getBean("animal");
        animal.say();
    }
}

通過 setActiveProfiles() 手動指定當前環境,當值爲 dev 或 test 時,得到的運行結果符合預期。

四、更多註解

回到 SpringBoot,除了一開始介紹的 @ConditionalOnClass 外,SpringBoot 還封裝了這些註解,供我們使用:

註解 註釋
@ConditionalOnSingleCandidate 類似於@ConditionalOnBean
但只有在確定了給定bean類的單個候選項時纔會加載bean。
@ConditionalOnMissingBean 當給定的 Bean 不存在時返回 true
各類型間是 or 的關係
@ConditionalOnBean 與上面相反,要求bean存在
@ConditionalOnMissingClass 當給定的類名在類路徑上不存在時返回true
各類型間是 and 的關係
@ConditionalOnClass 與上面相反,要求類存在
@ConditionalOnExpression Spel 表達式成立,返回 true
@ConditionalOnJava 運行時的Java版本號包含給定的版本號,返回 true
@ConditionalOnProperty 屬性匹配條件滿足,返回 true
@ConditionalOnWebApplication web環境存在時,返回 true
@ConditionalOnNotWebApplication web環境不存在時,返回 true
@ConditionalOnResource 指定的資源存在時,返回 true

五、參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章