在springboot中,開發的確變的簡單了很多,但是,開發者現在希望開發傻瓜式的方便搞定項目中的各種奇怪的需求最好了,不用燒腦,本來程序猿的生活就是枯燥的,不要再給自己添加更多的煩惱。
今天,就為了方便這點,介紹下,如何解決在開發過程中,一些場景下,為了實現一個配置模塊中,基於開關量或者選擇配置項,實現不同功能,例如,在一個session共享模塊當中,解決session是基於header傳遞還是基於cookie傳遞這兩種應用場景,有些應用中希望基於header傳遞sessionId,但是有些應用中希望基於cookie傳遞sessionId,然后,session共享模塊,是一個非常基礎的組件,差不多是一個開箱即用的功能塊。所以呢,最好能配置好,然后只需要基於配置文件中的某個選項,就能實現運行在不同的工作模式下。這個能否做到呢?真的只需要改一下配置中的開關量就能實現嗎?
能否實現,這里賣個關子,先不說,介紹完了本篇博文后,細心的讀者一定知道答案,或者說一定能明白能否做,怎么做!
第一大點:先介紹一下springboot中能夠支持的或者說封裝好的常用的條件注入的注解
1 @ConditionalOnBean
1.1 基本使用案例
@Component @ConditionalOnBean(name="aBean") public class BBean { private final ABean aBean; public BBean(ABean aBean) { // ... } }
1.2 使用說明
只有當beang的名稱為aBean存在的時候,才會注入BBean。
2 @ConditionalOnMissingBean
2.1 基本案例
@Bean @ConditionalOnMissingBean(name = "notExistsBean") public BeanToCreate createOneBean() { return new BeanToCreate("notExistsBean"); }
2.2 使用說明
只有當bean名稱為notExistsBean不存在的時候,BeanToCreate類型的bean才會被創建,和@ConditionalOnBean的使用方式相反
3 @ConditionalOnClass
3.1 基本使用案例
@Bean @ConditionalOnClass(DependedClz.class) public InjectIfClzExists injectIfClzExists() { return new InjectIfClzExists("dependedClz"); }
3.2 使用說明
只有當Class為DependedClz.class存在的時候,才會注入類型為InjectIfClzExists的bean,使用上和@ConditionalOnBean有些類似。
4 @ConditionalOnMissingClass
4.1 使用案例
@Bean @ConditionalOnMissingClass("com.shihuc.bean.clz.DependedClz") public InjectIfClzNotExists injectIfClzNotExists() { return new InjectIfClzNotExists("com.shihuc.bean.clz.DependedClz"); }
4.2 使用說明
只有當類com.shihuc.bean.clz.DependedClz不存在的時候,才會注入類型為InjectIfClzNotExists的bean。
5 @ConditionalOnProperty
5.1 基本使用案例
springboot的項目中配置文件application.properties文件中有如下配置:
#..... section.condition_field=noti section.condition_property=test #...
@Bean @ConditionalOnProperty("section.condition_field") public PropertyExistBean propertyExistBean() { return new PropertyExistBean("section.condition_field"); }
5.2 使用說明
主要是根據配置文件中的參數,來決定是否需要創建這個bean,這樣就給了我們一個根據配置來控制Bean的選擇的手段了,這個非常的好用。因為application.properties文件中存在section.condition_field這個屬性,所以,PropertyExistBean這個bean會被創建出來。
5.3 擴展用法
5.3.1 注解定義
@Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD }) @Documented @Conditional(OnPropertyCondition.class) public @interface ConditionalOnProperty { /** * Alias for {@link #name()}. * @return the names * 注意,這個value和name不能同時使用 */ String[] value() default {}; /** * A prefix that should be applied to each property. The prefix automatically ends * with a dot if not specified. * @return the prefix */ String prefix() default ""; /** * The name of the properties to test. If a prefix has been defined, it is applied to * compute the full key of each property. For instance if the prefix is * {@code app.config} and one value is {@code my-value}, the full key would be * {@code app.config.my-value} * <p> * Use the dashed notation to specify each property, that is all lower case with a "-" * to separate words (e.g. {@code my-long-property}). * @return the names */ String[] name() default {}; /** * The string representation of the expected value for the properties. If not * specified, the property must <strong>not</strong> be equal to {@code false}. * @return the expected value */ String havingValue() default ""; /** * Specify if the condition should match if the property is not set. Defaults to * {@code false}. * @return if should match if the property is missing */ boolean matchIfMissing() default false; }
當我想實現配置文件中存在屬性aaa.bbb且其屬性的值為ccc時,才注入bean實例DDDD(名為dddd)。
@Bean("dddd") @ConditionalOnProperty(value="aaa.bbbb", havingValue="ccc") public DDDD propertyExistBean() { return new DDDD("aaa.bbb"); }
6 @ConditionalOnExpression
6.1 使用案例
配置文件application.properties中存在下面的配置內容:
conditional.flag=true
java對應代碼:
@Bean @ConditionalOnExpression("#{'true'.equals(environment['conditional.flag'])}") public ExpressTrueBean expressTrueBean() { return new ExpressTrueBean("express true"); }
6.2 使用說明
相比較前面的Bean,Class是否存在,配置參數property是否存在或者有某個值而言,這個依賴SPEL表達式的,使用起來就功能顯得更加強大了;其主要就是執行Spel表達式,根據返回的true/false來判斷是否滿足條件。
第二大點: spring基於Condition接口和@Conditional注解進行注入bean
這個相當於是條件注入bean的根源解決方案,上述其他幾個ConditionalOnXXXX的注解,都是這個Conditional注解的具體場景的定制版,假如沒有能夠滿足自己的應用場景的,或者說要自己實現一個比較特殊的條件注入呢,例如多個條件同時成立之類,怎么辦呢,那就需要通過實現Condition接口然后基於@Conditional注解進行使用了。
1 @Conditional注解定義
//此注解可以標注在類和方法上 @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Conditional { /** * All {@link Condition}s that must {@linkplain Condition#matches match} * in order for the component to be registered. */ Class<? extends Condition>[] value(); }
注意,這個注解就一個參數value,且入參是一個Condition的Class的數組。
2 Condition是什么?
@FunctionalInterface public interface Condition { /** * Determine if the condition matches. * @param context the condition context * @param metadata metadata of the {@link org.springframework.core.type.AnnotationMetadata class} * or {@link org.springframework.core.type.MethodMetadata method} being checked * @return {@code true} if the condition matches and the component can be registered, * or {@code false} to veto the annotated component's registration */ boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); }
3. 使用案例
假設屬性配置文件中,有兩個環境參數,一個是溫度temp,一個是濕度humi,只有當溫度高於30度,且濕度大於50個點時,啟用Linux,當溫度小於30度且濕度小於50個點時,啟用Windows,這個只是為了說明在一個@Conditional里面將多個條件滿足該如何實現,還有其他的業務場景,可以參照這個案例。
3.1 配置文件參數
#溫度數據,攝氏溫度 conditional.prop.temp=29 #濕度數據,百分比,這里不帶百分號,相當於擴大100倍,使用的時候除以100 conditional.prop.humi=51
3.2 定義bean
有一個HeWoBean的接口,以及兩個實現類HelloBean和WorldBean。
/** * @Author: chengsh05 * @Date: 2019/8/29 16:17 */ public interface HeWoBean { public String toString(); }
/** * @Author: chengsh05 * @Date: 2019/8/29 15:52 */ public class HelloBean implements HeWoBean { public String getHhh() { return hhh; } public void setHhh(String hhh) { this.hhh = hhh; } public String getEee() { return eee; } public void setEee(String eee) { this.eee = eee; } String hhh; String eee; public HelloBean(String hh, String ee) { this.hhh = hh; this.eee = ee; } @Override public String toString() { return this.hhh + ", " + this.eee; } }
/** * @Author: chengsh05 * @Date: 2019/8/29 15:54 */ public class WorldBean implements HeWoBean { public String getWww() { return www; } public void setWww(String www) { this.www = www; } public String getOoo() { return ooo; } public void setOoo(String ooo) { this.ooo = ooo; } String www; String ooo; public WorldBean(String ww, String oo) { this.www = ww; this.ooo = oo; } @Override public String toString() { return this.www + ", " + this.ooo; } }
3. condition接口實現類及@Conditional應用
/** * @Author: chengsh05 * @Date: 2019/8/29 9:08 * @Description: 配置信息中,溫度和濕度條件滿足的時候,即當溫度temp大於30度,濕度大於50%,啟用Linux */ public class LinuxTime implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { String tempStr = context.getEnvironment().getProperty("conditional.prop.temp"); float temp = Float.valueOf(tempStr); String humiStr = context.getEnvironment().getProperty("conditional.prop.humi"); float humi = Float.valueOf(humiStr); if(temp > 30 && humi > 60){ return true; } return false; } }
/** * @Author: chengsh05 * @Date: 2019/8/29 9:07 * @Description: 配置信息中,溫度和濕度條件滿足的時候,即當溫度temp小於30度,濕度小於50%,啟用windows */ public class WindowsTime implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { String tempStr = context.getEnvironment().getProperty("conditional.prop.temp"); float temp = Float.valueOf(tempStr); String humiStr = context.getEnvironment().getProperty("conditional.prop.humi"); float humi = Float.valueOf(humiStr); if(temp < 30 && humi < 60){ return true; } return false; } }
/** * @Author: chengsh05 * @Date: 2019/8/29 15:50 */ @Configuration public class MyConditional { @Bean("mybean") @Conditional(LinuxTime.class) public HelloBean createHello() { return new HelloBean("hello", "Linux"); } @Bean("mybean") @Conditional(WindowsTime.class) public WorldBean createWorld() { return new WorldBean("world", "Windows"); } }
4.應用驗證
/** * @Author: chengsh05 * @Date: 2019/8/29 16:03 */ @Controller @RequestMapping("/condition") public class ConditionalController { @Autowired @Qualifier("mybean") private HeWoBean myBean; @RequestMapping("/check") @ResponseBody public void check() { System.out.println("///||||\\\\ ==> " + myBean.toString()); } }
分析來看,LinuxTime因為沒有滿足溫度temp和濕度humi的條件(即在配置文件中的參數),所以,LinuxTime這個bean在MyConditional這個配置類中是不會被創建出來的,即最終HeHoBean這個就只有WorldBean被注入到spring容器了。打印的日志,也證實了這個。
///||||\\ ==> world, Windows
總結:
1. 基於springboot內置的條件注解,開發一些應用,基於某種條件進行bean的注入還是很方便的,基本可以解決大部分常見場景需求。
2. 基於內置的條件注入注解的組合使用,可以實現多條件約束的bean的注入需求,只有多個條件注入條件都成立時,對應的bean才會被注入到spring的容器。
3. 內置注解不管單獨用還是組合使用,都不能搞定你的應用需求,那么可以選擇實現condition接口,基於@Conditional注解來自己完成條件注入的需求了。
到這里,看官們,你是否有結論了,關於前面提到的,session共享模塊,基於配置參數開關量,靈活切換模塊工作在header模式還是cookie模式?答案是可以的,至於如何實現,結合我這里的介紹,是能得到答案的。