领先一步
VMware 提供培训和认证,助您加速前进。
了解更多Spring Framework 6.2 发布说明提供了对所有新功能的更详细介绍。我不会在这里重复所有内容,但以下是一些引起我注意的功能
MvcTester
以及测试中大幅改进的模拟 Bean。@Fallback
Bean 的概念,这本质上是 @Primary
Bean 的镜像。我一直热切期待 @ReflectiveScan
、片段渲染、改进的测试支持以及 @Fallback
。让我们看看其中一些实际应用的例子!
@Fallback
Bean假设您有两个类型为 Foo
的 Bean,并希望将它们注入到某个地方。如果您知道您更喜欢其中一个 Bean 被注入,则可以将该 Bean 指定为 @Primary
Bean,只要只有一个 @Primary
Bean,Spring 就会从两个(或三个,或任意数量)备选项中选择它。但是,如何做相反的操作呢?您如何告诉 Spring 仅在没有其他可用的情况下选择某个 Bean?您可能会问,为什么 Bean 的数量会是动态的?假设您有一些 Bean 仅在激活 Profile 或通过 @Conditional
测试时才可用。您可以将一个 Bean 指定为备用 (fallback) Bean;如果 Spring 没有更好的选择——没有标记 @Fallback
或标记为 @Primary
的 Bean——那么它将选择该备用 (fallback) Bean。
@Fallback
算法影响的是注入时的算法。因此,如果您有多个 Foo
Bean,但只想注入其中一个,则需要使用 @Primary
或新的 @Fallback
。但这不会改变 ApplicationContext
中可用的 Bean 数量。如果您注入 Foo
的所有实例(例如使用 Foo[] foos
或 Map<String,Foo> beansByNameAndInstance
),那么这将反映所有实例,包括标记有 @Primary
、@Fallback
等的 Bean。
一个示例
package com.example.bootiful_34.framework;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Fallback;
import org.springframework.context.annotation.Primary;
@Configuration
class FallbackDemoConfiguration {
@Bean
@Fallback
DefaultNoOpMessageProvider defaultNoOpFoo() {
return new DefaultNoOpMessageProvider();
}
@Bean
SimpleMessageProvider foo() {
return new SimpleMessageProvider();
}
@Bean
@Primary
SophisticatedMessageProvider sophisticatedFoo() {
return new SophisticatedMessageProvider();
}
@Bean
ApplicationRunner fallbackDemoConfigurationRunner(MessageProvider messageProvider) {
return args -> System.out.println(messageProvider.message());
}
}
class DefaultNoOpMessageProvider implements MessageProvider {
@Override
public String message() {
return "default noop implementation";
}
}
class SimpleMessageProvider implements MessageProvider {
@Override
public String message() {
return "simple implementation";
}
}
class SophisticatedMessageProvider implements MessageProvider {
@Override
public String message() {
return "\uD83E\uDD14 + \uD83C\uDFA9";
}
}
在这个示例中,有三个类型为 MessageProvider
的 Bean,Spring 需要从中区分开来,选择其中一个注入到 ApplicationRunner
中。在这种情况下,按照代码所示,Spring 会选择 SophisticatedMessageProvider
。注释掉 SophisticatedMessageProvider
的 Bean 定义,并在 SimpleMessageProvider
上添加 @Profile("foo")
,Spring 就会选择 DefaultNoOpMessageProvider
实例。取消注释 SimpleMessageProvider
,Spring 会立即再次选择它。很不错。
测试有时需要在 Spring Environment
抽象中设置不同的属性,以改变支持测试的行为。因此,Spring 长期以来提供了一种机制——用 @DynamicPropertySource
注解的静态方法——您可以在 Spring 测试支持启动您的 Bean 配置之前向 Spring Environment
贡献属性,以便在测试期间正确配置。这里有一个示例。
package com.example.bootiful_34.testing;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import static com.example.bootiful_34.testing.Messages.ONE;
import static com.example.bootiful_34.testing.Messages.TWO;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest
@Import(PropertiesConfiguration.class)
class PropertyTest {
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("dynamic.message.one", () -> ONE);
}
@Test
void properties(@Autowired ApplicationContext ac) {
var environment = ac.getEnvironment();
assertEquals(ONE, environment.getProperty("dynamic.message.one"));
assertEquals(TWO, environment.getProperty("dynamic.message.two"));
}
}
在这个类中,我们为 dynamic.message.one
贡献值,并将其指向 Messages
中定义的一些静态变量。
package com.example.bootiful_34.testing;
class Messages {
static final String ONE = "this is a first message";
static final String TWO = "this is a second message";
}
但是 dynamic.message.two
呢?它是使用新方式定义的。
package com.example.bootiful_34.testing;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.DynamicPropertyRegistrar;
import org.springframework.test.context.DynamicPropertyRegistry;
import static com.example.bootiful_34.testing.Messages.TWO;
@TestConfiguration
class PropertiesConfiguration {
@Bean
SimplePropertyRegistrar simplePropertyRegistrar() {
return new SimplePropertyRegistrar();
}
static class SimplePropertyRegistrar implements DynamicPropertyRegistrar {
@Override
public void accept(DynamicPropertyRegistry registry) {
registry.add("dynamic.message.two", () -> TWO);
}
}
}
这不是很棒吗?任何在测试上下文中注册的实现了 DynamicPropertyRegistrar
接口的 Bean 都可以为测试上下文的 Environment
贡献值。简单而优雅。
MockMvc
测试和更巧妙的 Bean 替换我喜欢 Spring 的 MockMvc
类,它让我能够轻松地测试给定的 Spring MVC HTTP 端点——而且使用了流畅的 DSL。然而,它总是有点麻烦,因为它感觉不像 AssertJ,或者说与 AssertJ 不兼容,而且其 DSL 流畅性较差。这些测试就像 AssertJ 测试海洋中的孤岛。但现在情况变了。隆重推出 MockMvcTester
!
package com.example.bootiful_34.testing;
import com.example.bootiful_34.framework.MessageProvider;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.test.context.DynamicPropertyRegistrar;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.bean.override.convention.TestBean;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.context.WebApplicationContext;
@SpringBootTest
@SuppressWarnings("unused")
class GreetingsControllerTest {
private static final String TEST_MESSAGE = "this is a first message";
private final MockMvcTester mockMvc;
@TestBean
private MessageProvider messageProvider;
GreetingsControllerTest(@Autowired WebApplicationContext wac) {
this.mockMvc = MockMvcTester.from(wac);
}
static MessageProvider messageProvider() {
return () -> TEST_MESSAGE;
}
@Test
void message() throws Exception {
var mvcTestResult = this.mockMvc.get().uri("/hello").accept(MediaType.APPLICATION_JSON).exchange();
Assertions.assertThat(mvcTestResult.getResponse().getContentAsString()).contains(TEST_MESSAGE);
}
}
您通常可以直接实例化并传入一个 Controller 实例来创建它,或者使用 ApplicationContext
来初始化它。在这个示例中,我们采用了后一种方法。
为了让这个示例更清晰,我还使用了三个用于替换 Bean 的新注解之一——@TestBean
。顺便说一下,这些注解现在位于 Spring Framework 中,不再是 Spring Boot 独有!@TestBean
告诉 Spring 您打算用另一个符合您规范的 Bean 来替换给定的 Bean。它通过调用一个与同一类中用 @TestBean
注解的字段同名的 static
方法来派生该实例。