前言

  • @EnableAutoConfiguration自动配置功能实现可以看到SpringBoot大量使用了各种@Conditional根据条件,决定类是否加载到Spring Ioc容器中

解析

定义解析

  • 下面是@Conditional的定义,可以看到可以在类或者方法上面使用,value需要传入一个Class数组,并且这个类需要继承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}s that must {@linkplain Condition#matches match}
* in order for the component to be registered.
*/
Class<? extends Condition>[] value();

}
  • Condition.java
1
2
3
4
5
6
7
8
9
10
11
12
13
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);

}
  • Condition.java接口用于定义是否符合条件

    • @param conditionContext:判断条件能使用的上下文环境

      • ConditionContext.java
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        public interface ConditionContext {

        BeanDefinitionRegistry getRegistry();

        ConfigurableListableBeanFactory getBeanFactory();

        Environment getEnvironment();

        ResourceLoader getResourceLoader();

        ClassLoader getClassLoader();
        }
    • @param annotatedTypeMetadata:注解所在位置的注释信息

使用示例

  • 这是一个简单的例子,现在问题来了,如果我想根据当前操作系统来注入Person实例,windows下注入billlinux下注入linus,怎么实现呢?

  • 首先,创建一个WindowsCondition类,conditionContext提供了多种方法,方便获取各种信息,也是SpringBoot@ConditonalOnXX注解多样扩展的基础。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class WindowsCondition implements Condition {

@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
// 获取ioc使用的beanFactory
ConfigurableListableBeanFactory beanFactory = conditionContext.getBeanFactory();
// 获取类加载器
ClassLoader classLoader = conditionContext.getClassLoader();
// 获取当前环境信息
Environment environment = conditionContext.getEnvironment();
// 获取bean定义的注册类
BeanDefinitionRegistry registry = conditionContext.getRegistry();

// 获得当前系统名
String property = environment.getProperty("os.name");
// 包含Windows则说明是windows系统,返回true
if (property.contains("Windows")){
return true;
}
return false;
}
}
  • 接着,创建LinuxCondition类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LinuxCondition implements Condition {

@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {

Environment environment = conditionContext.getEnvironment();

String property = environment.getProperty("os.name");
if (property.contains("Linux")){
return true;
}
return false;
}
}
  • 1、标注在方法上,一个方法只能注入一个bean实例,所以@Conditional标注在方法上只能控制一个bean实例是否注入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

@Configuration
public class BeanConfig {

// 只有一个类时,大括号可以省略
// 如果WindowsCondition的实现方法返回true,则注入这个bean
@Conditional({WindowsCondition.class})
@Bean(name = "bill")
public Person person1(){
return new Person("Bill Gates",62);
}

// 如果LinuxCondition的实现方法返回true,则注入这个bean
@Conditional({LinuxCondition.class})
@Bean("linus")
public Person person2(){
return new Person("Linus",48);
}
}
  • 2、标注在类上,一个类中可以注入很多实例,@Conditional标注在类上就决定了一批bean是否注入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Conditional({WindowsCondition.class})
@Configuration
public class BeanConfig {

@Bean(name = "bill")
public Person person1(){
return new Person("Bill Gates",62);
}

@Bean("linus")
public Person person2(){
return new Person("Linus",48);
}
}

