【SpringBoot 3.x 第155节】Testcontainers 不止用于测试:本地开发容器化!
🏆本文收录于《滚雪球学SpringBoot 3.x》,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。
该专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot3教程导航帖】,你想学习的都被收集在内,快速投入学习!!两不误。
若还想学习更多,可直接订阅 《Spring Boot实战合集》,一次订阅,持续学习,后续更新内容无需重复付费,适合长期收藏与系统进阶。
演示环境说明:
- 开发工具:IDEA 2021.3
- JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
- Spring Boot版本:3.5.4(于25年7月24日发布)
- Maven版本:3.8.2 (或更高)
- Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
- 操作系统:Windows 11
全文目录:
- 1. 为什么今天要重新认识 Testcontainers?
- 2. Spring Boot 3.x 为什么非常适合本地开发容器化?
- 3. 什么叫“Testcontainers 不止用于测试”?
- 4. 本地开发容器化的核心思想
- 5. 一张图看懂整体架构
- 6. 环境准备:从 0 到 1 搭建运行基础
- 7. 第一个案例:把 MySQL 作为 Spring Bean 启动
- 8. 第二个案例:把 Redis 作为 Spring Bean 启动
- 9. 第三个案例:把 Kafka 作为 Spring Bean 启动
- 10. Spring Boot 3.x 中与 Testcontainers 集成的几种方式
- 11. 为什么把依赖服务声明成 Spring Bean 更优雅?
- 12. 多容器协作:一个应用同时连接 MySQL + Redis + Kafka
- 13. 完整实战:订单系统本地开发容器化
- 14. 代码逐段解析:每个类到底做了什么?
- 15. 配置设计:不同环境如何切换
- 16. 生命周期管理:容器什么时候启动、什么时候关闭?
- 17. 资源清理:如何避免“容器越跑越多”
- 18. 性能与体验优化:让本地开发更顺手
- 19. 常见问题排查:启动失败、端口冲突、镜像拉取慢怎么办
- 20. 与 Docker Compose、Dev Services、传统本地安装对比
- 21. 团队协作价值:为什么这不是个人技巧,而是工程能力
- 22. 最佳实践清单
- 23. 适用边界:什么场景适合,什么场景不适合
- 24. 总结:Testcontainers 作为本地开发基础设施的未来价值
- 25. 附录:完整可运行示例代码
-
- 25.1 DemoApplication.java
- 25.2 LocalDevelopmentContainersConfiguration.java
- 25.3 Order.java
- 25.4 OrderRepository.java
- 25.5 CreateOrderRequest.java
- 25.6 OrderCacheService.java
- 25.7 OrderEventProducer.java
- 25.8 OrderEventConsumer.java
- 25.9 OrderService.java
- 25.10 OrderController.java
- 25.11 StartupRunner.java
- 25.12 application.yml
- 25.13 application-local-dev.yml
- 25.14 init.sql
- 25.15 启动命令
- 25.16 测试接口示例
- 🧧 学习福利 · 限时开放 🧧
- 🫵 Who am I?
1. 为什么今天要重新认识 Testcontainers?
很多开发者第一次接触 Testcontainers,往往是在自动化测试场景中:
- 跑集成测试时临时起一个 MySQL;
- 测试 Redis 缓存逻辑时临时起一个 Redis;
- 测试消息队列流程时临时起一个 Kafka;
- 测试结束之后再自动清理容器。
这种认知当然没有错,但它只说对了一半。
真正强大的地方在于:Testcontainers 从来都不只是测试工具,它本质上是一个“用 Java 代码声明并控制容器生命周期”的基础设施能力。
换句话说,只要你的项目运行时需要依赖某些外部服务,而这些服务本身可以容器化,那么 Testcontainers 就不应该只在 test 目录里出现,它同样可以服务于:
- 本地开发环境;
- Demo 演示环境;
- 培训教学环境;
- 临时联调环境;
- CI 中的非测试启动阶段;
- 某些轻量开发工作站。
过去,我们在新电脑拉起一个项目时,最容易遇到的问题不是 Java 代码编译不过,而是环境配置不一致:
- 你本地装的是 MySQL 8.0,我装的是 5.7;
- 你 Redis 开了密码,我没开;
- 你 Kafka 用 KRaft,我还在 ZooKeeper 模式;
- 你容器映射了 3306,我本机 3306 已经被别的服务占了;
- 你 README 里写了三页安装文档,新同学照着操作依然失败。
这些问题的共同本质是:开发依赖环境不是代码的一部分,而是散落在每个人电脑上的隐式状态。
而 Testcontainers 的价值,恰恰是把这部分“隐式状态”重新拉回到代码世界里,让开发依赖基础设施也具备版本化、可追踪、可复现、可启动、可销毁的能力。
这就是本文要讲的核心主题:
Testcontainers 不止用于测试,它完全可以成为 Spring Boot 3.x 项目的本地开发基础设施引擎。
2. Spring Boot 3.x 为什么非常适合本地开发容器化?
如果把时间拨回到 Spring Boot 2.x 早期,虽然我们也能在代码中手动启动容器,但整体体验并不算丝滑。开发者通常需要手动处理:
- 容器启动时机;
- 应用配置注入;
- 容器地址和端口绑定;
- Bean 初始化顺序;
- 开发环境与测试环境之间的配置复用;
- 资源销毁与重复启动问题。
到了 Spring Boot 3.x,尤其是 3.1 及之后,官方对 Testcontainers 的支持有了明显提升。它不只是“兼容”,而是开始把 Testcontainers 当作一种正式的开发与测试集成方式来看待。
这背后有几个关键变化。
2.1 Spring Boot 3.x 的技术基础更现代
Spring Boot 3.x 建立在更现代的 Java 与 Spring Framework 基础之上,配合 Jakarta EE 9+ 命名空间迁移,使整个生态在以下方面更统一:
- 对云原生和容器化理念的接受度更高;
- 对外部化配置、自动装配和环境隔离的抽象更成熟;
- 对开发体验(Developer Experience)更重视;
- 对微服务依赖组件(数据库、缓存、消息队列)的集成更规范。
这意味着:
Spring Boot 3.x 天生就更适合和“声明式基础设施”结合。
2.2 Service Connection 让容器与应用连接更自然
Spring Boot 3.x 引入了对 @ServiceConnection 的支持,极大降低了 Testcontainers 接入成本。
以前,开发者经常要手写一堆属性:
spring.datasource.urlspring.datasource.usernamespring.datasource.passwordspring.data.redis.hostspring.data.redis.portspring.kafka.bootstrap-servers
而在较新的 Spring Boot 3.x 集成方式中,某些容器可以被 Spring Boot 自动识别为服务连接来源,从而自动把连接信息装配给相关自动配置模块。
这带来的改变不是“少写几行配置”这么简单,而是:
容器不再只是测试代码里的辅助对象,而是可以正式进入 Spring 应用上下文,成为开发运行时的一部分。
2.3 开发环境也开始追求可复现
现代团队越来越强调“开发环境即代码(Development Environment as Code)”。
测试环境要可复现,开发环境同样应该可复现。Spring Boot 3.x 正好提供了把这种理念落地的好机会:
- 自动配置负责应用层;
- Testcontainers 负责依赖服务层;
- 配置文件负责环境切换;
- Bean 生命周期负责容器与应用协同启动。
因此,Spring Boot 3.x 与 Testcontainers 的组合,不只是工具组合,而是一种新的本地开发组织方式。
3. 什么叫“Testcontainers 不止用于测试”?
很多人看到 Testcontainers 这个名字,会下意识认为它只能在测试代码里使用。这其实是名称带来的误导。
从能力上说,Testcontainers 提供的是:
- 用 Java API 声明容器;
- 以编程方式启动、停止、复用容器;
- 获取动态映射端口与连接参数;
- 等待容器就绪;
- 执行初始化脚本;
- 组合多个依赖服务;
- 集成 Docker 网络与日志。
你会发现,这些能力没有一项天然只能用于测试。
它只是最先在测试领域被广泛接受,因为测试最容易需要“临时起一个真实依赖服务”。
但如果我们换个角度想:
本地开发不也同样需要一个真实依赖服务吗?
例如你正在开发一个订单系统,应用依赖:
- MySQL 存订单;
- Redis 做缓存与分布式锁;
- Kafka 发订单创建事件;
- 可能还需要 Elasticsearch、MinIO、RabbitMQ 等等。
传统做法通常有三种:
方案一:每个人本机手动安装
优点:
- 看似直接;
- 不依赖 Docker;
- 某些人已经熟悉。
缺点:
- 版本不统一;
- 迁移麻烦;
- 污染本机环境;
- 新同学上手成本高;
- 多项目依赖冲突严重。
方案二:手写 Docker Compose
优点:
- 比手动安装更统一;
- 容器可以一起启动;
- 配置集中管理。
缺点:
- 仍然和应用代码是两套系统;
- 容器配置与 Spring 配置之间需要人为维护映射;
- 对不同项目复制粘贴严重;
- 难以细粒度嵌入应用生命周期。
方案三:用 Testcontainers 直接在应用启动时声明依赖
优点:
- 依赖服务配置与应用代码强绑定;
- 启动逻辑可编程;
- 端口动态分配,减少冲突;
- Spring Boot 能自动消费连接信息;
- 更适合做 demo、培训、快速拉起与本地联调;
- 生命周期更可控。
缺点:
- 需要团队接受“开发运行时容器化”思维;
- 首次镜像拉取会花时间;
- 并不适合所有类型的依赖服务。
所以,“不止用于测试”的真正含义不是一句口号,而是:
把外部依赖服务通过代码纳入 Spring Boot 应用的开发运行时管理范围。
4. 本地开发容器化的核心思想
在真正开始写代码之前,我们先把思路讲透。很多技术文章的问题是代码很多,但读者并没有真正建立系统性理解,最后只能机械照抄,稍微换个场景就不会了。
这里我用一句话概括:
本地开发容器化,就是把原本需要手工安装、配置、启动和清理的依赖服务,改成由应用在启动时自动声明、自动拉起、自动注入、自动回收。
这背后至少包含四个核心动作。
4.1 声明依赖
把 MySQL、Redis、Kafka 这些依赖服务,明确写进代码里,而不是只写在 README 文档里。
过去的 README 常见写法是:
- 安装 MySQL;
- 创建数据库
demo; - 创建用户
demo/demo123; - 启动 Redis;
- 启动 Kafka;
- 修改配置文件后运行项目。
这种写法的问题在于:文档不是系统,文档不会执行。
而容器声明则是把这段“人工操作说明”变成可执行代码。
4.2 启动依赖
声明还不够,你还需要一个统一机制,在应用启动前把这些依赖真的拉起来。
Testcontainers 帮你完成:
- 镜像拉取;
- 容器创建;
- 端口映射;
- 健康检查;
- 等待服务就绪;
- 暴露连接参数。
4.3 注入连接信息
MySQL 启动后,真正的 JDBC 地址通常不是固定死的,因为本地端口可能动态映射。Redis、Kafka 也是类似情况。
所以应用层不能再假设:
- MySQL 一定在
localhost:3306 - Redis 一定在
localhost:6379 - Kafka 一定在
localhost:9092
而要改成:
- 由容器提供真实连接信息;
- 由 Spring Boot 自动或半自动注入。
这一步是 Spring Boot 3.x 与 Testcontainers 配合最有价值的地方。
4.4 销毁与复用
开发环境和测试环境最大的不同之一,是“开发者可能会反复启动应用”。
因此我们还必须考虑:
- 容器关闭时机;
- 是否启用容器复用;
- 是否保留数据;
- 如何避免孤儿容器;
- 如何清理卷与网络资源。
只有把这些问题想清楚,Testcontainers 才能真正胜任本地开发容器化,而不只是临时演示工具。
5. 一张图看懂整体架构
先看整体模型图。

