转载

跟我学Spring之@Conditional注解

在Spring项目中,我们希望bean的注入不是必须的,而是依赖条件的。

只有当项目中引入特定依赖库、或者只有当某个bean被创建、或者设置了某个环境变量时,才会创建这个bean。

在Spring4之前,这种条件注入的方式还不支持,在Spring4之后引入了一个新的注解 @Conditional ,这个注解作用在@Bean注解修饰的方式上。它能够通过判断指定条件是否满足来决定是否创建这样的Bean。

使用@Conditional注解需要满足一定条件:

@Conditional注解的类要实现Condition接口,它提供了一个matches()方法。只有matches()方法返回true时, 则被@Conditional注解修饰的bean就会被创建出来,否则不会创建(即matches方法返回false)。

接下来,我们对@Conditionl注解进行深入探讨。

@Conditional分析

@Conditional是Spring4提供的新注解,源码如下:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {

    /**
    * All {@link Condition Conditions} that must {@linkplain Condition#matches match}
    * in order for the component to be registered.
    */
    Class<? extends Condition>[] value();

}

可以看出,注解被用于标注类和方法,在运行时生效。

通过value()方法可以看出,它要求传入一个Class数组,并且需要继承Condition接口。

我们接着看下Condition接口源码。

Condition接口

@FunctionalInterface
public interface Condition {
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

业务类需要实现matches方法,返回true则对@Conditional标注的bean进行注入,否则不注入。这就是所谓的条件注入。

这里注意,matches() 方法参数 ConditionContext 为 Condition设计的接口类,调用者能够从中获取到Spring容器的以下信息:

//获取bean定义的注册类
BeanDefinitionRegistry getRegistry();

// 获取ioc使用的beanFactory
@Nullable
ConfigurableListableBeanFactory getBeanFactory();

//获取当前环境信息
Environment getEnvironment();

//获取当前使用的资源加载器
ResourceLoader getResourceLoader();

//获取类加载器
@Nullable
ClassLoader getClassLoader();

我们写一个demo来对@Conditional进行更为直观的展示。

样例展示

首先定一个Bean,作为条件注入的目标对象。当注解生效则注入该bean,否则不予注入。

public class Computer {

    public Computer(String name, Double price) {
        this.name = name;
        this.price = price;
    }

    private String name;
    private Double price;

    ...省略getter setter...
}

我们定义一个电脑pojo类。

接着创建一个BeanConfig类,注入两个Computer实例,作为测试的基准。

@Configuration
public class BeanConfig {

    @Bean(name = "msi")
    public Computer computer1(){
        return new Computer("MSI",7000.00);
    }

    @Bean(name = "dell")
    public Computer computer2(){
        return new Computer("dell",5000.00);
    }
}

这里我们创建了两个Computer的实例(微星、戴尔),并为其设置名称与价格。

测试一下是否成功注入了bean。

public static void main(String[] args) {
    ConfigurableApplicationContext applicationContext = 
        SpringApplication.run(DemoSnowalkerApplication.class, args);
    Computer msi = (Computer) applicationContext.getBeanFactory().getBean("msi");
    Computer dell = (Computer) applicationContext.getBeanFactory().getBean("dell");
    System.out.println(msi);
    System.out.println(dell);
}

demo工程使用spring2.2.1进行构建,我们尝试在main方法中通过bName的方式获取两个注入的bean。运行结果如下:

Computer{name='MSI', price=7000.0}
Computer{name='dell', price=5000.0}

可以看到到目前为止bean是成功注入的,这种方式为静态注入。

接着我们就编写代码实现条件注入。

首先我们定义一个场景,在不同的环境下,注入不同的Computer实例,如:dev环境下注入msi(微星),prod下注入dell(戴尔),该如何实现呢?

我们的思路是根据环境变量中设置的env参数的不同,选择不同的bean进行注入,即:

env=dev,  注入msi实例
env=prod,注入dell实例

这里就需要请@Conditional一显身手了。首先我们需要实现Condition接口。

实现Condition接口

这里需要分别实现dev、prod下的两个condition实现类。

DevCondition(开发环境Condition实现)

public class DevCondition implements Condition {

    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        //获取ioc使用的beanFactory
        ConfigurableListableBeanFactory beanFactory = conditionContext.getBeanFactory();
        //获取类加载器
        ClassLoader classLoader = conditionContext.getClassLoader();
        //获取当前环境信息
        Environment environment = conditionContext.getEnvironment();
        //获取bean定义的注册类
        BeanDefinitionRegistry registry = conditionContext.getRegistry();

