前言 不知道大家在使用Spring Boot开发的日常中有没有用过@Conditionalxxx
注解,比如@ConditionalOnMissingBean
。相信看过Spring Boot源码的朋友一定不陌生。
@Conditionalxxx
这类注解表示某种判断条件成立时才会执行相关操作。掌握该类注解,有助于日常开发,框架的搭建。
今天这篇文章就从前世今生介绍一下该类注解。
Spring Boot 版本 本文基于的Spring Boot的版本是2.3.4.RELEASE
。
@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 @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Conditional { 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 29 public interface ConditionContext { BeanDefinitionRegistry getRegistry () ; @Nullable ConfigurableListableBeanFactory getBeanFactory () ; 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 9 10 11 12 13 14 15 16 17 18 public class WindowsCondition implements Condition { @Override public boolean matches (ConditionContext conditionContext, AnnotatedTypeMetadata metadata) { Environment environment = conditionContext.getEnvironment(); String property = environment.getProperty("os.name" ); if (property.contains("Windows" )){ return true ; } return false ; } }
Linux环境判断源码如下 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class LinuxCondition implements Condition { @Override public boolean matches (ConditionContext conditionContext, AnnotatedTypeMetadata metadata) { Environment environment = conditionContext.getEnvironment(); String property = environment.getProperty("os.name" ); if (property.contains("Linux" )){ return true ; } return false ; } }
配置类中结合@Bean
注入不同的Bean,如下 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Configuration public class CustomConfig { @Bean("winP") @Conditional(value = {WindowsCondition.class}) public Person personWin () { return new Person(); } @Bean("LinuxP") @Conditional(value = {LinuxCondition.class}) public Person personLinux () { return new Person(); }
简单的测试一下,如下 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @SpringBootTest class SpringbootInterceptApplicationTests { @Autowired(required = false) @Qualifier(value = "winP") private Person winP; @Autowired(required = false) @Qualifier(value = "LinuxP") private Person linP; @Test void contextLoads () { System.out.println(winP); System.out.println(linP); } }
Windows环境下执行单元测试,输出如下 :
1 2 com.example.springbootintercept.domain.Person@885e7ff null
很显然,判断生效了,Windows环境下只注入了WINP
。
条件判断在什么时候执行? 条件判断的执行分为两个阶段,如下:
**配置类解析阶段(ConfigurationPhase.PARSE_CONFIGURATION
)**:在这个阶段会得到一批配置类的信息和一些需要注册的Bean
。
**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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public interface ConfigurationCondition extends Condition { ConfigurationPhase getConfigurationPhase () ; enum ConfigurationPhase { PARSE_CONFIGURATION, REGISTER_BEAN } }
这个接口在需要指定执行阶段的时候可以实现,比如需要根据某个Bean是否在IOC容器中来注入指定的Bean,则需要指定执行阶段为Bean的注册阶段 (ConfigurationPhase.REGISTER_BEAN
)。
多个Condition的执行顺序 @Conditional
中的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 class Condition1 implements Condition { @Override public boolean matches (ConditionContext context, AnnotatedTypeMetadata metadata) { System.out.println(this .getClass().getName()); return true ; } } class Condition2 implements Condition { @Override public boolean matches (ConditionContext context, AnnotatedTypeMetadata metadata) { System.out.println(this .getClass().getName()); return true ; } } class Condition3 implements Condition { @Override public boolean matches (ConditionContext context, AnnotatedTypeMetadata metadata) { System.out.println(this .getClass().getName()); return true ; } } @Configuration @Conditional({Condition1.class, Condition2.class, Condition3.class}) public class MainConfig5 {}
上述例子会依次按照Condition1
、Condition2
、Condition3
执行。
默认按照先后顺序执行,但是当我们需要指定顺序呢?很简单,有如下三种方式:
实现PriorityOrdered
接口,指定优先级
实现Ordered
接口接口,指定优先级
使用@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 37 38 39 @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 MainConfig6 {}
根据排序的规则,PriorityOrdered
的会排在前面,然后会再按照order
升序,最后可以顺序是:Condtion3->Condtion2->Condtion1
Spring Boot中常用的一些注解 Spring Boot中大量使用了这些注解,常见的注解如下:
@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为自定义配置提供的便利。
总结 @Conditional
注解在Spring Boot中演变的注解很多,需要着重了解,特别是后期框架整合的时候会大量涉及。