1. Spring Boot Condition功能与作用

@Conditional是基于条件的自动化配置注解, 由Spring 4框架推出的新特性。

在一个服务工程, 通常会存在多个配置环境, 比如常见的DEV(开发环境)、SIT(系统内部集成测试环境)、UAT(用户验收测试环境)、PRD(生产环境)等。在Spring3系列版本中通过@Profile实现,传入对应的环境标识, 系统自动加载不同环境的配置。spring4版本正式推出Condition功能, 在spring5版本, @Profile做了改进,底层是通过Condition实现, 看下Condition接口的UML结构:

可以看到两个抽象类应用实现了Condition接口, 一个是Spring Context下的ProfileCondition, 另一个就是SpringBootCondition。

SpringBootCondition下面有很多实现类,也是满足Spring
Boot的各种Condition需要, 图中只是列出了部分实现, 每个实现类下面, 都会有对应的注解来协助处理。

2. Conditional条件化系列注解介绍

Conditional的注解 Conditional的处理类 Conditional的说明
@ConditionalOnBean OnBeanCondition Spring容器中是否存在对应的实例。可以通过实例的类型、类名、注解、昵称去容器中查找(可以配置从当前容器中查找或者父容器中查找或者两者一起查找)
@ConditionalOnClass OnClassCondition 类加载器中是否存在对应的类。可以通过Class指定(value属性)或者Class的全名指定(name属性)如果是多个类或者多个类名的话,关系是”与”关系,也就是说这些类或者类名都必须同时在类加载器中存在
@ConditionalOnExpression OnExpressionCondition 判断SpEL 表达式是否成立
@ConditionalOnMissingBean OnBeanCondition Spring容器中是否缺少对应的实例。可以通过实例的类型、类名、注解、昵称去容器中查找(可以配置从当前容器中查找或者父容器中查找或者两者一起查找)
@ConditionalOnMissingClass OnClassCondition 跟ConditionalOnClass的处理逻辑一样,只是条件相反,在类加载器中不存在对应的类
@ConditionalOnProperty OnPropertyCondition 应用环境中的屬性是否存在。提供prefix、name、havingValue以及matchIfMissing属性。prefix表示属性名的前缀,name是属性名,havingValue是具体的属性值,matchIfMissing是个boolean值,如果属性不存在,这个matchIfMissing为true的话,会继续验证下去,否则属性不存在的话直接就相当于匹配不成功
@ConditionalOnResource OnResourceCondition 是否存在指定的资源文件。只有一个属性resources,是个String数组。会从类加载器中去查询对应的资源文件是否存在
@ConditionalOnSingleCandidate OnBeanCondition Spring容器中是否存在且只存在一个对应的实例。只有3个属性value、type、search。跟ConditionalOnBean中的这3种属性值意义一样
@ConditionalOnWebApplication OnWebApplicationCondition 应用程序是否是Web程序,没有提供属性,只是一个标识。会从判断Web程序特有的类是否存在,环境是否是Servlet环境,容器是否是Web容器等

SpringBootCondition下面包含的主要条件化注解说明:

  • @ConditionalOnBean: 当Spring容器存在某个Bean则触发实现。

  • @ConditionalOnMissingBean: 当Spring容器不存在某个Bean则不触发。

  • @ConditionalOnSingleCandidate: 当Spring容器中只有一个指定Bean,或者多个时是首选 Bean。

  • @ConditionalOnClass: 当环境路径下有指定的类, 则触发实现。

  • @ConditionalOnMissingClass: 当环境路径下没有指定类则不触发实现。

  • @ConditionalOnProperty: 判断属性如果存在指定的值则触发实现。

  • @ConditionalOnResource: 判断存在指定的资源则触发实现。

  • @ConditionalOnExpression: 基于 某个SpEL 表达式作判断实现。

  • @ConditionalOnJava:基于JDK的版本作判断实现。

  • @ConditionalOnJndi:基于指定的 JNDI 作判断实现。

  • @ConditionalOnNotWebApplication:判断当前项目定义如果不是 Web 应用则不触发实现。

  • @ConditionalOnWebApplication:判断当前项目定义如果是 Web 应用则触发实现。

它们内部都是基于@Conditional实现。

3. Conditional条件化注解的实现原理

