写在前面

当bean是单实例,并且没有设置懒加载时,Spring容器启动时,就会实例化bean,并将bean注册到IOC容器中,以后每次从IOC容器中获取bean时,直接返回IOC容器中的bean,而不用再创建新的bean了。

若bean是单实例,并且使用@Lazy注解设置了懒加载,则Spring容器启动时,不会立即实例化bean,自然就不会将bean注册到IOC容器中了,只有第一次获取bean的时候,才会实例化bean,并且将bean注册到IOC容器中。

若bean是多实例,则Spring容器启动时,不会实例化bean,也不会将bean注册到IOC容器中,只是在以后每次从IOC容器中获取bean的时候,都会创建一个新的bean返回。

其实,Spring支持按照条件向IOC容器中注册bean,满足条件的bean就会被注册到IOC容器中,不满足条件的bean就不会被注册到IOC容器中。接下来,我们就一起来探讨一下Spring中是如何实现按照条件向IOC容器中注册bean的。

@Conditional注解概述

@Conditional注解可以按照一定的条件进行判断,满足条件向容器中注册bean,不满足条件就不向容器中注册bean。

@Conditional注解是由Spring Framework提供的一个注解,它位于 org.springframework.context.annotation包内,定义如下。

从@Conditional注解的源码来看,@Conditional注解不仅可以添加到类上,也可以添加到方法上。在@Conditional注解中,还存在着一个Condition类型或者其子类型的Class对象数组,Condition是个啥呢?我们点进去看一下。

可以看到,它是一个接口。所以,我们使用@Conditional注解时,需要写一个类来实现Spring提供的Condition接口,它会匹配@Conditional所符合的方法(这句话怎么说的那么不明白啊!),然后我们就可以使用我们在@Conditional注解中定义的类来检查了。

我们可以在哪些场合使用@Conditional注解呢?@Conditional注解的使用场景如下图所示。

向Spring容器注册bean

不带条件注册bean

我们在MainConfig2配置类中新增person01()方法和person02()方法,并为这两个方法添加@Bean注解,如下所示。

package com.meimeixia.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;import com.meimeixia.bean.Person;@Configuration
public class MainConfig2 {@Lazy@Bean("person")public Person person() {System.out.println("给容器中添加咱们这个Person对象...");return new Person("美美侠", 25);}@Bean("bill")public Person person01() {return new Person("Bill Gates", 62);}@Bean("linus")public Person person02() {return new Person("linus", 48);}}

那么,这两个bean默认是否会被注册到Spring容器中去呢?我们新建一个测试用例来测试一下,即在IOCTest类中新建一个test06()方法,如下所示。

@Test
public void test06() {AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig2.class);// 我们现在就来看一下IOC容器中Person这种类型的bean都有哪些String[] namesForType = applicationContext.getBeanNamesForType(Person.class);for (String name : namesForType) {System.out.println(name);}}

我们运行以上test06()方法,发现输出的结果信息如下所示。

从输出结果中可以看出,同时输出了bill和linus。说明默认情况下,Spring容器会将单实例并且非懒加载的bean注册到IOC容器中。

接下来,我们再输出bean的名称和bean实例对象信息,此时我们只须在test06()方法中添加如下的代码片段即可。

@Test
public void test06() {AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig2.class);// 我们现在就来看一下IOC容器中Person这种类型的bean都有哪些String[] namesForType = applicationContext.getBeanNamesForType(Person.class);for (String name : namesForType) {System.out.println(name);}Map<String, Person> persons = applicationContext.getBeansOfType(Person.class); // 找到这个Person类型的所有beanSystem.out.println(persons);
}

再次运行以上test06()方法,输出的结果如下所示。

可以看到,输出了注册到容器中的bean实例对象的详细信息。

注意:这里测试时,我将Person类进行了相应的调整,将toString()方法的注释放开了,如下所示。

