Spring Boot 中 Builder 模式与依赖注入的深度整合实践

小强酱_3463

小强酱_3463

2026-07-14

759人浏览

原创

Spring Boot 中 Builder 模式与依赖注入的深度整合实践

本文详解如何在 spring boot 测试中安全、优雅地将 builder 设计模式与 spring 依赖注入(di)结合,解决 objectmapper、entitymanager 等容器管理 bean 在手动构造对象时无法自动注入的核心矛盾,并提供可复用、符合 ioc 原则的工程化方案。

本文详解如何在 spring boot 测试中安全、优雅地将 builder 设计模式与 spring 依赖注入(di)结合,解决 objectmapper、entitymanager 等容器管理 bean 在手动构造对象时无法自动注入的核心矛盾,并提供可复用、符合 ioc 原则的工程化方案。

在 Spring Boot 集成测试中,Builder 模式常用于构建结构复杂、参数繁多的断言器(如 GetCommentPageExpectationTester),以提升测试代码的可读性与复用性。然而,当该 Builder 或其构建的目标对象需要访问 Spring 容器托管的 Bean(如 ObjectMapper、EntityManager、RestTemplate 等)时,直接通过 new Builder(...) 创建实例会导致依赖注入失效——因为 Spring 容器并未参与对象生命周期管理,@Autowired 字段将保持为 null。

这并非 Builder 模式与 DI 的本质冲突,而是使用方式未适配 Spring 的 IoC 容器语义。关键在于:Builder 本身也应作为 Spring Bean 管理,而非普通 POJO。以下是经过验证的、生产就绪的整合方案:

✅ 推荐方案:将 Builder 声明为 Prototype Bean 并通过 ObjectFactory 获取

Spring 允许将内部静态类(如 Builder)显式注册为 Bean。配合 @Scope(SCOPE_PROTOTYPE) 和 ObjectFactory,可在保持 Builder 链式调用能力的同时,确保每次获取的 Builder 实例都由容器初始化并完成依赖注入。

@SpringBootTest
@Import(GetCommentPageExpectationTester.Builder.class) // 显式导入 Builder 为 Bean
class CommentControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private String token;

    @Autowired
    private ObjectFactory<getcommentpageexpectationtester.builder> builderFactory; // 关键:按需获取新实例

    @Test
    @Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, 
         value = "classpath:/sql/GetCommentTest/before.sql")
    @Sql(executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, 
         value = "classpath:/sql/GetCommentTest/after.sql")
    void getPageOneDefaultTest() throws Exception {
        MockHttpServletResponse response = mockMvc.perform(get("/api/comments/page/1")
                        .header(HttpHeaders.AUTHORIZATION, token))
                .andExpect(status().isOk())
                .andReturn()
                .getResponse();

        // 使用 ObjectFactory 获取已注入依赖的 Builder 实例
        GetCommentPageExpectationTester.Builder builder = builderFactory.getObject();
        GetCommentPageExpectationTester tester = builder
                .setExpectedPageCount(10)
                .setExpectedPageDtoListSize(10)
                .setExpectedOwnerUsername("mickey_m")
                .build(); // build() 内部可安全使用注入的 ObjectMapper

        tester.test();
    }
}

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // 必须声明为 prototype!
public static class Builder {

    @Autowired
    private ObjectMapper objectMapper; // ✅ 容器自动注入

    private final String serializedResponseBody;

    public Builder(MockHttpServletResponse response) throws UnsupportedEncodingException {
        this.serializedResponseBody = response.getContentAsString();
    }

    public Builder setExpectedPageCount(int expectedPageCount) { /* ... */ return this; }
    public Builder setExpectedPageDtoListSize(int size) { /* ... */ return this; }
    // 其他 setter 省略...

    public GetCommentPageExpectationTester build() {
        // 将注入的 objectMapper 传递给目标对象构造器
        return new GetCommentPageExpectationTester(this, objectMapper);
    }
}

public class GetCommentPageExpectationTester implements ExpectationTester {

    private final String serializedResponseBody;
    private final int expectedPageCount;
    private final int expectedPageDtoListSize;
    private final String expectedOwnerUsername;
    private final ObjectMapper objectMapper; // ✅ 来自 Builder,非 new ObjectMapper()

