如何在运行时动态创建Spring DataSource并按需加入Spring上下文?
运行时动态创建并注册DataSource的可行方案
针对你这种启动时未知数据库名称、需要随客户端入驻动态创建数据源且无需重启应用的场景,完全可以通过AbstractRoutingDataSource扩展+Spring动态Bean注册的方式实现,具体方案如下:
核心实现思路
- 自定义
DynamicRoutingDataSource继承AbstractRoutingDataSource,根据请求头中的client-id作为路由键选择对应数据源 - 实现数据源管理类,负责缓存已创建的数据源、动态创建新数据源并注册到Spring上下文
- 通过请求拦截器提前触发数据源创建,确保请求到达业务层时数据源已就绪
具体代码实现
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
相关产品推荐
相关产品推荐

