You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用多MariaDB容器测试Spring Boot多数据源应用?

多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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 10:30:55