问题说明
条件加载是springboot自动配置的刚需,其中有两个条件@ConditionalOnClass和@ConditionalOnMissingClass非常特别,它是基于class类是否存在判断的。场景的使用场景如下:
import A;
import B;
@Configuration
class C{
@Bean
@ConditionalOnClass(A.class)
A getBeanA() {
return new A();
}
@Bean
@ConditionalOnMissingClass("A")
B getBeanB() {
return new B();
}
}
在我们日常开发过程中,如果class A不存在,编译器会提示编译错误。在java运行过程中,当我们遇到class类不存在的时候会报ClassNotFoundException。上面的代码看起来无法成功初始化我们想要的类。但实际情况是spring通过这个机制正确的选择了具体的实现类,这是为什么呢?
JVM ClassLoader理论回顾
java代码的生命周期
类生命周期
何时开始类的初始化
Java虚拟机规范中并没有进行强制约束什么情况下需要开始类加载过程。但是对于初始化阶段,虚拟机规范则是严格规定了仅在类被“主动引用”时才会初始化。
主动引用
- 创建类的实例
- 访问类的静态变量(除常量【被final修辞的静态变量】)。
- 访问类的静态方法
- 反射如(Class.forName(“my.xyz.Test”))
- 当初始化一个类时,发现其父类还未初始化,则先出发父类的初始化
- 虚拟机启动时,定义了main()方法的那个类先初始化
被动引用
- 子类调用父类的静态变量,子类不会被初始化。只有父类被初始化。对于静态字段,只有直接定义这个字段的类才会被初始化.
- 通过数组定义来引用类,不会触发类的初始化
- 访问类的常量,不会初始化类
关于java文件头的import
import的存在纯粹是为了方便写代码,在编译后的class文件中没有import区。在Java源码编译器进行编译时,每个名字都会经过解析(resolution)找到其全名(canonical form)。
ClassNotFoundException
当程序试图使用Class.forname()、Classloader#findSystemClass()、Classloader#loadClass()方法通过字符串名的形式加载此类时,会抛出ClassNotFoundException。
问题分析
从以上理论分析我们可以看出,我们在加载class C时,如果触发没有new A()就不会初始化class A,就不会有ClassNotFoundException。那问题又来了:如果class A不存在,springboot是如何选择执行getBeanA()方法还是getBeanB()方法呢?
springboot Bean加载机制和选择机制
springboot Bean加载分为Bean扫描注册和Bean初始化。
Bean扫描
springboot 通过ConfigurationClassBeanDefinitionReader读取@Configuration注解标识的类。在扫描候选资源时,spring并没有通过Classloader#loadClass()来加载class文件,而是通过Classloader.getResource()获得class二进制文件,通过ClassReader对二进制文件进行ASM语法解析,从而得到候选类和注解的元数据。也就是说在spring扫描需要加载的Bean时,所有候选类都没有加载初始化。
Bean加载判断
当spring获得候选bean的元信息时,需要判断这个bean是否真的需要被加载。这个就是spring的ConditionEvaluator体系。
比如在AnnotatedBeanDefinitionReader#doRegisterBean()中有判断:
AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(annotatedClass);
if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
return;
}
在获得ConditionalOnClass注解的元数据时还有个特殊处理,我们声明@ConditionalOnClass(A.class)时,使用的是Class类。为了后续操作时不引起class加载,AnnotatedElementUtils#getMergedAnnotationAttributes(element,annotationName,classValuesAsString,nestedAnnotationsAsMap)将class类转换成了class的名称字符串。
在最终判断class是否存在时,jvm其实还是抛出了ClassNotFoundException,只是异常被吞没了。(~ ̄▽ ̄)~
//OnClassCondition$MatchType.isPresent(String, ClassLoader) line: 219
private static boolean isPresent(String className, ClassLoader classLoader) {
//省略若干行
try {
forName(className, classLoader);
return true;
}
catch (Throwable ex) {
return false;//我就在这里,只是你看不到 O(∩_∩)O
}
}
所以关于这个问题的正确说法应该是,class类不存在时spring条件加载能够正常执行,而不是不报异常。
至此,当Class A不存在时,Bean B被注册到spring容器中,等待初始化。
Bean初始化
在初始化Bean时,AbstractAutowireCapableBeanFactory#createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)直接通过MethodProxy.invokeSuper()反射调用了C#getBeanB()。C#getBeanA()将永远不会被调用,默默地在角落里死去。。。
JVM类生命周期概述:加载时机与加载过程: https://blog.csdn.net/justloveyou_/article/details/72466105