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配置文件。那么在这个配置文件里,最核心的标签是什么呢,当然就是,表示spring bean的容器,里面会放置一个个的标签,用于声明要实例化和交由spring 容器管理的bean。同样,我们这里的@Configuration注解就相当于标签,这个类里会有多个方法,以@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,只有这样声明了 ,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源码时可能还会遇到其他条件装配标签,可参考本文介绍思路和测试方法自行研究。这些标签使用本身不复杂,但是对具体含义和使用场景比较抽象,建议大家多动手,从正反两方面测试,便于理解和加深印象。

你可能感兴趣的:(Spring Boot原理解析之Conditional条件装配)