在Spring项目中,我们希望bean的注入不是必须的,而是依赖条件的。
只有当项目中引入特定依赖库、或者只有当某个bean被创建、或者设置了某个环境变量时,才会创建这个bean。
在Spring4之前,这种条件注入的方式还不支持,在Spring4之后引入了一个新的注解 @Conditional ,这个注解作用在@Bean注解修饰的方式上。它能够通过判断指定条件是否满足来决定是否创建这样的Bean。
使用@Conditional注解需要满足一定条件:
@Conditional注解的类要实现Condition接口,它提供了一个matches()方法。只有matches()方法返回true时, 则被@Conditional注解修饰的bean就会被创建出来,否则不会创建(即matches方法返回false)。
接下来,我们对@Conditionl注解进行深入探讨。
@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接口源码。
@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接口。
这里需要分别实现dev、prod下的两个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; } }
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实例加载,这符合我们的预期。
我们注意到,@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注解,并且能够准确的加以应用。
版权声明:
原创不易,洗文可耻。除非注明,本博文章均为原创,转载请以链接形式标明本文地址。