如何用DataSourceLookup获取Spring Boot application.yml中配置的命名DataSource
需求明确:在Spring Boot REST接口中,根据请求传入的databaseName参数动态选择对应命名的数据源执行JDBC查询;要求在application.yml中配置多数据源,不依赖容器级JNDI,且连接池支持高并发。
原application.yml配置:
spring: datasource: source1: driver-class-name: com.mysql.cj.jdbc.Driver # 注意:mysql-connector-java 8+ 需使用该驱动类 url: jdbc:mysql://dbserver.local:3306/source1?useSSL=false&serverTimezone=UTC username: myuser password: mypass source2: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://dbserver.local:3306/source2?useSSL=false&serverTimezone=UTC username: myuser password: mypass
之前的错误原因
1. JndiDataSourceLookup的问题
你最初使用的JndiDataSourceLookup是为容器级JNDI数据源设计的,而独立运行的Spring Boot应用没有容器提供的JNDI上下文,执行时抛出如下错误:
javax.naming.NoInitialContextException: Need to specify class name in environment or system property, or in an application resource file: java.naming.factory.initial at java.naming/javax.naming.spi.NamingManager.getInitialContext(NamingManager.java:702) ~[na:na] at java.naming/javax.naming.InitialContext.getDefaultInitCtx(InitialContext.java:305) ~[na:na] at java.naming/javax.naming.InitialContext.getURLOrDefaultInitCtx(InitialContext.java:342) ~[na:na] at java.naming/javax.naming.InitialContext.lookup(InitialContext.java:409) ~[na:na] at org.springframework.jndi.JndiTemplate.lambda$lookup$0(JndiTemplate.java:157) ~[spring-context-5.3.30.jar:5.3.30] at org.springframework.jndi.JndiTemplate.execute(JndiTemplate.java:92) ~[spring-context-5.3.30.jar:5.3.30] at org.springframework.jndi.JndiTemplate.lookup(JndiTemplate.java:157) ~[spring-context-5.3.30.jar:5.3.30] at org.springframework.jndi.JndiTemplate.lookup(JndiTemplate.java:179) ~[spring-context-5.3.30.jar:5.3.30] at org.springframework.jndi.JndiLocatorSupport.lookup(JndiLocatorSupport.java:105) ~[spring-context-5.3.30.jar:5.3.30] at org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup.getDataSource(JndiDataSourceLookup.java:45) ~[spring-jdbc-5.3.30.jar:5.3.30]
该方案完全不符合“无需配置容器级JNDI资源”的需求。
2. BeanFactoryDataSourceLookup的问题
你后来尝试的BeanFactoryDataSourceLookup是从Spring容器中查找Bean,但Spring Boot不会自动将spring.datasource.source1这类嵌套配置转化为DataSourceBean,执行时抛出:
org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'source1' available
根本原因是source1对应的DataSourceBean未注册到Spring容器中。
正确实现方案
1. 选择高并发连接池
Spring Boot默认集成的HikariCP是当前性能最优的连接池,天生支持高并发,只需引入spring-boot-starter-jdbc依赖即可自动启用。
2. 编写配置类,注册多数据源Bean
手动读取yml中的多数据源配置,将每个数据源封装为HikariDataSource并注册为Spring Bean,Bean名称与yml中的数据源名称(source1、source2)保持一致。
@Configuration @ConfigurationProperties(prefix = "spring.datasource") public class MultiDataSourceConfig { private Map<String, DataSourceProperties> dataSources = new HashMap<>(); // 读取yml中spring.datasource下的所有子配置 public Map<String, DataSourceProperties> getDataSources() { return dataSources; } public void setDataSources(Map<String, DataSourceProperties> dataSources) { this.dataSources = dataSources; } // 为每个数据源创建并注册Bean @Bean public Map<String, DataSource> dataSourceMap() { Map<String, DataSource> dataSourceMap = new HashMap<>(); BeanDefinitionRegistry registry = (BeanDefinitionRegistry) ApplicationContextProvider.getApplicationContext(); for (Map.Entry<String, DataSourceProperties> entry : dataSources.entrySet()) { String dsName = entry.getKey(); DataSourceProperties properties = entry.getValue(); // 使用HikariCP构建数据源 HikariDataSource dataSource = properties.initializeDataSourceBuilder() .type(HikariDataSource.class) .build(); // 可额外配置HikariCP参数,适配高并发场景 dataSource.setMaximumPoolSize(20); dataSource.setConnectionTimeout(30000); dataSourceMap.put(dsName, dataSource); // 将数据源注册为Spring Bean,名称对应yml中的数据源名 BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(HikariDataSource.class); builder.addPropertyValue("driverClassName", dataSource.getDriverClassName()); builder.addPropertyValue("jdbcUrl", dataSource.getJdbcUrl()); builder.addPropertyValue("username", dataSource.getUsername()); builder.addPropertyValue("password", dataSource.getPassword()); builder.addPropertyValue("maximumPoolSize", dataSource.getMaximumPoolSize()); builder.addPropertyValue("connectionTimeout", dataSource.getConnectionTimeout()); registry.registerBeanDefinition(dsName, builder.getBeanDefinition()); } return dataSourceMap; } }
配套ApplicationContext工具类,用于获取上下文注册Bean:
@Component public class ApplicationContextProvider implements ApplicationContextAware { private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext context) throws BeansException { applicationContext = context; } public static ApplicationContext getApplicationContext() { return applicationContext; } }
3. 控制器中动态获取数据源
现在可以直接通过ApplicationContext根据名称获取对应的DataSourceBean,或使用BeanFactoryDataSourceLookup(此时Bean已存在,可正常查找)。
方式一:直接通过ApplicationContext获取
@RestController public class SQLDataController { private static final Logger log = LoggerFactory.getLogger(SQLDataController.class); private final ApplicationContext applicationContext; @Autowired public SQLDataController(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } @GetMapping(value = "/csv/**", produces = "text/csv") public void streamCsv(@RequestParam String databaseName) throws Exception { // 根据参数获取对应数据源 DataSource dataSource = applicationContext.getBean(databaseName, DataSource.class); try (Connection conn = dataSource.getConnection()) { // 执行JDBC查询、生成CSV输出等逻辑 // ... } catch (SQLException e) { log.error("获取数据库连接失败", e); throw new RuntimeException("数据库操作异常", e); } } }
方式二:使用BeanFactoryDataSourceLookup
@RestController public class SQLDataController { private static final Logger log = LoggerFactory.getLogger(SQLDataController.class); private final DataSourceLookup dataSourceLookup; @Autowired public SQLDataController(BeanFactory beanFactory) { this.dataSourceLookup = new BeanFactoryDataSourceLookup(beanFactory); } @GetMapping(value = "/csv/**", produces = "text/csv") public void streamCsv(@RequestParam String databaseName) throws Exception { DataSource dataSource = dataSourceLookup.getDataSource(databaseName); try (Connection conn = dataSource.getConnection()) { // 执行JDBC操作逻辑 // ... } catch (SQLException e) { log.error("数据库操作失败", e); throw new RuntimeException("数据库操作异常", e); } } }
关键说明
- 驱动类适配:mysql-connector-java 8.x版本必须使用
com.mysql.cj.jdbc.Driver,旧驱动类com.mysql.jdbc.Driver已废弃。 - 连接池参数配置:HikariCP的参数可直接在yml中针对每个数据源配置,例如:
spring: datasource: source1: hikari: maximum-pool-size: 25 connection-timeout: 20000 - Bean名称一致性:必须确保注册的DataSource Bean名称与yml中的数据源名称完全一致,才能实现动态匹配。
内容的提问来源于stack exchange,提问作者Kevin Day

