Spring Boot Conditional注解

@Conditional注解是从 Spring4.0 才有的,可以用在任何类型或者方法上面,通过@Conditional注解可 以配置一些条件判断,当所有条件都满足的时候,@Conditional标注的目标才会被Spring容器处理。

@Conditional的使用很广,比如控制某个 Bean 是否需要注册,在Spring Boot中的变形很多,比如 @ConditionalOnMissingBean@ConditionalOnBean 等等

该注解的源码其实很简单,只有一个属性 value ,表示判断的条件(一个或者多个),是 org.springframework.context.annotation.Condition 类型,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {

/**
* All {@link Condition} classes that must {@linkplain Condition#matches match}
* in order for the component to be registered.
*/
Class<? extends Condition>[] value();

}

@Conditional注解实现的原理很简单,就是通过 org.springframework.context.annotation.Condition 这个接口判断是否应该执行操作。

Condition接口

@Conditional 注解判断条件与否取决于 value 属性指定的 Condition 实现,其中有一个 matches() 方法, 返回 true 表示条件成立,反之不成立,接口如下:

1
2
3
4
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

matches中的两个参数如下:

  • context:条件上下文, ConditionContext接口类型的,可以用来获取容器中上下文信息。
  • metadata:用来获取被@Conditional标注的对象上的所有注解信息。

ConditionContext接口

这个接口很重要,能够从中获取Spring上下文的很多信息,比如 ConfigurableListableBeanFactory ,源码如下:

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
public interface ConditionContext {
/**
* 返回bean定义注册器,可以通过注册器获取bean定义的各种配置信息
*/
BeanDefinitionRegistry getRegistry();

/**
* 返回ConfigurableListableBeanFactory类型的bean工厂,相当于一个ioc容器对象
*/
@Nullable
ConfigurableListableBeanFactory getBeanFactory();

/**
* 返回当前spring容器的环境配置信息对象
*/
Environment getEnvironment();

/**
* 返回资源加载器
*/
ResourceLoader getResourceLoader();

/**
* 返回类加载器
*/
@Nullable
ClassLoader getClassLoader();
}

自定义Condition

假设有这样一个需求,需要根据运行环境注入不同的 Bean , Windows 环境和 Linux 环境注入不同的 Bean 。实现很简单,分别定义不同环境的判断条件,实现org.springframework.context.annotation.Condition即可。

windows环境的判断条件

1
2
3
4
5
6
7
8
public class WindowsCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment environment = context.getEnvironment();
String property = environment.getProperty("os.name");
return property != null && property.contains("Windows");
}
}

Mac环境的判断条件

1
2
3
4
5
6
7
8
public class MacCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment environment = context.getEnvironment();
String property = environment.getProperty("os.name");
return property != null && property.contains("Mac");
}
}

配置类中结合 @Bean 注入不同的Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class CustomConfig {

@Bean("win")
@Conditional(value = {WindowsCondition.class})
public Object win() {
return new Object();
}

@Bean("mac")
@Conditional(value = {MacCondition.class})
public Object mac() {
return new Object();
}
}

简单的测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SpringBootTest
class ConditionalApplicationTest {

@Autowired(required = false)
@Qualifier("win")
private Object winObject;

@Autowired(required = false)
@Qualifier("mac")
private Object macObject;

@Test
public void contextLoads() {
System.out.println("win:" + winObject);
System.out.println("mac:" + macObject);
}

}

Mac环境下执行单元测试,输出如下 :

1
2
win:null
mac:java.lang.Object@7da39774

条件判断在什么时候执行?

条件判断的执行分为两个阶段,如下:

  1. 配置类解析阶段( ConfigurationPhase.PARSE_CONFIGURATION ):在这个阶段会得到一批配置类的信息和一些需要注册的 Bean 。
  2. Bean注册阶段( ConfigurationPhase.REGISTER_BEAN ):将配置类解析阶段得到的配置类和需要注册的Bean注入到容器中。

默认都是配置解析阶段,其实也就够用了,但是在Spring Boot中使用了 ConfigurationCondition ,这个接口可以自定义执行阶段,比如 @ConditionalOnMissingBean 都是在Bean注册阶段执行,因为需要从容器中判断Bean。

这个两个阶段有什么不同呢?

