Bootiful Spring Boot 3.4:Spring Framework

工程 | Josh Long | 2024 年 11 月 24 日 | ...

Spring Framework 6.2 的发布说明详细介绍了所有新功能。我不会在这里赘述,但以下是一些吸引我眼球的功能:

  • 自动装配排序中的泛型类型安全得到改进。
  • 更智能、更优化的 Spring 表达式语言表达式。
  • 在 Web 应用程序以及 WebJars 支持中更高效地处理资源。
  • 对 Spring 的 JMS 支持和 STOMP-over-WebSocket 支持进行了优化。
  • 通过新的 HTMLUnit 依赖项、Spring MVC 测试的 AssertJ 风格 MvcTester 以及大大改进的测试中的模拟 bean,提供了改进的测试支持。
  • 支持 @Fallback bean 的概念,它本质上是 @Primary bean 的镜像。
  • 后台 bean 初始化。
  • 改进了数据到构造函数的绑定。
  • 片段渲染!这部分献给 HTMX 用户!您现在可以在一次请求中渲染多个视图,或创建渲染视图的流。
  • 改进了 @ExceptionHandler 以支持内容协商。
  • 精炼了 URL 解析和处理。
  • 使用 @Reflective 和新的 @ReflectiveScan 注解,可以更轻松地反射非 Spring 管理的 bean。

我热切期待 @ReflectiveScan、片段渲染、改进的测试支持以及 @Fallback。让我们来看看其中的一些实际应用!

@Fallback bean

例如,您有两个 Foo 类型的 bean,并想将它们注入到某个地方。如果您知道其中一个 bean 比另一个更受偏好,您可以指定该 bean 是 @Primary bean,Spring 将会从两个(或三个,或任意数量)备选 bean 中选择它,只要只有一个 @Primary bean。但如何做相反的事情呢?如何告诉 Spring 仅在没有其他选择时才选择某个 bean?您可能会问,为什么会有动态数量的 bean?假设 bean 仅在配置文件激活时或通过 @Conditional 测试可用。您可以将一个 bean 指定为*备用* bean;如果 Spring 没有更好的选择——没有标记为 @Fallback 或标记为 @Primary 的 bean——那么它将选择*备用* bean。

@Fallback 算法会影响*注入*时的算法。因此,如果您有多个 Foo,但只想注入一个,您需要使用 @Primary 或新的 @Fallback。但这不会改变 ApplicationContext 中有多少 bean 可用。如果您注入所有 Foo 实例(如使用 Foo[] foosMap<String,Foo> beansByNameAndInstance),那么这将反映所有实例,包括那些被标记为 @Primary@Fallback 等的实例。

一个例子

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 定义,并将 @Profile("foo") 添加到 SimpleMessageProvider,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 贡献值。简单而优雅。

AssertJ 兼容的 MockMvc 测试和更巧妙的 bean 替换

我喜欢 Spring 的 MockMvc 类,它允许我轻松地测试——而且是用一种流畅的 DSL——给定的 Spring MVC HTTP 端点。然而,它一直有点令人沮丧的是,它感觉不像 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);
	}

}

您可以正常创建它,方法是实例化并直接传入一个控制器实例,或者使用 ApplicationContext 进行初始化。在此示例中,我们采用了后一种方法。

为了使此示例更简洁,我还使用了三个用于替换 bean 的新注解中的一个——@TestBean。顺便说一下,这些注解在 Spring Framework 中,而不再是 Spring Boot 独有的!@TestBean 告诉 Spring 您打算用您指定的另一个 bean 替换给定的 bean。它通过调用同一类中用 @TestBean 标注的字段同名的 `static` 方法来派生该实例。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有