JavaSpringBoot
@Conditionalxxx这类注解表示某种判断条件成立时才会执行相关操作。掌握该类注解,有助于日常开发,框架的搭建。来介绍一下该类注解。

Spring Boot 版本

本文基于的Spring Boot的版本是2.3.4.RELEASE。

**@Conditional**

@Conditional注解是从Spring4.0才有的,可以用在任何类型或者方法上面,通过@Conditional注解可以配置一些条件判断,当所有条件都满足的时候,被@Conditional标注的目标才会被Spring容器处理。
@Conditional的使用很广,比如控制某个Bean是否需要注册,在Spring Boot中的变形很多,比如@ConditionalOnMissingBean@ConditionalOnBean等等,如下:
image.png
该注解的源码其实很简单,只有一个属性value,表示判断的条件(一个或者多个),是org.springframework.context.annotation.Condition类型,源码如下:

  1. @Target({ElementType.TYPE, ElementType.METHOD})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. public @interface Conditional {
  5. /**
  6. * All {@link Condition} classes that must {@linkplain Condition#matches match}
  7. * in order for the component to be registered.
  8. */
  9. Class<? extends Condition>[] value();
  10. }

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

**Condition**接口

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

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

matches中的两个参数如下:

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

    ConditionContext接口

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

    1. public interface ConditionContext {
    2. /**
    3. * 返回bean定义注册器,可以通过注册器获取bean定义的各种配置信息
    4. */
    5. BeanDefinitionRegistry getRegistry();
    6. /**
    7. * 返回ConfigurableListableBeanFactory类型的bean工厂,相当于一个ioc容器对象
    8. */
    9. @Nullable
    10. ConfigurableListableBeanFactory getBeanFactory();
    11. /**
    12. * 返回当前spring容器的环境配置信息对象
    13. */
    14. Environment getEnvironment();
    15. /**
    16. * 返回资源加载器
    17. */
    18. ResourceLoader getResourceLoader();
    19. /**
    20. * 返回类加载器
    21. */
    22. @Nullable
    23. ClassLoader getClassLoader();
    24. }

    如何自定义Condition?

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

    1. /**
    2. * 操作系统的匹配条件,如果是windows系统,则返回true
    3. */
    4. public class WindowsCondition implements Condition {
    5. @Override
    6. public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) {
    7. //获取当前环境信息
    8. Environment environment = conditionContext.getEnvironment();
    9. //获得当前系统名
    10. String property = environment.getProperty("os.name");
    11. //包含Windows则说明是windows系统,返回true
    12. if (property.contains("Windows")){
    13. return true;
    14. }
    15. return false;
    16. }
    17. }

    Linux环境判断源码如下

    1. /**
    2. * 操作系统的匹配条件,如果是windows系统,则返回true
    3. */
    4. public class LinuxCondition implements Condition {
    5. @Override
    6. public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) {
    7. Environment environment = conditionContext.getEnvironment();
    8. String property = environment.getProperty("os.name");
    9. if (property.contains("Linux")){
    10. return true;
    11. }
    12. return false;
    13. }
    14. }

    配置类中结合**@Bean**注入不同的Bean,如下

    1. @Configuration
    2. public class CustomConfig {
    3. /**
    4. * 在Windows环境下注入的Bean为winP
    5. * @return
    6. */
    7. @Bean("winP")
    8. @Conditional(value = {WindowsCondition.class})
    9. public Person personWin(){
    10. return new Person();
    11. }
    12. /**
    13. * 在Linux环境下注入的Bean为LinuxP
    14. * @return
    15. */
    16. @Bean("LinuxP")
    17. @Conditional(value = {LinuxCondition.class})
    18. public Person personLinux(){
    19. return new Person();
    20. }
    21. }

    简单的测试一下,如下

    1. @SpringBootTest
    2. class SpringbootInterceptApplicationTests {
    3. @Autowired(required = false)
    4. @Qualifier(value = "winP")
    5. private Person winP;
    6. @Autowired(required = false)
    7. @Qualifier(value = "LinuxP")
    8. private Person linP;
    9. @Test
    10. void contextLoads() {
    11. System.out.println(winP);
    12. System.out.println(linP);
    13. }
    14. }

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

    1. com.example.springbootintercept.domain.Person@885e7ff
    2. null

    很显然,判断生效了,Windows环境下只注入了WINP。

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

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

  3. 配置类解析阶段(**ConfigurationPhase.PARSE_CONFIGURATION**):在这个阶段会得到一批配置类的信息和一些需要注册的Bean。

  4. Bean注册阶段(**ConfigurationPhase.REGISTER_BEAN**):将配置类解析阶段得到的配置类和需要注册的Bean注入到容器中。