从这张图你可以看到,本地开发容器化并不是“应用起来后再手工连接 Docker”,而是:
- Spring 容器开始启动;
- 开发容器配置类被加载;
- Testcontainers 容器对象作为 Bean 被创建;
- 容器先启动并就绪;
- Spring Boot 获得连接信息;
- 数据源、缓存、消息组件自动完成配置;
- 最终业务代码像连接真实环境一样工作。
这就实现了从“安装式依赖”到“声明式依赖”的转变。
6. 环境准备:从 0 到 1 搭建运行基础
在写代码前,先明确本文示例所基于的技术栈。
6.1 推荐环境
- JDK 17 或以上
- Maven 3.9+
- Docker Desktop / Colima / Rancher Desktop / Linux Docker Engine
- Spring Boot 3.1+(建议 3.2 或 3.3 及以上分支中任一稳定版本)
- Testcontainers 对应稳定版本
说明:Spring Boot 3.x 对 Java 17 及以上支持更友好,很多文章也会以 Java 17 作为起点。本篇示例统一按 Java 17 编写。
6.2 为什么必须有 Docker?
Testcontainers 虽然是 Java 库,但它底层仍然需要一个可用的容器运行时。最常见的就是 Docker。
它并不是“替代 Docker”,而是“通过 Java 编程方式使用 Docker”。
所以本篇主题虽然叫“本地 MySQL/Redis/Kafka 免安装”,本质含义是:
- 不再单独安装这些服务;
- 但你仍然需要一个统一的容器运行时;
- 这个运行时通常就是 Docker。
6.3 Maven 依赖
下面先给出一个基础版 Maven 配置,后文所有示例都基于它演进。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>boot-testcontainers-dev</artifactId>
<version>1.0.0</version>
<name>boot-testcontainers-dev</name>
<description>Spring Boot 3.x 本地开发容器化示例</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Web 开发依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JPA 依赖,用于操作 MySQL -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Kafka 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-kafka</artifactId>
</dependency>
<!-- Actuator,便于观察运行状态 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Testcontainers 核心 -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
</dependency>
<!-- MySQL 容器支持 -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
</dependency>
<!-- Kafka 容器支持 -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>kafka</artifactId>
</dependency>
<!-- Spring Boot 对 Testcontainers 的支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
</dependency>
<!-- 开发辅助:配置处理器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Lombok,可选;不用也没关系 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
代码说明
这份依赖里有几个特别重要的点。
第一,spring-boot-testcontainers 虽然名字里也带 test,但它的价值并不局限于测试目录,它提供的是 Spring Boot 与 Testcontainers 的集成能力。
第二,MySQL 与 Kafka 有专门模块,因为它们的容器行为与连接信息获取方式更专业化。
第三,Redis 在很多场景下可以直接用 GenericContainer 完成,不一定非要用专门模块。
7. 第一个案例:把 MySQL 作为 Spring Bean 启动
现在开始进入本文最关键的部分:把依赖服务声明成 Spring Bean。
这里先做一个最小闭环,只启动 MySQL,让 Spring Boot 应用直接连上它。
7.1 项目结构设计
建议采用下面这种结构:
src/main/java
└── com/example/demo
├── DemoApplication.java
├── config
│ └── LocalDevContainersConfig.java
├── order
│ ├── Order.java
│ ├── OrderRepository.java
│ ├── OrderService.java
│ └── OrderController.java
└── runner
└── StartupRunner.java
7.2 启动类
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 应用启动类
*/
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
7.3 本地开发容器配置类
package com.example.demo.config;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.testcontainers.containers.MySQLContainer;
/**
* 本地开发容器配置类
*
* 说明:
* 1. 仅在 local-dev 环境下生效;
* 2. 把 MySQL 容器声明为 Spring Bean;
* 3. 使用 @ServiceConnection 让 Spring Boot 自动识别数据库连接信息。
*/
@Configuration
@Profile("local-dev")
public class LocalDevContainersConfig {
/**
* 声明一个 MySQL 容器 Bean。
*
* @return MySQLContainer 实例
*/
@Bean
@ServiceConnection
public MySQLContainer<?> mysqlContainer() {
return new MySQLContainer<>("mysql:8.0.36")
// 设置数据库名
.withDatabaseName("demo_db")
// 设置用户名
.withUsername("demo")
// 设置密码
.withPassword("demo123")
// 可选:初始化 SQL 脚本,路径放在 classpath 下
.withInitScript("sql/init.sql");
}
}
这一段为什么重要?
这段代码是全文的关键转折点。
它意味着:
- MySQL 不再是“你自己装一个”;
- 也不再是“你先 docker run 一下”;
- 而是应用本身在
local-dev环境启动时,自动声明并启动自己的 MySQL。
更重要的是,@ServiceConnection 告诉 Spring Boot:
这个 Bean 不是普通对象,而是一个可提供服务连接信息的容器。
于是 Spring Boot 能自动把该容器对应的 JDBC URL、用户名、密码等注入到数据源自动配置中。
也就是说,我们甚至不需要在 application.yml 里写死 MySQL 地址。
7.4 初始化 SQL 脚本
src/main/resources/sql/init.sql
-- 创建订单表
CREATE TABLE IF NOT EXISTS t_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64) NOT NULL,
product_name VARCHAR(128) NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
status VARCHAR(32) NOT NULL,
created_at DATETIME NOT NULL
);
7.5 实体类
package com.example.demo.order;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单实体类
*/
@Entity
@Table(name = "t_order")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 订单编号 */
@Column(name = "order_no", nullable = false, length = 64)
private String orderNo;
/** 商品名称 */
@Column(name = "product_name", nullable = false, length = 128)
private String productName;
/** 订单金额 */
@Column(name = "amount", nullable = false)
private BigDecimal amount;
/** 订单状态 */
@Column(name = "status", nullable = false, length = 32)
private String status;
/** 创建时间 */
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getOrderNo() {
return orderNo;
}
public void setOrderNo(String orderNo) {
this.orderNo = orderNo;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
}
7.6 Repository
package com.example.demo.order;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* 订单仓储接口
*/
public interface OrderRepository extends JpaRepository<Order, Long> {
}
7.7 Service
package com.example.demo.order;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
/**
* 订单服务类
*/
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
/**
* 创建订单
*
* @param productName 商品名称
* @param amount 金额
* @return 保存后的订单
*/
public Order createOrder(String productName, BigDecimal amount) {
Order order = new Order();
order.setOrderNo("ORD-" + UUID.randomUUID().toString().replace("-", ""));
order.setProductName(productName);
order.setAmount(amount);
order.setStatus("CREATED");
order.setCreatedAt(LocalDateTime.now());
return orderRepository.save(order);
}
/**
* 查询所有订单
*
* @return 订单列表
*/
public List<Order> findAll() {
return orderRepository.findAll();
}
}
7.8 Controller
package com.example.demo.order;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.List;
/**
* 订单控制器
*/
@RestController
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
/**
* 创建订单接口
*/
@PostMapping("/orders")
public Order createOrder(@RequestParam String productName,
@RequestParam BigDecimal amount) {
return orderService.createOrder(productName, amount);
}
/**
* 查询订单列表接口
*/
@GetMapping("/orders")
public List<Order> listOrders() {
return orderService.findAll();
}
}
7.9 application.yml
spring:
application:
name: boot-testcontainers-dev
jpa:
hibernate:
ddl-auto: none
show-sql: true
properties:
hibernate:
format_sql: true
management:
endpoints:
web:
exposure:
include: health,info,beans,env
7.10 本地开发 profile
spring:
config:
activate:
on-profile: local-dev
7.11 启动方式
mvn spring-boot:run -Dspring-boot.run.profiles=local-dev
应用启动时,你会发现:
- Spring Boot 开始启动;
LocalDevContainersConfig被加载;- MySQL 容器自动拉起;
- Spring Boot 自动拿到数据库连接信息;
- JPA 正常初始化;
- 应用接口可直接访问。
这就是“开发环境免安装 MySQL”的真正含义。
你没有安装 MySQL 服务,没有手工建库,没有手工配地址,但应用照样连上了真实 MySQL。
8. 第二个案例:把 Redis 作为 Spring Bean 启动
仅有数据库还不够。现代 Spring Boot 项目几乎都会配缓存。接下来,我们把 Redis 也纳入本地开发容器体系。
8.1 Redis 容器配置
package com.example.demo.config;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
/**
* Redis 本地开发容器配置
*/
@Configuration
@Profile("local-dev")
public class RedisContainerConfig {
/**
* Redis 容器 Bean
*
* 这里使用 GenericContainer 即可满足常见开发需求。
*/
@Bean
@ServiceConnection(name = "redis")
public GenericContainer<?> redisContainer() {
return new GenericContainer<>(DockerImageName.parse("redis:7.2"))
// 暴露 Redis 默认端口
.withExposedPorts(6379)
// 指定启动命令,关闭持久化,适合本地开发演示
.withCommand("redis-server", "--appendonly", "no");
}
}
为什么这里要写 name = "redis"?
因为 GenericContainer 本身是通用容器,Spring Boot 无法仅凭类型知道它代表 Redis。
所以通过 @ServiceConnection(name = "redis") 显式告诉 Spring Boot:
这个通用容器对外提供的是 Redis 服务连接。
这样 Redis 自动配置就能识别它。
8.2 Redis 业务示例:订单查询缓存
先写一个简单缓存服务,把订单查询结果放进 Redis。
package com.example.demo.order;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Optional;
/**
* 订单缓存服务
*/
@Service
public class OrderCacheService {
private final StringRedisTemplate stringRedisTemplate;
public OrderCacheService(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 缓存订单状态
*
* @param orderNo 订单编号
* @param status 订单状态
*/
public void cacheOrderStatus(String orderNo, String status) {
String key = "order:status:" + orderNo;
// 设置缓存,并指定 10 分钟过期
stringRedisTemplate.opsForValue().set(key, status, Duration.ofMinutes(10));
}
/**
* 查询缓存中的订单状态
*
* @param orderNo 订单编号
* @return 状态结果
*/
public Optional<String> getOrderStatus(String orderNo) {
String key = "order:status:" + orderNo;
return Optional.ofNullable(stringRedisTemplate.opsForValue().get(key));
}
}
8.3 在订单创建时写入缓存
package com.example.demo.order;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
/**
* 订单服务类
*/
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final OrderCacheService orderCacheService;
public OrderService(OrderRepository orderRepository,
OrderCacheService orderCacheService) {
this.orderRepository = orderRepository;
this.orderCacheService = orderCacheService;
}
/**
* 创建订单
*/
public Order createOrder(String productName, BigDecimal amount) {
Order order = new Order();
order.setOrderNo("ORD-" + UUID.randomUUID().toString().replace("-", ""));
order.setProductName(productName);
order.setAmount(amount);
order.setStatus("CREATED");
order.setCreatedAt(LocalDateTime.now());
Order saved = orderRepository.save(order);
// 写入 Redis 缓存
orderCacheService.cacheOrderStatus(saved.getOrderNo(), saved.getStatus());
return saved;
}
public List<Order> findAll() {
return orderRepository.findAll();
}
}
8.4 增加缓存查询接口
package com.example.demo.order;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 订单控制器
*/
@RestController
public class OrderController {
private final OrderService orderService;
private final OrderCacheService orderCacheService;
public OrderController(OrderService orderService,
OrderCacheService orderCacheService) {
this.orderService = orderService;
this.orderCacheService = orderCacheService;
}
@PostMapping("/orders")
public Order createOrder(@RequestParam String productName,
@RequestParam BigDecimal amount) {
return orderService.createOrder(productName, amount);
}
@GetMapping("/orders")
public List<Order> listOrders() {
return orderService.findAll();
}
/**
* 查询 Redis 中缓存的订单状态
*/
@GetMapping("/orders/cache/{orderNo}")
public Map<String, Object> getCachedStatus(@PathVariable String orderNo) {
return Map.of(
"orderNo", orderNo,
"status", orderCacheService.getOrderStatus(orderNo).orElse("NOT_FOUND")
);
}
}
到这里,你的本地开发环境已经无需单独安装 MySQL 和 Redis。
这比传统 docker-compose.yml 最大的优势在于:
- Redis 和 MySQL 何时启动由 Spring 决定;
- 配置注入由 Spring Boot 完成;
- 开发 profile 控制是否启用;
- 业务代码无需关心服务地址写死在哪里。
9. 第三个案例:把 Kafka 作为 Spring Bean 启动
很多团队本地开发最头疼的服务之一其实是消息队列。数据库和缓存大家相对熟悉,但 Kafka 的本地安装、版本适配、网络配置、主题管理往往更容易出问题。
这也是 Testcontainers 在本地开发中最能体现价值的场景之一。
9.1 Kafka 容器配置
package com.example.demo.config;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.testcontainers.kafka.KafkaContainer;
import org.testcontainers.utility.DockerImageName;
/**
* Kafka 本地开发容器配置
*/
@Configuration
@Profile("local-dev")
public class KafkaContainerConfig {
/**
* Kafka 容器 Bean
*/
@Bean
@ServiceConnection
public KafkaContainer kafkaContainer() {
return new KafkaContainer(DockerImageName.parse("apache/kafka:3.7.0"));
}
}
注:不同版本 Testcontainers / Kafka 镜像在 API 与镜像选择上可能略有差异,实际项目中请以当前依赖版本支持情况为准。本文示例重点是思路与组织方式。
9.2 Kafka 配置
spring:
kafka:
consumer:
group-id: order-local-group
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
因为 @ServiceConnection 已经提供了 bootstrap-servers,这里无需手写 Kafka 地址。
9.3 发送订单创建事件
package com.example.demo.order;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
/**
* 订单事件生产者
*/
@Service
public class OrderEventProducer {
private final KafkaTemplate<String, String> kafkaTemplate;
public OrderEventProducer(KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
/**
* 发送订单创建事件
*
* @param orderNo 订单编号
*/
public void sendOrderCreatedEvent(String orderNo) {
// 发送到 order-created 主题
kafkaTemplate.send("order-created", orderNo, "订单已创建:" + orderNo);
}
}
9.4 消费订单创建事件
package com.example.demo.order;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
/**
* 订单事件消费者
*/
@Component
public class OrderEventConsumer {
private static final Logger log = LoggerFactory.getLogger(OrderEventConsumer.class);
/**
* 监听订单创建主题
*
* @param message 消息内容
*/
@KafkaListener(topics = "order-created", groupId = "order-local-group")
public void onOrderCreated(String message) {
log.info("收到 Kafka 消息:{}", message);
}
}
9.5 创建订单时发送消息
package com.example.demo.order;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
/**
* 订单服务类
*/
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final OrderCacheService orderCacheService;
private final OrderEventProducer orderEventProducer;
public OrderService(OrderRepository orderRepository,
OrderCacheService orderCacheService,
OrderEventProducer orderEventProducer) {
this.orderRepository = orderRepository;
this.orderCacheService = orderCacheService;
this.orderEventProducer = orderEventProducer;
}
/**
* 创建订单
*/
public Order createOrder(String productName, BigDecimal amount) {
Order order = new Order();
order.setOrderNo("ORD-" + UUID.randomUUID().toString().replace("-", ""));
order.setProductName(productName);
order.setAmount(amount);
order.setStatus("CREATED");
order.setCreatedAt(LocalDateTime.now());
Order saved = orderRepository.save(order);
// 写入缓存
orderCacheService.cacheOrderStatus(saved.getOrderNo(), saved.getStatus());
// 发送 Kafka 事件
orderEventProducer.sendOrderCreatedEvent(saved.getOrderNo());
return saved;
}
public List<Order> findAll() {
return orderRepository.findAll();
}
}
现在你已经做到了:
- 本地无需安装 MySQL;
- 本地无需安装 Redis;
- 本地无需安装 Kafka;
- 直接运行 Spring Boot 应用即可获得完整依赖服务。
这已经不是“用容器做测试”了,而是完整的 本地开发容器化。
10. Spring Boot 3.x 中与 Testcontainers 集成的几种方式
写到这里,很多读者会问:是不是只有“把容器声明成 Bean”这一种方式?
答案是:不是。
在 Spring Boot 3.x 中,常见整合方式至少有以下几类。
10.1 方式一:测试类中使用 @Container
这是最传统的 Testcontainers 使用方式。
特点:
- 主要服务于测试;
- 生命周期绑定测试类;
- 不适合直接拿来做本地开发运行时。
示意代码:
@Testcontainers
@SpringBootTest
class OrderServiceTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0.36");
}
这种写法大家比较熟,但它不适合作为本文主角,因为我们要的是“应用启动即拥有依赖服务”。
10.2 方式二:通过 DynamicPropertySource 注入属性
这是在测试里也常见的一种方式。
特点:
- 灵活;
- 需要手工注册属性;
- 比
@ServiceConnection更底层。
示意:
@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
这种方式在本地开发也能用,但写法更“手工”,不如 Spring Boot 3.x 的 @ServiceConnection 优雅。
10.3 方式三:将容器声明为 Bean,并通过 @ServiceConnection 集成
这正是本文重点推荐的方式。
特点:
- 容器进入 Spring 上下文;
- 适用于开发运行时;
- 与 Profile、自动配置、Bean 生命周期天然契合;
- 代码组织更符合 Spring Boot 思维。
10.4 方式四:启动器类中手工启动容器并设置系统属性
例如在 main 方法中先启动容器,再 System.setProperty(...)。
这种方式理论上也能做,但并不优雅。问题包括:
- 和 Spring 生命周期割裂;
- 配置注入太原始;
- 可维护性差;
- 不利于多容器扩展。
小结
如果你的目标是:
- 让本地开发免安装依赖服务;
- 让容器和 Spring Boot 3.x 深度融合;
- 让配置管理、生命周期、自动装配更自然;
那么最推荐的方式就是:
在特定 Profile 下,把依赖服务声明成 Spring Bean,并借助
@ServiceConnection连接到 Spring Boot 自动配置体系。
11. 为什么把依赖服务声明成 Spring Bean 更优雅?
这一节非常重要,因为它解释的是“为什么要这么设计”,而不是“怎么写代码”。
很多初学者会觉得:“能跑不就行了吗?为什么非要做成 Bean?”
从工程视角看,这里面至少有六层收益。
11.1 统一生命周期
Bean 的本质不是“一个对象”,而是“受 Spring 容器管理的对象”。
一旦容器服务被声明为 Bean,它就自动拥有:
- 明确的创建时机;
- 受控的初始化顺序;
- 可感知的依赖关系;
- 可统一的销毁流程。
这和“在 main 方法里随便 new 一下容器再 start”完全不是一个层级。
11.2 与 Profile 天然结合
你通常不会希望生产环境也去启动本地开发容器。因此,最合理的办法就是:
local-dev:启用本地开发容器;test:用于测试容器;prod:连接真实基础设施。
Bean 配合 @Profile 可以让这套切换非常自然。
11.3 与自动配置自然衔接
Spring Boot 的强项是自动配置,而自动配置的前提是:
- 有确定的 Bean;
- 有环境属性;
- 有可识别的连接信息。
容器声明为 Bean 后,Spring Boot 才能更容易推断这些信息。
11.4 更容易扩展与组合
今天你只有 MySQL,明天你可能加 Redis,后天可能加 Kafka,再后面还可能加 Elasticsearch、MinIO、MailHog、RabbitMQ。
如果容器都是 Bean,那么:
- 每个服务都有自己配置类;
- 每个服务都可按 Profile 打开/关闭;
- 每个服务都能被独立替换;
- 整体结构会非常清晰。
11.5 更适合团队协作
团队协作最怕“约定靠嘴”。
而 Spring Bean 化意味着:
- 依赖服务不再依靠口头说明;
- 新同学只要运行指定 Profile 即可;
- CI 也更容易复用同样的启动模式;
- 文档不再承担过多“环境安装说明”职责。
11.6 更符合“基础设施即代码”理念
今天我们已经接受:
- 配置可以代码化;
- 测试可以代码化;
- 部署可以代码化;
- 运维脚本可以代码化。
那么开发依赖环境当然也应该尽可能代码化。
把依赖服务声明成 Bean,正是把开发基础设施正式纳入应用代码体系的一种做法。
12. 多容器协作:一个应用同时连接 MySQL + Redis + Kafka
前面三个案例是分别演示,真正项目里通常是多依赖一起存在。下面给出一个聚合版配置。
12.1 聚合配置类
package com.example.demo.config;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.kafka.KafkaContainer;
import org.testcontainers.utility.DockerImageName;
/**
* 本地开发容器聚合配置
*/
@Configuration
@Profile("local-dev")
public class LocalDevelopmentContainersConfiguration {
/**
* MySQL 容器
*/
@Bean
@ServiceConnection
public MySQLContainer<?> mysqlContainer() {
return new MySQLContainer<>("mysql:8.0.36")
.withDatabaseName("demo_db")
.withUsername("demo")
.withPassword("demo123")
.withInitScript("sql/init.sql");
}
/**
* Redis 容器
*/
@Bean
@ServiceConnection(name = "redis")
public GenericContainer<?> redisContainer() {
return new GenericContainer<>(DockerImageName.parse("redis:7.2"))
.withExposedPorts(6379)
.withCommand("redis-server", "--appendonly", "no");
}
/**
* Kafka 容器
*/
@Bean
@ServiceConnection
public KafkaContainer kafkaContainer() {
return new KafkaContainer(DockerImageName.parse("apache/kafka:3.7.0"));
}
}
12.2 多依赖协作模型图

