@Conditional注解

@Conditional注解是从spring4.0才有的,可以用在任何类型或者方法上面,通过@Conditional注解可以配置一些条件判断,当所有条件都满足的时候,被@Conditional标注的目标才会被spring容器处理。
@Conditional源码:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {Class<? extends Condition>[] value();
}

这个注解只有一个value参数,Condition类型的数组,Condition是一个接口,表示一个条件判断,内部有个方法返回true或false,当所有Condition都成立的时候,@Conditional的结果才成立。

Condition接口

用来表示条件判断的接口,源码如下:

@FunctionalInterface
public interface Condition {/*** 判断条件是否匹配* context:条件判断上下文*/boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

是一个函数式接口,内部只有一个matches方法,用来判断条件是否成立的,2个参数:

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

ConditionContext接口

这个接口中提供了一些常用的方法,可以用来获取spring容器中的各种信息,看一下源码:

public interface ConditionContext {/*** 返回bean定义注册器,可以通过注册器获取bean定义的各种配置信息*/BeanDefinitionRegistry getRegistry();/*** 返回ConfigurableListableBeanFactory类型的bean工厂,相当于一个ioc容器对象*/@NullableConfigurableListableBeanFactory getBeanFactory();/*** 返回当前spring容器的环境配置信息对象*/Environment getEnvironment();/*** 返回资源加载器*/ResourceLoader getResourceLoader();/*** 返回类加载器*/@NullableClassLoader getClassLoader();
}
  • 比较关键性的问题:条件判断在什么时候执行?
    Spring对配置类的处理主要分为2个阶段:
  1. 配置类解析阶段
    会得到一批配置类的信息,和一些需要注册的bean

  2. bean注册阶段
    将配置类解析阶段得到的配置类和需要注册的bean注册到spring容器中

  • 看一下什么是配置类
    类中有下面任意注解之一的就属于配置类:

类上有@Compontent注解
类上有@Configuration注解
类上有@CompontentScan注解
类上有@Import注解
类上有@ImportResource注解
类中有@Bean标注的方法

判断一个类是不是一个配置类,是否的是下面这个方法,有兴趣的可以看一下:

org.springframework.context.annotation.ConfigurationClassUtils#isConfigurationCandidate

spring中处理这2个过程会循环进行,直到完成所有配置类的解析及所有bean的注册。

Spring对配置类处理过程
源码位置:

org.springframework.context.annotation.ConfigurationClassPostProcessor#processConfigBeanDefinitions

整个过程大致的过程如下:

  1. 通常我们会通过new AnnotationConfigApplicationContext()传入多个配置类来启动spring容器

  2. spring对传入的多个配置类进行解析

  3. 配置类解析阶段:这个过程就是处理配置类上面6中注解的过程,此过程中又会发现很多新的配置类,比如@Import导入的一批新的类刚好也符合配置类,而被@CompontentScan扫描到的一些类刚好也是配置类;此时会对这些新产生的配置类进行同样的过程解析

  4. bean注册阶段:配置类解析后,会得到一批配置类和一批需要注册的bean,此时spring容器会将这批配置类作为bean注册到spring容器,同样也会将这批需要注册的bean注册到spring容器

  5. 经过上面第3个阶段之后,spring容器中会注册很多新的bean,这些新的bean中可能又有很多新的配置类

  6. Spring从容器中将所有bean拿出来,遍历一下,会过滤得到一批未处理的新的配置类,继续交给第3步进行处理

  7. step3到step6,这个过程会经历很多次,直到完成所有配置类的解析和bean的注册
    从上面过程中可以了解到:

  8. 可以在配置类上面加上@Conditional注解,来控制是否需要解析这个配置类,配置类如果不被解析,那么这个配置上面6种注解的解析都会被跳过

  9. 可以在被注册的bean上面加上@Conditional注解,来控制这个bean是否需要注册到spring容器中

  10. 如果配置类不会被注册到容器,那么这个配置类解析所产生的所有新的配置类及所产生的所有新的bean都不会被注册到容器

一个配置类被spring处理有2个阶段:

