Spring中@Conditional注解的详解和应用
一、@Conditional注解的作用
该注解是Spring4.0之后才有的,该注解可以放在任意类型或者方法上。通过@Conditional可以配置一些条件判断,当所有条件都满足时,被该@Conditional注解标注的目标才会被Spring处理。
注解源码:
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Document
public @interface Confitional{
Class<? extend Condition> [] value();
}
- value:Condition类型的数组,Condition是一个接口,表示一个条件判断,内部有个方法返回true或false,当所有Condition都成立的时候,@Conditional的结果才成立
Condition接口:
内部有个match方法,判断条件是否成立的。
@FunctionalInterface
public interface Condition {
//判断条件是否匹配 context:条件判断上下文
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
有两个参数:context和metadata
- context:ConditionContext 接口类型的,用来获取容器中的bean的信息
- metadata:用来获取被@Conditional标注的对象上的所有注解信息。
ConditionContext接口:
public interface ConditionContext {
//返回bean定义注册器,可以通过注册器获取bean定义的各种配置信息
BeanDefinitionRegistry getRegistry();
//返回ConfigurableListableBeanFactory类型的bean工厂,相当于一个ioc容器对象
@Nullable
ConfigurableListableBeanFactory getBeanFactory();
//返回当前spring容器的环境配置信息对象
Environment getEnvironment();
//返回资源加载器
ResourceLoader getResourceLoader();
//返回类加载器
@Nullable
ClassLoader getClassLoader();
}
二、条件判断在什么时候执行?
2.1 什么是配置类?
类上面有@Configuration,@Component,@ComponentScan,@Import,@Bean,@ImportResource这些注解时,被标注的类就是配置类.
//判断一个类是不是一个配置类,是否的是下面这个方法,有兴趣的可以看一下:
//org.springframework.context.annotation.ConfigurationClassUtils#isConfigurationCandidate
public static boolean isConfigurationCandidate(AnnotationMetadata metadata) {
// Do not consider an interface or an annotation...
if (metadata.isInterface()) {
return false;
}
// Any of the typical annotations found?
for (String indicator : candidateIndicators) {
if (metadata.isAnnotated(indicator)) {
return true;
}
}
// Finally, let's look for @Bean methods...
return hasBeanMethods(metadata);
}
static boolean hasBeanMethods(AnnotationMetadata metadata) {
try {
return metadata.hasAnnotatedMethods(Bean.class.getName());
}
catch (Throwable ex) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to introspect @Bean methods on class [" + metadata.getClassName() + "]: " + ex);
}
return false;
}
}
2.2 Spring对配置类的处理阶段
配置类解析阶段:
得到一些配置类信息,和一批需要注册的bean
bean的注册阶段:
将配置类解析阶段得到的配置类作为bean和配置类里需要注册的bean都注册到容器中
2.3 @Conditional使用的步骤
- 自定义一个类,实现Condition或ConfigurationCondition接口,实现matches方法
- 在目标对象上使用@Conditional注解,并指定value的指为自定义的Condition类型
- 启动spring容器加载资源,此时@Conditional就会起作用了
2.4 @Conditional条件判断在什么时候执行
- 在配置类上加@Conditional注解,来控制这个配置类是否需要被解析,如果配置类不解析,则配置类上面的上面6个注解的解析都会被跳过。
- 在被注册的bean上加@Conditional注解,来控制这个bean是否需要被注册到Spring容器中。
- 当配置类不需要被注册到容器时,那这个配置类解析所产生的新的配置类和所产生的新的bean都不会被注册到容器中
总结:
(1)@Conditional的参数是实现了Condition接口的类。
(2)当@Conditional放在需要被控制注册的bean上时,则只是被标注的bean不会被注册,配置类和其他bean会被注册。
(3)当@Conditional放在配置类上时,那么实现类里的条件对两个阶段都有效,此时无法精确控制具体某个阶段;如果想要具体控制某个阶段,比如可以解析,但不让注册bean,此时需要用到ConfigurationCondition接口
ConfigurationCondition接口:
该接口是Condition接口的子类,getConfigurationPhase方法用来指定条件判断的阶段,
判断是在解析配置类的时候过滤还是在注册bean的时候过滤
public interface ConfigurationCondition extends Condition {
/**
* 条件判断的阶段,是在解析配置类的时候过滤还是在创建bean的时候过滤
*/
ConfigurationPhase getConfigurationPhase();
/**
* 表示阶段的枚举:2个值
*/
enum ConfigurationPhase {
/**
* 配置类解析阶段,如果条件为false,配置类将不会被解析
*/
PARSE_CONFIGURATION,
/**
* bean注册阶段,如果为false,bean将不会被注册
*/
REGISTER_BEAN
}
}
三、案例
3.1 阻止配置类的解析
1、自定义Condition接口的实现类:
Condition实现类中当有一个条件为false的时候,spring就会跳过处理这个配置类
public class ConditionImpl implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return false;
}
}
2、配置类
@Configuration
@Conditional(ConditionImpl.class)
public class MyConfig {
@Bean
public String name(){
return "zkc";
}
}
3、测试类
@Test
public void test(){
ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
//获取所有String类型的bean
Map<String, String> beans = context.getBeansOfType(String.class);
beans.forEach((k,v)->{
System.out.println(String.format("%s--->%s",k,v));
});
}
从容器中获取String类型的Bean,此时没有输出。
将@Conditional从配置类上取掉,再次运行
name--->zkc
3.2 阻止bean的注册
2、配置类
@Configuration
public class MyConfig {
@Bean
@Conditional(ConditionImpl.class)
public String name(){
return "zkc";
}
@Bean
public String address(){
return "杭州";
}
}
3、测试类
@Test
public void test(){
ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
//获取所有String类型的bean
Map<String, String> beans = context.getBeansOfType(String.class);
beans.forEach((k,v)->{
System.out.println(String.format("%s--->%s",k,v));
});
}
结果:
可以看到name没有被注册
address--->杭州
3.3 当bean不存在的时候才注册
需求:
IService接口有两个实现类Service1和Service2,这两个类会放在2个配置类中通过@Bean的方式来注册到容器,此时我们想加个限制,只允许有一个IService类型的bean被注册到容器。
1、创建接口IService,Service1,Service2
public interface IService {}
public class Service1 implements IService {}
public class Service2 implements IService {}
2、创建Condition接口实现类
public class ConditionImpl implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//获取bean工厂
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
//从容器中获取IService类型的Bean
Map<String, IService> beansOfType = beanFactory.getBeansOfType(IService.class);
//判断是否为空
return beansOfType.isEmpty();
}
}
3、配置类中注册两个Service
@Configuration
public class MyConfig {
@Bean
@Conditional(ConditionImpl.class)
public IService service1(){
return new Service1();
}
@Bean
@Conditional(ConditionImpl.class)
public IService service2(){
return new Service2();
}
}
结果:
可以看到之后一个ISevice类型的bean被注册
service1--->com.zjhc.service.Service1@3c22fc4c
将两个方法上的@Conditional去掉之后,再次运行
service1--->com.zjhc.service.Service1@6af93788
service2--->com.zjhc.service.Service2@ef9296d
3.4 根据环境选择配置类
平常我们做项目的时候,有开发环境、测试环境、线上环境,每个环境中有些信息是不一样的,比如数据库的配置信息,下面我们来模拟不同环境中使用不同的配置类来注册不同的bean
1、自定义一个注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Conditional(EnvCondtion.class)
public @interface EnvConditional {
//环境枚举(测试环境,生产环境,开发环境)
enum ENV{TEST,DEV,PRO}
ENV value() default ENV.DEV; //默认生产环境
}
2、创建条件类
条件类会解析配置类上面@EnvConditional注解,得到环境信息。然后和目前的环境对比,决定返回true还是false
public class EnvCondtion implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//获取当前的环境
EnvConditional.ENV curEnv = EnvConditional.ENV.DEV;
//获取使用条件类上的@EnvConditional注解
MultiValueMap<String, Object> allAnnotationAttributes = metadata.getAllAnnotationAttributes(EnvConditional.class.getName());
Assert.notNull(allAnnotationAttributes,"该注解不存在");
//获取使用条件的类上的@EnvConditional注解的参数
EnvConditional.ENV env = (EnvConditional.ENV) allAnnotationAttributes.get("value").get(0);
return curEnv.equals(env);
}
}
3、创建三个配置类
@Configuration
@EnvConditional(EnvConditional.ENV.TEST)
public class TestConfig {
@Bean
public String name(){
return "我是测试环境的配置类";
}
}
@Configuration
@EnvConditional(EnvConditional.ENV.DEV)
public class DevConfig {
@Bean
public String name(){
return "我是开发环境的配置类";
}
}
@Configuration
@EnvConditional(EnvConditional.ENV.PRO)
public class ProConfig {
@Bean
public String name(){
return "我是生产环境的配置类";
}
}
@Configuration
@Import({TestConfig.class, DevConfig.class,ProConfig.class})
public class MyConfig {}
测试:
@Test
public void test2() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig2.class);
System.out.println(context.getBean("name"));
}
结果:
我是开发环境的配置类
修改一下EnvCondtion 的代码,切换到生产环境:
//获取当前的环境
EnvConditional.ENV curEnv = EnvConditional.ENV.PRO;
我是生产环境的配置类
3.5 多个Condition条件类指定优先级
3.5.1 默认情况
@Condtional中value指定多个Condtion的时候,默认情况下会按参数顺序执行
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 {
}
测试:
@Test
public void test5() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig5.class);
}
运行输出:
com.javacode2018.lesson001.demo25.test5.Condition1
com.javacode2018.lesson001.demo25.test5.Condition2
com.javacode2018.lesson001.demo25.test5.Condition3
com.javacode2018.lesson001.demo25.test5.Condition1
com.javacode2018.lesson001.demo25.test5.Condition2
com.javacode2018.lesson001.demo25.test5.Condition3
com.javacode2018.lesson001.demo25.test5.Condition1
com.javacode2018.lesson001.demo25.test5.Condition2
com.javacode2018.lesson001.demo25.test5.Condition3
可以看出输出的属性和@Conditional中value值的顺序是一样的
3.5.2 指定Condition的顺序
自定义的Condition可以实现PriorityOrdered接口或者继承Ordered接口,或者使用@Order注解,通过这些来指定这些Condition的优先级
排序规则:先按PriorityOrdered排序,然后按照order的值进行排序;
下面这几个都可以指定order的值
接口:org.springframework.core.Ordered,有个getOrder方法用来返回int类型的值
接口:org.springframework.core.PriorityOrdered,继承了Ordered接口,所以也有getOrder方法
注解:org.springframework.core.annotation.Order,有个int类型的value参数指定Order的大小
@Order(1) //@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 { //@2
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println(this.getClass().getName());
return true;
}
@Override
public int getOrder() { //@3
return 0;
}
}
class Condition3 implements Condition, PriorityOrdered { //@4
@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})//@5
public class MainConfig6 {
}
@1:Condition1通过@Order指定顺序,值为1
@2:Condition2通过实现了Ordered接口来指定顺序,@3:getOrder方法返回1
@4:Condition3实现了PriorityOrdered接口,实现这个接口需要重写getOrder方法,返回1000
@5:Condtion顺序为1、2、3
测试结果:
根据排序的规则,PriorityOrdered的会排在前面,然后会再按照order升序:
com.javacode2018.lesson001.demo25.test6.Condition3
com.javacode2018.lesson001.demo25.test6.Condition2
com.javacode2018.lesson001.demo25.test6.Condition1
3.6 ConfigurationCondition使用
判断bean存不存在的问题,通常会使用ConfigurationCondition这个接口,阶段为:REGISTER_BEAN,这样可以确保条件判断是在bean注册阶段执行的
需求:当容器中有Service这种类型的bean的时候,BeanConfig2才生效。
配置类的处理会依次经过2个阶段:配置类解析阶段和bean注册阶段,Condition接口类型的条件会对这两个阶段都有效,解析阶段的时候,容器中是还没有Service这个bean的,配置类中通过@Bean注解定义的bean在bean注册阶段才会被注册到spring容器,所以BeanConfig2在解析阶段去容器中是看不到Service这个bean的,所以就被拒绝了。
此时我们需要用到ConfigurationCondition了,让条件判断在bean注册阶段才起效。
1、来一个普通的类:Service
public class Service {}
2、来一个配置类BeanConfig1,通过配置类注册上面这个Service
@Configuration
public class BeanConfig1 {
@Bean
public Service service() {
return new Service();
}
}
3、再来一个配置类:BeanConfig2
@Configuration
public class BeanConfig2 {
@Bean
public String name() {
return "路人甲Java";
}
}
5、来一个总的配置类
@Configuration
@Import({BeanConfig1.class, BeanConfig2.class})
public class MainConfig7 {
}
6、ConfigurationCondition实现类
public class MyConfigurationCondition implements ConfigurationCondition {
@Override
public ConfigurationPhase getConfigurationPhase() {
//只在注册bean的时候生效
return ConfigurationPhase.REGISTER_BEAN;
}
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//获取spring容器
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
//判断容器中是否存在Service类型的bean,
boolean existsService = !beanFactory.getBeansOfType(Service1.class).isEmpty();
return existsService;
}
}
7、BeanConfig2配置类上
@Conditional(MyConfigurationCondition.class)
8、运行输出
name--->zzzz
四、总结
- @Conditional注解可以标注在spring需要处理的对象上(配置类、@Bean方法),相当于加了个条件判断,通过判断的结果,让spring觉得是否要继续处理被这个注解标注的对象
- spring处理配置类大致有2个过程:解析配置类、注册bean,这两个过程中都可以使用@Conditional来进行控制spring是否需要处理这个过程
- Condition默认会对2个过程都有效
- ConfigurationCondition控制得更细一些,可以控制到具体那个阶段使用条件判断