Spring Boot原理解析之Conditional条件装配
Spring Boot可以使用条件装配来灵活地指定什么时候将哪些bean实例化并纳入容器,条件装配是spring boot自动配置机制(auto configure)的重要一环,也是理解spring boot原理的重要基础。本文以实例为引导,展示spring条件装配的常用使用场景,其间也会涉及一些spring的原理。阅读本文,要求有一些spring和spring boot的基本使用经验,最好对java config配置有一定了解。
条件装配主要以@ConditionalOnXXX系列注解和@Conditional注解两大类注解结合java config配置来实现。其中@ConditionalOnXXX相当于@Conditional的多种预置特殊场景,提供装配bean的多种特殊条件。
(一)实验环境介绍
1. 搭建一个基本maven工程,包含spring容器的基本配置即可,无需web和jdbc相关依赖。为方便起见,你也可以使用spring boot向导快速搭建一个spring boot工程并导入依赖,但是不要使用spring boot自带的启动类运行。我们会自己创建容器启动类(也是我们的测试类),用比较原始的方式创建一个spring容器,便于我们观察bean是否被spring创建。
2. 实验组件。
我们的实验用组件就三大类,包含main方法的测试类,几个java bean以及一个spring的配置类。
1)测试类。创建几个包含主函数的普通java类即可,我们在main函数中手工创建spring容器,并用getBean方法获取相应的bean并打印地址,观察该bean是否被创建。当然,如果你想使用junit单元测试类也是可以的。我们这里设置了三个测试类,结构和内容都相似,你也可以只创建一个,我们只是为了测试代码更清晰一些,因为要测试不同的条件装配。具体代码见下文详细说明。
实验组件说明
2)几个普通的java类,用于充当要纳入spring容器管理的bean,无需继承任何类和实现任何接口(pojo)
这里取名为C1,C2。。。,你也可以根据自己习惯自行取名。出于良好的编程习惯,可以显式地增加一个无参构造器,可以什么内容都不写,下面不再赘述。
public class C1 {
public C1(){
}
}
3) 一个spring 配置类
这是我们实验的重点,具体内容在下节说明。
(二)编写配置类ConditionalConfig
/**
* 条件装配测试
* 使用conditional相关注解配置的一系列bean
*/
@Configuration
public class ConditionalConfig {
/**
* 无条件声明bean
* @return
*/
@Bean
public C1 getC1(){
C1 c1 = new C1();
return c1;
}
...
该类用于完成所有实验所用bean的配置,也是个pojo,不需要继承任何类或实现任何接口,但是需要以一个叫@Configuration的注解标注。这里使用了java config的配置方式,是spring boot用注解代替XML,实现几乎零配置的一个重要机制。如果没有接触过java config的配置,可以把该配置类想象成一个spring的xml配置文件。那么在这个配置文件里,最核心的标签是什么呢,当然就是<beans>,表示spring bean的容器,里面会放置一个个的<bean>标签,用于声明要实例化和交由spring 容器管理的bean。同样,我们这里的@Configuration注解就相当于<beans>标签,这个类里会有多个方法,以@Bean的注解标注,看成一个个的
<bean>标签,同样用于声明bean。也就是说两种方式含义相同形式不同
当然这里有个问题,无论我们从容器获取bean,还是进行依赖注入,通常都是使用bean的id。用XML配置文件的方式,可以通过bean标签的id属性设置bean的id,那么使用java config的方式如果设置?其实默认就是以每一个用于创建bean的方法名作为bean的id比如这里的getC1,getC2等等,随后我们会在测试中验证。如果你确实觉得这样的名字比较奇怪,也可以在@Bean注解中增加一个自定义bean id,比如myC1(实际上是为注解的value属性赋值 类似于spring mvc的requestmapping注解),就像这样
@Bean("myC1")
public C1 getC1(){
C1 c1 = new C1();
return c1;
}
另外我们使用java config的方式在方法中实例化bean的方式通常就是直接new出来,要求有相应的构造器
也可以通过bean所在类自己提供的工厂方法或者反射等其他方式来实例化bean,有兴趣可以参阅其他资料。实例化以后,该对象就会存在于当前spring容器中。对于c1 bean,我们让它无条件创建
作为其他条件装配bean的一个辅助条件 下面介绍ConditionalOnXX系列注解
1. @ConditionalOnClass
该注解表示根据某个指定的类是否存在于classpath中来决定是否实例化一个bean。
Conditional} that only matches when the specified classes are on the classpath
注意该注解的target是type和method,也就是只能用于类型和方法上。这里我们注解在创建C2的java config方法上
@ConditionalOnClass(value = C1.class)// C1存在于classpath中才会加载C2
@Bean
public C2 getC2(){
C2 c2 = new C2();
return c2;
}
意思是要(调用该方法)创建C2,前提是C1的实例要存在于classpath中。存在则创建C2,不存在则不创建,这就以另一个bean构建了创建当前bean的条件。运行程序观察结果(注意测试代码和运行方法会在下文介绍 这里只是通过控制台输出先给大家展示每种条件的含义 现在只需要知道我们是通过spring容器的getBean(bean id)方法获取相应的bean的)
com.example.springbootconditiondemo.C1@5db250b4
com.example.springbootconditiondemo.C2@223f3642
Process finished with exit code 0
这里可以看到由于C1是无条件创建并纳入spring容器的,所以肯定是存在于classpath中的,也就是说创建C2的前提条件是满足的,所以C1,C2两个实例均创建并纳入spring容器。这也是比较简单的一种条件装配注解
这是正向的例子 我们再来看下反向的例子 也就是以一个不存在于当前classpath的类为条件
这里我们要修改@ConditionalOnClass注解的属性
不再使用value, 而是使用name属性指定一个字符串类型的全路径类名 比如
/**
* 通过@ConditionalOnXX注解,指定是否加载该bean
* @return
*/
@ConditionalOnClass(name = "C8")// C8存在于classpath中才会加载C2
@Bean
public C2 getC2(){
C2 c2 = new C2();
return c2;
}
C8是不存在的一个类 所以创建C2的条件不满足 我们再次进行测试
com.example.springbootconditiondemo.C1@184cf7cf
Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.springbootconditiondemo.C2' available
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:346)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:337)
at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1123)
at com.example.springbootconditiondemo.App.main(App.java:18)
可以看到c1依然可以正常创建 但是C2确无法创建
这就进一步体现了@ConditionalOnclass注解对创建某个bean的控制作用
2. @ConditionalOnBean
当指定的(另一个)bean存在于spring容器(上下文)中才执行该方法加载bean,比如我们把刚才加载C2的方法修改一下,将@ConditionalOnClass注解替换成@ConditionalOnBean注解。表示必须要C1 bean存在于当前spring 上下文中才会执行该方法实例化C2
@ConditionalOnBean(value = C1.class) //C1存在于spring容器中才会加载C2
@Bean
public C2 getC2(){
C2 c2 = new C2();
return c2;
}
运行测试观察结果:
17:26:26.230 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'getC4'
com.example.springbootconditiondemo.C1@45b9a632
com.example.springbootconditiondemo.C2@25d250c6
跟之前类似,C1,C2两个bean均实例化并加载。大家可能会有疑问,这个注解同@ConditionalOnClass有什么区别呢。从目前来看,只要C1类存在于classpath中,无论是用@ConditionalOnClass注解还是@ConditionalOnBean注解,getC2()方法总会执行,似乎没什么区别。好,现在让我们来做一个实验。将getC1()方法上的@Bean注解注释掉,同时注释掉测试程序中获取C1的getBean方法,其他地方不改,运行测试程序
//@Bean("myC1")
public C1 getC1(){
C1 c1 = new C1();
return c1;
}
会发现出现异常,无法获取C2 bean。
No qualifying bean of type 'com.example.springbootconditiondemo.C2' available
原因是什么,是因为C1 bean不存在于spring 容器中,基于@ConditionalOnBean指定的条件,必须要C1 bean存在于spring 容器中。我们前面说过,@Bean注解的含义同xml配置文件中<bean id=""/>一样,用于声明一个bean,只有这样声明了 ,spring才会将其纳入容器中。那么此时C1这个类是否存在于classpath中呢,答案当然是肯定的,没有实例在spring 容器中并不代表该类没有被JVM加载。我们将getC2()方法上的条件注解再换回@ConditionalOnClass进行测试
@ConditionalOnClass(C1.class)
//@ConditionalOnClass(name = "C8")// C1存在于classpath中才会加载C2
// @ConditionalOnBean(value = C1.class) //C1存在于spring容器中才会加载C2
//@ConditionalOnMissingBean(value = C1.class)//C1不存在于spring容器中才会加载C2
@Bean
public C2 getC2(){
C2 c2 = new C2();
return c2;
}
会发现C2能够正常从spring容器获取
com.example.springbootconditiondemo.C2@4fe767f3
也就是getC2方法执行的条件是满足的,这就是@ConditionalOnClass和@ConditionalOnBean这两个注解的区别
3. @ConditionalOnJava
这个条件注解比较简单,顾名思义,判断依据是当前jdk版本是否与注解中指定版本一致,一致则执行,否则不执行。来看例子:
/**
* 满足指定java版本时加载
* @return
*
* value属性指定jdk版本,range属性指定是高于等于该版本还是小于该版本
*/
@Bean
@ConditionalOnJava(value = JavaVersion.EIGHT,range = ConditionalOnJava.Range.EQUAL_OR_NEWER)
public C4 getC4(){
C4 c4 = new C4();
return c4;
}
@ConditionalOnJava注解主要有两个属性,一个value指定jdk版本号,另一个是range,指示需要高于等于等于指定版本还是低于指定版本,用一个叫Range的枚举表示,默认是前者。所以,如果是这种情况,可以不配置range,只配置value即可。这个注解比较好理解,就不演示测试结果了。
4. @Conditional
如果出现@ConditionalOnXX不能满足要求,通常是需要根据一些更复杂的业务逻辑判断,那么可以使用@Conditional注解自定义判断规则,来看配置的例子
/**
* 通过@Conditional注解,自己指定是否加载该bean的逻辑
* @return
*/
@Bean
@Conditional(value = MyConditional1.class)
@Deprecated
public C3 getC3(){
C3 c3 = new C3();
return c3;
}
同样注解到标注@Bean的方法上,具体的判断逻辑由自定义的MyConditional1类来实现。该类必须要实现org.springframework.context.annotation.Condition接口,并且重写其matches()方法,该方法返回boolean,用于只是条件是否满足。
public class MyConditional1 implements Condition {
/**
* @param context 应用上下文环境,可以通过该参数获取用于帮助条件判断的辅助类,比如Environment,BeanFactory,ResouceLoader等
* @param metadata 注解元数据,用于获取注解相关的信息
* @return
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
注意该方法的两个参数,这是我们获取被标注对象的上下文和相关注解元信息的重要组件,我们的判断逻辑通常围绕着这两个参数编写。
1)ConditionContext 是应用上下文环境,我们可以通过该参数获取用于帮助条件判断的辅助类,比如Environment,BeanFactory,ResouceLoader等
2)注解元数据,用于获取被注解类或方法的其他注解相关的信息
来看例子,首先来看ConditionContext的使用,这里我们通过该参数获取spring的Environment接口,并通过该接口再获取环境相关信息,比如应用端口号,JAVA_HOME,MAVEN_HOME等信息都可以获取
//1.通过context参数获取Environment
Integer serverPort = context.getEnvironment().getProperty("server.port", Integer.TYPE);
System.out.println("当前应用端口号为:" + serverPort);
String mavenHome = context.getEnvironment().getProperty("MAVEN_HOME");
System.out.println("当前mavenhome:" + mavenHome);
测试结果:
当前应用端口号为:8888
19:08:35.027 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Found key 'MAVEN_HOME' in PropertySource 'systemEnvironment' with value of type String
当前mavenhome:G:\devSoftware\apache-maven-3.6.1
再来看另一个参数AnnotatedTypeMetadata,它可以获取被@Conditional标注的组件(一般是方法)上有哪些注解,以及这些注解的元数据(属性等)。比如我们这里测试该方法上是否有@Deprecated注解,并作为Conditional的判断条件。如果有,则返回true,否则返回false。
metadata.isAnnotated(Deprecated.class.getName())
由于在前面的配置类中,对于C3实例化方法getC3()做了@Deprecated标注,所以getBean时能获取C3,反之则不可以,请大家自行测试。
另外
ConditionContext 参数除了能获取环境信息外,还可以获取当前spring的BeanFactory(
context.getBeanFactory()
)以及classpath中的文件(
context.getResourceLoader();
)等,如果你需要使用这些信息进行Conditional判断,请自行参考源码。
(三)编写测试程序
最后,我们来展示一下如何编写测试程序。测试代码很简单,就是一个普通类加一个main函数,核心就是创建一个spring的应用上下文(也是BeanFactory),Spring Boot是帮我们自动创建了。注意,这里的应用上下文(ApplicationContext)的实现类选用AnnotationConfigApplicationContext,因为我们以注解的方式来配置,而不是使用XML文档形式。
public class App {
public static void main(String[] args) {
AnnotationConfigApplicationContext acx = new AnnotationConfigApplicationContext();
//注册一个配置类,形成spring beans容器,相当于加载xml配置文件
acx.register(ConditionalConfig.class);
//初始化一个spring 容器
acx.refresh();
//测试c1,c2两个bean之间的依赖
// System.out.println(acx.getBean(C1.class));
System.out.println(acx.getBean(C2.class));
}
}
上面的第二句就是指定我们的java config配置类,就是我们前面用的用@Configuration标注的那个ConditionalConfig,相当于加载一个XML格式的bean配置文件。其他就是初始化容器,正常从容器中getBean出你要找的实例(spring bean),跟XML配置的形式没有太大区别。我们这里使用了3个测试类只是为了更清晰,逻辑和功能基本一样,完整源码会放在附件中。
以上就是我们简单介绍的spring的Conditional条件装配机制。限于篇幅,只选择了几个常用的@ConditionalOnXX和@Conditional介绍,大家在实际工作或阅读spring boot源码时可能还会遇到其他条件装配标签,可参考本文介绍思路和测试方法自行研究。这些标签使用本身不复杂,但是对具体含义和使用场景比较抽象,建议大家多动手,从正反两方面测试,便于理解和加深印象。