SpringBootCondition 的进击

  • 为了满足更加丰富的 Condition(条件)的需要,Spring Boot 进一步拓展了更多的实现类,如下图所示:
  • org.springframework.boot.autoconfigure.condition.SpringBootCondition ,是 Spring Boot 实现Condition 的抽象类,且是 Spring Boot 所有 Condition 实现类的基类,分别对应如下注解:

    • @ConditionalOnBean:当容器里有指定 Bean 的条件下。
    • @ConditionalOnMissingBean:当容器里没有指定 Bean 的情况下。
    • @ConditionalOnSingleCandidate:当指定 Bean 在容器中只有一个,或者虽然有多个但是指定首选 Bean
    • @ConditionalOnClass:当类路径下有指定类的条件下。
    • @ConditionalOnMissingClass:当类路径下没有指定类的条件下。
    • @ConditionalOnProperty:指定的属性是否有指定的值
    • @ConditionalOnResource:类路径是否有指定的值
    • @ConditionalOnExpression:基于 SpEL 表达式作为判断条件。
    • @ConditionalOnJava:基于 Java 版本作为判断条件
    • @ConditionalOnJndi:在 JNDI 存在的条件下差在指定的位置
    • @ConditionalOnNotWebApplication:当前项目不是 Web 项目的条件下
    • @ConditionalOnWebApplication:当前项目是 Web项 目的条件下。
  • @ConditionalOnBean 注解解析

    • 查看该注解,可以看到实际上还是在@Conditional的基础上进行了包装

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

      Class<?>[] value() default {};

      String[] type() default {};

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

      String[] name() default {};

      SearchStrategy search() default SearchStrategy.ALL;
      }
    • OnBeanCondition.class

哪里生效的

  • 查看哪里生效的可以直接在matches()方法打好断点,见下图可以看到是从refresh()方法开始触发,然后执行BeanFactoryPostProcessor钩子接口,最终跳到org.springframework.context.annotation.ConditionEvaluator#shouldSkip()方法
  • 进入org.springframework.context.annotation.ConditionEvaluator#shouldSkip()方法,这里的condition.matches(this.context, metadata)方法就执行了Condition的接口方法
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class ConditionEvaluator {

private final ConditionContextImpl context;


/**
* Create a new {@link ConditionEvaluator} instance.
*/
public ConditionEvaluator(BeanDefinitionRegistry registry, Environment environment, ResourceLoader resourceLoader) {
this.context = new ConditionContextImpl(registry, environment, resourceLoader);
}


/**
* Determine if an item should be skipped based on {@code @Conditional} annotations.
* The {@link ConfigurationPhase} will be deduced from the type of item (i.e. a
* {@code @Configuration} class will be {@link ConfigurationPhase#PARSE_CONFIGURATION})
* @param metadata the meta data
* @return if the item should be skipped
*/
public boolean shouldSkip(AnnotatedTypeMetadata metadata) {
return shouldSkip(metadata, null);
}

/**
* Determine if an item should be skipped based on {@code @Conditional} annotations.
* @param metadata the meta data
* @param phase the phase of the call
* @return if the item should be skipped
*/
public boolean shouldSkip(AnnotatedTypeMetadata metadata, ConfigurationPhase phase) {
if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
return false;
}

if (phase == null) {
if (metadata instanceof AnnotationMetadata &&
ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
}
return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
}

List<Condition> conditions = new ArrayList<Condition>();
for (String[] conditionClasses : getConditionClasses(metadata)) {
for (String conditionClass : conditionClasses) {
Condition condition = getCondition(conditionClass, this.context.getClassLoader());
conditions.add(condition);
}
}

AnnotationAwareOrderComparator.sort(conditions);

for (Condition condition : conditions) {
ConfigurationPhase requiredPhase = null;
if (condition instanceof ConfigurationCondition) {
requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
}
if (requiredPhase == null || requiredPhase == phase) {
// 执行matches()方法
if (!condition.matches(this.context, metadata)) {
return true;
}
}
}

return false;
}

其他

总结

  • 使用@Condition注解可以根据不同条件创建不同BeanSpringBoot在此基础上又定义了一些定制版的@Condition,这样可以更方便的实现自动配置

  • @Condition实现原理:

    • 是在执行AnnotationConfigApplicationContext#refresh方法,调用invokeBeanFactoryPostProcessors,执行 BeanFactoryPostProcessorpostProcessBeanDefinitionRegistry 方法
    • 会加载bean的定义信息
    • 会执行ConditionEvaluator#shouldSkip判断这个类是否应该被跳过
    • 然后就会调用我们自定义的ColorCondition#matches方法
    • 如果返回false,则不会注册对应beanioc容器中

参考