这个模型虽然简单,但它说明了一件非常关键的事:
从应用视角看,MySQL、Redis、Kafka 不再是三套外部安装说明,而是三种由应用启动过程主动申请并接入的开发依赖。
这是开发体验上的巨大变化。
13. 完整实战:订单系统本地开发容器化
接下来我们把前面的能力整合成一个更完整的案例。这个案例会模拟一个最小订单系统,具备如下流程:
- 接收创建订单请求;
- 订单写入 MySQL;
- 订单状态写入 Redis;
- 发送 Kafka 消息;
- 消费 Kafka 消息做日志记录;
- 提供订单查询接口;
- 提供缓存状态查看接口。
13.1 请求流程图

这张图把整个业务闭环串起来了。你会发现,虽然依赖服务都运行在容器里,但从业务代码角度看,它们和“真实环境服务”没有本质区别。
这就是 Testcontainers 最强的地方:
它让本地开发尽量接近真实运行依赖,而不是只靠内存模拟。
13.2 DTO 设计
package com.example.demo.order;
import java.math.BigDecimal;
/**
* 创建订单请求对象
*/
public class CreateOrderRequest {
/** 商品名称 */
private String productName;
/** 金额 */
private BigDecimal amount;
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
}
13.3 更规范的 Controller
package com.example.demo.order;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 订单控制器
*/
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
private final OrderCacheService orderCacheService;
public OrderController(OrderService orderService,
OrderCacheService orderCacheService) {
this.orderService = orderService;
this.orderCacheService = orderCacheService;
}
/**
* 创建订单
*/
@PostMapping
public Order create(@RequestBody CreateOrderRequest request) {
return orderService.createOrder(request.getProductName(), request.getAmount());
}
/**
* 查询所有订单
*/
@GetMapping
public List<Order> list() {
return orderService.findAll();
}
/**
* 查询缓存状态
*/
@GetMapping("/{orderNo}/status-cache")
public Map<String, Object> statusCache(@PathVariable String orderNo) {
return Map.of(
"orderNo", orderNo,
"status", orderCacheService.getOrderStatus(orderNo).orElse("NOT_FOUND")
);
}
}
13.4 启动后自动打印连接信息(调试用)
package com.example.demo.runner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.kafka.KafkaContainer;
/**
* 应用启动完成后打印容器连接信息,便于调试观察
*/
@Component
@Profile("local-dev")
public class StartupRunner implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(StartupRunner.class);
private final MySQLContainer<?> mysqlContainer;
private final GenericContainer<?> redisContainer;
private final KafkaContainer kafkaContainer;
public StartupRunner(MySQLContainer<?> mysqlContainer,
GenericContainer<?> redisContainer,
KafkaContainer kafkaContainer) {
this.mysqlContainer = mysqlContainer;
this.redisContainer = redisContainer;
this.kafkaContainer = kafkaContainer;
}
@Override
public void run(ApplicationArguments args) {
log.info("===== 本地开发容器已就绪 =====");
log.info("MySQL JDBC URL: {}", mysqlContainer.getJdbcUrl());
log.info("MySQL Username: {}", mysqlContainer.getUsername());
log.info("Redis Host: {}", redisContainer.getHost());
log.info("Redis Port: {}", redisContainer.getMappedPort(6379));
log.info("Kafka Bootstrap Servers: {}", kafkaContainer.getBootstrapServers());
}
}
为什么这里值得特别讲?
这段代码不是必须的,但很适合教学与排障。
因为很多初学者会怀疑:
- 容器真的启动了吗?
- 应用到底连到了哪个地址?
- 为什么我没写 3306,数据库也能连通?
- Kafka 的地址是不是自动注入了?
通过打印连接信息,读者就能直观看到:
- Testcontainers 使用了动态端口;
- Spring Boot 不是依赖固定端口,而是依赖真实映射结果;
- 容器 Bean 确实已经进入应用上下文。
14. 代码逐段解析:每个类到底做了什么?
很多教程最大的问题不是没代码,而是代码贴完就结束。对于初学者来说,这很容易形成“看懂了假象”,真正让他自己写时还是无从下手。
所以这一节我们把关键角色逐个拆开解释。
14.1 配置类:负责声明依赖基础设施
LocalDevelopmentContainersConfiguration 的职责不是业务,而是“开发运行基础设施声明”。
你可以把它理解为:
- 业务代码是应用层;
- 容器配置类是开发基础设施层;
- 两者通过 Spring 容器连接起来。
这是一种很好的分层方式。
如果没有这层,容器相关代码就会散落在启动类、main 方法、工具类、测试类甚至文档里,长期来看会非常混乱。
14.2 @Profile("local-dev"):负责限定使用场景
这一点非常关键。
因为 Testcontainers 本地开发容器化不是要替代生产基础设施,而是要服务于本地开发体验。通过 Profile 隔离,你就能做到:
- 本地开发时:自动起容器;
- 集成环境时:连接环境中已有服务;
- 生产环境时:连接正式中间件。
这意味着本文方案是增强开发体验,而不是粗暴地把所有环境混为一谈。
14.3 @ServiceConnection:负责连接 Spring Boot 自动配置
这是 Spring Boot 3.x 非常值得学习的点。
如果没有它,你得手写大量连接属性注册逻辑;而有了它,Spring Boot 会尽量理解这个容器提供的服务类型,并自动喂给相关自动配置模块。
换句话说,它相当于在容器和 Spring Boot 自动配置之间搭了一座桥。
14.4 业务层完全不需要知道容器细节
这是工程设计中非常漂亮的一点。
OrderService、OrderController、OrderCacheService、OrderEventProducer 这些业务类都不关心:
- MySQL 是本机安装的还是容器启动的;
- Redis 是固定端口还是随机映射;
- Kafka 是手工部署还是代码拉起。
它们只关心:
- 数据源能不能用;
- RedisTemplate 能不能注入;
- KafkaTemplate 能不能发送消息。
这说明我们的设计达到了一个很重要的目标:
基础设施变化被隔离在配置层,业务层保持稳定。
这就是好架构的特征之一。
15. 配置设计:不同环境如何切换
很多人一看到本地开发容器化,第一反应是:“那我线上怎么办?测试怎么办?”
其实这并不是冲突关系,而是环境分层问题。
15.1 推荐的环境划分
建议最少拆成三类:
local-dev:本地开发容器化;test:自动化测试;prod:生产或类生产环境。
15.2 推荐的配置组织方式
application.yml
spring:
application:
name: boot-testcontainers-dev
jpa:
hibernate:
ddl-auto: none
show-sql: true
properties:
hibernate:
format_sql: true
management:
endpoints:
web:
exposure:
include: health,info,beans,env
application-local-dev.yml
spring:
config:
activate:
on-profile: local-dev
jpa:
open-in-view: false
kafka:
consumer:
group-id: order-local-group
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
application-prod.yml
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:mysql://prod-mysql:3306/order_db?useSSL=false&serverTimezone=Asia/Shanghai
username: prod_user
password: prod_password
data:
redis:
host: prod-redis
port: 6379
kafka:
bootstrap-servers: prod-kafka-1:9092,prod-kafka-2:9092
配置设计原则
核心原则是:
- 通用配置放公共文件;
- 环境差异配置放对应 profile;
- 本地开发尽量不写死外部依赖地址;
- 生产环境明确连接真实基础设施。
这样配置边界会非常清晰。
16. 生命周期管理:容器什么时候启动、什么时候关闭?
现在我们来讲一个特别容易被忽略、但实际上决定这套方案是否好用的问题:生命周期。
如果你只会把容器跑起来,却不理解生命周期,那你的开发环境很可能越用越乱。
16.1 容器什么时候启动?
当容器被声明为 Spring Bean 后,它的启动通常发生在 Bean 初始化阶段或 Spring Boot 与 Testcontainers 集成处理过程中。
从整体顺序看,大致如下:

也就是说,依赖容器需要先于真正依赖它的业务基础组件就绪。
这也是为什么 Testcontainers 与 Spring Boot 深度整合很重要:否则你必须手动保证初始化先后顺序。
16.2 容器什么时候关闭?
正常情况下,当 JVM 退出时,Testcontainers 会配合资源回收机制关闭容器。对于声明为 Bean 的容器,在应用上下文关闭时也会触发销毁逻辑。
但这里要区分两类场景:
场景一:一次性运行
比如:
- 演示一个 demo;
- 本地启动后测试功能;
- 应用关闭后不再需要依赖环境。
这种场景下,容器随应用关闭一起回收,最符合直觉。
场景二:反复重启应用
比如开发者在 IDE 中频繁重启、热部署、调试。
这时如果每次都完全销毁再重建容器,就可能带来:
- 启动等待变长;
- 数据被重置;
- 镜像与容器操作开销增大。
因此你需要考虑“容器复用策略”,这一点后面会详细讲。
17. 资源清理:如何避免“容器越跑越多”
谈本地开发容器化,必须认真讲资源清理,否则文章就是不负责任的。
因为很多初学者照着示例做完,最容易遇到的不是“起不来”,而是“后来越来越乱”:
- 容器残留;
- 匿名卷越来越多;
- 镜像占空间;
- Docker 网络堆积;
- 本机磁盘被吃满。
17.1 容器回收的基本原则
你要知道,Testcontainers 的设计初衷就是临时容器,因此它通常具备较好的自动清理能力。但这并不意味着你可以完全不关心清理。
至少要理解以下几种资源:
- 容器本身;
- 镜像;
- 数据卷;
- 网络;
- 日志占用;
- 本地缓存。
17.2 为什么本地开发更容易出现清理问题
因为测试通常是“跑完就结束”,而本地开发是“反复启停”。
一旦以下情况出现,就容易留下痕迹:
- IDE 强制终止进程;
- Docker 本身异常重启;
- 容器复用配置使用不当;
- 开发者手工
docker stop但没删卷; - 多个项目共享镜像但没有统一策略。
17.3 你应该建立的清理意识
建议团队内部至少约定以下几条:
- 本地开发容器只服务当前项目,不要顺手拿来当长期数据库;
- 演示性环境默认不追求数据永久保存;
- 周期性清理未使用容器和卷;
- 对是否启用复用做团队统一约定;
- 对镜像版本统一管理,避免每个人各拉各的。
17.4 示例:在文档中显式说明清理命令
虽然我们不靠文档来启动环境,但文档仍然应该告诉开发者如何清理。
# 查看当前容器
docker ps -a
# 删除已停止容器
docker container prune -f
# 删除未使用镜像
docker image prune -f
# 删除未使用卷(谨慎)
docker volume prune -f
# 删除未使用网络
docker network prune -f
为什么这一节不能省略?
因为一篇真正负责任的工程实践文章,不应该只谈“如何启动”,还必须谈“如何收尾”。
如果你只教读者把容器跑起来,却不告诉他长期使用后怎么维护,那就很容易把本来提升体验的方案,变成新的环境负担。
18. 性能与体验优化:让本地开发更顺手
很多人第一次尝试本地开发容器化时,会担心一个问题:
这样会不会很慢?
答案是:有成本,但通常可接受,而且可以优化。
18.1 首次启动慢是正常的
第一次启动时,通常要经历:
- 拉镜像;
- 创建容器;
- 初始化数据库;
- Kafka 完成 broker 启动;
- Spring Boot 再完成应用初始化。
因此第一次运行比“本机已长期安装好的服务”慢,是正常现象。
但首次成本不代表长期成本高。
18.2 为什么长期体验反而可能更好
因为一旦你不再手工维护 MySQL、Redis、Kafka:
- 不再单独升级和修补本机安装;
- 不再处理端口冲突;
- 不再查“你到底连的是哪一个实例”;
- 新项目切换更轻松;
- 新同学入门更省时间。
从整体工程时间成本看,很多团队会发现这笔账是划算的。
18.3 可选优化:启用容器复用
某些场景下,你可能希望应用重启时不销毁容器,而是继续复用已有容器,以提升开发效率。
这一策略的核心价值在于:
- 减少重复启动时间;
- 保留临时数据;
- 提高频繁调试效率。
但要注意,复用并不适合所有团队,因为它会带来状态残留问题。
示例:对容器启用复用
@Bean
@ServiceConnection
public MySQLContainer<?> mysqlContainer() {
return new MySQLContainer<>("mysql:8.0.36")
.withDatabaseName("demo_db")
.withUsername("demo")
.withPassword("demo123")
// 启用容器复用(需要本地环境允许)
.withReuse(true)
.withInitScript("sql/init.sql");
}
复用的利弊
优点:
- 启动更快;
- 调试体验更好;
- 数据能保留。
缺点:
- 状态不再绝对干净;
- 容器清理要更谨慎;
- 团队成员对环境一致性要求更高时不一定适合。
经验建议
- 教学文章、快速 demo:可以不开复用,保证干净;
- 个人高频调试:可以考虑打开复用;
- 团队统一开发基线:最好明确规定是否允许复用。
18.4 初始化脚本不要太重
本地开发环境的目标是“快速进入业务开发”,不是“完整恢复一套生产数据”。
因此:
- SQL 初始化脚本应尽量精简;
- 不要导入海量数据;
- Kafka 主题初始化也尽量轻量;
- Redis 不要预加载过多内容。
你真正要的是“功能可验证”,而不是“本地环境像生产一样重”。
19. 常见问题排查:启动失败、端口冲突、镜像拉取慢怎么办
这一节是实战里最常被收藏的部分。我们集中把常见问题讲透。
19.1 问题一:Docker 未启动或不可用
典型表现
- 应用启动时报无法连接 Docker;
- Testcontainers 初始化失败;
- 日志提示找不到容器运行时。
排查思路
- 先确认 Docker Desktop 或 Docker Engine 已启动;
- 命令行执行
docker version; - 执行
docker ps看是否可用; - 确认当前用户有权限访问 Docker;
- macOS / Linux 下检查 socket 权限。
核心理解
Testcontainers 不是魔法,它本质上要驱动底层容器运行时。所以只要 Docker 不可用,Testcontainers 就不可能工作。
19.2 问题二:镜像拉取很慢
典型原因
- 网络环境导致拉取国外镜像慢;
- 首次拉取大镜像;
- Kafka 镜像体积较大;
- 镜像源不可达。
处理建议
- 提前准备团队统一镜像缓存;
- 在公司内网构建镜像代理;
- 选择更合适的镜像版本;
- 首次启动给开发者明确预期。
经验提醒
不要因为“第一次拉镜像慢”就否定整个方案。镜像一旦到本机,后续体验通常会好很多。
19.3 问题三:端口冲突
为什么 Testcontainers 反而更少冲突?
因为它通常采用动态端口映射,而不是强制占用本机固定端口。Spring Boot 再通过实际映射值建立连接。
这比传统写死 localhost:3306 更灵活。
但为什么有时仍然会报冲突?
可能是:
- 你手动指定了固定端口;
- 应用本身端口和其他程序冲突;
- Docker Desktop 内部网络异常;
- 复用容器时旧实例占用状态不一致。
建议
本地开发容器化尽量遵循“容器端口固定、宿主端口动态”的思路,不要过早追求本机固定映射。
19.4 问题四:应用启动太慢
排查顺序
- 看是否首次拉镜像;
- 看数据库初始化脚本是否过重;
- 看 Kafka 是否占主要启动时间;
- 看应用自身自动配置是否太多;
- 考虑是否需要容器复用。
经验建议
对初学者项目,建议先只接 MySQL 和 Redis,Kafka 可以后加。先让读者建立正确心智,再上复杂依赖。
19.5 问题五:容器起来了,但 Spring Boot 还是连不上
可能原因
@ServiceConnection没加;- 容器类型无法被自动识别;
- 使用
GenericContainer却没显式指定服务名; - profile 没激活;
- 配置文件里又写了错误的固定地址覆盖了自动注入。
排查建议
- 打开应用日志;
- 打印容器实际地址;
- 查看
env和beans端点; - 临时关闭手工配置,观察自动注入是否生效。
这也是为什么我前面专门写了 StartupRunner。调试期尽量让信息透明,排障成本会大幅降低。
20. 与 Docker Compose、Dev Services、传统本地安装对比
技术选型不能只讲自己方案的优点,还要把边界讲清楚。下面我们做一个系统对比。
20.1 与传统本地安装对比
传统安装的优点
- 对单一长期项目来说,启动速度快;
- 不需要容器基础设施;
- 某些老项目团队习惯了。
传统安装的缺点
- 污染本机环境;
- 版本难统一;
- 多项目冲突严重;
- 新成员接入成本高;
- 卸载、升级和迁移麻烦。
Testcontainers 的优势
- 依赖环境代码化;
- 可复现;
- 可隔离;
- 更接近真实运行服务;
- 更适合多项目切换。
20.2 与 Docker Compose 对比
Docker Compose 是很多团队的第一选择,它确实很有价值。但它与 Testcontainers 并不是谁绝对替代谁,而是关注点不同。
Docker Compose 更适合
- 团队统一维护一套外部环境编排;
- 跨语言项目共享依赖服务;
- 独立于应用代码运行;
- 有专门运维或平台工程习惯。
Testcontainers 更适合
- Java / Spring Boot 项目内聚式管理依赖;
- 想让环境声明跟随代码走;
- 希望容器启动与 Spring 生命周期打通;
- 希望在测试与本地开发之间复用思路;
- 教学、demo、快速启动场景。
一句话对比
- Docker Compose 更像项目外部环境清单;
- Testcontainers 更像应用内部可编程依赖基础设施。
20.3 与某些框架的 Dev Services 对比
有些现代框架也提供“开发时自动拉起依赖服务”的能力,本质理念和本文很接近。
但在 Spring Boot 世界里,Testcontainers 的优势在于:
- 生态成熟;
- 细粒度控制强;
- 可以跨测试、开发、演示多场景复用;
- 与 Spring Boot 3.x 集成后体验足够自然。
也就是说,Spring Boot 并没有缺少这种能力,只是它通过 Testcontainers 的方式来实现。
21. 团队协作价值:为什么这不是个人技巧,而是工程能力
到这里,你应该已经看出来了:本文讲的不是一个“炫技小技巧”,而是一种团队工程能力建设方式。
21.1 新同学接入成本显著下降
最常见的团队痛点就是新人第一天无法把项目跑起来。原因通常不是 Java 不会,而是环境太复杂:
- 数据库要装;
- 缓存要装;
- 消息队列要装;
- 版本还得对;
- 某些配置要改;
- 还要手工初始化数据。
如果把这些依赖服务都做成开发容器 Bean,那么新人上手路径会明显缩短:
- 安装 JDK;
- 安装 Docker;
- 拉代码;
- 启动
local-devprofile; - 开始开发。
从团队生产力角度看,这价值非常大。
21.2 降低“环境差异导致的问题”
很多本地 bug 不是代码 bug,而是环境 bug。
例如:
- 某人 Redis 配置了密码,另一个人没配;
- 某人 Kafka 版本不同,协议不兼容;
- 某人 MySQL 字符集不同;
- 某人机器上数据残留影响了结果。
本地开发容器化的价值就在于尽量把这些差异压缩到最小。
21.3 文档从“安装手册”变成“使用说明”
一份成熟项目文档不应该有一半篇幅都在教人手工装中间件。
如果依赖环境已经代码化,文档就能更专注于:
- 项目结构;
- 业务模块;
- 开发规范;
- 启动方式;
- 排障说明。
这会显著提高文档质量。
21.4 测试与开发思路统一
这也是本文特别值得强调的一点。
很多团队把“测试环境容器化”和“开发环境搭建”当成两套完全不同的事情。其实它们底层是可以共用理念的:
- 都是在代码里声明依赖服务;
- 都是在需要时自动拉起;
- 都是在结束时可清理;
- 都强调可复现和隔离。
换句话说,Testcontainers 把测试工程与开发工程拉到了同一条现代化思路上。
22. 最佳实践清单
下面给出一份可直接用于团队落地的最佳实践清单。
22.1 只在特定 Profile 启用
不要让本地开发容器默认侵入所有环境。推荐用 local-dev 单独隔离。
22.2 把每类依赖服务单独封装
建议按服务拆配置类,例如:
MysqlContainerConfigRedisContainerConfigKafkaContainerConfig
或者小项目统一写在一个聚合配置类中,但逻辑要清楚。
22.3 优先使用 @ServiceConnection
只要 Spring Boot 3.x 与对应容器类型支持,优先使用它,而不是手工写一堆属性绑定。
22.4 初始化脚本轻量化
本地开发要的是快速启动,不是完整恢复生产环境。脚本越轻越好。
22.5 业务代码不要感知容器存在
业务层应该依赖标准 Spring 抽象,而不是直接到处拿容器对象。容器信息最多只在调试或基础设施层暴露。
22.6 明确是否允许复用
是否启用 .withReuse(true) 必须有团队约定。不要每个人各自一套。
22.7 提供清理指南
项目 README 至少要告诉开发者:
- 如何启动;
- 如何停止;
- 如何清理残留资源;
- 常见故障如何排查。
22.8 不要过度容器化
并不是所有依赖都适合在本地用 Testcontainers 拉起。比如某些超重型组件、需要复杂集群拓扑的组件,就要谨慎评估。
23. 适用边界:什么场景适合,什么场景不适合
一篇高质量文章必须告诉读者:什么时候该用,什么时候别硬用。
23.1 很适合的场景
场景一:教学与专栏示例
这几乎是最适合的场景。读者不需要先装半天环境,直接就能跑代码、看结果、学知识。
场景二:中小型 Spring Boot 项目本地开发
尤其是依赖服务数量有限(2~5 个左右)且容器化成熟的项目。
场景三:团队快速 onboarding
新成员入门时,环境统一收益非常大。
场景四:Demo 与 PoC
快速原型验证时,不值得为短期项目手工维护整套本机依赖。
23.2 不太适合的场景
场景一:超重型依赖或复杂集群依赖
例如某些需要复杂多节点模拟的系统,本地用 Testcontainers 可能成本较高。
场景二:对本地数据持续性要求极强
如果开发依赖环境需要长期积累大量本地数据,且容器重建代价高,就要谨慎评估是否采用复用或转向其他方案。
场景三:团队根本没有容器基础设施能力
如果团队成员连 Docker 基础都不具备,贸然推行可能反而制造新的学习门槛。这时应先补齐容器基础教育。
场景四:跨语言统一依赖更适合 Compose
如果多个不同技术栈项目要共享同一套开发依赖,Docker Compose 可能更利于统一维护。
结论
Testcontainers 本地开发容器化不是银弹,但它在 Spring Boot 3.x 项目里的适用面已经足够广,而且价值非常现实。
24. 总结:Testcontainers 作为本地开发基础设施的未来价值
让我们回到文章标题:
Testcontainers 不止用于测试:本地开发容器化
这句话真正要传达的,不是“你可以这么玩”,而是“你应该重新看待开发环境的组织方式”。
在 Spring Boot 3.x 时代,我们完全可以把本地开发基础设施做得像业务代码一样:
- 有明确声明;
- 有清晰边界;
- 有自动装配;
- 有生命周期;
- 有资源清理;
- 有环境隔离;
- 有团队可复用性。
Testcontainers 之所以重要,不只是因为它能起一个 MySQL、一个 Redis、一个 Kafka;而是因为它让我们有机会把“本地环境搭建”从低价值重复劳动,升级为可编程、可维护、可协作的工程资产。
从更长远的角度看,这种思想和现代软件工程的发展方向是高度一致的:
- 基础设施即代码;
- 环境即代码;
- 开发体验工程化;
- 测试与开发基础设施统一化;
- 尽量降低隐式状态和人工步骤。
所以,如果你是 Spring Boot 3.x 开发者,请不要把 Testcontainers 只停留在“集成测试工具”的认知层面。它完全可以成为你本地开发体系中的重要一环。
尤其当你的项目同时依赖 MySQL、Redis、Kafka 这些经典组件时,这种价值会非常直观。
一句话收尾:
真正优秀的开发环境,不是靠每个人手工把依赖装对,而是靠项目本身把依赖环境声明清楚、启动清楚、使用清楚、清理清楚。
而在 Spring Boot 3.x 中,Testcontainers 正是实现这件事的一把非常趁手的工具。
25. 附录:完整可运行示例代码
为了方便你直接整理发布,这里把核心代码按文件再完整汇总一次。
25.1 DemoApplication.java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 应用启动类
*/
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
25.2 LocalDevelopmentContainersConfiguration.java
package com.example.demo.config;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.kafka.KafkaContainer;
import org.testcontainers.utility.DockerImageName;
/**
* 本地开发容器聚合配置类
*/
@Configuration
@Profile("local-dev")
public class LocalDevelopmentContainersConfiguration {
/**
* MySQL 容器 Bean
*/
@Bean
@ServiceConnection
public MySQLContainer<?> mysqlContainer() {
return new MySQLContainer<>("mysql:8.0.36")
.withDatabaseName("demo_db")
.withUsername("demo")
.withPassword("demo123")
.withInitScript("sql/init.sql");
}
/**
* Redis 容器 Bean
*/
@Bean
@ServiceConnection(name = "redis")
public GenericContainer<?> redisContainer() {
return new GenericContainer<>(DockerImageName.parse("redis:7.2"))
.withExposedPorts(6379)
.withCommand("redis-server", "--appendonly", "no");
}
/**
* Kafka 容器 Bean
*/
@Bean
@ServiceConnection
public KafkaContainer kafkaContainer() {
return new KafkaContainer(DockerImageName.parse("apache/kafka:3.7.0"));
}
}
25.3 Order.java
package com.example.demo.order;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单实体类
*/
@Entity
@Table(name = "t_order")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 订单编号 */
@Column(name = "order_no", nullable = false, length = 64)
private String orderNo;
/** 商品名称 */
@Column(name = "product_name", nullable = false, length = 128)
private String productName;
/** 金额 */
@Column(name = "amount", nullable = false)
private BigDecimal amount;
/** 状态 */
@Column(name = "status", nullable = false, length = 32)
private String status;
/** 创建时间 */
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getOrderNo() {
return orderNo;
}
public void setOrderNo(String orderNo) {
this.orderNo = orderNo;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
}
25.4 OrderRepository.java
package com.example.demo.order;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* 订单仓储接口
*/
public interface OrderRepository extends JpaRepository<Order, Long> {
}
25.5 CreateOrderRequest.java
package com.example.demo.order;
import java.math.BigDecimal;
/**
* 创建订单请求对象
*/
public class CreateOrderRequest {
/** 商品名称 */
private String productName;
/** 金额 */
private BigDecimal amount;
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
}
25.6 OrderCacheService.java
package com.example.demo.order;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Optional;
/**
* 订单缓存服务
*/
@Service
public class OrderCacheService {
private final StringRedisTemplate stringRedisTemplate;
public OrderCacheService(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 缓存订单状态
*/
public void cacheOrderStatus(String orderNo, String status) {
String key = "order:status:" + orderNo;
stringRedisTemplate.opsForValue().set(key, status, Duration.ofMinutes(10));
}
/**
* 查询缓存中的订单状态
*/
public Optional<String> getOrderStatus(String orderNo) {
String key = "order:status:" + orderNo;
return Optional.ofNullable(stringRedisTemplate.opsForValue().get(key));
}
}
25.7 OrderEventProducer.java
package com.example.demo.order;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
/**
* 订单事件生产者
*/
@Service
public class OrderEventProducer {
private final KafkaTemplate<String, String> kafkaTemplate;
public OrderEventProducer(KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
/**
* 发送订单创建事件
*/
public void sendOrderCreatedEvent(String orderNo) {
kafkaTemplate.send("order-created", orderNo, "订单已创建:" + orderNo);
}
}
25.8 OrderEventConsumer.java
package com.example.demo.order;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
/**
* 订单事件消费者
*/
@Component
public class OrderEventConsumer {
private static final Logger log = LoggerFactory.getLogger(OrderEventConsumer.class);
/**
* 监听订单创建主题
*/
@KafkaListener(topics = "order-created", groupId = "order-local-group")
public void onOrderCreated(String message) {
log.info("收到 Kafka 消息:{}", message);
}
}
25.9 OrderService.java
package com.example.demo.order;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
/**
* 订单服务类
*/
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final OrderCacheService orderCacheService;
private final OrderEventProducer orderEventProducer;
public OrderService(OrderRepository orderRepository,
OrderCacheService orderCacheService,
OrderEventProducer orderEventProducer) {
this.orderRepository = orderRepository;
this.orderCacheService = orderCacheService;
this.orderEventProducer = orderEventProducer;
}
/**
* 创建订单
*/
public Order createOrder(String productName, BigDecimal amount) {
Order order = new Order();
order.setOrderNo("ORD-" + UUID.randomUUID().toString().replace("-", ""));
order.setProductName(productName);
order.setAmount(amount);
order.setStatus("CREATED");
order.setCreatedAt(LocalDateTime.now());
Order saved = orderRepository.save(order);
orderCacheService.cacheOrderStatus(saved.getOrderNo(), saved.getStatus());
orderEventProducer.sendOrderCreatedEvent(saved.getOrderNo());
return saved;
}
/**
* 查询所有订单
*/
public List<Order> findAll() {
return orderRepository.findAll();
}
}
25.10 OrderController.java
package com.example.demo.order;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 订单控制器
*/
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
private final OrderCacheService orderCacheService;
public OrderController(OrderService orderService,
OrderCacheService orderCacheService) {
this.orderService = orderService;
this.orderCacheService = orderCacheService;
}
/**
* 创建订单
*/
@PostMapping
public Order create(@RequestBody CreateOrderRequest request) {
return orderService.createOrder(request.getProductName(), request.getAmount());
}
/**
* 查询所有订单
*/
@GetMapping
public List<Order> list() {
return orderService.findAll();
}
/**
* 查询订单状态缓存
*/
@GetMapping("/{orderNo}/status-cache")
public Map<String, Object> statusCache(@PathVariable String orderNo) {
return Map.of(
"orderNo", orderNo,
"status", orderCacheService.getOrderStatus(orderNo).orElse("NOT_FOUND")
);
}
}
25.11 StartupRunner.java
package com.example.demo.runner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.kafka.KafkaContainer;
/**
* 应用启动后打印容器信息
*/
@Component
@Profile("local-dev")
public class StartupRunner implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(StartupRunner.class);
private final MySQLContainer<?> mysqlContainer;
private final GenericContainer<?> redisContainer;
private final KafkaContainer kafkaContainer;
public StartupRunner(MySQLContainer<?> mysqlContainer,
GenericContainer<?> redisContainer,
KafkaContainer kafkaContainer) {
this.mysqlContainer = mysqlContainer;
this.redisContainer = redisContainer;
this.kafkaContainer = kafkaContainer;
}
@Override
public void run(ApplicationArguments args) {
log.info("===== 本地开发容器已就绪 =====");
log.info("MySQL JDBC URL: {}", mysqlContainer.getJdbcUrl());
log.info("MySQL Username: {}", mysqlContainer.getUsername());
log.info("Redis Host: {}", redisContainer.getHost());
log.info("Redis Port: {}", redisContainer.getMappedPort(6379));
log.info("Kafka Bootstrap Servers: {}", kafkaContainer.getBootstrapServers());
}
}
25.12 application.yml
spring:
application:
name: boot-testcontainers-dev
jpa:
hibernate:
ddl-auto: none
show-sql: true
properties:
hibernate:
format_sql: true
management:
endpoints:
web:
exposure:
include: health,info,beans,env
25.13 application-local-dev.yml
spring:
config:
activate:
on-profile: local-dev
kafka:
consumer:
group-id: order-local-group
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
25.14 init.sql
CREATE TABLE IF NOT EXISTS t_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64) NOT NULL,
product_name VARCHAR(128) NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
status VARCHAR(32) NOT NULL,
created_at DATETIME NOT NULL
);
25.15 启动命令
mvn spring-boot:run -Dspring-boot.run.profiles=local-dev
25.16 测试接口示例
创建订单
curl -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{
"productName": "Spring Boot 3 实战课程",
"amount": 199.00
}'
查询订单列表
curl http://localhost:8080/api/orders
查询缓存中的订单状态
curl http://localhost:8080/api/orders/你的订单编号/status-cache
…
ok,同学们,本节课就上到这儿,下课~
🧧 学习福利 · 限时开放 🧧
当然,无论你是计算机专业在读学生,还是对编程充满兴趣的入门者,都强烈建议系统学习SpringBoot全体系专栏:👉 「滚雪球学 Spring Boot」;涵盖SpringBoot所有教学内容。
该专栏以“循序渐进 + 实战驱动”为核心理念,从基础到进阶到就业到架构师逐层展开,帮助你快速建立完整的 Spring Boot 技术体系,带你玩转SpringBoot框架。
📌 学习承诺:
通过该专栏,你将能够:
- 快速掌握 Spring Boot 核心开发能力
- 构建完整的后端项目认知体系
- 实现从“入门”到“独立开发”的跃迁
就像“滚雪球”一样,知识不断积累、能力持续放大,实现指数级成长 🚀
最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。
同时欢迎大家关注技术号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。
ps:本文涉及所有源代码,均已上传至Gitee开源,供同学们直接对照学习 Gitee传送门,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗
🫵 Who am I?
我是 bug菌:
- 热活于 CSDN | 稀土掘金 | InfoQ | 51CTO | 华为云开发者社区 | 阿里云开发者社区 | 腾讯云开发者社区 | 开源中国 | 博客园 | 墨天轮 等各大技术社区;
- CSDN 博客之星 Top30、华为云多年度十佳博主&卓越贡献奖、掘金多年度人气作者 Top40;
- CSDN、掘金、InfoQ、51CTO 等平台签约及优质作者;
- 全网粉丝累计 30w+。
更多高质量技术内容及成长资料,可查看这个合集入口 👉 点击查看 👈️
硬核技术号 「猿圈奇妙屋」 期待你的加入,一起进阶、一起打怪升级。
- End -
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)