  • 配置类解析阶段、
  • bean注册阶段(将配置类作为bean被注册到spring容器)。

如果将Condition接口的实现类作为配置类上@Conditional中,那么这个条件会对两个阶段都有效,此时通过Condition是无法精细的控制某个阶段的,如果想控制某个阶段,比如可以让他解析,但是不能让他注册,此时就就需要用到另外一个接口了:ConfigurationCondition

ConfigurationCondition接口

看一下这个接口的源码:

public interface ConfigurationCondition extends Condition {/*** 条件判断的阶段,是在解析配置类的时候过滤还是在创建bean的时候过滤*/ConfigurationPhase getConfigurationPhase();/*** 表示阶段的枚举:2个值*/enum ConfigurationPhase {/*** 配置类解析阶段,如果条件为false,配置类将不会被解析*/PARSE_CONFIGURATION,/*** bean注册阶段,如果为false,bean将不会被注册*/REGISTER_BEAN}
}

ConfigurationCondition接口相对于Condition接口多了一个getConfigurationPhase方法,用来指定条件判断的阶段,是在解析配置类的时候过滤还是在创建bean的时候过滤。

@Conditional使用的3步骤

  1. 自定义一个类,实现Condition或ConfigurationCondition接口,实现matches方法
  2. 在目标对象上使用@Conditional注解,并指定value的值为自定义的Condition类型
  3. 启动spring容器加载资源,此时@Conditional就会起作用了

栗子

1. 阻止配置类的处理

在配置类上面使用@Conditional,这个注解的value指定的Condition当有一个为false的时候,spring就会跳过处理这个配置类。

自定义一个Condition类:

package com.yuan11.annotation;import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;/*** @title: MyCondition1* @Author yuan11* @Date: 2022/6/27 23:57* @Version 1.0*/
public class MyCondition1 implements Condition {@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {return false;}
}

matches方法内部我们可以随意发挥,此处为了演示效果就直接返回false。

来个配置类,在配置类上面使用上面这个条件,此时会让配置类失效,如下:

package com.yuan11.annotation;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;/*** @title: Config3* @Author yuan11* @Date: 2022/6/27 23:59* @Version 1.0*/
@Conditional(MyCondition1.class) //@1
@Configuration
public class Config3 {@Beanpublic String name() { //@2return "yuan11";}
}

@1:使用了自定义的条件类
@2:通过@Bean标注这name这个方法,如果这个配置类成功解析,会将name方法的返回值作为bean注册到spring容器

  • Test
public class Test {public static void main(String[] args) {//1.通过AnnotationConfigApplicationContext创建spring容器,参数为@Conditional标注的类AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config3.class);Map<String, String> serviceMap = context.getBeansOfType(String.class);serviceMap.forEach((beanName, bean) -> {System.out.println(String.format("%s->%s", beanName, bean));});}
}

从容器中获取String类型的bean,运行方法没有任何输出。

我们可以将Config3上面的@Conditional去掉,再次运行输出:

name->yuan11

2. 阻止bean的注册

package com.yuan11.annotation;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;/*** @title: Config4* @Author yuan11* @Date: 2022/6/28 0:05* @Version 1.0*/
@Configuration
public class Config4 {@Conditional(MyCondition1.class) //@1@Beanpublic String name() {return "yuan11";}@Beanpublic String address() {return "西安市";}
}

上面2个方法上面使用了@Bean注解来定义了2个bean,name方法上面使用了@Conditional注解,这个条件会在name这个bean注册到容器之前会进行判断,当条件为true的时候,name这个bean才会被注册到容器。

  • Test
    Test中新增个测试用例来加载上面这个配置类,从容器中获取String类型所有bean输出,代码如下:
public class Test {public static void main(String[] args) {//1.通过AnnotationConfigApplicationContext创建spring容器,参数为@Import标注的类AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config4.class);Map<String, String> serviceMap = context.getBeansOfType(String.class);serviceMap.forEach((beanName, bean) -> {System.out.println(String.format("%s->%s", beanName, bean));});}
}

运行输出:

address->西安市

可以看到容器中只有一个address被注册了,而name这个bean没有被注册。

3. bean不存在的时候才注册

