在Spring中實(shí)現(xiàn)類似SpringBoot的環(huán)境檢測(cè)能力
前言
? 在Boot 你的應(yīng)用一文中提到了有時(shí)候我們需要檢測(cè)當(dāng)前時(shí)環(huán)境是否匹配我們的運(yùn)行時(shí)要求,并根據(jù)不同的環(huán)境進(jìn)行個(gè)性化的適配粒竖。
? Spring4已經(jīng)引入了簡(jiǎn)單的擴(kuò)展接口
@Conditional
和Condition
鸳惯,允許大家自行去識(shí)別環(huán)境信息汰规,但也僅此而已波附,并沒(méi)有內(nèi)置一些可以讓大家在實(shí)際場(chǎng)景中使用的條件判定器杂曲。? 真正將
@Conditional
和Condition
發(fā)揚(yáng)光大的是SpringBoot诈火,在SpringBoot是全面采用了AutoConfiguration
和@Conditional
將自動(dòng)配置的強(qiáng)大功能展現(xiàn)得淋漓盡致兽赁,內(nèi)置了超過(guò)10種不同類型支持超過(guò)100種不同場(chǎng)景的環(huán)境檢測(cè)器。比如:檢測(cè)當(dāng)前環(huán)境中是否存在某個(gè)Class,檢測(cè)當(dāng)前容器中是否定義了某個(gè)SpringBean刀崖,檢測(cè)當(dāng)前是否有某個(gè)配置項(xiàng)惊科,配置項(xiàng)的值是多少等等。所有的環(huán)境檢測(cè)器都在org.springframework.boot.autoconfigure.condition
下面亮钦,大家可以去翻閱源碼學(xué)習(xí)了解馆截。
@Conditional 與 Condition 介紹
? 前文提到在 Spring 框架中僅僅提供了這兩個(gè)擴(kuò)展點(diǎn),并沒(méi)有能運(yùn)用在實(shí)際應(yīng)用場(chǎng)景中的環(huán)境檢測(cè)器蜂莉,這一節(jié)我們將分析這兩個(gè)接口蜡娶,并實(shí)現(xiàn)一個(gè)簡(jiǎn)單的環(huán)境檢測(cè)功能。
? 以下是@Conditional
的源碼:
/**
* Indicates that a component is only eligible for registration when all
* {@linkplain #value() specified conditions} match.
*
* <p>A <em>condition</em> is any state that can be determined programmatically
* before the bean definition is due to be registered (see {@link Condition} for details).
*
* <p>The {@code @Conditional} annotation may be used in any of the following ways:
* <ul>
* <li>as a type-level annotation on any class directly or indirectly annotated with
* {@code @Component}, including {@link Configuration @Configuration} classes</li>
* <li>as a meta-annotation, for the purpose of composing custom stereotype
* annotations</li>
* <li>as a method-level annotation on any {@link Bean @Bean} method</li>
* </ul>
*
* <p>If a {@code @Configuration} class is marked with {@code @Conditional}, all of the
* {@code @Bean} methods, {@link Import @Import} and {@link ComponentScan @ComponentScan}
* annotations associated with that class will be subject to the conditions.
*
* <p>NOTE: {@code @Conditional} annotations are not inherited; any conditions from
* superclasses or from overridden methods are not being considered.
*
* @author Phillip Webb
* @since 4.0
* @see Condition
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
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();
}
? 這是一個(gè)注解巡语,從注釋中我們看到這是 @Since 4.0
的翎蹈,即在 Spring4 開(kāi)始提供的,用來(lái)指定一系列的配置條件男公,當(dāng)所有指定的條件都滿足時(shí)荤堪,被 @Configuration
中標(biāo)注的 @Bean
,@Import
枢赔,@ComponentScan
才會(huì)生效澄阳。
? 它接受一個(gè)Condition
數(shù)組,用來(lái)標(biāo)記所有的篩選條件踏拜,當(dāng)所有的Condition.matches
條件均返回true
時(shí)即可認(rèn)為該Conditional
成立碎赢,從而完成環(huán)境檢測(cè)。
? Condition
接口只有一個(gè)方法速梗,源碼如下:
/**
* A single {@code condition} that must be {@linkplain #matches matched} in order
* for a component to be registered.
*
* <p>Conditions are checked immediately before the bean-definition is due to be
* registered and are free to veto registration based on any criteria that can
* be determined at that point.
*
* <p>Conditions must follow the same restrictions as {@link BeanFactoryPostProcessor}
* and take care to never interact with bean instances. For more fine-grained control
* of conditions that interact with {@code @Configuration} beans consider the
* {@link ConfigurationCondition} interface.
*
* @author Phillip Webb
* @since 4.0
* @see ConfigurationCondition
* @see Conditional
* @see ConditionContext
*/
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 registration.
*/
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
? 只需要實(shí)現(xiàn)matches
方法并根據(jù)自己的需要完成環(huán)境檢測(cè)判定即可肮塞。
?
簡(jiǎn)單用法示例
? 下面我們用一個(gè)小的示例來(lái)演示這兩個(gè)接口的使用方法,假設(shè)需求:根據(jù)不同的操作系統(tǒng)注冊(cè)不同的 MXBean 服務(wù)
1. 實(shí)現(xiàn)在不同操作系統(tǒng)環(huán)境下的條件判定
? 這個(gè)過(guò)程我們就簡(jiǎn)化地判定當(dāng)前的os.name
就可以姻锁,代碼如下:
Windows 環(huán)境的判定器:
/**
* 判定當(dāng)前環(huán)境是否為 Windows 的條件
*
* @author <a href="mailto:huangfengjing@gmail.com">Ivan</a>
* created on 2018/12/10.
*/
public class WindowsCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getEnvironment().getProperty("os.name").contains("Windows");
}
}
Linux 環(huán)境的判定器:
/**
* 判定當(dāng)前環(huán)境是否為 Windows 的條件
*
* @author <a href="mailto:huangfengjing@gmail.com">Ivan</a>
* created on 2018/12/10.
*/
public class WindowsCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getEnvironment().getProperty("os.name").contains("Linux");
}
}
2. 在Bean注冊(cè)時(shí)帶上條件注解
? 有了第1步的的條件判定器枕赵,那么在我們進(jìn)行 @Configuraiton
的Bean注冊(cè)時(shí)就可以將這些條件附帶上,讓Spring容器根據(jù)不同的條件加載不同的Bean配置位隶。代碼如下所示:
/**
* 根據(jù)不同的操作系統(tǒng)加載不同的 MXBean
*
* @author <a href="mailto:huangfengjing@gmail.com">Ivan</a>
* created on 2018/12/10.
*/
@Configuration
public class ConditionalMXBeanConifg {
@Bean
@Conditional(WindowsCondition.class)
public BaseMXBean windowsMXBeanService() {
return new WindowsMXBean();
}
@Bean
@Conditional(LinuxCondition.class)
public BaseMXBean linuxMXBeanService() {
return new LinuxMXBean();
}
}
根據(jù)以上的配置拷窜,在不同的操作系統(tǒng)環(huán)境下,Spring會(huì)分別注冊(cè)不同的 MXBean 涧黄。
高級(jí)用法示例
? 在簡(jiǎn)單用法示例中我們可以看到篮昧,雖然實(shí)現(xiàn)了不同環(huán)境下的判定識(shí)別,但還是太簡(jiǎn)單了笋妥,還是比較靜態(tài)的懊昨,如果我們要像SpringBoot那樣動(dòng)態(tài)的判定當(dāng)前環(huán)境中是否存在某個(gè)類,Spring容器中是否存在某個(gè)Bean定義該怎么做呢春宣?下面我們將演示這幾種更高級(jí)的用法疚颊。
判定當(dāng)前 classpath 下是否存在某個(gè)類
? 這類條件判定器主要用在一些模板類SDK中狈孔,根據(jù)當(dāng)前用戶是否依賴了某些類來(lái)確定是否要定義相應(yīng)的模板、工具材义、服務(wù)等均抽。如同應(yīng)用分發(fā) base-boot-starter 的使用說(shuō)明中對(duì)于 OA 權(quán)限平臺(tái)的判定一樣,當(dāng)用戶沒(méi)有添加OA權(quán)限平臺(tái)這個(gè)MAVEN依賴時(shí)其掂,應(yīng)用仍然能智能判定而不是拋出 NoClassDefFoundError
油挥。
-
首先定義一個(gè)自定義的注解,供用戶使用判定
/** * 是否存在某個(gè)類的條件判定注解 * * @author <a href="mailto:huangfengjing@gmail.com">Ivan</a> * Time: 2018/12/7 : 19:34 */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(OnClassCondition.class) public @interface ConditionalOnClass { /** * 必須存在的類 * * @return 必須存在的類 */ Class<?>[] value() default {}; /** * 必須存在的類名 * * @return 必須存在的類名 */ String[] name() default {}; }
當(dāng)用戶在注解某個(gè)@Bean
進(jìn)款熬,可以添加這個(gè)注解來(lái)進(jìn)行判定深寥。這個(gè)注解本身還依賴另外一個(gè)注解@Conditional(OnClassCondition.class)
,表示掃Spring容器在掃描到某個(gè)類定義被標(biāo)注了@ConditionalOnClass
時(shí)贤牛,會(huì)執(zhí)行里面的OnClassCondition
來(lái)完成條件判定惋鹅。
-
定義真正的條件判定器
OnClassCondition
/** * 判定某個(gè)類是否存在的條件 * * @author <a href="mailto:huangfengjing@gmail.com">Ivan</a> * Time: 2018/12/7 : 19:29 */ public class OnClassCondition extends BaseCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { MultiValueMap<String, Object> attributes = metadata .getAllAnnotationAttributes(ConditionalOnClass.class.getName(), true); if (null == attributes) { return false; } List<String> candidates = new ArrayList<>(); addAll(candidates, attributes.get("value")); addAll(candidates, attributes.get("name")); for (String candidate : candidates) { if (!ClassUtils.isPresent(candidate, null)) { return false; } } return true; } }
其實(shí)也很簡(jiǎn)單,就是從注解中獲取當(dāng)前用戶要判定是否存在的Class(支持類定義殉簸,和全類名)闰集,然后在當(dāng)前 classpath 下去查找這個(gè)類是否存在即完成判定過(guò)程。
-
使用自定義的注解
使用起來(lái)就比較簡(jiǎn)單了般卑,加上注解即可武鲁,如下所示:
@ConditionalOnClass(SSOFilter.class) @Configuration public class SsoAutoConfiguration {...}
判定當(dāng)前Spring容器中是否定義了某個(gè)Bean
? 這類判定主要用在如下的場(chǎng)景:某些組件需要依賴某個(gè)SpringBean,如果當(dāng)前Spring容器中不存在這個(gè)Bean蝠检,那么就要添加一個(gè)沐鼠,如果存在就不能再添加,防止產(chǎn)生NoSuchBeanDefinitionException
或者NoUniqueBeanDefinitionException
異常叹谁。
? 其實(shí)現(xiàn)過(guò)程其實(shí)和@ConditionalOnClass
大同小異饲梭,最主要的區(qū)別在于Condition.matches
方法一個(gè)是判定類是否存在,一個(gè)是判定Bean是否存在焰檩。代碼如下:
/**
* 判定某個(gè) bean 是否存在的條件
*
* @author <a href="mailto:huangfengjing@gmail.com">Ivan</a>
* Time: 2018/12/7 : 19:29
*/
@Slf4j
public class OnBeanCondition extends BaseCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
MultiValueMap<String, Object> conditionOnBeanAttrs = metadata
.getAllAnnotationAttributes(ConditionalOnBean.class.getName(), true);
if (null != conditionOnBeanAttrs) {
return matchBean(context, conditionOnBeanAttrs, metadata);
}
MultiValueMap<String, Object> conditionOnMissingBeanAttrs = metadata
.getAllAnnotationAttributes(ConditionalOnMissingBean.class.getName(), true);
if (conditionOnMissingBeanAttrs != null) {
return matchMissingBean(context, conditionOnMissingBeanAttrs, metadata);
}
return false;
}
private boolean matchBean(ConditionContext context, MultiValueMap<String, Object> attributes, AnnotatedTypeMetadata metadata) {
if (attributes == null) {
return false;
}
BeanFactory beanFactory = context.getBeanFactory();
List<String> classNameCandidates = new ArrayList<>();
addAll(classNameCandidates, attributes.get("value"));
try {
for (String clsName : classNameCandidates) {
beanFactory.getBean(Class.forName(clsName));
}
} catch (Exception e) {
log.debug("沒(méi)有找到需要的 Bean: {}", e.getMessage());
return false;
}
List<String> beanNameCandidates = new ArrayList<>();
addAll(beanNameCandidates, attributes.get("name"));
for (String beanName : beanNameCandidates) {
if (!beanFactory.containsBean(beanName)) {
log.debug("沒(méi)有找到需要的 bean: {}", beanName);
return false;
}
}
return true;
}
private boolean matchMissingBean(ConditionContext context, MultiValueMap<String, Object> attributes, AnnotatedTypeMetadata metadata) {
// ... 和 matchBean 相反憔涉,判定是否不存在某個(gè) Bean,省略
}
}
總結(jié)
? 本文主要講解了如何通過(guò)@Conditional
和Condition
實(shí)現(xiàn)環(huán)境檢測(cè)的能力锅尘,并從源碼及示例兩方面演示了從簡(jiǎn)單到高級(jí)的用法支持监氢。其它的諸如判定當(dāng)時(shí)配置項(xiàng)中的值以及資源判定的實(shí)現(xiàn)原理都差不多布蔗,感興趣的可以翻閱應(yīng)用分發(fā) base-boot-starter
的源碼藤违。
當(dāng)然,這里高級(jí)應(yīng)用里面的判定規(guī)則并不如SpringBoot中的功能強(qiáng)大纵揍,但應(yīng)付常規(guī)的應(yīng)用已經(jīng)足夠顿乒,當(dāng)不滿足需求時(shí),通過(guò)本文的講解讀者應(yīng)該也已經(jīng)了解到了如何自行擴(kuò)展泽谨,或者聯(lián)系我協(xié)助璧榄。