SpringBoot基礎篇Bean之條件注入之註解使用

一灰灰發表於2018-10-21

bean的條件注入,除了前面一篇博文中介紹的通過@Conditional註解配合Condition介面的實現之外,還提供了更多簡化的註解使用方式,省略了自己實現Condtion介面,本篇博文主要介紹下面幾個常用的註解使用方式

  • @ConditionalOnBean
  • @ConditionalOnMissingBean
  • @ConditionalOnClass
  • @ConditionalOnMissingClass
  • @ConditionalOnProperty
  • @ConditionalOnExpression

I. Bean的存在與否作為條件

當Bean不存在時,建立一個預設的Bean,在Spring的生態中可以說比較常見了;接下來看下這種方式可以怎麼用

1. @ConditionalOnBean

要求bean存在時,才會建立這個bean;如我提供了一個bean名為RedisOperBean,用於封裝redis相關的操作;但是我這個bean需要依賴restTemplate這個bean,只有當應用引入了redis的相關依賴,並存在RestTemplate這個bean的時候,我這個bean才會生效

假設bean的定義如下

@Component
@ConditionalOnBean(name="redisTemplate")
public class RedisOperBean {
  private final RedisTemplate redisTemplate;
  public RedisOperBean(RedisTemplate redisTemplate) {
      // ...
  }
}
複製程式碼

這樣的好處就是我提供的這個第三方包,如果被使用者A間接依賴(但是A本身不需要操作redis),也不會因為建立RedisOperBean而拋異常

產生異常的原因是因為找不到RestTemplate的bean,因此無法例項化RedisOperBean,從而丟擲異常

a. 註解定義

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnBean {
  // bean型別
	Class<?>[] value() default {};

	// bean型別
	String[] type() default {};

	// 要求bean上擁有指定的註解
	Class<? extends Annotation>[] annotation() default {};

	// bean names
	String[] name() default {};

	SearchStrategy search() default SearchStrategy.ALL;
}
複製程式碼

b. 測試用例

構建一個簡單的測試用例,先定義一個基礎的bean

public class DependedBean {
}
複製程式碼

再定義一個依賴只有上面的bean存在時,才會載入的bean

public class LoadIfBeanExist {

    private String name;

    public LoadIfBeanExist(String name) {
        this.name = name;
    }

    public String getName() {
        return "load if bean exists: " + name;
    }
}
複製程式碼

接下來就是bean的定義了

@Bean
public DependedBean dependedBean() {
    return new DependedBean();
}

/**
 * 只有當DependedBean 存在時,才會建立bean: `LoadIfBeanExist`
 *
 * @return
 */
@Bean
@ConditionalOnBean(name = "dependedBean")
public LoadIfBeanExist loadIfBeanExist() {
    return new LoadIfBeanExist("dependedBean");
}
複製程式碼

根據上面的測試用例,LoadIfBeanExist是會被正常載入的; 具體結果看後面的例項演示

2. ConditionalOnMissingBean

和前面一個作用正好相反的,上面是要求存在bean,而這個是要求不存在

a. 介面定義

public @interface ConditionalOnMissingBean {
	Class<?>[] value() default {};

	String[] type() default {};

	/**
	 * The class type of beans that should be ignored when identifying matching beans.
	 */
	Class<?>[] ignored() default {};

	/**
	 * The class type names of beans that should be ignored when identifying matching
	 * beans.
	 */
	String[] ignoredType() default {};

	Class<? extends Annotation>[] annotation() default {};

	String[] name() default {};

	SearchStrategy search() default SearchStrategy.ALL;
}
複製程式碼

b. 測試用例

同樣定義一個bean不存在時,才建立的bean

public class LoadIfBeanNotExists {
    public String name;

    public LoadIfBeanNotExists(String name) {
        this.name = name;
    }

    public String getName() {
        return "load if bean not exists: " + name;
    }
}
複製程式碼

對應的bean配置如下

/**
 * 只有當沒有notExistsBean時,才會建立bean: `LoadIfBeanNotExists`
 *
 * @return
 */
@Bean
@ConditionalOnMissingBean(name = "notExistsBean")
public LoadIfBeanNotExists loadIfBeanNotExists() {
    return new LoadIfBeanNotExists("notExistsBean");
}
複製程式碼

因為沒有notExistsBean,所以上面這個bean也應該被正常註冊

3. 例項演示

因為bean的是否存在和class的是否存在有較大的相似性,因此例項演示放在下一小節,一起測試

II. Class的存在與否作為條件

從使用來看,和前面基本上沒有太大的區別,無非就是將bean換成了class;這樣就可以避免因為Class Not Found導致的編譯異常了

1. @ConditionalOnClass

要求class存在

a. 註解定義

public @interface ConditionalOnClass {
	Class<?>[] value() default {};

	/**
	 * The classes names that must be present.
	 * @return the class names that must be present.
	 */
	String[] name() default {};

}
複製程式碼

b. 測試用例

先定義一個class

public class DependedClz {
}
複製程式碼

然後依賴class存在的bean

public class LoadIfClzExists {
    private String name;

    public LoadIfClzExists(String name) {
        this.name = name;
    }