  • 需求
    IService接口有两个实现类Service1和Service1,这两个类会放在2个配置类中通过@Bean的方式来注册到容器,此时我们想加个限制,只允许有一个IService类型的bean被注册到容器。

可以在@Bean标注的2个方法上面加上条件限制,当容器中不存在IService类型的bean时,才将这个方法定义的bean注册到容器,下面来看代码实现。

  • 代码实现
    条件判断类:OnMissingBeanCondition
package com.yuan11.annotation;import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;import java.util.Map;/*** @title: OnMissingBeanCondition* @Author yuan11* @Date: 2022/6/28 0:17* @Version 1.0*/
public class OnMissingBeanCondition implements Condition {@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {//获取bean工厂ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();//从容器中获取IService类型beanMap<String, IService> serviceMap = beanFactory.getBeansOfType(IService.class);//判断serviceMap是否为空return serviceMap.isEmpty();}
}

上面matches方法中会看容器中是否存在IService类型的bean,不存在的时候返回true

IService接口

package com.yuan11.annotation;
public interface IService {}

接口有2个实现类
Service1

package com.yuan11.annotation;
public class Service1 implements IService {}

Service2

package com.yuan11.annotation;
public class Service2 implements IService {}

来一个配置类负责注册Service1、Service2到容器

@Configuration
public class BeanConfig1 {@Conditional(OnMissingBeanCondition.class) //@1@Beanpublic IService service1() {return new Service1();}
}
@Configuration
public class BeanConfig2 {@Conditional(OnMissingBeanCondition.class) //@1@Beanpublic IService service2() {return new Service2();}
}

@1:方法之前使用了条件判断

来一个总的配置类,导入另外2个配置类

package com.yuan11.annotation;import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;/*** @title: Config5* @Author yuan11* @Date: 2022/6/28 0:25* @Version 1.0*/
@Configuration
@Import({BeanConfig1.class,BeanConfig2.class}) //@1
public class Config5 {}

@1:通过@Import将其他2个配置类导入

-Test

public class Test {public static void main(String[] args) {//1.通过AnnotationConfigApplicationContext创建spring容器,参数为@Import标注的类AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config5.class);Map<String, IService>  serviceMap = context.getBeansOfType(IService.class);serviceMap.forEach((beanName, bean) -> {System.out.println(String.format("%s->%s", beanName, bean));});}
}

运行输出:

service1->com.yuan11.annotation.Service1@58c1c010

可以看出容器中只有一个IService类型的bean。
可以将@Bean标注的2个方法上面的@Conditional去掉,再运行会输出:

service1->com.yuan11.annotation.Service1@3d36e4cd
service2->com.yuan11.annotation.Service2@6a472554

4. 根据环境选择配置类

平常我们做项目的时候,有开发环境、测试环境、线上环境,每个环境中有些信息是不一样的,比如数据库的配置信息,下面我们来模拟不同环境中使用不同的配置类来注册不同的bean。

自定义一个条件的注解

package com.yuan11.annotation;import org.springframework.context.annotation.Conditional;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @title: EnvConditional* @Author yuan11* @Date: 2022/6/28 0:44* @Version 1.0*/
@Conditional(EnvCondition.class) //@1
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EnvConditional {//环境(测试环境、开发环境、生产环境)enum Env { //@2TEST, DEV, PROD}//环境Env value() default Env.DEV; //@3
}

@1:注意这个注解比较特别,这个注解上面使用到了@Conditional注解,这个地方使用到了一个自定义Conditione类:EnvCondition
@2:枚举,表示环境,定义了3个环境
@3:这个参数用指定环境
上面这个注解一会我们会用在不同环境的配置类上面

下面来3个配置类
让3个配置类分别在不同环境中生效,会在这些配置类上面使用上面自定义的@EnvConditional注解来做条件限定。
每个配置类中通过@Bean来定义一个名称为name的bean,一会通过输出这个bean来判断哪个配置类生效了。

下面来看3个配置类的代码

@Configuration
@EnvConditional(EnvConditional.Env.TEST)//@1
public class TestBeanConfig {@Beanpublic String name() {return "我是测试环境!";}
}

@1指定的测试环境

@Configuration
@EnvConditional(EnvConditional.Env.DEV)//@1
public class DevBeanConfig {@Beanpublic String name() {return "我是开发环境!";}
}

@1指定的开发环境

@Configuration
@EnvConditional(EnvConditional.Env.PROD)//@1
public class ProdBeanConfig {@Beanpublic String name() {return "我是生产环境!";}
}

@1指定的生产环境

条件类:EnvCondition
条件类会解析配置类上面@EnvConditional注解,得到环境信息。
然后和目前的环境对比,决定返回true还是false

public class EnvCondition implements Condition {@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {//当前需要使用的环境EnvConditional.Env curEnv = EnvConditional.Env.DEV; //@1//获取使用条件的类上的EnvCondition注解中对应的环境EnvConditional.Env env = (EnvConditional.Env) metadata.getAllAnnotationAttributes(EnvConditional.class.getName()).get("value").get(0);return env.equals(curEnv);}
}

@1:这个用来指定当前使用的环境,此处假定当前使用的是开发环境,这个我们以后可以任意发挥,比如将这些放到配置文件中,此处方便演示效果。

@Configuration
@Import({TestBeanConfig.class, DevBeanConfig.class, ProdBeanConfig.class})
public class Config6 {}

总配置类,导入环境配置

-Test

public class Test {public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config6.class);System.out.println(context.getBean("name"));}
}

运行输出

我是开发环境!

可以看到开发环境生效了。

修改一下EnvCondition的代码,切换到生产环境:

EnvConditional.Env curEnv = EnvConditional.Env.PROD;
再次运行test方法输出:

我是生产环境!

生产环境配置类生效了

5. Condition指定优先级

@Condtional中value指定多个Condtion的时候,默认情况下会按顺序执行

指定Condition的顺序
自定义的Condition可以实现PriorityOrdered接口或者继承Ordered接口,或者使用@Order注解,通过这些来指定这些Condition的优先级。

排序规则:先按PriorityOrdered排序,然后按照order的值进行排序;也就是:PriorityOrdered asc,order值 asc

下面这几个都可以指定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 {@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {System.out.println(this.getClass().getName());return true;}
}
class Condition2 implements Condition, Ordered { //@2@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {System.out.println(this.getClass().getName());return true;}@Overridepublic int getOrder() { //@3return 0;}
}
class Condition3 implements Condition, PriorityOrdered { //@4@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {System.out.println(this.getClass().getName());return true;}@Overridepublic 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升序,最后可以顺序是:

Condtion3->Condtion2->Condtion1

6. ConfigurationCondition使用

ConfigurationCondition使用的比较少,很多地方对这个基本上也不会去介绍,Condition接口基本上可以满足99%的需求了,但是springboot中却大量用到了ConfigurationCondition这个接口。

判断bean存不存在的问题,通常会使用ConfigurationCondition这个接口,阶段为:REGISTER_BEAN,这样可以确保条件判断是在bean注册阶段执行的。

对springboot比较熟悉的,它里面有很多@Conditionxxx这样的注解,可以去看一下这些注解,很多都实现了ConfigurationCondition接口。

Spring中这块的源码
@Conditional注解是被下面这个类处理的

org.springframework.context.annotation.ConfigurationClassPostProcessor

总结