    GetCommentPageExpectationTester(Builder builder, ObjectMapper objectMapper) {
        this.serializedResponseBody = builder.serializedResponseBody;
        this.expectedPageCount = builder.expectedPageCount;
        this.expectedPageDtoListSize = builder.expectedPageDtoListSize;
        this.expectedOwnerUsername = builder.expectedOwnerUsername;
        this.objectMapper = objectMapper; // ✅ 复用容器单例,保证配置一致(如 JavaTimeModule)
    }

    @Override
    public void test() throws JsonProcessingException {
        Data<page>> data = 
            objectMapper.readValue(serializedResponseBody, Data.class);
        // ... 断言逻辑
    }
}</page></getcommentpageexpectationtester.builder>

⚠️ 注意事项与最佳实践

  • 避免 @Component 直接标注内部静态类:Spring 默认不扫描嵌套类,必须通过 @Import 显式注册,否则 @Autowired 不生效。

  • Prototype 是必须的:若 Builder 为 Singleton(默认作用域),多次 builderFactory.getObject() 将返回同一实例,导致字段状态污染;SCOPE_PROTOTYPE 保障每次获取全新、干净的 Builder。

  • 不要在 Builder 中 @Autowired 目标对象的依赖:Builder 的职责是组装参数,而非执行业务逻辑。依赖应由 Builder 构造目标对象时传入,保持职责单一。

    Spring Boot Actuator Analyzer
    Spring Boot Actuator Analyzer

    分析Spring Boot Actuator端点的安全性、健康检查、指标暴露及生产配置——审计信息、健康状态和自定义端点。

    下载
  • 替代思路:使用 @TestConfiguration 定制测试专用 Bean(适用于更复杂场景):

    @SpringBootTest
    @Import(TestConfig.class)
    class CommentControllerIntegrationTest {
        // ...
    }
    
    @TestConfiguration
    static class TestConfig {
        @Bean
        @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
        GetCommentPageExpectationTester.Builder expectationTesterBuilder(ObjectMapper mapper) {
            return new GetCommentPageExpectationTester.Builder(mapper); // 手动注入
        }
    }

? 进阶建议:向声明式断言演进

正如答案中所提示,过度复杂的 ExpectationTester 可能掩盖了测试意图。更推荐结合 AssertJ 自定义断言,将 JSON 解析、字段校验等逻辑封装为可复用、类型安全的断言方法:

// 自定义断言示例
public class PageCommentAssert extends AbstractAssert<pagecommentassert data>>> {
    public PageCommentAssert(Data<page>> actual) {
        super(actual, PageCommentAssert.class);
    }

    public PageCommentAssert hasPageCount(int expected) {
        isNotNull();
        assertThat(actual.getData().getCount()).isEqualTo(expected);
        return this;
    }

    public PageCommentAssert hasOwnerUsername(String username) {
        isNotNull();
        assertThat(actual.getData().getDtos().get(0).getOwner().getUsername())
            .isEqualTo(username);
        return this;
    }
}

// 测试中直接使用
assertThat(response).hasPageCount(10).hasOwnerUsername("mickey_m");</page></pagecommentassert>

这种方式既消除了 Builder 的复杂度,又完全兼容 Spring DI(ObjectMapper 可注入到 Assert 类中),同时大幅提升测试可读性与维护性。

综上,Builder 模式与 Spring DI 完全兼容——只需将其纳入容器生命周期管理,并借助 ObjectFactory 或 @TestConfiguration 实现按需实例化。这不仅是技术可行的,更是遵循“依赖由容器提供,对象由容器创建”这一 Spring 核心哲学的正确实践。

相关文章

PHP速学视频免费教程(入门到精通)
PHP速学视频免费教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

spring boot

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

2023.06.15

11517

6

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

2023.07.05

8622

9

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

2023.07.31

7412

8

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

2023.08.01

1224

3

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

2023.08.02

988

3

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

2023.08.02

1476

5

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

2023.08.02

3029

5

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

2023.08.03

20271

3

配置java环境变量
配置java环境变量

配置Java环境变量是为了让操作系统能够识别和使用Java的相关命令和功能。本专题为大家提供配置java环境变量相关文章,帮助大家解决问题。

2023.08.03

1335

8

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Spring Boot 官方快速入门指南
Spring Boot 官方快速入门指南

共0课时 | 0人学习

Spring Boot 官方参考文档
Spring Boot 官方参考文档

共0课时 | 0人学习

尚硅谷新版SpringBoot3教程
尚硅谷新版SpringBoot3教程

共0课时 | 0人学习