跟我学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注解,并且能够准确的加以应用。
版权声明:
原创不易,洗文可耻。除非注明,本博文章均为原创,转载请以链接形式标明本文地址。