示例
像往常一样本文中的示例代码可以通过GitHub获取。这里是仓库的地址: github.com/piomin/spri…
对自动配置进行测试
我们以一个不寻常的方式开始——测试。Spring Boot提供了一个非常易用的机制用于测试。我们只需要在JUnit的单元测试中创建一个ApplicationContextRunner
的实例就可以轻松的操纵Spring容器中的classpath、属性文件和配置类。归功于此,我们甚至不再需要在配置类上使用@Configuration
注解就可以对其进行测试。
public class MyConfiguration {
@Bean
@ConditionalOnClass(MyBean2.class)
public MyBean1 myBean1() {
return new MyBean1();
}
}
复制代码
上述代码中的myBean1
被@ConditionalOnClass
进行注解过后,它将对类MyBean2
的产生依赖。让我们先来看一下我们demo项目中全部的类。
上面代码中的myBean1
被@ConditionalOnClass
进行注解过后将对类MyBean2
产生依赖。让我们先来看一看我们demo项目中的类。
如图所示,类MyBean2
在classpath中是存在的。因此我们需要在Spring容器中移除MyBean2
,在它被移除后在Spring容器中尝试获取MyBean1
的实例将会抛出NoSuchBeanDefinitionException
异常。我们可以使用FilteredClassLoader
类达到在classpath中移除MyBean2
的目的:
@Test(expected = NoSuchBeanDefinitionException.class)
public void testMyBean1() {
final ApplicationContextRunner contextRunner = new ApplicationContextRunner();
contextRunner.withUserConfiguration(MyConfiguration.class)
.withClassLoader(new FilteredClassLoader(MyBean2.class))
.run(context -> {
MyBean1 myBean1 = context.getBean(MyBean1.class);
Assert.assertEquals("I'm MyBean1", myBean1.me());
});
}
复制代码
@ConditionalOnProperty
@ConditionalOnProperty
是一个相当有趣的注解。它可以检测属性的值或者存在性,只有检测通过时才会注入相应的Bean到Spring容器中。让我们假设在我们的配置类中有myBean2
的定义:
@Bean
@ConditionalOnProperty("myBean2.enabled")
public MyBean2 myBean2() {
return new MyBean2();
}
复制代码
我们添加属性myBean2.enabled
到ApplicationContextRunner
中然后运行单元测试,此时结果会给你一些惊喜,MyBean2
在Spring容器中并不存在(会抛出NoSuchBeanDefinitionException
异常)。
这是为什么?在Spring Boot的文档中给出了答案,文档中说默认情况下只要属性存在它的值不是false
都能使得注解@ConditionalOnProperty
检测通过。由于我们使用代码withPropertyValues("myBean2.enabled=false")
给属性值设置成了false
,所以才会导致检测不通过:
@Test(expected = NoSuchBeanDefinitionException.class)
public void testMyBean2Negative() {
final ApplicationContextRunner contextRunner = new ApplicationContextRunner();
contextRunner
.withPropertyValues("myBean2.enabled=false")
.withUserConfiguration(MyConfiguration.class)
.run(context -> {
MyBean2 myBean2 = context.getBean(MyBean2.class);
Assert.assertEquals("I'm MyBean2", myBean2.me());
});
}
复制代码
属性值不是false
的意思是只要你不给属性值写上false
这五个字母,随便你给其它的值(包括空值)就可以使得检测通过。这里是一个检测通过的例子:
为了使Bean依赖于某个具体的属性值,我们需要使用字段havingValue
。假设我们的Bean定义如下,它依赖于属性myBean5.disabled
的值,在该属性的值为false
时myBean5
可见。
@Bean
@ConditionalOnProperty(value = "myBean5.disabled", havingValue = "false")
public MyBean5 myBean5() {
return new MyBean5();
}
复制代码
这里对应的测试与前面的很相似:
多条件
我们可以在一个Bean定义上使用不同的注解,但是不能多次使用同一个注解。不过可以在同一个注解中指定多个类、Bean和属性。同一个Bean定义上面的多个条件注解将会按照AND
的方式进行组合。下面的myBean4
依赖属性multipleBeans.enabled
、MyBean1
和MyBean2
的存在。
@Bean
@ConditionalOnProperty("multipleBeans.enabled")
@ConditionalOnBean({MyBean1.class, MyBean2.class})
public MyBean4 myBean4() {
return new MyBean4();
}
复制代码
下面的测试验证了由于MyBean2
不可用导致获取MyBean4
时抛出NoSuchBeanDefinitionException
异常的场景:
@Test(expected = NoSuchBeanDefinitionException.class)
public void testMyBean4Negative() {
final ApplicationContextRunner contextRunner = new ApplicationContextRunner();
contextRunner
.withUserConfiguration(MyConfiguration.class)
.withPropertyValues("multipleBeans.enabled")
.run(context -> {
MyBean4 myBean4 = context.getBean(MyBean4.class);
Assert.assertEquals("I'm MyBean4", myBean4.me());
});
}
复制代码
由于myBean2
依赖属性myBean2.enabled
,我们需要添加这个属性到测试中。在下面的测试中MyBean4
所依赖的条件全部被满足,所以MyBean4
在Spring容器中可用:
现在让我们考虑这样一个场景,我们希望把一个Bean定义上的多个条件按照OR
的关系进行组合。对于这个场景没有可以直接使用的注解。我们需要创建一个类,它继承了Spring的AnyNestedCondition
类。然后定义如下所示的三个条件:
public class MyBeansOrPropertyCondition extends AnyNestedCondition {
public MyBeansOrPropertyCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnBean(MyBean1.class)
static class MyBean1ExistsCondition {
}
@ConditionalOnBean(MyBean2.class)
static class MyBean2ExistsCondition {
}
@ConditionalOnProperty("multipleBeans.enabled")
static class MultipleBeansPropertyExists {
}
}
复制代码
上面定义的这个类将会作为条件使用。下面是myBean6
的定义:
@Bean
@Conditional(MyBeansOrPropertyCondition.class)
public MyBean6 myBean6() {
return new MyBean6();
}
复制代码
下面的测试中,因为我们定义三个条件的组合关系是OR
,所以只要MyBean1
存在就可以在Spring容器中获取到MyBean6
。值得一提的是Spring Boot还提供了NoneNestedCondition
类用于处理嵌套类中的条件都不通过的情形。
自动配置的加载顺序
我们的程序中也许定义了多个被@Configuration
注解过的配置类,当存在多个配置类时,我们可以很容易地通过@AutoConfigureAfter
、 @AutoConfigureBefore
和 @AutoConfigureOrder
注解控制它们的加载顺序。在使用ApplicationContextRunner
进行测试时,配置类的加载顺序会按调用withUserConfiguration
函数时所传入的参数顺序进行加载。
@Test
public void testOrder() {
final ApplicationContextRunner contextRunner = new ApplicationContextRunner();
contextRunner
.withUserConfiguration(MyConfiguration.class, MyConfigurationOverride.class)
.withPropertyValues("myBean2.enabled")
.run(context -> {
MyBean2 myBean2 = context.getBean(MyBean2.class);
Assert.assertEquals("I'm MyBean2 overridden", myBean2.me());
});
}
复制代码
让我们看一看下面这个场景,我们分别在两个配置类中声明两个同样的Bean。下面是其中一个:
public class MyBean2 {
private String me = "I'm MyBean2";
public String me() {
return me;
}
void setMe(String me) {
this.me = me;
}
}
复制代码
我们在另一个配置类中再定义一个同样的Bean来对上面的Bean进行覆盖:
public class MyConfigurationOverride {
@Bean
public MyBean2 myBean2() {
MyBean2 b = new MyBean2();
b.setMe("I'm MyBean2 overridden");
return b;
}
}
复制代码
此时会发生什么?后加载的配置类中定义的Bean将会覆盖掉之前定义的Bean,如下所示:
其它的条件注解
还有一些其它的条件注解,比如:@ConditionalOnExpression
、@ConditionalOnSingleCandidate
和 @ConditionalOnWebApplication
。更详细的信息你可以在Spring Boot的文档上找到。还有个注解是@ConditionalOnJava
,它可以让你根据Java的版本来加载Bean。下面定义的MyBean3
只有在Java的版本大于8时才会加载。
@Bean
@ConditionalOnJava(range = ConditionalOnJava.Range.EQUAL_OR_NEWER, value = JavaVersion.NINE)
public MyBean3 myBean3() {
return new MyBean3();
}
复制代码
下面的测试可以在Java8下面运行通过:
总结
在本文中我演示了一些关于Spring Boot自动配置的功能。在Spring Boot中创建自动配置是简单的或有趣的。下面是所有测试的运行结果:
进一步阅读
How Spring Auto-Configuration Works
7 Things to Know Before Getting Started With Spring Boot
Tutorial: Reactive Spring Boot, Part 5 — Auto-Configuration for Shared Beans