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

如何在运行时动态创建Spring DataSource并按需加入Spring上下文?

运行时动态创建并注册DataSource的可行方案

针对你这种启动时未知数据库名称、需要随客户端入驻动态创建数据源且无需重启应用的场景,完全可以通过AbstractRoutingDataSource扩展+Spring动态Bean注册的方式实现,具体方案如下:

核心实现思路

  1. 自定义DynamicRoutingDataSource继承AbstractRoutingDataSource,根据请求头中的client-id作为路由键选择对应数据源
  2. 实现数据源管理类,负责缓存已创建的数据源、动态创建新数据源并注册到Spring上下文
  3. 通过请求拦截器提前触发数据源创建,确保请求到达业务层时数据源已就绪

具体代码实现

1. 自定义动态路由数据源

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        // 从当前请求上下文获取客户端ID
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            return attributes.getRequest().getHeader("client-id");
        }
        return null;
    }
}

2. 数据源管理类(创建+注册+缓存)

@Component
public class DataSourceManager {
    // 缓存已创建的数据源,避免重复创建
    private final Map<String, DataSource> dataSourceCache = new ConcurrentHashMap<>();
    private final ConfigurableListableBeanFactory beanFactory;
    private final BeanDefinitionRegistry beanDefinitionRegistry;

    public DataSourceManager(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry beanDefinitionRegistry) {
        this.beanFactory = beanFactory;
        this.beanDefinitionRegistry = beanDefinitionRegistry;
    }

    // 根据客户端ID获取或创建数据源
    public DataSource getOrCreateDataSource(String clientId) {
        return dataSourceCache.computeIfAbsent(clientId, this::createAndRegisterDataSource);
    }

    private DataSource createAndRegisterDataSource(String clientId) {
        // 根据客户端ID拼接数据库连接信息(可根据实际配置调整)
        String dbUrl = String.format("jdbc:mysql://localhost:3306/db_%s?useSSL=false&serverTimezone=UTC", clientId);
        String username = "your_db_username";
        String password = "your_db_password";

        // 使用HikariCP连接池(性能最优的连接池之一)
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(dbUrl);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");

        // 将数据源注册到Spring上下文
        String beanName = "dataSource_" + clientId;
        BeanDefinitionBuilder beanBuilder = BeanDefinitionBuilder.genericBeanDefinition(HikariDataSource.class);
        beanBuilder.addPropertyValue("jdbcUrl", dbUrl);
        beanBuilder.addPropertyValue("username", username);
        beanBuilder.addPropertyValue("password", password);
        beanBuilder.addPropertyValue("driverClassName", "com.mysql.cj.jdbc.Driver");
        beanDefinitionRegistry.registerBeanDefinition(beanName, beanBuilder.getBeanDefinition());
        beanFactory.initializeBean(dataSource, beanName);

        // 更新路由数据源的目标映射并刷新
        DynamicRoutingDataSource routingDataSource = beanFactory.getBean(DynamicRoutingDataSource.class);
        Map<Object, Object> targetDataSources = new HashMap<>(routingDataSource.getTargetDataSources());
        targetDataSources.put(clientId, dataSource);
        routingDataSource.setTargetDataSources(targetDataSources);
        routingDataSource.afterPropertiesSet(); // 触发Spring刷新数据源配置

        return dataSource;
    }
}

3. 数据源配置类

@Configuration
public class DataSourceConfig {
    @Bean
    public DynamicRoutingDataSource dynamicRoutingDataSource() {
        DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
        // 设置默认数据源(比如存储公共配置的数据库)
        routingDataSource.setDefaultTargetDataSource(defaultDataSource());
        routingDataSource.setTargetDataSources(new HashMap<>());
        return routingDataSource;
    }

    @Bean
    public DataSource defaultDataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/common_db?useSSL=false&serverTimezone=UTC");
        dataSource.setUsername("your_db_username");
        dataSource.setPassword("your_db_password");
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return dataSource;
    }
}

4. 请求拦截器(提前触发数据源创建)

@Component
public class DataSourceInterceptor implements HandlerInterceptor {
    private final DataSourceManager dataSourceManager;

    public DataSourceInterceptor(DataSourceManager dataSourceManager) {
        this.dataSourceManager = dataSourceManager;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String clientId = request.getHeader("client-id");
        if (clientId != null) {
            // 提前创建并注册数据源
            dataSourceManager.getOrCreateDataSource(clientId);
        }
        return true;
    }
}

// 配置拦截器生效
@Configuration
public class WebConfig implements WebMvcConfigurer {
    private final DataSourceInterceptor dataSourceInterceptor;

    public WebConfig(DataSourceInterceptor dataSourceInterceptor) {
        this.dataSourceInterceptor = dataSourceInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(dataSourceInterceptor).addPathPatterns("/**");
    }
}

关键注意事项

  • 必须使用连接池:绝对不能每次请求创建新的JDBC连接,一定要用HikariCP这类成熟的连接池,避免性能崩溃
  • 校验客户端合法性:在拦截器或入驻接口中要校验client-id的有效性,防止恶意请求触发不存在的数据库连接
  • 数据库前置创建:客户端入驻流程中要先完成对应数据库的创建(比如执行CREATE DATABASE db_xxx),再允许后续请求创建数据源
  • 缓存线程安全:用ConcurrentHashMap做缓存,保证多线程下的数据源创建线程安全

内容的提问来源于stack exchange,提问作者orion_kid

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 19:40:26