  1. @Conditional注解可以标注在spring需要处理的对象上(配置类、@Bean方法),相当于加了个条件判断,通过判断的结果,让spring觉得是否要继续处理被这个注解标注的对象
  2. spring处理配置类大致有2个过程:解析配置类、注册bean,这两个过程中都可以使用@Conditional来进行控制spring是否需要处理这个过程
  3. Condition默认会对2个过程都有效
  4. ConfigurationCondition控制得更细一些,可以控制到具体那个阶段使用条件判断

《深入浅出Spring》Spring注解@Conditional相关推荐

  1. Spring Boot 条件注解@Conditional介绍

    一.概述 Spring Boot 是自以为是的,当 Spring Boot 在classpath中找到相关依赖项时,它会为模块提供默认(自动)配置. 举例,Spring Boot 提供了: 如 cla ...

  2. Java 必须掌握的 20+ 种 Spring 常用注解

    阅读文本大概需要 9 分钟. 作者:Java填坑之路 注解本身没有功能的,就和 xml 一样.注解和 xml 都是一种元数据,元数据即解释数据的数据,这就是所谓配置. 本文主要罗列 Spring|Sp ...

  3. Spring Boot注解

    文章目录 简介 @SpringBootApplication @EnableAutoConfiguration 条件自动配置 @ConditionalOnClass 和 @ConditionalOnM ...

