前言
在Boot 你的应用一文中提到了有时候我们需要检测当前时环境是否匹配我们的运行时要求,并根据不同的环境进行个性化的适配。
Spring4已经引入了简单的扩展接口
@Conditional
和Condition
,允许大家自行去识别环境信息,但也仅此而已,并没有内置一些可以让大家在实际场景中使用的条件判定器。 真正将
@Conditional
和Condition
发扬光大的是SpringBoot,在SpringBoot是全面采用了AutoConfiguration
和@Conditional
将自动配置的强大功能展现得淋漓尽致,内置了超过10种不同类型支持超过100种不同场景的环境检测器。比如:检测当前环境中是否存在某个Class,检测当前容器中是否定义了某个SpringBean,检测当前是否有某个配置项,配置项的值是多少等等。所有的环境检测器都在org.springframework.boot.autoconfigure.condition
下面,大家可以去翻阅源码学习了解。
@Conditional 与 Condition 介绍
前文提到在 Spring 框架中仅仅提供了这两个扩展点,并没有能运用在实际应用场景中的环境检测器,这一节我们将分析这两个接口,并实现一个简单的环境检测功能。
以下是@Conditional
的源码:
1 | /** |
这是一个注解,从注释中我们看到这是 @Since 4.0
的,即在 Spring4 开始提供的,用来指定一系列的配置条件,当所有指定的条件都满足时,被 @Configuration
中标注的 @Bean
,@Import
,@ComponentScan
才会生效。
它接受一个Condition
数组,用来标记所有的筛选条件,当所有的Condition.matches
条件均返回true
时即可认为该Conditional
成立,从而完成环境检测。
Condition
接口只有一个方法,源码如下:
1 | /** |
只需要实现matches
方法并根据自己的需要完成环境检测判定即可。
简单用法示例
下面我们用一个小的示例来演示这两个接口的使用方法,假设需求:根据不同的操作系统注册不同的 MXBean 服务
1. 实现在不同操作系统环境下的条件判定
这个过程我们就简化地判定当前的os.name
就可以,代码如下:
Windows 环境的判定器:
1 | /** |
Linux 环境的判定器:
1 | /** |
2. 在Bean注册时带上条件注解
有了第1步的的条件判定器,那么在我们进行 @Configuraiton
的Bean注册时就可以将这些条件附带上,让Spring容器根据不同的条件加载不同的Bean配置。代码如下所示:
1 | /** |
根据以上的配置,在不同的操作系统环境下,Spring会分别注册不同的 MXBean 。
高级用法示例
在简单用法示例中我们可以看到,虽然实现了不同环境下的判定识别,但还是太简单了,还是比较静态的,如果我们要像SpringBoot那样动态的判定当前环境中是否存在某个类,Spring容器中是否存在某个Bean定义该怎么做呢?下面我们将演示这几种更高级的用法。
判定当前 classpath 下是否存在某个类
这类条件判定器主要用在一些模板类SDK中,根据当前用户是否依赖了某些类来确定是否要定义相应的模板、工具、服务等。如同应用分发 base-boot-starter 的使用说明中对于 OA 权限平台的判定一样,当用户没有添加OA权限平台这个MAVEN依赖时,应用仍然能智能判定而不是抛出 NoClassDefFoundError
。
首先定义一个自定义的注解,供用户使用判定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27/**
* 是否存在某个类的条件判定注解
*
* @author <a href="mailto:huangfengjing@gmail.com">Ivan</a>
* Time: 2018/12/7 : 19:34
*/
({ElementType.TYPE, ElementType.METHOD})
(RetentionPolicy.RUNTIME)
.class) (OnClassCondition
public @interface ConditionalOnClass {
/**
* 必须存在的类
*
* @return 必须存在的类
*/
Class<?>[] value() default {};
/**
* 必须存在的类名
*
* @return 必须存在的类名
*/
String[] name() default {};
}
当用户在注解某个@Bean
进,可以添加这个注解来进行判定。这个注解本身还依赖另外一个注解@Conditional(OnClassCondition.class)
,表示扫Spring容器在扫描到某个类定义被标注了@ConditionalOnClass
时,会执行里面的OnClassCondition
来完成条件判定。
定义真正的条件判定器
OnClassCondition
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29/**
* 判定某个类是否存在的条件
*
* @author <a href="mailto:huangfengjing@gmail.com">Ivan</a>
* Time: 2018/12/7 : 19:29
*/
public class OnClassCondition extends BaseCondition implements Condition {
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;
}
}其实也很简单,就是从注解中获取当前用户要判定是否存在的Class(支持类定义,和全类名),然后在当前 classpath 下去查找这个类是否存在即完成判定过程。
使用自定义的注解
使用起来就比较简单了,加上注解即可,如下所示:
1
2
3.class) (SSOFilter
@Configuration
public class SsoAutoConfiguration {...}
判定当前Spring容器中是否定义了某个Bean
这类判定主要用在如下的场景:某些组件需要依赖某个SpringBean,如果当前Spring容器中不存在这个Bean,那么就要添加一个,如果存在就不能再添加,防止产生NoSuchBeanDefinitionException
或者NoUniqueBeanDefinitionException
异常。
其实现过程其实和@ConditionalOnClass
大同小异,最主要的区别在于Condition.matches
方法一个是判定类是否存在,一个是判定Bean是否存在。代码如下:
1 | /** |
总结
本文主要讲解了如何通过@Conditional
和Condition
实现环境检测的能力,并从源码及示例两方面演示了从简单到高级的用法支持。其它的诸如判定当时配置项中的值以及资源判定的实现原理都差不多,感兴趣的可以翻阅应用分发 base-boot-starter
的源码。
当然,这里高级应用里面的判定规则并不如SpringBoot中的功能强大,但应付常规的应用已经足够,当不满足需求时,通过本文的讲解读者应该也已经了解到了如何自行扩展,或者联系我协助。