@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);
}

有两个参数:contextmetadata

  • 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条件判断在什么时候执行

  1. 在配置类上加@Conditional注解,来控制这个配置类是否需要被解析,如果配置类不解析,则配置类上面的上面6个注解的解析都会被跳过。
  2. 在被注册的bean上加@Conditional注解,来控制这个bean是否需要被注册到Spring容器中。
  3. 当配置类不需要被注册到容器时,那这个配置类解析所产生的新的配置类和所产生的新的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 {
}
@1Condition1通过@Order指定顺序,值为1
@2Condition2通过实现了Ordered接口来指定顺序,@3:getOrder方法返回1
@4Condition3实现了PriorityOrdered接口,实现这个接口需要重写getOrder方法,返回1000
@5Condtion顺序为123

测试结果:
根据排序的规则,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控制得更细一些,可以控制到具体那个阶段使用条件判断
  • 1
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一位不知名民工

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值