超详细分析Spring的@Conditional注解

797 阅读8分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 23 天,点击查看活动详情

大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈


前言

已知@Conditional注解用于指定能够注册为容器中的bean的条件。那么本篇文章将结合示例工程,从源码入手,分析@Conditional注解的如下几个方面。

  1. @Conditional注解的作用时机;
  2. Condition的执行顺序;
  3. 多个Condition之间的关系。

Springboot版本:2.4.1

Spring版本:5.3.2

正文

一. 示例工程搭建

示例工程结构如下所示。

示例工程结构

Condition接口实现类如下所示。

public class MyControllerCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        System.out.println(this.getClass().getName());
        return true;
    }

}

public class MyDaoCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        System.out.println(this.getClass().getName());
        return true;
    }

}

public class MyFurtherCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        System.out.println(this.getClass().getName());
        return true;
    }

}

public class MyRepositoryCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        System.out.println(this.getClass().getName());
        return true;
    }

}

public class MyServiceCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        System.out.println(this.getClass().getName());
        return true;
    }

}

业务类定义如下所示。

@Controller
@Conditional(MyControllerCondition.class)
public class MyController {}

@Conditional(MyDaoCondition.class)
public class MyDao {}

public class MyFurtherService {}

@Conditional(MyRepositoryCondition.class)
public class MyRepository {}

public class MyService {}

配置类MyFurtherConfig定义如下。

@Configuration
@Conditional(MyFurtherCondition.class)
public class MyFurtherConfig {

    @Bean
    public MyFurtherService myFurtherService() {
        return new MyFurtherService();
    }

}

配置类MyConfig定义如下。

@ComponentScan
@Configuration
@Import(MyDao.class)
public class MyConfig {

    @Bean
    @Conditional(MyServiceCondition.class)
    public MyService myService() {
        return new MyService();
    }

    @Bean
    public MyRepository myRepository() {
        return new MyRepository();
    }

}

测试类如下所示。

public class MyTest {

    public static void main(String[] args) {
        ApplicationContext applicationContext
                = new AnnotationConfigApplicationContext(MyConfig.class);
    }

}

在示例工程中,一共演示了如下几种使用@Conditional注解的情况。

  1. 业务类由@Conditional注解和@Controller(@Service,@Repository和@Component注解均可)注解修饰,并且业务类通过@ComponentScan注解扫描并注册到容器中
    1. MyController
  2. 业务类由@Conditional注解修饰,并且业务类通过@Import注解直接导入并注册到容器中
    1. MyDao
  3. 业务类通过@Bean注解修饰的方法注册到容器中,同时@Bean注解修饰的方法由@Conditional注解修饰
    1. MyService
  4. 配置类由@Conditional注解和@Configuration注解修饰,并且配置类通过@ComponentScan注解扫描并注册到容器中
    1. MyFurtherConfig

还有一种@Conditional注解无效的情况。

  1. 业务类由@Conditional注解修饰,并且业务类通过@Bean注解修饰的方法注册到容器中
    1. MyRepository

二. @Conditional作用时机

通过@Conditional注解修饰的类,在被注册为Spring中的BeanDefinition时,会多进行一步条件判断,如果判断返回true,则继续执行注册为BeanDefinition的逻辑,否则放弃注册。

@Conditional注解需要配合Condition接口使用,使用@Conditional注解时,需要通过@Conditional注解导入Condition接口的实现类,后续在向容器注册BeanDefinition的某些阶段,会调用到Condition接口的实现类实现的matches() 方法来进行判断。下面给出会调用到Condition接口的实现类实现的matches() 方法来进行判断的阶段。

  1. ConfigurationClassParserprocessConfigurationClass() 方法解析ConfigurationClass时,如果ConfigurationClass对应的类由@Conditional注解修饰,那么会调用到Condition接口实现类的判断逻辑;
  2. ConfigurationClassParserdoProcessConfigurationClass() 方法解析ConfigurationClass对应的类的@ComponentScan注解时,会调用到ComponentScanAnnotationParserparse() 方法开启对@ComponentScan注解内容的处理,在为@ComponentScan注解扫描范围内的每个目标类创建BeanDefinition时,如果目标类由@Conditional注解修饰,那么会调用到Condition接口实现类的判断逻辑(目标类:由@Controller,@Service,@Repository或@Configuration注解修饰的类);
  3. ConfigurationClassBeanDefinitionReaderConfigurationClass解析为BeanDefinition时,如果ConfigurationClass对应的类由@Conditional注解修饰,那么会调用到Condition接口实现类的判断逻辑,如果判断返回值为false并且ConfigurationClass在注册表中已经存在一个BeanDefinition,那么还需要将这个BeanDefinition从注册表移除;
  4. ConfigurationClassBeanDefinitionReaderConfigurationClass解析为BeanDefinition时,处理每个BeanMethod时,如果BeanMethod对应的方法由@Conditional注解修饰,那么会调用到Condition接口实现类的判断逻辑。

时序图如下。

作用时机时序图

(看不清可以点击图片并放大)

三. Condition执行顺序

Condition接口的实现类可以由@Conditional注解导入,并且@Conditional注解可以一次性导入多个Condition接口的实现类,并且默认情况下按导入顺序执行。如下给出一个例子进行说明。

业务如下所示。