package com.meimeixia.bean;public class Person {private String name;private Integer age;public String getName() {return name;}public void setName(String name) {this.name = name;}public Integer getAge() {return age;}public void setAge(Integer age) {this.age = age;}public Person(String name, Integer age) {super();this.name = name;this.age = age;}public Person() {super();// TODO Auto-generated constructor stub}@Overridepublic String toString() {return "Person [name=" + name + ", age=" + age + "]";}}

带条件注册bean

现在,我们就要提出一个新的需求了,比如,如果当前操作系统是Windows操作系统,那么就向Spring容器中注册名称为bill的Person对象;如果当前操作系统是Linux操作系统,那么就向Spring容器中注册名称为linus的Person对象。要想实现这个需求,我们就得要使用@Conditional注解了。

这里,有小伙伴可能会问,如何获取操作系统的类型呢?别急,这个问题很简单,我们继续向下看。

使用Spring中的AnnotationConfigApplicationContext类就能够获取到当前操作系统的类型,如下所示。

AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig2.class);
ConfigurableEnvironment environment = applicationContext.getEnvironment(); // 拿到IOC运行环境
// 动态获取坏境变量的值,例如操作系统的名字
String property = environment.getProperty("os.name"); // 获取操作系统的名字,例如Windows 10
System.out.println(property);

我们将上述代码整合到IOCTest类中的test06()方法中,如下所示。

@Test
public void test06() {AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig2.class);// 我们现在就来看一下IOC容器中Person这种类型的bean都有哪些String[] namesForType = applicationContext.getBeanNamesForType(Person.class);ConfigurableEnvironment environment = applicationContext.getEnvironment(); // 拿到IOC运行环境// 动态获取坏境变量的值,例如操作系统的名字String property = environment.getProperty("os.name"); // 获取操作系统的名字,例如Windows 10System.out.println(property);for (String name : namesForType) {System.out.println(name);}Map<String, Person> persons = applicationContext.getBeansOfType(Person.class); // 找到这个Person类型的所有beanSystem.out.println(persons);
}

然后,我们运行以上test06()方法,会看到输出了如下图所示的结果信息。

由于我使用的操作系统是Windows 10操作系统,所以在结果信息中输出了Windows 10。

到这里,我们成功获取到了操作系统的类型,接下来就要来实现上面那个需求了。此时,我们可以借助Spring中的@Conditional注解来实现。

