如何用多MariaDB容器测试Spring Boot多数据源应用?
背景
我正在开发一个Spring Boot应用,使用全局数据库和本地数据库(单实例多Schema),生产环境的数据库配置可正常运行(以下仅展示本地配置,全局配置结构一致,仅包名与Bean名称不同):
@Configuration @EnableTransactionManagement @EnableJpaRepositories( basePackages = "com.myapp.local", entityManagerFactoryRef = "localEntityManagerFactory", transactionManagerRef = "localTransactionManager" ) public class LocalDbConfig { @Value("${spring.datasource.local.url}") private String url; @Value("${spring.datasource.local.username}") private String username; @Value("${spring.datasource.local.password}") private String password; @Value("${spring.datasource.local.driver-class-name}") private String driverClassName; @Primary @Bean(name = "localDbDataSource") public DataSource localDbDataSource() { return DataSourceBuilder.create() .url(url) .username(username) .password(password) .driverClassName(driverClassName) .build(); } @Primary @Bean(name = "localEntityManagerFactory") public LocalContainerEntityManagerFactoryBean localEntityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("localDbDataSource") DataSource localDataSource) { Map<String, String> props = new HashMap<>(); props.put("hibernate.physical_naming_strategy", CamelCaseToUnderscoresNamingStrategy.class.getName()); return builder .dataSource(localDataSource) .packages("com.myapp.local") .properties(props) .build(); } @Primary @Bean(name = "localTransactionManager") public PlatformTransactionManager localTransactionManager(@Qualifier("localEntityManagerFactory") EntityManagerFactory localEntityManagerFactory) { return new JpaTransactionManager(localEntityManagerFactory); } }
问题现象
我尝试用两个MariaDB容器做集成测试,容器可正常启动,但应用始终抛出以下错误:
"Waiting for database connection to become available at jdbc:mysql://x.x.x.x:xxxx/test using query 'SELECT 1'"
现有TestContainers初始化配置
我的TestContainersInitializer代码如下:
@TestConfiguration public class TestContainersInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { private static final Network SHARED_NETWORK = Network.newNetwork(); private static final MariaDBContainer<?> localMariaDB = new MariaDBContainer<>(DockerImageName.parse("mariadb:latest")) .withNetwork(SHARED_NETWORK) .withDatabaseName("local_db") .withUsername("root") .withPassword("test") .withReuse(true); private static final MariaDBContainer<?> globalMariaDB = new MariaDBContainer<>(DockerImageName.parse("mariadb:latest")) .withNetwork(SHARED_NETWORK) .withDatabaseName("global_db") .withUsername("root") .withPassword("test") .withReuse(true); private static final KeycloakContainer keycloak = new KeycloakContainer() .withNetwork(SHARED_NETWORK) .withRealmImportFile("test-realm-export.json") .withAdminUsername("keycloakadmin") .withAdminPassword("keycloakadmin") .withReuse(true); private static final KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:latest")) .withNetwork(SHARED_NETWORK) .withReuse(true); static { Startables.deepStart(localMariaDB, globalMariaDB, keycloak, kafka).join(); } @Override public void initialize(@NotNull ConfigurableApplicationContext applicationContext) { TestPropertyValues.of( "spring.datasource.local.url=" + localMariaDB.getJdbcUrl(), "spring.datasource.local.username=" + localMariaDB.getUsername(), "spring.datasource.local.password=" + localMariaDB.getPassword(), "spring.datasource.local.driver-class-name=" + localMariaDB.getDriverClassName(), "spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect", "spring.datasource.global.url=" + globalMariaDB.getJdbcUrl(), "spring.datasource.global.username=" + globalMariaDB.getUsername(), "spring.datasource.global.password=" + globalMariaDB.getPassword(), "spring.datasource.global.driver-class-name=" + globalMariaDB.getDriverClassName(), "spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect", "keycloak.server-url=http://localhost:" + keycloak.getFirstMappedPort(), "spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:" + keycloak.getFirstMappedPort() + "/realms/app", "spring.kafka.bootstrap-servers=" + kafka.getBootstrapServers() ).applyTo(applicationContext.getEnvironment()); } }
在集成测试基类IntegrationTestBase中通过以下方式引用:
@ContextConfiguration(initializers = TestContainersInitializer.class)
尝试过的方案:独立测试数据库配置
我还编写了独立的测试数据库配置类,尝试覆盖生产环境的Bean:
@TestConfiguration public class TestDatabaseConfiguration { @Primary @Bean public DataSource localDataSource(Environment env) { return DataSourceBuilder.create() .url(env.getProperty("spring.datasource.local.url")) .username(env.getProperty("spring.datasource.local.username")) .password(env.getProperty("spring.datasource.local.password")) .driverClassName(env.getProperty("spring.datasource.local.driver-class-name")) .build(); } @Bean @Qualifier("globalDataSource") public DataSource globalDataSource(Environment env) { return DataSourceBuilder.create() .url(env.getProperty("spring.datasource.global.url")) .username(env.getProperty("spring.datasource.global.username")) .password(env.getProperty("spring.datasource.global.password")) .driverClassName(env.getProperty("spring.datasource.global.driver-class-name")) .build(); } @Primary @Bean public LocalContainerEntityManagerFactoryBean localEntityManagerFactory( EntityManagerFactoryBuilder builder, DataSource localDataSource) { Map<String, String> properties = new HashMap<>(); properties.put("hibernate.hbm2ddl.auto", "validate"); properties.put("hibernate.dialect", "org.hibernate.dialect.MariaDBDialect"); return builder .dataSource(localDataSource) .packages("com.utitech.bidhubbackend.local", "com.utitech.bidhubbackend.common.fileupload") .properties(properties) .build(); } @Bean public LocalContainerEntityManagerFactoryBean globalEntityManagerFactory( EntityManagerFactoryBuilder builder, @Qualifier("globalDataSource") DataSource globalDataSource) { Map<String, String> properties = new HashMap<>(); properties.put("hibernate.hbm2ddl.auto", "validate"); properties.put("hibernate.dialect", "org.hibernate.dialect.MariaDBDialect"); return builder .dataSource(globalDataSource) .packages("com.utitech.bidhubbackend.global", "com.utitech.bidhubbackend.common.fileupload") .properties(properties) .build(); } @Primary @Bean public PlatformTransactionManager localTransactionManager( @Qualifier("localEntityManagerFactory") EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager(entityManagerFactory); } @Bean public PlatformTransactionManager globalTransactionManager( @Qualifier("globalEntityManagerFactory") EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager(entityManagerFactory); } }
同时在IntegrationTestBase中添加属性spring.main.allow-bean-definition-overriding=true允许Bean覆盖,但问题依旧。
求助
需要采取哪些步骤解决连接问题,成功用多MariaDB容器测试应用?是否需要修改现有配置,或有更合适的多数据源测试方案?
解决方案
1. 移除容器共享网络
你给MariaDB容器添加的SHARED_NETWORK会干扰宿主机器到容器的端口映射逻辑——TestContainers默认会自动将容器端口映射到宿主随机端口,应用直接通过宿主端口连接即可,不需要共享网络(除非容器之间需要互相访问,而你的应用是在宿主运行)。
修改TestContainersInitializer,移除所有.withNetwork(SHARED_NETWORK)配置:
private static final MariaDBContainer<?> localMariaDB = new MariaDBContainer<>(DockerImageName.parse("mariadb:latest")) .withDatabaseName("local_db") .withUsername("root") .withPassword("test") .withReuse(true); private static final MariaDBContainer<?> globalMariaDB = new MariaDBContainer<>(DockerImageName.parse("mariadb:latest")) .withDatabaseName("global_db") .withUsername("root") .withPassword("test") .withReuse(true);
2. 增加容器就绪等待策略
Startables.deepStart会并行启动容器,但MariaDB启动需要时间,可能出现容器标记为启动但数据库服务未就绪的情况。给每个MariaDB容器添加日志等待策略,确保数据库完全可用:
private static final MariaDBContainer<?> localMariaDB = new MariaDBContainer<>(DockerImageName.parse("mariadb:latest")) .withDatabaseName("local_db") .withUsername("root") .withPassword("test") .withReuse(true) .waitingFor(Wait.forLogMessage(".*ready for connections.*", 1)); private static final MariaDBContainer<?> globalMariaDB = new MariaDBContainer<>(DockerImageName.parse("mariadb:latest")) .withDatabaseName("global_db") .withUsername("root") .withPassword("test") .withReuse(true) .waitingFor(Wait.forLogMessage(".*ready for connections.*", 1));
3. 修正数据库URL与方言配置
错误日志显示连接的是test数据库,说明容器的指定数据库未正确生效。可以手动拼接URL确保指向正确数据库:
"spring.datasource.local.url=jdbc:mariadb://localhost:" + localMariaDB.getMappedPort(3306) + "/local_db?useSSL=false&allowPublicKeyRetrieval=true", "spring.datasource.global.url=jdbc:mariadb://localhost:" + globalMariaDB.getMappedPort(3306) + "/global_db?useSSL=false&allowPublicKeyRetrieval=true",
同时,不要给两个数据源设置同一个全局方言属性,应分别配置:
// 本地数据源方言 "spring.jpa.local.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect", // 全局数据源方言 "spring.jpa.global.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect",
4. 移除冗余的测试配置类
你的生产配置已经通过@Value读取环境属性,TestContainers初始化器已经将容器配置注入到环境中,不需要额外编写TestDatabaseConfiguration覆盖Bean。删除该类,同时去掉spring.main.allow-bean-definition-overriding=true,让生产配置直接读取TestContainers注入的属性即可。
5. 固定MariaDB版本兼容性
mariadb:latest可能与你的JDBC驱动版本不兼容,建议指定稳定版本,比如10.6:
new MariaDBContainer<>(DockerImageName.parse("mariadb:10.6"))
内容的提问来源于stack exchange,提问作者Adrienn