  4. Java 必须掌握的 12 种 Spring 常用注解

    转载自  Java 必须掌握的 12 种 Spring 常用注解 1.声明bean的注解 @Component 组件,没有明确的角色 @Service 在业务逻辑层使用(service层) @Repo ...

  5. spring条件注解有哪些_Spring4有条件

    spring条件注解有哪些 Spring 4引入了一个称为Conditional的新功能,该功能针对于生成bean的Spring组件,并注视这些bean的生成,实质上,它提供了一种条件生成bean的方 ...

  6. 6.dubbo常用的xml配置有哪些_【面试篇】必须掌握的Spring 常用注解

    阅读文本大概需要5分钟. 注解本身没有功能的,就和 xml 一样.注解和 xml 都是一种元数据,元数据即解释数据的数据,这就是所谓配置. 本文主要罗列 Spring|Spring MVC相关注解的简 ...

  7. 超级详细的Spring Boot 注解总结

    日常编程中我相信大家肯定都用过spring,也用过spring的注解,哪怕面试的时候也经常会被问到一些spring和spring boot注解的作用和含义等,那么这篇就带大家来看看超级详细的Sprin ...

  8. 四、Spring中使用@Conditional按照条件注册Bean

    以前其实是写过@Condtional注解的笔记的,这里附上链接: Spring中的@conditional注解 但已经忘记的差不多了,所以今天再重新学习下,可以互补着学习 @Contional:按照一 ...

  9. Spring基于注解的方式二

    Spring基于注解二 上一次介绍了很多的关于spring的基本的注解,这篇文章描述一下关于Spring注解的基本的原理,从简单的例子入手 @Configuration @Import({Color. ...

最新文章

  1. Hbase(5)——python用happybase操作Hbase
  2. .Android开发在Eclipse环境中无法显示提示信息This element neither has attached
  3. 放大器的传递函数_保证放大器的稳定性什么最重要?反馈电阻一定要选对!
  4. Taro+react开发(8)--控制跳转
  5. 容器、Docker、虚拟机,别再傻傻分不清
  6. linux vim文本编辑器
  7. XCODE快捷键和功能汇总篇(不断更新)
  8. 苹果搜索机器人因代理服务器配置不当泄漏内部 IP
  9. java 短地址_URL短地址压缩算法 微博短地址原理解析(再转与Java实现) | 学步园
  10. IP地址、子网掩码、有效子网数、有效主机数
  11. 离散型特征的处理方法
  12. LaTex中让页码从正文开始编号
  13. 利用R语言对RNA-Seq进行探索分析与差异表达分析
  14. 巴比特 | 元宇宙每日必读:一文了解虚拟人的3大业务方向和7种赚钱方式
  15. 个人看过较好的电影推荐
  16. 基于 USB PD DRP的充电宝原理解析
  17. 技术不是越来越简单,而是框架是你的羁绊
  18. 函数的返回值 return(基础)
  19. 【九度】题目1375:陈博的完美主义(25分)
  20. Linux 探索之旅 | 第二部分第一课:终端 Terminal,好戏上场

热门文章

  1. Java设计模式之原型模式(浅克隆,深克隆)
  2. 金融危机的几个阶段(摘)
  3. 【JavaScript】js基础第01天笔记
  4. 二重积分(Double Integral)
  5. 搭建个人 Leanote 云笔记本
  6. 华清远见嵌入式培训---入学
  7. uniqueidentifier
  8. GJM :Unity3D 介绍
  9. 单机canal监听binlog
  10. Laravel:依赖注入