【译】Spring Boot自动配置背后的帽子戏法

img

示例

像往常一样本文中的示例代码可以通过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项目中的类。

img

如图所示,类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.enabledApplicationContextRunner中然后运行单元测试,此时结果会给你一些惊喜,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这五个字母,随便你给其它的值(包括空值)就可以使得检测通过。这里是一个检测通过的例子:

img

为了使Bean依赖于某个具体的属性值,我们需要使用字段havingValue。假设我们的Bean定义如下,它依赖于属性myBean5.disabled的值,在该属性的值为falsemyBean5可见。

@Bean
@ConditionalOnProperty(value = "myBean5.disabled", havingValue = "false")
public MyBean5 myBean5() {
    return new MyBean5();
}
复制代码

这里对应的测试与前面的很相似:

img

多条件

我们可以在一个Bean定义上使用不同的注解,但是不能多次使用同一个注解。不过可以在同一个注解中指定多个类、Bean和属性。同一个Bean定义上面的多个条件注解将会按照AND的方式进行组合。下面的myBean4依赖属性multipleBeans.enabledMyBean1MyBean2的存在。

@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容器中可用:

img

现在让我们考虑这样一个场景,我们希望把一个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 类用于处理嵌套类中的条件都不通过的情形。

img

自动配置的加载顺序

我们的程序中也许定义了多个被@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,如下所示:

img

其它的条件注解

还有一些其它的条件注解,比如:@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下面运行通过:

img

总结

在本文中我演示了一些关于Spring Boot自动配置的功能。在Spring Boot中创建自动配置是简单的或有趣的。下面是所有测试的运行结果:

img

进一步阅读

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

原文地址:Magic Around Spring Boot Auto-Configuration

猜你喜欢

转载自juejin.im/post/5e5512786fb9a07ca13713e4