    public String getName() {
        return "load if exists clz: " + name;
    }
}
複製程式碼

接下來就是Bean的配置

/**
 * 當引用了 {@link DependedClz} 類之後,才會建立bean: `LoadIfClzExists`
 *
 * @return
 */
@Bean
@ConditionalOnClass(DependedClz.class)
public LoadIfClzExists loadIfClzExists() {
    return new LoadIfClzExists("dependedClz");
}
複製程式碼

因為類存在,所以測試時,這個bean應該被正常註冊

2. @ConditionalOnMissingClass

class不存在時,才會載入bean

a. 註解定義

public @interface ConditionalOnMissingClass {
	String[] value() default {};
}
複製程式碼

b. 測試用例

定義一個class缺少時才會建立的bean

public class LoadIfClzNotExists {
    private String name;

    public LoadIfClzNotExists(String name) {
        this.name = name;
    }

    public String getName() {
        return "load if not exists clz: " + name;
    }
}
複製程式碼

bean的配置如下

/**
 * 當系統中沒有 com.git.hui.boot.conditionbean.example.depends.clz.DependedClz類時,才會建立這個bean
 *
 * @return
 */
@Bean
@ConditionalOnMissingClass("com.git.hui.boot.conditionbean.example.depends.clz.DependedClz")
public LoadIfClzNotExists loadIfClzNotExists() {
    return new LoadIfClzNotExists("com.git.hui.boot.conditionbean.example.depends.clz.DependedClz");
}
複製程式碼

因為上面這個類存在,所以這個bean不應該被正常註冊

3. 例項演示

起一個rest服務,測試下上面的四個bean是否正常

@RestController
@RequestMapping("depends")
public class DependRest {

    @Autowired
    private LoadIfBeanExist loadIfBeanExist;
    @Autowired
    private LoadIfBeanNotExists loadIfBeanNotExists;
    @Autowired
    private LoadIfClzExists loadIfClzExists;
    @Autowired(required = false)
    private LoadIfClzNotExists loadIfClzNotExists;

    @GetMapping(path = "show")
    public String show() {
        Map<String, String> result = new HashMap<>(4);
        // 存在
        result.put("loadIfBeanExist", loadIfBeanExist == null ? "null ==> false!" : loadIfBeanExist.getName());
        // 存在
        result.put("loadIfBeanNotExists",
                loadIfBeanNotExists == null ? "null ==> false!" : loadIfBeanNotExists.getName());
        // 存在
        result.put("loadIfClzExists", loadIfClzExists == null ? "null ==> false!" : loadIfClzExists.getName());
        // 不存在
        result.put("loadIfClzNotExists", loadIfClzNotExists == null ? "null ==> true!" : loadIfClzNotExists.getName());

        return JSONObject.toJSONString(result);
    }
}
複製程式碼

根據前面的分析,返回的結果應該是三個存在,一個不存在;下圖執行和我們預期一致

條件依賴註冊演示

III. 配置屬性作為條件

主要是根據配置引數,來決定是否需要建立這個bean,這樣就給了我們一個根據配置來控制Bean的選擇的手段了,如前面一篇博文中根據配置來選擇是隨機生成boolean還是隨機生成int;只需要更改配置即可

1. @ConditionalOnProperty

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnPropertyCondition.class)
public @interface ConditionalOnProperty {
	/**
	 * Alias for {@link #name()}.
	 * @return the names
	 */
	String[] value() default {};

	// 配置字首
	String prefix() default "";

  // 配置名
	String[] name() default {};

	// 要求配置存在,且包含某個值
	String havingValue() default "";

	// 即便沒有配置,也依然建立
	boolean matchIfMissing() default false;
}
複製程式碼

2. 例項測試

a. 測試用例

測試幾個常用的姿勢,一是根據配置是否存在,來決定是否建立

public class PropertyExistBean {
    private String name;

    public PropertyExistBean(String name) {
        this.name = name;
    }

    public String getName() {
        return "property : " + name;
    }
}

public class PropertyNotExistBean {
    private String name;

    public PropertyNotExistBean(String name) {
        this.name = name;
    }

    public String getName() {
        return "no property" + name;
    }
}
複製程式碼

對應的bean配置如下

/**
 * 配置存在時才會載入這個bean
 *
 * @return
 */
@Bean
@ConditionalOnProperty("conditional.property")
public PropertyExistBean propertyExistBean() {
    return new PropertyExistBean(environment.getProperty("conditional.property"));
}

/**
 * 即便配置不存在時,也可以載入這個bean
 *
 * @return
 */
@Bean
@ConditionalOnProperty(name = "conditional.property.no", matchIfMissing = true)
public PropertyNotExistBean propertyNotExistBean() {
    return new PropertyNotExistBean("conditional.property");
}
複製程式碼

當配置存在,且value匹配時

public class PropertyValueExistBean {
    public String name;

    public PropertyValueExistBean(String name) {
        this.name = name;
    }

    public String getName() {
        return "property value exist: " + name;
    }
}

public class PropertyValueNotExistBean {
    public String name;

    public PropertyValueNotExistBean(String name) {
        this.name = name;
    }