public class MyService {}

定义两个Condition接口的实现类(后续称为条件类),如下所示。

public class MyFirstCondition implements Condition {

    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        System.out.println(this.getClass().getName());
        return true;
    }

}

public class MySecondCondition implements Condition {

    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        System.out.println(this.getClass().getName());
        return true;
    }

}

配置类如下所示,注意条件类的导入顺序。

@Configuration
public class MyConfig {

    @Bean
    @Conditional({MySecondCondition.class, MyFirstCondition.class})
    public MyService myService() {
        return new MyService();
    }

}

测试类如下所示。

public class MyTest {

    public static void main(String[] args) {
        ApplicationContext applicationContext
                = new AnnotationConfigApplicationContext(MyConfig.class);
    }

}

打印结果如下。

Condition执行顺序图

可见条件类的调用顺序和条件类导入顺序是一致的。如果想要指定顺序,可以让条件类实现Ordered接口,或者实现PriorityOrdered接口,或者使用@Order注解修饰条件类。基于上述例子,再添加三个条件类,如下所示。

public class MyFirstOrderCondition implements Condition, Ordered {

    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        System.out.println(this.getClass().getName());
        return true;
    }

    public int getOrder() {
        return 10;
    }

}

public class MySecondOrderCondition implements Condition, PriorityOrdered {

    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        System.out.println(this.getClass().getName());
        return true;
    }

    public int getOrder() {
        return 10;
    }

}

@Order(5)
public class MyThirdOrderCondition implements Condition {

    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        System.out.println(this.getClass().getName());
        return true;
    }

}

再修改一下配置类。

@Configuration
public class MyConfig {

    @Bean
    @Conditional({MySecondCondition.class, MyFirstCondition.class,
            MyFirstOrderCondition.class, MySecondOrderCondition.class, MyThirdOrderCondition.class})
    public MyService myService() {
        return new MyService();
    }

}

运行测试程序,打印如下。

Condition优先级测试

通过上述打印结果可以总结如下。

  1. 实现了PriorityOrdered接口的条件类总是先于实现了Ordered接口的条件类执行;
  2. order值小的条件类总是先于order值大的条件类执行;
  3. 实现了PriorityOrdered接口,Ordered接口或者由@Order注解修饰的条件类总是先于普通条件类执行。

四. 多个Condition之间的关系

如果一个@Conditional注解导入了多个条件类,那么这些条件类的判断结果之间的关系是什么样的呢,下面对这个问题进行分析。Spring中有一个叫做ConditionEvaluator的类,这个类的shouldSkip() 方法专门用于调用条件类的判断逻辑,如下所示。

public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
    // 如果当前类不由@Conditional注解修饰,直接返回false,表示不应该跳过当前类
    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);
    }

    // 把所有条件类实例化出来并添加到conditions集合中
    List<Condition> conditions = new ArrayList<>();
    for (String[] conditionClasses : getConditionClasses(metadata)) {
        for (String conditionClass : conditionClasses) {
            Condition condition = getCondition(conditionClass, this.context.getClassLoader());
            conditions.add(condition);
        }
    }

    AnnotationAwareOrderComparator.sort(conditions);

    // 遍历每个条件类,并调用其matches()方法来进行判断
    // 只要有一个条件类的matches()方法返回false,则表示应该跳过当前类
    for (Condition condition : conditions) {
        ConfigurationPhase requiredPhase = null;
        if (condition instanceof ConfigurationCondition) {
            requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
        }
        if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
            return true;
        }
    }

    return false;
}

ConditionEvaluator#shouldSkip方法返回true时表示当前类需要被跳过,而只要有一个条件类的matches() 方法返回false时,ConditionEvaluator#shouldSkip方法就会返回true,同时也不会再去执行剩下的条件类的matches() 方法。

也就是多个Condition之间是与的关系,只有满足所有的Condition,那么这个bean才能被注册到容器中。

总结

首先是@Conditional注解如何使用,总结如下。

  1. 业务类由@Conditional注解和@Controller(@Service,@Repository和@Component注解均可)注解修饰,并且业务类通过@ComponentScan注解扫描并注册到容器中;
  2. 业务类由@Conditional注解修饰,并且业务类通过@Import注解直接导入并注册到容器中;
  3. 业务类通过@Bean注解修饰的方法注册到容器中,同时@Bean注解修饰的方法由@Conditional注解修饰;
  4. 配置类由@Conditional注解和@Configuration注解修饰,并且配置类通过@ComponentScan注解扫描并注册到容器中。

然后是@Conditional注解作用时机,总的概括就是在将一个对象注册为容器中的BeanDefinition的过程中,会在各个环节调用到Condition接口实现类的判断逻辑来判断是否需要终止注册流程。

接下来是多个Condition的执行顺序的总结,总结如下。

  1. 默认情况按导入顺序来执行;
  2. 实现了PriorityOrdered接口的条件类总是先于实现了Ordered接口的条件类执行;
  3. order值小的条件类总是先于order值大的条件类执行;
  4. 实现了PriorityOrdered接口,Ordered接口或者由@Order注解修饰的条件类总是先于普通条件类执行。

最后是多个Condition之间的关系,关系是的关系,也就是只要有一个Condition不满足,那么这个bean就不会被注册。


大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 23 天,点击查看活动详情