要想使用@Conditional注解,我们需要实现Condition接口来为@Conditional注解设置条件,所以,这里我们创建了两个实现Condition接口的类,它们分别是LinuxCondition和WindowsCondition,如下所示。

  • LinuxCondition

    package com.meimeixia.condition;import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
    import org.springframework.beans.factory.support.BeanDefinitionRegistry;
    import org.springframework.context.annotation.Condition;
    import org.springframework.context.annotation.ConditionContext;
    import org.springframework.core.env.Environment;
    import org.springframework.core.type.AnnotatedTypeMetadata;/**
    * 判断操作系统是否是Linux系统
    * @author liayun
    *
    */
    public class LinuxCondition implements Condition {/*** ConditionContext:判断条件能使用的上下文(环境)* AnnotatedTypeMetadata:当前标注了@Conditional注解的注释信息*/@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {// 判断操作系统是否是Linux系统// 1. 获取到bean的创建工厂(能获取到IOC容器使用到的BeanFactory,它就是创建对象以及进行装配的工厂)ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();// 2. 获取到类加载器ClassLoader classLoader = context.getClassLoader();// 3. 获取当前环境信息,它里面就封装了我们这个当前运行时的一些信息,包括环境变量,以及包括虚拟机的一些变量Environment environment = context.getEnvironment();// 4. 获取到bean定义的注册类BeanDefinitionRegistry registry = context.getRegistry();String property = environment.getProperty("os.name");if (property.contains("linux")) {return true;}return false;}}
    

    这里我得好好说道说道通过context的getRegistry()方法获取到的bean定义的注册对象,即BeanDefinitionRegistry对象了。它到底是个啥呢?我们可以点进去看一下它的源码,如下所示,可以看到它是一个接口。

    在上图中我对BeanDefinitionRegistry接口的源码作了一点简要的说明。知道了,Spring容器中所有的bean都可以通过BeanDefinitionRegistry对象来进行注册,因此我们可以通过它来查看Spring容器中到底注册了哪些bean。而且仔细查看一下BeanDefinitionRegistry接口中声明的各个方法,你就知道我们还可以通过BeanDefinitionRegistry对象向Spring容器中注册一个bean、移除一个bean、查询某一个bean的定义信息或者判断Spring容器中是否包含有某一个bean的定义。

    因此,我们可以在这儿做更多的判断,比如说我可以判断一下Spring容器中是不是包含有某一个bean,就像下面这样,如果Spring容器中果真包含有名称为person的bean,那么就做些什么事情,如果没包含,那么我们还可以利用BeanDefinitionRegistry对象向Spring容器中注册一个bean。

    package com.meimeixia.condition;import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
    import org.springframework.beans.factory.support.BeanDefinitionRegistry;
    import org.springframework.context.annotation.Condition;
    import org.springframework.context.annotation.ConditionContext;
    import org.springframework.core.env.Environment;
    import org.springframework.core.type.AnnotatedTypeMetadata;/**
    * 判断操作系统是否是Linux系统
    * @author liayun
    *
    */
    public class LinuxCondition implements Condition {/*** ConditionContext:判断条件能使用的上下文(环境)* AnnotatedTypeMetadata:当前标注了@Conditional注解的注释信息*/@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {// 判断操作系统是否是Linux系统// 1. 获取到bean的创建工厂(能获取到IOC容器使用到的BeanFactory,它就是创建对象以及进行装配的工厂)ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();// 2. 获取到类加载器ClassLoader classLoader = context.getClassLoader();// 3. 获取当前环境信息,它里面就封装了我们这个当前运行时的一些信息,包括环境变量,以及包括虚拟机的一些变量Environment environment = context.getEnvironment();// 4. 获取到bean定义的注册类BeanDefinitionRegistry registry = context.getRegistry();// 在这儿还可以做更多的判断,比如说我判断一下Spring容器中是不是包含有某一个bean,就像下面这样,如果Spring容器中果真包含有名称为person的bean,那么就做些什么事情...boolean definition = registry.containsBeanDefinition("person");String property = environment.getProperty("os.name");if (property.contains("linux")) {return true;}return false;}}
    
  • WindowsCondition

    package com.meimeixia.condition;import org.springframework.context.annotation.Condition;
    import org.springframework.context.annotation.ConditionContext;
    import org.springframework.core.env.Environment;
    import org.springframework.core.type.AnnotatedTypeMetadata;/**
    * 判断操作系统是否是Windows系统
    * @author liayun
    *
    */
    public class WindowsCondition implements Condition {@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {Environment environment = context.getEnvironment();String property = environment.getProperty("os.name");if (property.contains("Windows")) {return true;}return false;}}
    

然后,我们就需要在MainConfig2配置类中使用@Conditional注解添加条件了。添加该注解后的方法如下所示。

package com.meimeixia.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;import com.meimeixia.bean.Person;
import com.meimeixia.condition.LinuxCondition;
import com.meimeixia.condition.WindowsCondition;@Configuration
public class MainConfig2 {@Lazy@Bean("person")public Person person() {System.out.println("给容器中添加咱们这个Person对象...");return new Person("美美侠", 25);}@Conditional({WindowsCondition.class})@Bean("bill")public Person person01() {return new Person("Bill Gates", 62);}@Conditional({LinuxCondition.class})@Bean("linus")public Person person02() {return new Person("linus", 48);}}

此时,我们再次运行IOCTest类中的test06()方法,发现输出的结果信息如下所示。

可以看到,输出结果中不再含有名称为linus的bean了,这说明程序中检测到当前操作系统为Windows 10之后,没有向Spring容器中注册名称为linus的bean。

此外,@Conditional注解也可以标注在类上,标注在类上的含义是:只有满足了当前条件,这个配置类中配置的所有bean注册才能生效,也就是对配置类中的组件进行统一设置。

package com.meimeixia.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;import com.meimeixia.bean.Person;
import com.meimeixia.condition.LinuxCondition;
import com.meimeixia.condition.WindowsCondition;// 对配置类中的组件进行统一设置
@Conditional({WindowsCondition.class}) // 满足当前条件,这个类中配置的所有bean注册才能生效
@Configuration
public class MainConfig2 {@Lazy@Bean("person")public Person person() {System.out.println("给容器中添加咱们这个Person对象...");return new Person("美美侠", 25);}@Bean("bill")public Person person01() {return new Person("Bill Gates", 62);}@Conditional({LinuxCondition.class})@Bean("linus")public Person person02() {return new Person("linus", 48);}}

此时,我们在运行IOCTest类中的test06()方法时,设置一个-Dos.name=linux参数,就像下图所示的那样,这是我们将操作系统模拟为了linux系统。

运行以上test06()方法之后,你会发现输出的结果信息如下所示。

可以看到,没有任何bean的定义信息输出,这是因为程序检测到了当前操作系统为linux,没有向Spring容器中注册任何bean的缘故导致的。

@Conditional的扩展注解

@Conditional与@Profile这俩注解的对比

Spring 3.0也有一些和@Conditional相似的注解,它们是Spring SPEL表达式和Spring Profiles注解,但是Spring 4.0之后的@Conditional注解要比@Profile注解更加高级。@Profile注解用来加载应用程序的环境,该注解仅限于根据预定义属性编写条件检查,而@Conditional注解则没有此限制。

Spring中的@Profile和@Conditional这俩注解都是用来检查If...then...else的语义。然而,Spring 4.0之后的@Conditional注解是@Profile注解的更新用法。