上面看到, Spring Boot 有很多内置的多条件化注解, 都是基于@Conditional实现,
那么@Conditionnal又是如何实现? 它的作用范围是什么? 是如何生效的?

  • Conditional源码

    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Conditional {/*** contion条件的具体实现类, 必须实现Condition接口*/Class<? extends Condition>[] value();}

    @Target标示它的作用范围是在类或方法上。它是如何被调用生效的? 我们来写下测试类, 进行调试,
    分析调用栈。

  • 自定义Conditional

    创建com.mirson.spring.boot.research.condition.CustomerMatchCondition

    @Log4j2
    public class CustomerMatchCondition implements Condition {@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {log.info("Process in CustomerMatchCondition.matches method. ");return false;}
    }

    创建引用该Condition的配置类,

    com.mirson.spring.boot.research.startup.CusomterConditional

    @Configuration
    @Conditional(CustomerMatchCondition.class)
    @Log4j2
    public class CusomterConditional {public Object newObj() {log.info("Process in CusomterConditional.newObj method.");return new Object();}
    }

    启动调试,分析调用栈:

    可以看到, 先从第一步调用refresh调用容器初始化,再到第二步处理Bean配置定义信息, 最后调用注解的doScan扫描方法,这样就能够找到我们自定义的CustomerMatchCondition,调用Condtion定义的matches接口实现, 决定是否要执行CustomerConditional 的newObject方法。

4. Conditional核心之matches匹配接口

matchs方法是做规则校验处理, SpringBootCondition源码:

public abstract class SpringBootCondition implements Condition {private final Log logger = LogFactory.getLog(getClass());@Overridepublic final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {// 根据注解信息, 获取类或方法名称String classOrMethodName = getClassOrMethodName(metadata);try {// 获取实现类的处理匹配结果ConditionOutcome outcome = getMatchOutcome(context, metadata);// 日志打印匹配结果logOutcome(classOrMethodName, outcome);// ConditionEvaluationReport中记录处理结果信息recordEvaluation(context, classOrMethodName, outcome);return outcome.isMatch();}catch (NoClassDefFoundError ex) {throw new IllegalStateException("Could not evaluate condition on " + classOrMethodName + " due to "+ ex.getMessage() + " not " + "found. Make sure your own configuration does not rely on "+ "that class. This can also happen if you are "+ "@ComponentScanning a springframework package (e.g. if you "+ "put a @ComponentScan in the default package by mistake)", ex);}catch (RuntimeException ex) {throw new IllegalStateException("Error processing condition on " + getName(metadata), ex);}}...
}
  • 获取使用了Conditional的类或方法名称信息。
  • 根据Conditional条件规则判断, 获取返回处理结果。
  • 判断是否开启日志记录功能,打印处理结果。
  • 记录处理结果至ConditionEvaluationReport的outcomes属性中。最后返回布尔值的处理结果。它是通过 ConfigurationClassPostProcessor中的processConfigBeanDefinitions方法调用, 可以看到它是在Bean创建之前就先调用,归属Bean配置定义信息的逻辑处理,且在validate方法之前处理。调用机制要理解清楚,我们管理配置。

5. Conditional核心之条件化注解具体实现

以ConditionalOnBean为例, 进行分析, 源码:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnBean {...}

采用Conditional注解, 具体条件判断逻辑在OnBeanCondition类中实现, 源码:

@Order(Ordered.LOWEST_PRECEDENCE)
class OnBeanCondition extends FilteringSpringBootCondition implements ConfigurationCondition {/*** Bean definition attribute name for factory beans to signal their product type (if* known and it can't be deduced from the factory bean class).*/public static final String FACTORY_BEAN_OBJECT_TYPE = BeanTypeRegistry.FACTORY_BEAN_OBJECT_TYPE;@Overridepublic ConfigurationPhase getConfigurationPhase() {return ConfigurationPhase.REGISTER_BEAN;}...
}

OnBeanCondition类的作用是判断容器中有无指定的Bean实例, 如果存在, 则条件生效。

它实现了抽象类FilteringSpringBootCondition的getOutcomes方法,同时实现了SpringBootCondition的getMatchOutcome方法, 两个核心方法接口,一个是获取定义的匹配条件,一个是返回匹配的结果信息, OnBeanCondition子类去实现具体的判断逻辑, 根据定义的条件输出判断结果。

  • getOutcomes方法

    方法源码:

    @Overrideprotected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,AutoConfigurationMetadata autoConfigurationMetadata) {// 创建数组, 记录自动化配置的类信息ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length];// 遍历处理for (int i = 0; i < outcomes.length; i++) {String autoConfigurationClass = autoConfigurationClasses[i];if (autoConfigurationClass != null) {// 获取具有ConditionalOnBean注解设置的BeanSet<String> onBeanTypes = autoConfigurationMetadata.getSet(autoConfigurationClass, "ConditionalOnBean");// 记录outcomes, 条件配置信息outcomes[i] = getOutcome(onBeanTypes, ConditionalOnBean.class);if (outcomes[i] == null) {// 为空, 则降级获取ConditionalOnSingleCandidate配置信息Set<String> onSingleCandidateTypes = autoConfigurationMetadata.getSet(autoConfigurationClass,"ConditionalOnSingleCandidate");outcomes[i] = getOutcome(onSingleCandidateTypes, ConditionalOnSingleCandidate.class);}}}return outcomes;}
    

    该方法作用是扫描在META-INF的spring.factories文件中定义的配置类, 检测是否包含对应的条件标注,
    也就是是否使用了@OnBeanCondition标注,存在则会记录, 进入后续方法逻辑处理。

    可以看到, 通过outcomes数组来记录所有采用了Conditional的Autoconfiguration配置类。

    扩展分析:

    我们讲解的OnBeanCondition只是其中一个条件注解, 跟踪代码分析, 同组的还有OnClassConditional和OnWebApplicationCondition条件注解,启动处理顺序是:

    OnClassConditional->OnWebApplicationCondition->OnBeanCondition,

    spring.factories中大部份配置的Autoconfiguration都是采用OnClassConditional来作依赖类的条件判断。

  • getMatchOutcomes方法

    @Overridepublic ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {ConditionMessage matchMessage = ConditionMessage.empty();// 判断注解类型, ConditionalOnBean处理逻辑if (metadata.isAnnotated(ConditionalOnBean.class.getName())) {BeanSearchSpec spec = new BeanSearchSpec(context, metadata, ConditionalOnBean.class);MatchResult matchResult = getMatchingBeans(context, spec);if (!matchResult.isAllMatched()) {String reason = createOnBeanNoMatchReason(matchResult);return ConditionOutcome                      .noMatch(ConditionMessage.forCondition(ConditionalOnBean.class, spec).because(reason));}matchMessage = matchMessage.andCondition(ConditionalOnBean.class, spec).found("bean", "beans").items(Style.QUOTE, matchResult.getNamesOfAllMatches());}// ConditionalOnSingleCandidate注解处理逻辑if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {BeanSearchSpec spec = new SingleCandidateBeanSearchSpec(context, metadata,ConditionalOnSingleCandidate.class);MatchResult matchResult = getMatchingBeans(context, spec);if (!matchResult.isAllMatched()) {return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnSingleCandidate.class, spec).didNotFind("any beans").atAll());}else if (!hasSingleAutowireCandidate(context.getBeanFactory(), matchResult.getNamesOfAllMatches(),spec.getStrategy() == SearchStrategy.ALL)) {return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnSingleCandidate.class, spec).didNotFind("a primary bean from beans").items(Style.QUOTE, matchResult.getNamesOfAllMatches()));}matchMessage = matchMessage.andCondition(ConditionalOnSingleCandidate.class, spec).found("a primary bean from beans").items(Style.QUOTE, matchResult.getNamesOfAllMatches());}// ConditionalOnMissingBean注解处理逻辑if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {BeanSearchSpec spec = new BeanSearchSpec(context, metadata, ConditionalOnMissingBean.class);MatchResult matchResult = getMatchingBeans(context, spec);if (matchResult.isAnyMatched()) {String reason = createOnMissingBeanNoMatchReason(matchResult);return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnMissingBean.class, spec).because(reason));}matchMessage = matchMessage.andCondition(ConditionalOnMissingBean.class, spec).didNotFind("any beans").atAll();}return ConditionOutcome.match(matchMessage);}
    

    上面的getOutcomes方法记录了需要匹配处理的条目,该方法是作具体判断实现。 这里支持三种条件注解: ConditionalOnBean、ConditionalOnSingleCandidate和ConditionalOnMissingBean。实际内部逻辑都会调用getMatchingBeans方法。处理完成之后, 返回ConditionMessage对象,最后通过ConditionOutcome包装返回处理结果。

  • getMatchingBeans方法

    该方法是做具体检测是否符合条件注解所配置的信息,主要包含三种类型判断,
    一种是Bean Type 也就是class类型, 第二种是annotation标注, 最后一种是Name属性判断。

    protected final MatchResult getMatchingBeans(ConditionContext context, BeanSearchSpec beans) {ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();// 判断bean的搜寻策略, ANCESTORS为搜索所有父容器的上下文定义if (beans.getStrategy() == SearchStrategy.ANCESTORS) {BeanFactory parent = beanFactory.getParentBeanFactory();Assert.isInstanceOf(ConfigurableListableBeanFactory.class, parent, "Unable to use SearchStrategy.PARENTS");// 父容器转换beanFactory = (ConfigurableListableBeanFactory) parent;}MatchResult matchResult = new MatchResult();// 判断bean的搜寻策略, 是否为CURRENT当前上下文boolean considerHierarchy = beans.getStrategy() != SearchStrategy.CURRENT;TypeExtractor typeExtractor = beans.getTypeExtractor(context.getClassLoader());List<String> beansIgnoredByType = getNamesOfBeansIgnoredByType(beans.getIgnoredTypes(), typeExtractor,beanFactory, context, considerHierarchy);// 根据bean的类型遍历判断是否符合规则for (String type : beans.getTypes()) {// type类型的具体处理逻辑, 内部为嵌套调用Collection<String> typeMatches = getBeanNamesForType(beanFactory, type, typeExtractor,context.getClassLoader(), considerHierarchy);typeMatches.removeAll(beansIgnoredByType);if (typeMatches.isEmpty()) {matchResult.recordUnmatchedType(type);}else {matchResult.recordMatchedType(type, typeMatches);}}// 根据bean的注解遍历判断是否符合规则for (String annotation : beans.getAnnotations()) {List<String> annotationMatches = Arrays.asList(// Annotation类型的具体处理逻辑, 内部为嵌套调用getBeanNamesForAnnotation(beanFactory, annotation, context.getClassLoader(), considerHierarchy));annotationMatches.removeAll(beansIgnoredByType);if (annotationMatches.isEmpty()) {matchResult.recordUnmatchedAnnotation(annotation);}else {matchResult.recordMatchedAnnotation(annotation, annotationMatches);}}// 根据bean的名称遍历判断是否符合规则for (String beanName : beans.getNames()) {if (!beansIgnoredByType.contains(beanName) && containsBean(beanFactory, beanName, considerHierarchy)) {matchResult.recordMatchedName(beanName);}else {matchResult.recordUnmatchedName(beanName);}}return matchResult;}
    

    1) 首先会判断搜寻策略,是否需要搜寻父容器上下文, 支持三种模式,CURRENT: 当前上下文; ANCESTORS: 所有父容器的上下文定义; ALL: 就是支持以上两种搜寻策略。

    2) 其次就是根据注解的定义信息, 按三种方式进行判断, 内部按这三种, 类型、注解和名称做处理,如果是父级搜索,会采用递归调用, 检测是否存在, 进行匹配判断。方法调用层级:

    getBeanNamesForType(…) -》collectBeanNamesForType(…)

    getBeanNamesForAnnotation(…) -》collectBeanNamesForAnnotation(…)

以上就是以ConditionalOnBean为例, 对ConditionOnXXX的实现原理做了剖析, SpringBootCondition的其他实现类还有很多, 本章只抽取代表性常见的条件注解作分析,大家有兴趣可再研究其他条件注解的实现机制, 这里就不一一例举。

6. 总结

基于Conditional条件的自动化配置, 从SpringBootCondition实现原理到OnBeanCondition、AutoConfigurationImportFilter的剖析, 综合可以看出Spring Boot对于条件化注解的实现, 无论从层次结构, 还是内部逻辑处理的关联性, 都比较清晰明了,值得借鉴的是它的良好的扩展性设计,比如策略模式, 模板模式等,抽象类的合理运用设计, 没有出现接口泛滥, 强耦合性等问题, 也便于Spring Boot后续版本的功能扩展。

【Spring Boot 源码研究 】- 自动化装配条件化配置Conditional剖析相关推荐

  1. 【Spring Boot 源码研究 】- 自动化装配机制核心注解剖析

    1. 自动化装配介绍 Spring Boot针对mvc做了大量封装,简化开发者的使用,内部是如何管理资源配置,Bean配置,环境变量配置以及启动配置等? 实质是SpringBoot做了大量的注解封装, ...

  2. spring boot 源码解析15-spring mvc零配置

    前言 spring boot 是基于spring 4 的基础上的一个框架,spring 4 有一个新特效–>基于java config 实现零配置.而在企业的实际工作中,spring 都是和sp ...

  3. 【Spring Boot 源码研究 】- 自动化装配条件化配置AutoConfigurationImportFilter剖析

    1. AutoConfigurationImportFilter的作用 1.1 之前讲解了SpringBoot的Conditional的自动化条件配置,我们分析了内部是如何具体实现,在整个实现当中, ...

  4. spring boot 源码解析23-actuate使用及EndPoint解析

    前言 spring boot 中有个很诱人的组件–actuator,可以对spring boot应用做监控,只需在pom文件中加入如下配置即可: <dependency><group ...

  5. 【赠书福利】掘金爆火小册同名《Spring Boot源码解读与原理剖析》正式出书了!...

    关注我们丨文末赠书 承载着作者的厚望,掘金爆火小册同名读物<Spring Boot源码解读与原理剖析>正式出书! 本书前身是掘金社区销量TOP的小册--<Spring Boot源码解 ...

  6. 实战:Spring Boot源码解读与原理分析

    承载着作者的厚望,掘金爆火小册同名读物<Spring Boot源码解读与原理剖析>正式出书! 本书前身是掘金社区销量TOP的小册--<Spring Boot源码解读与原理剖析> ...

  7. Spring Boot源码简析 @EnableTransactionManagement

    相关阅读 Spring Boot源码简析 事务管理 Spring Boot源码简析 @EnableAspectJAutoProxy Spring Boot源码简析 @EnableAsync Sprin ...

  8. 【细读Spring Boot源码】重中之重refresh()

    前言 版本:spring-boot-2.7.3 | spring-context-5.3.22 在Spring Boot启动过程中[细读Spring Boot源码]启动步骤 主流程详情7中applic ...

  9. 精尽Spring Boot源码分析 - 内嵌Tomcat容器的实现

    概述 我们知道 Spring Boot 能够创建独立的 Spring 应用,内部嵌入 Tomcat 容器(Jetty.Undertow),让我们的 jar 无需放入 Servlet 容器就能直接运行. ...

最新文章

  1. 使用代理软件之后其他软件不能联网的解决方法
  2. (笔记)Mysql命令select from:查询表中的数据(记录)
  3. 无脑博士的试管们java_计蒜客 无脑博士和他的试管们
  4. SQL2012数据库加密方法
  5. CVPR 2021 | 基于跨任务场景结构知识迁移的单张深度图像超分辨率方法
  6. Java ObjectStreamField getOffset()方法与示例
  7. 使用C#和Excel进行报表开发(四)-复制单元格 【转】
  8. 一步步编写操作系统 34 内核利用bios中断获取物理内存大小
  9. 单片机c语言怎样添加自定义头文件,单片机C语言编程与或|头文件常见问题
  10. Python高级——多进程(multiprocessing)
  11. 加载resnet18的代码
  12. 第九章 Servlet工作原理解析
  13. mysql中递增列_如何使用JDBC在MySQL中创建带有自动递增列的表?
  14. windows下搭建ffmpeg环境
  15. 【leetcode】排序题(python)
  16. 【复习篇】高等代数第五版重难知识点整理(1)
  17. 北京政协:电子垃圾回收是亟待破解的难题
  18. Linux 安装python 3.8(Linux 的版本为 Centos 7)
  19. ad电阻原理图_arduino传感器专辑之光敏电阻模块
  20. 某云安全扫描对SQL Server潜入的观察日记

热门文章

  1. FLO 文件处理方式
  2. FlyAI平台竞赛入门记录
  3. easyui的input标签设置禁止输入
  4. linux下ftp管控,proftpd使用
  5. OpenGL glOrtho(left, right, bottom, top, near, far)
  6. 手机的这些隐藏功能,你知道几个?
  7. 不孝子嗣,妄称富甲一方
  8. hive修改表备注和表字段
  9. 剥离PC,惠普押注云计算胜算几何?
  10. return 开发时新的