    public String getName() {
        return "property value not exist: " + name;
    }
}
複製程式碼

對應的配置如下

@Bean
@ConditionalOnProperty(name = {"conditional.property"}, havingValue = "properExists")
public PropertyValueExistBean propertyValueExistBean() {
    return new PropertyValueExistBean("properExists");
}

@Bean
@ConditionalOnProperty(name = {"conditional.property"}, havingValue = "properNotExists")
public PropertyValueNotExistBean propertyValueNotExistBean() {
    return new PropertyValueNotExistBean("properNotExists");
}
複製程式碼

接下來就是配置的引數

conditional.property=properExists
複製程式碼

b. 例項演示

根據前面的分析,上面的四個bean中,PropertyExistBean, PropertyNotExistBean, PropertyValueExistBean 應該存在;而PropertyValueNotExistBean 因為配置值不匹配,不會建立

測試程式碼如下

@RestController
@RequestMapping(path = "property")
public class PropertyRest {

    @Autowired(required = false)
    private PropertyExistBean propertyExistBean;
    @Autowired(required = false)
    private PropertyNotExistBean propertyNotExistBean;
    @Autowired(required = false)
    private PropertyValueExistBean propertyValueExistBean;
    @Autowired(required = false)
    private PropertyValueNotExistBean propertyValueNotExistBean;

    @GetMapping(path = "show")
    public String show() {
        Map<String, String> result = new HashMap<>(4);
        // 存在
        result.put("propertyExistBean", propertyExistBean == null ? "null ===> false" : propertyExistBean.getName());
        // 存在
        result.put("propertyNotExistBean",
                propertyNotExistBean == null ? "null ===> false" : propertyNotExistBean.getName());
        // 存在
        result.put("propertyValueExistBean",
                propertyValueExistBean == null ? "null ==> false" : propertyValueExistBean.getName());
        // 不存在
        result.put("propertyValueNotExistBean",
                propertyValueNotExistBean == null ? "null ==> true" : propertyValueNotExistBean.getName());
        return JSONObject.toJSONString(result);
    }
}
複製程式碼

執行後結果如下,一如預期

gif.gif

IV. 表示式方式

相比較前面的Bean,Class是否存在,配置引數是否存在或者有某個值而言,這個依賴SPEL表示式的,就顯得更加的高階了;其主要就是執行Spel表示式,根據返回的true/false來判斷是否滿足條件

至於SPEL是什麼東西,後面會有專文進行解釋,此處不加以展開。下面以一個簡單的demo進行演示它的使用姿勢

1. @ConditionalOnExpression

介面定義

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

	/**
	 * The SpEL expression to evaluate. Expression should return {@code true} if the
	 * condition passes or {@code false} if it fails.
	 * @return the SpEL expression
	 */
	String value() default "true";
}
複製程式碼

2. 例項測試

用一個簡單的例子,當配置引數中,根據是否滿足某個條件來決定是否需要載入bean

a. 測試用例

定義一個滿足條件和一個不滿足的bean

public class ExpressFalseBean {
    private String name;

    public ExpressFalseBean(String name) {
        this.name = name;
    }

    public String getName() {
        return "express bean :" + name;
    }
}

public class ExpressTrueBean {
    private String name;

    public ExpressTrueBean(String name) {
        this.name = name;
    }

    public String getName() {
        return "express bean :" + name;
    }
}
複製程式碼

重點關注下bean的配置

@Configuration
public class ExpressAutoConfig {
    /**
     * 當存在配置,且配置為true時才建立這個bean
     * @return
     */
    @Bean
    @ConditionalOnExpression("#{'true'.equals(environment['conditional.express'])}")
    public ExpressTrueBean expressTrueBean() {
        return new ExpressTrueBean("express true");
    }

    /**
     * 配置不存在,或配置的值不是true時,才建立bean
     * @return
     */
    @Bean
    @ConditionalOnExpression("#{!'true'.equals(environment.getProperty('conditional.express'))}")
    public ExpressFalseBean expressFalseBean() {
        return new ExpressFalseBean("express != true");
    }
}
複製程式碼

對應的配置如下

conditional.express=true
複製程式碼

b. 例項演示

@RestController
@RequestMapping(path = "express")
public class ExpressRest {
    @Autowired(required = false)
    private ExpressTrueBean expressTrueBean;
    @Autowired(required = false)
    private ExpressFalseBean expressFalseBean;

    @GetMapping(path = "show")
    public String show() {
        Map<String, String> result = new HashMap<>(4);
        result.put("expressTrueBean", expressTrueBean == null ? "null ==> false" : expressTrueBean.getName());
        result.put("expressFalseBean", expressFalseBean == null ? "null ==> true": expressFalseBean.getName());
        return JSONObject.toJSONString(result);
    }
}
複製程式碼

上面的執行,expressTrueBean應該存在,另外一個為null,執行結果如下

gif.gif

III. 其他

0. 相關

a. 更多博文

基礎篇

應用篇

b. 專案原始碼

1. 一灰灰Blog

一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛

2. 宣告

盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激

3. 掃描關注

一灰灰blog

QrCode

相關文章