【String註解驅動開發】如何按照條件向Spring容器中註冊bean?這次我懂了!!

冰河團隊發表於2020-06-10

寫在前面

當bean是單例項,並且沒有設定懶載入時,Spring容器啟動時,就會例項化bean,並將bean註冊到IOC容器中,以後每次從IOC容器中獲取bean時,直接返回IOC容器中的bean,不再建立新的bean。

如果bean是單例項,並且使用@Lazy註解設定了懶載入,則Spring容器啟動時,不會例項化bean,也不會將bean註冊到IOC容器中,只有第一次獲取bean的時候,才會例項化bean,並且將bean註冊到IOC容器中。

如果bean是多例項,則Spring容器啟動時,不會例項化bean,也不會將bean註冊到IOC容器中,以後每次從IOC容器中獲取bean時,都會建立一個新的bean返回。

Spring支援按照條件向IOC容器中註冊bean,滿足條件的bean就會被註冊到IOC容器中,不滿足條件的bean就不會被註冊到IOC容器中。接下來,我們就一起來探討Spring中如何實現按照條件向IOC容器中註冊bean。

專案工程原始碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

@Conditional註解概述

@Conditional註解可以按照一定的條件進行判斷,滿足條件向容器中註冊bean,不滿足條件就不向容器中註冊bean。

@Conditional註解是由 SpringFramework 提供的一個註解,位於 org.springframework.context.annotation 包內,定義如下。

package org.springframework.context.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
	Class<? extends Condition>[] value();
}

從@Conditional註解的原始碼來看,@Conditional註解可以新增到類上,也可以新增到方法上。在@Conditional註解中,存在一個Condition型別或者其子型別的Class物件陣列,Condition是個啥?我們點進去看一下。

package org.springframework.context.annotation;

import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.core.type.AnnotatedTypeMetadata;
@FunctionalInterface
public interface Condition {
	boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

可以看到,Condition是一個函式式介面,對於函式式介面不瞭解的同學可以參見【Java8新特性】中的《【Java8新特性】還沒搞懂函式式介面?趕快過來看看吧!》一文。也可以直接檢視《Java8新特性專欄》來系統學習Java8的新特性。

所以,我們使用@Conditional註解時,需要一個類實現Spring提供的Condition介面,它會匹配@Conditional所符合的方法,然後我們可以使用我們在@Conditional註解中定義的類來檢查。

@Conditional註解的使用場景如下所示。