其实很简单的,配置类解析阶段只是将需要加载配置类和一些Bean(被 @Conditional 注解过滤掉之后)收集起来,而Bean注册阶段是将的收集来的Bean和配置类注入到容器中,如果在配置类解析阶段执行 Condition 接口的 matches() 接口去判断某些Bean是否存在IOC容器中,这个显然是不行的,因为这些Bean还未注册到容器中

什么是配置类,有哪些?

类上被 、 @ComponentScan@Import@ImportResource 、 标注的以及类中方法有 @Bean 的方法。如何判断配置类,在源码中有单独的方法: org.springframework.context.annotation.ConfigurationClassUtils#isConfigurationCandidate

ConfigurationCondition接口

这个接口是Condition 的子类,相比于Condition 接口就多了一个getConfigurationPhase()方法,可以自定义执行阶段。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface ConfigurationCondition extends Condition {
/**
* 条件判断的阶段,是在解析配置类的时候过滤还是在创建bean的时候过滤
*/
ConfigurationPhase getConfigurationPhase();

/**
* 表示阶段的枚举:2个值
*/
enum ConfigurationPhase {
/**
* 配置类解析阶段,如果条件为false,配置类将不会被解析
*/
PARSE_CONFIGURATION,
/**
* bean注册阶段,如果为false,bean将不会被注册
*/
REGISTER_BEAN
}
}

这个接口在需要指定执行阶段的时候可以实现,比如需要根据某个Bean是否在IOC容器中来注入指定的Bean,则需要指定执行阶段为Bean的注册阶段( ConfigurationPhase.REGISTER_BEAN )。

多个Condition的执行顺序

@Conditional 中的 Condition 判断条件可以指定多个,默认是按照先后顺序执行。

1
2
3
4
@Configuration
@Conditional({Condition1.class, Condition2.class, Condition3.class})
public class MainConfig {
}

上述例子会依次按照 Condition1 Condition2 Condition3 执行。

默认按照先后顺序执行,但是当我们需要指定顺序呢?很简单,有如下三种方式:

  1. 实现PriorityOrdered接口,指定优先级
  2. 实现Ordered接口接口,指定优先级
  3. 使用@Order注解来指定优先级
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
30
31
32
33
34
35
36
@Order(1)
class Condition1 implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
}
}
class Condition2 implements Condition, Ordered {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
}

@Override
public int getOrder() {
return 0;
}
}
class Condition3 implements Condition, PriorityOrdered {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
}

@Override
public int getOrder() {
return 1000;
}
}
@Configuration
@Conditional({Condition1.class, Condition2.class, Condition3.class})
public class MainConfig {
}

根据排序的规则,PriorityOrdered的会排在前面,然后会再按照 order 升序,最后可以顺序是:

Condtion3->Condtion2->Condtion1

Spring Boot中常用@Conditional的一些注解

@ConditionalOnBean:当容器中有指定Bean的条件下进行实例化。
@ConditionalOnMissingBean:当容器里没有指定Bean的条件下进行实例化。
@ConditionalOnClass:当classpath类路径下有指定类的条件下进行实例化。
@ConditionalOnMissingClass:当类路径下没有指定类的条件下进行实例化。
@ConditionalOnWebApplication:当项目是一个Web项目时进行实例化。
@ConditionalOnNotWebApplication:当项目不是一个Web项目时进行实例化。
@ConditionalOnProperty:当指定的属性有指定的值时进行实例化。
@ConditionalOnExpression:基于SpEL表达式的条件判断。
@ConditionalOnJava:当JVM版本为指定的版本范围时触发实例化。
@ConditionalOnResource:当类路径下有指定的资源时触发实例化。
@ConditionalOnJndi:在JNDI存在的条件下触发实例化。
@ConditionalOnSingleCandidate:当指定的Bean在容器中只有一个,或者有多个但是指定了首选的Bean时触发实例化。

比如在 WEB 模块的自动配置类 WebMvcAutoConfiguration 下有这样一段代码:

1
2
3
4
5
6
7
8
@Bean
@ConditionalOnMissingBean
public InternalResourceViewResolver defaultViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix(this.mvcProperties.getView().getPrefix());
resolver.setSuffix(this.mvcProperties.getView().getSuffix());
return resolver;
}

常见的@Bean@ConditionalOnMissingBean注解结合使用,意思是当容器中没有InternalResourceViewResolver这种类型的Bean才会注入。这样写有什么好处呢?好处很明显,可以让开发者自定义需要的视图解析器,如果没有自定义,则使用默认的,这就是Spring Boot为自定义配置提供的便利。