Spring 之 Condition,知其然,知其所以然

408 阅读7分钟
原文链接: mp.weixin.qq.com

正文共:3535 字 38 图

预计阅读时间:9 分钟

今天我们来了解一下 Condition 这个接口的功能,这个功能是从 Spring 4.0 新加入的,根据官方文档介绍,这个接口主要是用来做条件判断的,如果返回 true,则标注了相应注解的组件将会被注册到 Spring 容器当中。否则,将会被忽略,也就是说不会被注册到容器当中。

我猜测大家见到最多的这个功能应该是在 Spring Boot 的项目当中吧,在 Spring Boot 项目中的 autoconfigure 中,随处可见。比如:

ConditionalOnBean

ConditionalOnClass

ConditionalOnMissingBean

ConditionalOnMissingClass 等等,如果你看过内部实现,就能知道它们最终实现的接口都是 Condition。

由于微信内部不能添加外连接所以我把官方文档地址贴在此处。有兴趣的老铁可以打开看看哈。

https://docs.spring.io/spring/docs/5.0.16.RELEASE/spring-framework-reference/core.html#beans-java-conditional

可以看到这个接口里面有一个 matches 方法,接受两个参数,返回一个布尔值。和其搭配使用的是 Conditional 这个注解。

注解接受一个 Class 数组类型的值,但是Class 必须是上面 Condition 接口的子类,也就是说,该注解的值必须实现 Condition 接口,从这个地方我们可以分析得出,其实 Spring 在判断过程中,就是调用了 matches 方法,根据返回值,来确定最终的结果。看到这里我们应该大致明白是怎么回事了。

但是,不试试怎么知道呢。老规矩下图先看我的测试项目结构。

这个是没有加任何注解,我们先来看看运行结果。

接下里看看我自己写的注解吧,灰常简单。

功能很简单,大致就是实现了一个在当前 ClassLoader 下面查找给定的 Class,如果找到就返回 true,否则就返回 false。如果你还记得上面的项目结构图就能知道,我项目里面根本就没有 

com.liushangdev.spring.condition.Order

这个类,所以通过类加载器加载这个类时,自然就找不到 UserService 这个类,自然就无法注册到容器里面去了,所以当我获取这个类实例的时候就报错 了,我们看下执行结果吧。

白色部分日志,是我在捕捉到加载不到类报异常时,打印的日志,你还可以看到最熟悉的 Spring 报的错,找不到 Bean 定义。说明我们的注解已经生效了。 如果我把注解里面的值改成 User,那就可以了。

讲到这里我们的例子也跑完了,大家应该也明白怎么实现了,但是有一个问题,Spring 是在什么时候帮我们做这个事情的呢?怎么就能调用我们写的方法呢? 是不是有点好奇?接下来,就是我们的源码时刻。

首先奉上我蹩脚的 Spring Condition 流程图,我们看源码都离不开 Spring 的生命周期,Condition 的流程只是生命周期流程中的一个分支。

这个流程比较短,因为我把里面无关的流程去掉了,因为我们只关注 Condition 的流程,所以看起来只有这么几步,但是里面的代码非常复杂。重点关注我用红色标注的最后一步流程。

重点都在这个扩展的 ConfigurationClassPostProcessor,这个后置处理器是 Spring 生命周期中很重要的一个,它完成了 Bean 的扫描解析,以及对标注了Configuration 注解类的动态代理。

由于这个方法比较长,没有截取长图,分段截取了,主要担心网络不好的时候,加载不出来,见谅。

此方法主要用来解析标注了 Configuration 注解的类。从方法名就可以看出来。 首先获取了 Spring 容器中所有的 beanName 集合,主要是 Spring 内部的一些后置处理器,还有我们在程序刚开始的时候提供的配置类。

可以看到我调试的截图,除了我们自己的配置类,就只有 Spring 内部的后置处理器和一些其他的配置类。