  • 可以作為類級別的註解直接或者間接的與@Component相關聯,包括@Configuration類;
  • 可以作為元註解,用於自動編寫構造性註解;
  • 作為方法級別的註解,作用在任何@Bean方法上。

向Spring容器註冊bean

不帶條件註冊bean

我們在PersonConfig2類中新增person01()方法和person02()方法,併為兩個方法新增@Bean註解,如下所示。

@Bean("binghe001")
public Person person01(){
    return new Person("binghe001", 18);
}

@Bean("binghe002")
public Person person02(){
    return new Person("binghe002", 20);
}

那麼,這兩個bean預設是否會被註冊到Spring容器中呢,我們新建一個測試用例來測試一下。在SpringBeanTest類中新建testAnnotationConfig6()方法,如下所示。

@Test
public void testAnnotationConfig6(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    String[] names = context.getBeanNamesForType(Person.class);
    Arrays.stream(names).forEach(System.out::println);
}

我們執行testAnnotationConfig6()方法,輸出的結果資訊如下所示。

person
binghe001
binghe002

從輸出結果可以看出,同時輸出了binghe001和binghe002。說明預設情況下,Spring容器會將單例項並且非懶載入的bean註冊到IOC容器中。

接下來,我們再輸出bean的名稱和bean例項物件資訊,此時我們在testAnnotationConfig6()方法中新增相應的程式碼片段,如下所示。

@Test
public void testAnnotationConfig6(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    String[] names = context.getBeanNamesForType(Person.class);
    Arrays.stream(names).forEach(System.out::println);

    Map<String, Person> beans = context.getBeansOfType(Person.class);
    System.out.println(beans);
}

再次執行SpringBeanTest類中的testAnnotationConfig6()方法,輸出結果如下所示。

person
binghe001
binghe002
給容器中新增Person....
{person=Person(name=binghe002, age=18), binghe001=Person(name=binghe001, age=18), binghe002=Person(name=binghe002, age=20)}

可以看到,輸出了註冊到容器的bean。

帶條件註冊bean

現在,我們就要提出新的需求了,比如,如果當前作業系統是Windows作業系統,則向Spring容器中註冊binghe001;如果當前作業系統是Linux作業系統,則向Spring容器中註冊binghe002。此時,我們就需要使用@Conditional註解了。

這裡,有小夥伴可能會問:如何獲取作業系統的型別呢,別急,這個問題很簡單,我們繼續向下看。

使用Spring的ApplicationContext介面就能夠獲取到當前作業系統的型別,如下所示。

ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
Environment environment = context.getEnvironment();
String osName = environment.getProperty("os.name");
System.out.println(osName);

我們將上述程式碼整合到SpringBeanTest類中的testAnnotationConfig6()方法中,如下所示。

@Test
public void testAnnotationConfig6(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    Environment environment = context.getEnvironment();
    String osName = environment.getProperty("os.name");
    System.out.println(osName);

    String[] names = context.getBeanNamesForType(Person.class);
    Arrays.stream(names).forEach(System.out::println);

    Map<String, Person> beans = context.getBeansOfType(Person.class);
    System.out.println(beans);
}

接下來,我們執行SpringBeanTest類中的testAnnotationConfig6()方法,輸出的結果資訊如下所示。

Windows 10
person
binghe001
binghe002
給容器中新增Person....
{person=Person(name=binghe002, age=18), binghe001=Person(name=binghe001, age=18), binghe002=Person(name=binghe002, age=20)}

由於我使用的作業系統是Windows 10作業系統,所以在結果資訊中輸出了Windows 10。

到這裡,我們成功獲取到了作業系統的型別,接下來,就可以實現:如果當前作業系統是Windows作業系統,則向Spring容器中註冊binghe001;如果當前作業系統是Linux作業系統,則向Spring容器中註冊binghe002的需求了。此時,我們就需要藉助Spring的@Conditional註解來實現了。

要想使用@Conditional註解,我們需要實現Condition介面來為@Conditional註解設定條件,所以,這裡,我們建立了兩個實現Condition介面的類,分別為WindowsCondition和LinuxCondition,如下所示。

