Appearance
H2内存数据库和Testcontainers
本文介绍如何使用H2内存数据库和Testcontainers来进行数据库模拟。
1. 环境搭建
先搭建一个多数据源的Spring Boot测试环境。
1.1 数据库初始化
首先在Docker中启动两个数据库:
bash
docker run -d -p 3307:3306 -e MYSQL_ROOT_PASSWORD=abc123 --name stu-ds mysql:8.4bash
docker run -d -p 3308:3306 -e MYSQL_ROOT_PASSWORD=abc123 --name user-ds mysql:8.4分别初始化数据库:
sql
create database if not exists stu;
USE `stu`;
CREATE TABLE IF NOT EXISTS `stu` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '姓名',
`age` INT NULL DEFAULT NULL COMMENT '年龄',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='学生';
insert into stu.stu(name,age) values('张三',18),('李四',20);sql
create database if not exists user;
USE `user`;
CREATE TABLE IF NOT EXISTS `user` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` VARCHAR(64) NOT NULL COMMENT '登录名',
`nickname` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '昵称',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户';
INSERT INTO user.user(username, nickname) values('张三','zs'),('李四','ls');1.2 项目初始化
新建Spring Boot项目,引入以下依赖:

项目配置文件application-ds.yml内容如下:
yaml
spring:
datasource:
dynamic:
primary: stu
strict: true
datasource:
stu:
url: jdbc:mysql://127.0.0.1:3307/stu?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: abc123
driver-class-name: com.mysql.cj.jdbc.Driver
user:
url: jdbc:mysql://127.0.0.1:3308/user?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: abc123
driver-class-name: com.mysql.cj.jdbc.Driverapplication.yml内容如下:
yaml
spring:
application:
name: demo
profiles:
active: ds
server:
port: 8888项目结构如下:

注意在UserMapper中切换数据源:
java
@Mapper
@DS("user")
public interface UserMapper extends BaseMapper<User> {
}1.3 项目演示


2. H2
H2 是一个用 Java 编写的轻量级、开源的嵌入式关系型数据库管理系统 (RDBMS),支持内存和文件存储模式。它以高性能、支持标准 SQL、JDBC API 和内置 Web 控制台著称。常用于 Java 应用程序的单元测试、开发环境或作为小型应用的嵌入式数据库。
2.1 环境配置
本小节介绍如何使用H2作为内存数据库,进行单元测试。
首先引入依赖:
xml
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>然后在src/test/resource下新建application-test-ds.yml文件,内容如下:
yaml
spring:
datasource:
dynamic:
primary: stu
strict: true
datasource:
stu:
url: jdbc:h2:mem:stu;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;NON_KEYWORDS=USER
username: sa
password:
driver-class-name: org.h2.Driver
init:
schema: classpath:sql/stu-schema.sql
data: classpath:sql/stu-data.sql
user:
url: jdbc:h2:mem:user;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;NON_KEYWORDS=USER
username: sa
password:
driver-class-name: org.h2.Driver
init:
schema: classpath:sql/user-schema.sql
data: classpath:sql/user-data.sql在数据源配置中,我们使用H2内存数据库代替了MySQL,连接串如下:
txt
jdbc:h2:mem:stu;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;NON_KEYWORDS=USERjdbc:h2:mem:stu;:表示在内存中创建一个名为stu的数据库;MODE=MySQL:开启 MySQL 兼容模式,允许使用反引号 ,识别UNSIGNED、AUTO_INCREMENT等 MySQL 特有语法;DB_CLOSE_DELAY=-1:禁止数据库随连接关闭而销毁。在内存模式下,如果连接池回收了最后一个连接,数据会消失。-1确保数据库在整个 JVM 运行期间持续存在;DATABASE_TO_LOWER=TRUE:强制库名/表名小写。H2 默认会将user转为USER。开启此项后,它能匹配 MySQL 默认的小写习惯,避免Table not found;CASE_INSENSITIVE_IDENTIFIERS=TRUE:标识符大小写不敏感;NON_KEYWORDS=USER:排除关键字冲突;
之后,在配置中,使用init.schema和init.data来初始化数据库:
init.schema:初始化数据库表结构;init.data:初始化数据;
例如,stu-schema.sql内容如下:
sql
create schema if not exists stu;
USE `stu`;
-- H2 内存模式通常通过连接 URL 指定数据库,不需要显式 CREATE DATABASE 和 USE
-- 如果脚本必须包含,请确保 H2 版本支持,或在连接 URL 中开启 MySQL 模式
CREATE TABLE IF NOT EXISTS `stu` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '姓名',
`age` INT DEFAULT NULL COMMENT '年龄',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-- H2 不直接支持 MySQL 的 ON UPDATE 语法,通常在代码层面或通过触发器实现
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
);WARNING
注意,H2数据库和MySQL数据库语法不同,还是需要转换一下的。
stu-data.sql内容如下:
sql
insert into stu.stu(name,age) values('张三',18),('李四',20);在src/test/resources下新建application.yml文件,内容如下:
yaml
spring:
profiles:
active: test
config:
import: application-test-db.yml
logging:
level:
# 核心:显示 SQL 脚本执行过程
org.springframework.jdbc.datasource.init: DEBUG
# 如果想看到数据库连接的细节
org.springframework.jdbc.core.JdbcTemplate: DEBUG2.2 单元测试
环境配好后,我们就可以进行单元测试了:
java
@SpringBootTest
@ActiveProfiles("test")
public class StuServiceTest {
@Autowired
private StuService stuService;
@Test
void test_list(){
List<Stu> stuList = stuService.list();
assertThat(stuList).hasSize(2);
}
}当然,也可以结合@Sql注解,执行额外的sql:
java
@SpringBootTest
@ActiveProfiles("test")
@Sql(value = {"classpath:test-sql/stu.sql"})
@Transactional
public class StuServiceTest {
@Autowired
private StuService stuService;
@Test
void test_list(){
List<Stu> stuList = stuService.list();
assertThat(stuList)
.hasSize(3)
.extracting("name")
.contains("张三","李四","王五");
}
}test-sql/stu.sql内容如下:
sql
insert into stu.stu(name,age) values('王五',18);2.3 非主数据源事务回滚
如果我们要在单元测试中,对非主数据源执行DML操作,并且希望在测试方法后进行回滚,需要编程式事务手动回滚(不能简单使用@Transactional+@Sql方式执行sql与回滚事务),按照以下方式实现:
java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@ActiveProfiles("test")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class UserServiceTest {
@Autowired
private UserService userService;
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
@Order(1)
void test_1() {
// 1. 强制切换到 user 数据源 (必须在开启事务前执行)
DynamicDataSourceContextHolder.push("user");
// 2. 开启事务
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 3. 执行 SQL 脚本 (此时 jdbcTemplate 会处于 user 数据源的当前事务中)
Resource resource = new ClassPathResource("test-sql/user.sql");
jdbcTemplate.execute((ConnectionCallback<Void>) conn -> {
ScriptUtils.executeSqlScript(conn, resource);
return null;
});
// 4. 测试与断言
List<User> userList = userService.list();
assertThat(userList)
.hasSize(3)
.extracting("nickname")
.contains("zs","ls","ww");
}finally {
// 5. 测试完成后,回滚事务
transactionManager.rollback(status);
// 6. 清理数据源上下文,防止污染其他测试
DynamicDataSourceContextHolder.poll();
}
}
@Test
@Order(2)
void test_2(){
List<User> userList = userService.list();
assertThat(userList).hasSize(2);
}
}2.4 H2控制台
我们也可以在浏览器中查看内存数据库的内容,首先开启控制台:
yaml
spring:
h2:
console:
enabled: true然后在测试中,需要开启真实WEB环境:
java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)假设定义的端口为8080
之后,在浏览器中访问http://localhost:8080/h2-console:

在JDBC URL中填入YAML配置文件中的URL即可,点击Connect即可连接到控制台:

3. Testcontainers
Testcontainers允许在测试运行时,通过 Docker 自动启动真实的数据库(如 PostgreSQL, MySQL)、消息队列(如 Redis, RabbitMQ)或任何其他服务,从而替代内存数据库(如 H2),确保测试环境与生产环境高度一致。
3.1 简单使用
Testcontainers可以脱离Spring Boot环境,直接与测试框架集成使用。
TIP
注意,如果要使用Testcontainers,需要Docker-API compatible container runtime,也就是说本地要安装有Docker,或者使用Testcontainers Cloud。
下面的例子以 JUnit Jupiter 测试框架为例子,使用Testcontainers启动一个Redis容器,在程序中使用Jedis访问Redis。
首先引入以下依赖(org.junit.jupiter:junit-jupiter已引入):
xml
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>2.0.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<version>2.0.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>6.0.0</version>
</dependency>然后,测试类如下:
java
@Testcontainers
public class RedisInContainerTest {
@Container
GenericContainer redis = new GenericContainer(DockerImageName.parse("redis:8.0.2"))
.withExposedPorts(6379);
@Test
void test_01(){
// 获取容器中的redis的地址
String host = redis.getHost();
// 获取容器中的redis的端口
Integer port = redis.getFirstMappedPort();
// 通过jedis访问容器中的redis
JedisPool jedisPool = new JedisPool(new JedisPoolConfig(), host, port);
try (Jedis jedis = jedisPool.getResource()) {
jedis.set("hello", "redis");
System.out.println(jedis.get("hello"));
}
}
}@Testcontainers注解:这个注解是Testcontainers提供的,是JUnit Jupiter扩展,用于在测试的生命周期中管理容器的启停的。
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(TestcontainersExtension.class)
@Inherited
public @interface Testcontainers {
// 如果Docker环境不满足,那么是报测试禁用(扩展中的ExecutionCondition接口作用)还是报测试失败,默认是报告失败
boolean disabledWithoutDocker() default false;
// 如果在测试中定义了多个容器,是否并发启动容器,默认串行
boolean parallel() default false;
}TestcontainersExtension就是扩展类,定义如下:
java
public class TestcontainersExtension
implements BeforeEachCallback, BeforeAllCallback, AfterEachCallback, AfterAllCallback, ExecutionCondition {
}用于管理标注了@Container注解的字段:
- 如果
@Container字段是静态(static)的,那么在所有测试方法前启动容器,并且在所有测试方法后关闭容器,即实现BeforeAllCallback和AfterAllCallback钩子的作用; - 如果
@Container字段不是静态的(即实例字段),那么容器会在每个测试方法前启动,在每个测试方法后关闭,即实现BeforeEachCallback和AfterEachCallback钩子的作用;
因此,有了@Testcontainers和@Container,测试流程大致如下:
- 在测试方法运行前激活Testcontainers;
- 发现并快速检测本地Docker是否可用;
- 拉回镜像image;
- 启动容器,并等待容器启动完成;
- 运行测试方法;
- 在测试方法运行完成后,关闭并删除容器;
3.2 其他设置
3.2.1 创建容器
Testcontainers的通用容器提供了最大的便利,我们可以使用GenericContainer来创建任何容器,例如:
java
new GenericContainer(DockerImageName.parse("name:tag"))Testcontainers推荐使用DockerImageName.parse()来创建容器,传参有以下形式:
| 格式类型 | 示例字符串 | 适用场景 |
|---|---|---|
| 标准(Name:Tag) | redis:7-alpine | 最常用,指定具体版本以保证测试幂等性。 |
| 私有仓库 | my.registry.com/infra/redis:7 | 公司内部镜像源,需确保 Docker 已配置登录权限。 |
| 摘要(Digest) | redis@sha256:45b... | 最安全,通过哈希值锁定镜像,防止镜像被覆盖。 |
| 本地镜像 | my-local-image:latest | 测试自己刚构建好的本地镜像。 |
3.2.2 网络设置
很多时候,我们需要将容器端口暴露出来,可以使用withExposedPorts()方法:
java
public GenericContainer<?> container = new GenericContainer<>(
DockerImageName.parse("testcontainers/helloworld:1.1.0")
)
.withExposedPorts(8080, 8081);注意暴露的端口是容器的端口,而不是本机映射的端口。
为了避免端口冲突,Testcontainers会将暴露出来的容器端口随机映射到本机空闲端口上,因此,在容器启动后,需要使用以下方式获取本机映射的端口:
java
Integer firstMappedPort = container.getMappedPort(8080);
Integer secondMappedPort = container.getMappedPort(8081);如果一个容器只暴露一个端口,那么可以使用以下方式获取映射端口:
java
Integer firstMappedPort = container.getFirstMappedPort();获取容器IP地址可以使用以下方法(如果Docker是在本机,返回localhost):
java
String ipAddress = container.getHost();Testcontainers也支持新建网络,多个容器在同一个网络中启动,之后,这些容器的网络请求,就可以更简单了:
java
try (
Network network = Network.newNetwork();
GenericContainer<?> foo = new GenericContainer<>(TestImages.TINY_IMAGE)
.withNetwork(network)
.withNetworkAliases("foo")
.withCommand(
"/bin/sh",
"-c",
"while true ; do printf 'HTTP/1.1 200 OK\\n\\nyay' | nc -l -p 8080; done"
);
GenericContainer<?> bar = new GenericContainer<>(TestImages.TINY_IMAGE)
.withNetwork(network)
.withCommand("top")
) {
foo.start();
bar.start();
String response = bar.execInContainer("wget", "-O", "-", "http://foo:8080").getStdout();
assertThat(response).as("received response").isEqualTo("yay");
}3.2.3 执行命令
默认情况下,Testcontainers 会执行 Docker 镜像中 Dockerfile 定义的 CMD 指令。如果想在启动时覆盖它,可以使用 .withCommand():
java
public GenericContainer<?> redisWithCustomPort = new GenericContainer<>(DockerImageName.parse("redis:6-alpine"))
.withCommand("redis-server --port 7777");相当于 docker run <image> <command>。一旦使用了 withCommand,镜像原本默认的启动逻辑会被完全替换。
如果要在启动了的容器中执行命令,我们可以使用方法execInContainer(),例如:
java
// 在容器内创建一个文件
container.execInContainer("touch", "/somefile.txt");
// 执行 ls 命令并获取结果
Container.ExecResult lsResult = container.execInContainer("ls", "-al", "/");
// 获取输出信息
String stdout = lsResult.getStdout(); // 标准输出
String stderr = lsResult.getStderr(); // 错误输出
int exitCode = lsResult.getExitCode(); // 退出状态码(0 通常表示成功)类似于在终端使用的 docker exec 命令。
3.2.4 设置环境变量
许多镜像(如 MySQL, Postgres, Kafka)通过环境变量来控制初始化行为(如设置密码、创建初始数据库等)。
可以在启动容器时,通过withEnv()方法设置:
java
new GenericContainer(...)
.withEnv("API_TOKEN", "foo")
.withEnv("DB_PASSWORD", "secret");相当于 docker run -e API_TOKEN=foo。
3.2.5 文件复制
可以在容器启动时,将本机的文件复制到容器中:
java
GenericContainer<?> container = new GenericContainer<>(TestImages.TINY_IMAGE)
.withCommand("sleep", "3000")
.withCopyFileToContainer(
MountableFile.forClasspathResource("/mappable-resource/"),
"directoryInContainer"
)也可以通过Transferable,直接写入文件内容,设置文件权限:
java
GenericContainer<?> container = new GenericContainer<>(TestImages.TINY_IMAGE)
.withStartupCheckStrategy(new NoopStartupCheckStrategy())
.withCopyToContainer(Transferable.of("test", 0777), "/tmp/test")
.waitingFor(new WaitForExitedState(state -> state.getExitCodeLong() > 0))
.withCommand("sh", "-c", "ls -ll /tmp | grep '\\-rwxrwxrwx\\|test' && exit 100")在容器启动后,我们也可以从容器中复制文件到主机:
java
container.copyFileFromContainer(directoryInContainer + fileName, destinationOnHost);3.3 模块
Testcontainers模块是预先定义好的容器,使用更方便,模块列表参考:https://testcontainers.com/modules/
例如,如果我们想直接使用Redis容器,可以添加以下依赖:
xml
<dependency>
<groupId>com.redis</groupId>
<artifactId>testcontainers-redis</artifactId>
<version>2.2.2</version>
<scope>test</scope>
</dependency>然后直接使用以下容器:
java
RedisContainer redis = new RedisContainer(DockerImageName.parse("redis:8.0.2"));
redis.start();3.4 SpringBoot中使用
在Spring Boot项目中,我们也可以使用Testcontainers来进行测试。
首先引入以下依赖:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<version>${latest}</version>
<scope>test</scope>
</dependency>我们可以把容器当作Bean来对待:
java
@TestConfiguration(proxyBeanMethods = false)
public class MyTestConfiguration {
@Bean
@ServiceConnection("redis")
public GenericContainer<?> redisContainer() {
GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("redis:8.0.2"))
.withExposedPorts(6379);
return container;
}
}@ServiceConnection就是由spring-boot-testcontainers依赖提供的,主要作用是容器生命周期管理与自动配置:
容器生命周期管理:当把容器定义为Bean后,通过
@ServiceConnection表明该Bean是容器,此时,Spring会管理容器Bean的生命周期:- 容器Bean会在其他Bean之前创建和启动;
- 容器Bean会在其他Bean销毁之后停止;
自动配置:通常情况下,在Spring Boot中使用Redis等服务,我们要在配置文件application.yml中配置Redis的地址、密码等信息,现在使用了Testcontainers,Redis服务是在测试运行前启动的,端口号是随机的,应该如何配置呢?
@ServiceConnection提供了自动配置机制。在Spring Boot 3.1中,引入了
ConnectionDetails接口:javapublic interface ConnectionDetails { }这个接口用来提供有关容器的信息,例如主机名、动态端口、用户名、密码等。
对于不同的容器,有不同的实现,如果是Redis容器,那么Spring提供的实现是
RedisContainerConnectionDetails。Spring Boot 3.1 重构了配置查找底层逻辑。以前,Spring 总是直接读取
application.yml中的属性(如spring.datasource.url)。现在,Spring 会先检查 IoC 容器中是否存在ConnectionDetails的 Bean,如果存在,会先从ConnectionDetails中获取信息进行自动配置。
默认情况下,Container.getDockerImageName().getRepository()的返回值用来决定创建哪个ConnectionDetails,如下,@ServiceConnection用在静态字段上时:
java
@Testcontainers
@SpringBootTest
public class RedisInContainerTest {
@Container
@ServiceConnection
static GenericContainer redis = new GenericContainer(DockerImageName.parse("redis:8.0.2"))
.withExposedPorts(6379);
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
void test_02(){
stringRedisTemplate.opsForValue().set("hello", "spring redis");
String s = stringRedisTemplate.opsForValue().get("hello");
System.out.println(s);
}
}当在静态字段上时,可以通过反射获取到容器对象,然后调用Container.getDockerImageName().getRepository()决定创建哪个ConnectionDetails,最终自动配置过程就能获取到容器信息,完成相关组件的配置。
如果@ServiceConnection和@Bean一起在方法上时,并且返回值不是强类型容器(例如RedisContainer),必须指定name属性:
java
@TestConfiguration(proxyBeanMethods = false)
public class MyTestConfiguration {
@Bean
@ServiceConnection("redis")
public GenericContainer<?> redisContainer() {
GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("redis:8.0.2"))
.withExposedPorts(6379);
return container;
}
}如果返回值是强类型容器RedisContainer,Spring可以从返回值类型推断出要创建哪个ConnectionDetails。
在有的情况下,@ServiceConnection也不能完全生效,例如RedisContainerConnectionDetails没有实现getPassword()方法,如果我们启动的Redis容器需要密码,那么默认的@ServiceConnection就会失效:
java
@Testcontainers
@SpringBootTest
public class RedisInContainerTest {
@Container
@ServiceConnection
static GenericContainer redis = new GenericContainer(DockerImageName.parse("redis:8.0.2"))
.withExposedPorts(6379)
.withCommand("redis-server --requirepass 123");
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
void test_02(){
stringRedisTemplate.opsForValue().set("hello", "spring redis");
String s = stringRedisTemplate.opsForValue().get("hello");
System.out.println(s);
}
}以上结果:
Caused by: io.lettuce.core.RedisCommandExecutionException: NOAUTH HELLO must be called with the client already authenticated, otherwise the HELLO <proto> AUTH <user> <pass> option can be used to authenticate the client and select the RESP protocol version at the same time此时,我们可以使用@DynamicPropertySource:https://docs.spring.io/spring-framework/reference/testing/testcontext-framework/ctx-management/dynamic-property-sources.html
java
@Testcontainers
@SpringBootTest
public class RedisInContainerTest {
@Container
static GenericContainer redis = new GenericContainer(DockerImageName.parse("redis:8.0.2"))
.withExposedPorts(6379)
.withCommand("redis-server --requirepass 123");
@DynamicPropertySource
static void redisProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.redis.host", redis::getHost);
registry.add("spring.data.redis.port", redis::getFirstMappedPort);
registry.add("spring.data.redis.password", () -> "123");
}
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
void test_02(){
stringRedisTemplate.opsForValue().set("hello", "spring redis");
String s = stringRedisTemplate.opsForValue().get("hello");
System.out.println(s);
}
}也可以如下配置:
java
@TestConfiguration(proxyBeanMethods = false)
public class MyTestConfiguration {
@Bean
public GenericContainer<?> redisContainer() {
GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse("redis:8.0.2"))
.withCommand("redis-server --requirepass abc")
.withExposedPorts(6379);
return container;
}
@Bean
DynamicPropertyRegistrar apiPropertiesRegistrar(GenericContainer redisContainer) {
return registry -> {
registry.add("spring.data.redis.host", redisContainer::getHost);
registry.add("spring.data.redis.port", redisContainer::getFirstMappedPort);
registry.add("spring.data.redis.password", () -> "abc");
};
}
}java
@SpringBootTest
@Import(MyTestConfiguration.class)
public class RedisInContainer2Test {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
void test_02(){
stringRedisTemplate.opsForValue().set("hello", "spring redis");
String s = stringRedisTemplate.opsForValue().get("hello");
System.out.println(s);
}
}因此,使用@DynamicPropertySource可以配置多数据源MySQL。
3.5 多数据源
使用Testcontainers模拟多个MySQL数据库,引入依赖:
xml
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-mysql</artifactId>
<version>2.0.1</version>
<scope>test</scope>
</dependency>配置文件:
yaml
spring:
datasource:
dynamic:
primary: stu
strict: false3.5.1 案例一
java
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
@Testcontainers
public class ContainerBase {
@Container
static MySQLContainer stuMysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.4"))
.withDatabaseName("stu")
.withUsername("root")
.withPassword("abc123");
@Container
static MySQLContainer userMysql =new MySQLContainer<>(DockerImageName.parse("mysql:8.4"))
.withDatabaseName("user")
.withUsername("root")
.withPassword("abc123");
@DynamicPropertySource
static void bind(DynamicPropertyRegistry registry){
registry.add("spring.datasource.dynamic.datasource.stu.url",
() -> String.format("jdbc:mysql://%s:%d/stu?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true",
stuMysql.getHost(), stuMysql.getMappedPort(3306)));
registry.add("spring.datasource.dynamic.datasource.stu.username", stuMysql::getUsername);
registry.add("spring.datasource.dynamic.datasource.stu.password", stuMysql::getPassword);
registry.add("spring.datasource.dynamic.datasource.stu.driver-class-name", () -> "com.mysql.cj.jdbc.Driver");
registry.add("spring.datasource.dynamic.datasource.stu.init.schema", () -> "classpath:sql/stu-schema.sql");
registry.add("spring.datasource.dynamic.datasource.stu.init.data", () -> "classpath:sql/stu-data.sql");
// 绑定 user 数据源
registry.add("spring.datasource.dynamic.datasource.user.url",
() -> String.format("jdbc:mysql://%s:%d/user?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true",
userMysql.getHost(), userMysql.getMappedPort(3306)));
registry.add("spring.datasource.dynamic.datasource.user.username", userMysql::getUsername);
registry.add("spring.datasource.dynamic.datasource.user.password", userMysql::getPassword);
registry.add("spring.datasource.dynamic.datasource.user.driver-class-name", () -> "com.mysql.cj.jdbc.Driver");
registry.add("spring.datasource.dynamic.datasource.user.init.schema", () -> "classpath:sql/user-schema.sql");
registry.add("spring.datasource.dynamic.datasource.user.init.data", () -> "classpath:sql/user-data.sql");
}
}java
@SpringBootTest
@ActiveProfiles("test")
@Sql(value = {"classpath:test-sql/stu.sql"})
@Transactional
public class StuServiceTest extends ContainerBase {
@Autowired
private StuService stuService;
@Test
void test_list(){
List<Stu> stuList = stuService.list();
assertThat(stuList)
.hasSize(3)
.extracting("name")
.contains("张三","李四","王五");
}
}3.5.2 案例二
java
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.baomidou.dynamic.datasource.creator.DatasourceInitProperties;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.testcontainers.mysql.MySQLContainer;
import org.testcontainers.utility.DockerImageName;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@TestConfiguration(proxyBeanMethods = false)
public class MySqlContainersConfiguration {
// 定义第一个数据源:stu
@Bean
public MySQLContainer stuMysql() {
MySQLContainer container = new MySQLContainer(DockerImageName.parse("mysql:8.4"))
.withDatabaseName("stu")
.withUsername("root")
.withPassword("abc123");
return container;
}
// 定义第二个数据源:user
@Bean
public MySQLContainer userMysql() {
MySQLContainer container =new MySQLContainer(DockerImageName.parse("mysql:8.4"))
.withDatabaseName("user")
.withUsername("root")
.withPassword("abc123");
return container;
}
@Bean
public DynamicDataSourceProvider testDynamicDataSourceProvider(
MySQLContainer stuMysql,
MySQLContainer userMysql,
DefaultDataSourceCreator dataSourceCreator) {
return new DynamicDataSourceProvider() {
@Override
public Map<String, DataSource> loadDataSources() {
Map<String, DataSource> dataSourceMap = new HashMap<>();
// ================= 配置 stu 数据源 =================
DataSourceProperty stuProp = new DataSourceProperty();
stuProp.setPoolName("stu");
stuProp.setDriverClassName("com.mysql.cj.jdbc.Driver");
stuProp.setUrl(String.format("jdbc:mysql://%s:%d/stu?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true",
stuMysql.getHost(), stuMysql.getMappedPort(3306)));
stuProp.setUsername(stuMysql.getUsername());
stuProp.setPassword(stuMysql.getPassword());
DatasourceInitProperties stuInit = new DatasourceInitProperties();
stuInit.setSchema("classpath:sql/stu-schema.sql");
stuInit.setData("classpath:sql/stu-data.sql");
stuProp.setInit(stuInit);
// 使用 creator 创建并放入 Map
dataSourceMap.put("stu", dataSourceCreator.createDataSource(stuProp));
// ================= 配置 user 数据源 =================
DataSourceProperty userProp = new DataSourceProperty();
userProp.setPoolName("user");
userProp.setDriverClassName("com.mysql.cj.jdbc.Driver");
userProp.setUrl(String.format("jdbc:mysql://%s:%d/user?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true",
userMysql.getHost(), userMysql.getMappedPort(3306)));
userProp.setUsername(userMysql.getUsername());
userProp.setPassword(userMysql.getPassword());
DatasourceInitProperties userInit = new DatasourceInitProperties();
stuInit.setSchema("classpath:sql/user-schema.sql");
stuInit.setData("classpath:sql/user-data.sql");
userProp.setInit(userInit);
// 使用 creator 创建并放入 Map
dataSourceMap.put("user", dataSourceCreator.createDataSource(userProp));
return dataSourceMap;
}
};
}
}TIP
注意,使用DynamicDataSourceProvider来加载数据源,如果DynamicPropertyRegistrar会导致DynamicRoutingDataSource拿不到配置,最终导致加载失败。
java
@SpringBootTest
@Import({MySqlContainersConfiguration.class})
@ActiveProfiles("test")
@Sql(value = {"classpath:test-sql/stu.sql"})
@Transactional
public class StuServiceTest {
@Autowired
private StuService stuService;
@Test
void test_list(){
List<Stu> stuList = stuService.list();
assertThat(stuList)
.hasSize(3)
.extracting("name")
.contains("张三","李四","王五");
}
}参考资料
[1] https://www.h2database.com/
[2] https://java.testcontainers.org/
[3] https://docs.spring.io/spring-boot/reference/testing/testcontainers.html