接下来创建 BeanDefinitionHolder 集合,只是对 BeanDefinition 包装了一下,然后进行排序,如果实现了 Order 接口,会返回一个排序的数字,根据数字进行排序。 然后检测是否有自定义的 BeanNameGenerator。

创建 Configuration 类解析器,解析标注了这个注解的类。

由于程序中只提供了一个 Configuration 配置类,所以集合中只有一个元素,就是我们的配置类,然后判断 BeanDefinition 的类型,我们这里是注解类型的。

包装后再次调用内部方法。

请看第一个判断 shouldSkip,其实这里就是调用我们 matches 方法的代码,只是我们的配置类没有添加 Conditional 注解,所以肯定为 false。

if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {   return;}

接着下面会从 configurationClasses 集合中获取配置类,但是我们是第一次进入,肯定为空,所以并不会进入判断。

接着再次包装,这是一个循环,会查找是否有符合条件的父类,如果有则继续查找,直到没有为止,然后放入集合中。

查看是否有内部类,如果有则调用图 2 中的方法处理。接着处理 PropertySources 注解。

感觉过了一个世纪,才走到重点方法。我们看到,这里就是处理 ComponentScans 注解的地方,为什么是 Set 我就不用说了吧,因为里面放的是数组。 可以看到这里还有一个 shouldSkip 的判断,当然肯定是 false,和上面那个一样,Configuration 配置类,没有添加 Condition 注解。

接下来会循环 componentScans。由 componentScanParser.parse 方法解析每一个 ComponentScan。

这个方法比较长,但都是为了给 ClassPathBeanDefinitionScanner 扫描器设置属性,为了下面的 doScan 做准备。

findCandidateComponents 方法就是核心,它扫描了所有的类,并返回 BeanDefinition 集合,然后注册到容器当中。

此处判断是否有索引,主要是为了 Spring Boot 中提高扫描类的速度,加快启动,有一个依赖加上以后就可以了。

isCandidateComponent 方法就是执行 Condition 判断的方法。 获取到的 MetadataReader 对象会在初始化的时候,调用字节码技术来读取 Class 文件,获取 UserService 上面标注的注解元数据。为下面条件匹配时,判断注解是否被包含。

Spring 的封装太多了,让你怀疑人生,不要急哈,高潮马上来了。

是不是有点激动,终于看到了这行代码。 后面还有很多,哈哈。此处的 metadataReader.getAnnotationMetadata() 方法是获取 UserService 类上的注解,是不是就是我们在 UserService 类中声明的两个注解。

首先判断当前注解元数据中是否包含 Conditional 注解,查看我调试的信息,就知道,肯定包含。 然后执行下面判断,phase 为空,因为我们没有传入,接着你会看到这是一个递归调用,如果 phase 为空,则默认给一个值,然后调用自身方法。

接下来,获取 Conditional 注解中设置的值,这个值就是我们实现了 Condition 接口的类 LshCondition,因为可能设置多个,所以进行排序。

接下来就会进入我们自己实现 Condition 接口的类中,执行我们自己的条件匹配。

终于到我们自己的实现类了,由于我的项目中根本没有 Order 这个类,所以结果肯定是返回 false,而我的 value 里面设置的不是数组,所以只有一个值,循环只有一次。

接图 6,看到 if 块里面的条件,都是满足的,requirePhase 为空,我们的实现类返回了 false,但是执行取反操作,所以都满足,就返回了 true。

我们返回了 true,但是这里又加了取反,所以返回 false。

如果为 false,那么就无法为 UserService 生成 BeanDefinition。 那么返回的集合中也就没有 UserService 的 Bean 定义。

此时 Spring 容器中的 beanDefinitionNames 还是 7 个,我们来看一下。

是不是只有刚开始我们看到的 7 个 Bean,接下来执行红框中的方法,我们再来看结果。

是不是就把我们自定义实现 Condition 的类注入进去了。 各位看官,看到这里是不是必须给个赞。