  • WindowsCondition
package io.mykit.spring.plugins.register.condition;


import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
 * @author binghe
 * @version 1.0.0
 * @description Windows條件,判斷作業系統是否是Windows
 */
public class WindowsCondition implements Condition {
    /**
     * ConditionContext:判斷條件使用的上下文環境
     * AnnotatedTypeMetadata:註釋資訊
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //判斷是否是Linux系統
        //1.獲取到IOC容器使用的BeanFactory
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        //2.獲取類載入器
        ClassLoader classLoader = context.getClassLoader();
        //3.獲取當前的環境資訊
        Environment environment = context.getEnvironment();
        //4.獲取bean定義的註冊類,我們可以通過BeanDefinitionRegistry物件檢視
        //Spring容器中註冊了哪些bean,也可以通過BeanDefinitionRegistry物件向
        //Spring容器中註冊bean,移除bean,檢視bean的定義,檢視是否包含某個bean的定義
        BeanDefinitionRegistry registry = context.getRegistry();
        String property = environment.getProperty("os.name");
        return property.contains("Windows");
    }
}
  • LinuxCondition
package io.mykit.spring.plugins.register.condition;

import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
 * @author binghe
 * @version 1.0.0
 * @description Linux條件,判斷作業系統是否是Linux
 */
public class LinuxCondition implements Condition {
    /**
     * ConditionContext:判斷條件使用的上下文環境
     * AnnotatedTypeMetadata:註釋資訊
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //判斷是否是Linux系統
        //1.獲取到IOC容器使用的BeanFactory
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        //2.獲取類載入器
        ClassLoader classLoader = context.getClassLoader();
        //3.獲取當前的環境資訊
        Environment environment = context.getEnvironment();
        //4.獲取bean定義的註冊類,我們可以通過BeanDefinitionRegistry物件檢視
        //Spring容器中註冊了哪些bean,也可以通過BeanDefinitionRegistry物件向
        //Spring容器中註冊bean,移除bean,檢視bean的定義,檢視是否包含某個bean的定義
        BeanDefinitionRegistry registry = context.getRegistry();
        String property = environment.getProperty("os.name");
        return property.contains("linux");
    }
}

接下來,我們就需要在PersonConfig2類中使用@Conditional註解新增條件了。新增註解後的方法如下所示。

@Conditional({WindowsCondition.class})
@Bean("binghe001")
public Person person01(){
    return new Person("binghe001", 18);
}

@Conditional({LinuxCondition.class})
@Bean("binghe002")
public Person person02(){
    return new Person("binghe002", 20);
}

此時,我們再次執行SpringBeanTest類中的testAnnotationConfig6()方法,輸出的結果資訊如下所示。

Windows 10
person
binghe001
給容器中新增Person....
{person=Person(name=binghe002, age=18), binghe001=Person(name=binghe001, age=18)}

可以看到,輸出結果中不再含有名稱為binghe002的bean了,說明程式中檢測到當前作業系統為Windows10,沒有向Spring容器中註冊名稱為binghe002的bean。

@Conditional註解也可以標註在類上,標註在類上含義為:滿足當前條件,這個類中配置的所有bean註冊才能生效,大家可以自行驗證@Conditional註解標註在類上的情況

@Conditional的擴充套件註解

@ConditionalOnBean:僅僅在當前上下文中存在某個物件時,才會例項化一個Bean。
@ConditionalOnClass:某個class位於類路徑上,才會例項化一個Bean。
@ConditionalOnExpression:當表示式為true的時候,才會例項化一個Bean。
@ConditionalOnMissingBean:僅僅在當前上下文中不存在某個物件時,才會例項化一個Bean。
@ConditionalOnMissingClass:某個class類路徑上不存在的時候,才會例項化一個Bean。
@ConditionalOnNotWebApplication:不是web應用,才會例項化一個Bean。
@ConditionalOnBean:當容器中有指定Bean的條件下進行例項化。
@ConditionalOnMissingBean:當容器裡沒有指定Bean的條件下進行例項化。
@ConditionalOnClass:當classpath類路徑下有指定類的條件下進行例項化。
@ConditionalOnMissingClass:當類路徑下沒有指定類的條件下進行例項化。
@ConditionalOnWebApplication:當專案是一個Web專案時進行例項化。
@ConditionalOnNotWebApplication:當專案不是一個Web專案時進行例項化。
@ConditionalOnProperty:當指定的屬性有指定的值時進行例項化。
@ConditionalOnExpression:基於SpEL表示式的條件判斷。
@ConditionalOnJava:當JVM版本為指定的版本範圍時觸發例項化。
@ConditionalOnResource:當類路徑下有指定的資源時觸發例項化。
@ConditionalOnJndi:在JNDI存在的條件下觸發例項化。
@ConditionalOnSingleCandidate:當指定的Bean在容器中只有一個,或者有多個但是指定了首選的Bean時觸發例項化。

@Conditional 與@Profile 的對比

Spring3.0 也有一些和@Conditional 相似的註解,它們是Spring SPEL 表示式和Spring Profiles 註解 Spring4.0的@Conditional 註解要比@Profile 註解更加高階。@Profile 註解用來載入應用程式的環境。@Profile註解僅限於根據預定義屬性編寫條件檢查。 @Conditional註釋則沒有此限制。

Spring中的@Profile 和 @Conditional 註解用來檢查"If…then…else"的語義。然而,Spring4 @Conditional是@Profile 註解的更通用法。

  • Spring 3中的 @Profile僅用於編寫基於Environment變數的條件檢查。 配置檔案可用於基於環境載入應用程式配置。
  • Spring 4 @Conditional註解允許開發人員為條件檢查定義使用者定義的策略。 @Conditional可用於條件bean註冊。

好了,我們們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!

專案工程原始碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

寫在最後

如果覺得文章對你有點幫助,請微信搜尋並關注「 冰河技術 」微信公眾號,跟冰河學習Spring註解驅動開發。公眾號回覆“spring註解”關鍵字,領取Spring註解驅動開發核心知識圖,讓Spring註解驅動開發不再迷茫。

參考:
https://www.cnblogs.com/cxuanBlog/p/10960575.html

相關文章