默认都是配置解析阶段,其实也就够用了,但是在Spring Boot中使用了ConfigurationCondition,这个接口可以自定义执行阶段,比如@ConditionalOnMissingBean都是在Bean注册阶段执行,因为需要从容器中判断Bean。
这个两个阶段有什么不同呢?:其实很简单的,配置类解析阶段只是将需要加载配置类和一些Bean(被@Conditional注解过滤掉之后)收集起来,而Bean注册阶段是将的收集来的Bean和配置类注入到容器中,如果在配置类解析阶段执行Condition接口的**matches()**接口去判断某些Bean是否存在IOC容器中,这个显然是不行的,因为这些Bean还未注册到容器中
什么是配置类,有哪些?:类上被@Component@ComponentScan@Import@ImportResource@Configuration标注的以及类中方法有@Bean的方法。如何判断配置类,在源码中有单独的方法:org.springframework.context.annotation.ConfigurationClassUtils#isConfigurationCandidate

**ConfigurationCondition**接口

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

  1. public interface ConfigurationCondition extends Condition {
  2. /**
  3. * 条件判断的阶段,是在解析配置类的时候过滤还是在创建bean的时候过滤
  4. */
  5. ConfigurationPhase getConfigurationPhase();
  6. /**
  7. * 表示阶段的枚举:2个值
  8. */
  9. enum ConfigurationPhase {
  10. /**
  11. * 配置类解析阶段,如果条件为false,配置类将不会被解析
  12. */
  13. PARSE_CONFIGURATION,
  14. /**
  15. * bean注册阶段,如果为false,bean将不会被注册
  16. */
  17. REGISTER_BEAN
  18. }
  19. }

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

多个**Condition**的执行顺序

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

  1. class Condition1 implements Condition {
  2. @Override
  3. public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
  4. System.out.println(this.getClass().getName());
  5. return true;
  6. }
  7. }
  8. class Condition2 implements Condition {
  9. @Override
  10. public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
  11. System.out.println(this.getClass().getName());
  12. return true;
  13. }
  14. }
  15. class Condition3 implements Condition {
  16. @Override
  17. public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
  18. System.out.println(this.getClass().getName());
  19. return true;
  20. }
  21. }
  22. @Configuration
  23. @Conditional({Condition1.class, Condition2.class, Condition3.class})
  24. public class MainConfig5 {
  25. }

上述例子会依次按照Condition1、Condition2、Condition3执行。
默认按照先后顺序执行,但是需要指定顺序呢?很简单,有如下三种方式:

  1. 实现**PriorityOrdered**接口,指定优先级
  2. 实现**Ordered**接口接口,指定优先级
  3. 使用**@Order**注解来指定优先级

例子如下:

  1. @Order(1)
  2. class Condition1 implements Condition {
  3. @Override
  4. public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
  5. System.out.println(this.getClass().getName());
  6. return true;
  7. }
  8. }
  9. class Condition2 implements Condition, Ordered {
  10. @Override
  11. public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
  12. System.out.println(this.getClass().getName());
  13. return true;
  14. }
  15. @Override
  16. public int getOrder() {
  17. return 0;
  18. }
  19. }
  20. class Condition3 implements Condition, PriorityOrdered {
  21. @Override
  22. public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
  23. System.out.println(this.getClass().getName());
  24. return true;
  25. }
  26. @Override
  27. public int getOrder() {
  28. return 1000;
  29. }
  30. }
  31. @Configuration
  32. @Conditional({Condition1.class, Condition2.class, Condition3.class})
  33. public class MainConfig6 {
  34. }

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

Spring Boot中常用的一些注解

Spring Boot中大量使用了这些注解,常见的注解如下:

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

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

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

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

总结

@Conditional注解在Spring Boot中演变的注解很多,需要着重了解,特别是后期框架整合的时候会大量涉及。