        // 获取环境变量
        String env = environment.getProperty("env");
        if ("dev".equalsIgnoreCase(env)) {
            return true;
        }
        return false;
    }
}

ProdCondition(生产环境Condition实现)

public class ProdCondition implements Condition {

    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {

        Environment environment = conditionContext.getEnvironment();
        String env = environment.getProperty("env");
        if ("prod".equalsIgnoreCase(env)) {
            return true;
        }
        return false;
    }
}

我们在上文中已经对matches方法的两个参数的含义进行了解释。这里需要注意的是,matches方法参数中的conditionContext提供了多个方法,方便获取Bean的各种信息。

这些方法也是SpringBoot中派生注解@ConditonalOnXX的基础。

我们接下来就使用这两个Condition的实现类对上面的例子进行修改。

修改BeanConfig,为msi标注DevCondition,为dell标注ProdCondition。并为启动类配置env环境变量,笔者使用的是IDEA开发环境,因此在Run->Edit runconfigurations中编辑环境变量即可。

首先设置env=dev,修改启动类测试代码如下:

public static void main(String[] args) {
    ConfigurableApplicationContext applicationContext = 
        SpringApplication.run(DemoSnowalkerApplication.class, args);
    Map<String, Computer> computers = applicationContext.getBeansOfType(Computer.class);
    System.out.println(computers);
}

我们尝试加载Computer所有的实例,并进行打印。运行结果如下:

{msi=Computer{name='MSI', price=7000.0}}

只有msi实例加载,这符合我们的预期。

注入Condition实例的数组

我们注意到,@Conditional注解传入的是一个继承了Condition接口的Class数组。也就是说,我们完全可以在@Conditional注解的values中设置一个数组,传入多个Condition实现类,确保在所有的条件都满足才进行bean注入。

编写一个新的Condition实现类,matches方法返回true:

public class DefaultCondition implements Condition {
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        return true;
    }
}

修改BeanConfig类,msi这个bean的@Conditional注解中增加DefaultCondition。

@Bean(name = "msi")
@Conditional({DevCondition.class, DefaultCondition.class})
public Computer computer1(){
    return new Computer("MSI",7000.00);
}

再次运行测试方法,返回如下:

{msi=Computer{name='MSI', price=7000.0}}

可以看到,当多个condition返回均为true时,bean被注入了。我们修改DefaultCondition的matches方法返回false,再次运行测试方法,返回结果:

{}

可以看到,当有一个Condition返回false,则bean就不会被注入。这有点像逻辑运算下的“逻辑与”。这种方式支持我们在复杂条件下对bean进行注入的要求。

ps: @Conditional注解在方法上,只能注入一个实例;如果注解在类上,则当前类下的所有bean实例都能够被注入。这里就不进行测试了,感兴趣的同学可以自行尝试。

小结

本文我们主要了解了@Conditional注解的原理及其使用方法,并且知道了该注解是Spring Boot条件注入的基础。

在后续开发中如果遇到需要根据某个条件来决定Bean注入的场景,我们首先就应该想到Spring为我们提供的@Conditional注解,并且能够准确的加以应用。

版权声明:

原创不易,洗文可耻。除非注明,本博文章均为原创,转载请以链接形式标明本文地址。

原文  http://wuwenliang.net/2020/01/13/跟我学Spring之-Conditional注解/
正文到此结束
Loading...