DataSource 自动配置与 HikariCP
概述
Spring Boot 通过 DataSourceAutoConfiguration 自动配置 DataSource,默认使用 HikariCP 连接池。DataSourceProperties 提供配置绑定层,HikariDataSource 底层通过 HikariPool 管理连接,ConcurrentBag 实现高性能的并发连接分配。
本文将深入拆解 DataSource 自动配置与 HikariCP 的完整链路,涵盖 12 个细节点。
本文基于 Spring Boot 3.x + HikariCP 5.x 源码分析。
1. DataSourceAutoConfiguration 的 @ConditionalOnClass
1.1 源码
java
// DataSourceAutoConfiguration.java
@AutoConfiguration(
before = { SqlInitializationAutoConfiguration.class },
after = { DataSourceTransactionManagerAutoConfiguration.class })
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
// ...
}1.2 两个条件的含义
java
// 条件 1: @ConditionalOnClass(DataSource.class)
// DataSource.class 位于 javax.sql / java.sql 包
// JDK 自带,始终存在
// 所以这个条件恒为 true
// 条件 2: @ConditionalOnClass(EmbeddedDatabaseType.class)
// EmbeddedDatabaseType.class 位于 spring-jdbc 包
// 确保 spring-jdbc 在 classpath
// 依赖: spring-boot-starter-jdbc 或 spring-boot-starter-data-jpa
// 实际上这两个条件在 Web 应用中几乎总是满足
// 因为 spring-boot-starter-web 会间接依赖 spring-jdbc1.3 自动配置的执行顺序
DataSourceTransactionManagerAutoConfiguration ← 第 1 步
│ 创建 DataSourceTransactionManager
│
DataSourceAutoConfiguration ← 第 2 步
│
├─ EmbeddedDatabaseConfiguration
│ │ 条件: @ConditionalOnMissingBean(DataSource.class)
│ │ 作用: 没有 DataSource 时创建内嵌数据库
│ │ H2 或 HSQLDB
│ │
└─ PooledDataSourceConfiguration
│ 条件: @ConditionalOnMissingBean(DataSource.class)
│ 作用: 没有 DataSource 时创建连接池
│
├─ DataSourceConfiguration.Hikari
│ 条件: @ConditionalOnClass(HikariDataSource.class)
│ 默认: HikariCP 在 classpath → 自动选择 Hikari
│
├─ DataSourceConfiguration.Tomcat
│ 条件: @ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class)
│
├─ DataSourceConfiguration.Dbcp2
│ 条件: @ConditionalOnClass(org.apache.commons.dbcp2.BasicDataSource.class)
│
└─ DataSourceConfiguration.OracleUcp
条件: @ConditionalOnClass(OracleDataSource.class)
SqlInitializationAutoConfiguration ← 第 3 步
│ 创建 DataSourceInitializerInvoker
│ 执行 schema.sql 和 data.sql2. DataSourceConfiguration.Hikari 内部配置类
2.1 源码
java
// DataSourceConfiguration.java
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(
name = "spring.datasource.type",
havingValue = "com.zaxxer.hikari.HikariDataSource",
matchIfMissing = true) // 默认使用 Hikari
static class Hikari {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
HikariDataSource dataSource(DataSourceProperties properties) {
// 1. 从 DataSourceProperties 创建 HikariDataSource
HikariDataSource dataSource = createDataSource(properties,
HikariDataSource.class);
// 2. 设置 DataSourceProperties 的公共属性
// url, username, password, driverClassName
// 3. 设置 spring.datasource.hikari.* 的子属性
// maximumPoolSize, connectionTimeout, idleTimeout 等
return dataSource;
}
}2.2 三个条件的作用
java
// 条件 1: @ConditionalOnClass(HikariDataSource.class)
// 检查 classpath 是否有 HikariCP
// 依赖: spring-boot-starter-jdbc → HikariCP 自动包含
// 条件 2: @ConditionalOnMissingBean(DataSource.class)
// 用户自定义 DataSource → 跳过自动配置
// 例如:
// @Bean
// public DataSource dataSource() {
// return new MyCustomDataSource();
// }
// → @ConditionalOnMissingBean 检测到 → 跳过
// 条件 3: @ConditionalOnProperty(... matchIfMissing = true)
// 未配置 spring.datasource.type → 使用 Hikari(默认)
// 配置 spring.datasource.type=com.zaxxer.hikari.HikariDataSource → 使用 Hikari
// 配置 spring.datasource.type=org.apache.tomcat.jdbc.pool.DataSource → 使用 Tomcat 连接池
// 配置 spring.datasource.type=org.apache.commons.dbcp2.BasicDataSource → 使用 DBCP23. DataSourceProperties 到 HikariConfig 的属性映射
3.1 源码
java
// DataSourceProperties.java
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware {
// 通用 JDBC 属性
private String name = "testdb";
private String url; // JDBC URL
private String username; // 用户名
private String password; // 密码
private String driverClassName; // 驱动类名(可自动推断)
private String schema; // schema.sql 所在路径
// 连接池类型
private Class<? extends DataSource> type;
// 创建 DataSource
public <T extends DataSource> T initializeDataSourceBuilder()
throws Exception {
// 1. 创建 DataSourceBuilder
DataSourceBuilder<?> builder = DataSourceBuilder.create();
// 2. 设置公共属性
if (this.url != null) {
builder.url(this.url); // → HikariConfig.setJdbcUrl()
}
if (this.username != null) {
builder.username(this.username); // → HikariConfig.setUsername()
}
if (this.password != null) {
builder.password(this.password); // → HikariConfig.setPassword()
}
if (this.driverClassName != null) {
builder.driverClassName(this.driverClassName); // → HikariConfig.setDriverClassName()
}
// 3. 如果未指定 driverClassName,从 url 自动推断
// 例如: jdbc:mysql://... → com.mysql.cj.jdbc.Driver
return (T) builder.build();
}
}3.2 属性映射对照表
yaml
# application.yml 中的配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/db # → HikariConfig.jdbcUrl
username: root # → HikariConfig.username
password: secret # → HikariConfig.password
driver-class-name: com.mysql.cj.jdbc.Driver # → HikariConfig.driverClassName
hikari:
pool-name: MyPool # → HikariConfig.poolName
maximum-pool-size: 10 # → HikariConfig.maximumPoolSize
connection-timeout: 30000 # → HikariConfig.connectionTimeout
idle-timeout: 600000 # → HikariConfig.idleTimeout
max-lifetime: 1800000 # → HikariConfig.maxLifetime
minimum-idle: 5 # → HikariConfig.minimumIdlespring.datasource.* | HikariConfig 属性 | 说明 |
|---|---|---|
url | jdbcUrl | JDBC URL |
username | username | 用户名 |
password | password | 密码 |
driver-class-name | driverClassName | JDBC 驱动 |
hikari.pool-name | poolName | 连接池名称 |
hikari.maximum-pool-size | maximumPoolSize | 最大连接数 |
hikari.connection-timeout | connectionTimeout | 连接超时(ms) |
hikari.idle-timeout | idleTimeout | 空闲超时(ms) |
hikari.max-lifetime | maxLifetime | 最大存活时间(ms) |
hikari.minimum-idle | minimumIdle | 最小空闲连接数 |
4. spring.datasource.hikari.* 的子属性注入
4.1 绑定过程
java
// DataSourceConfiguration.Hikari.dataSource()
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
HikariDataSource dataSource(DataSourceProperties properties) {
// 1. 从 DataSourceProperties 创建 HikariDataSource
HikariDataSource dataSource = properties
.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
// 2. 设置公共属性
if (properties.getUrl() != null) {
dataSource.setJdbcUrl(properties.getUrl());
}
if (properties.getUsername() != null) {
dataSource.setUsername(properties.getUsername());
}
if (properties.getPassword() != null) {
dataSource.setPassword(properties.getPassword());
}
if (properties.getDriverClassName() != null) {
dataSource.setDriverClassName(properties.getDriverClassName());
}
// 3. 这里返回的 dataSource 会被 @ConfigurationProperties(prefix="spring.datasource.hikari") 拦截
// Spring 通过 Binder 将 spring.datasource.hikari.* 绑定到 HikariDataSource 的属性
// 例如: spring.datasource.hikari.maximum-pool-size=10 → dataSource.setMaximumPoolSize(10)
//
// 绑定过程:
// 1. @ConfigurationProperties(prefix="spring.datasource.hikari")
// 2. Binder.bind("spring.datasource.hikari", Bindable.of(HikariDataSource.class))
// 3. RelaxedNames 匹配: maximum-pool-size → maximumPoolSize
// 4. setMaximumPoolSize(10)
return dataSource;
}4.2 完整的配置绑定链
application.yml:
spring:
datasource:
url: jdbc:mysql://localhost:3306/db
hikari:
maximum-pool-size: 10
connection-timeout: 5000
idle-timeout: 300000
max-lifetime: 600000
绑定过程:
│
├─ DataSourceProperties
│ @ConfigurationProperties(prefix = "spring.datasource")
│ url = "jdbc:mysql://localhost:3306/db"
│
├─ DataSourceBuilder.build() → HikariDataSource()
│ 无参构造,此时 HikariConfig 的默认值:
│ maximumPoolSize = 10(HikariCP 默认)
│ connectionTimeout = 30000
│ idleTimeout = 600000
│ maxLifetime = 1800000
│
└─ @ConfigurationProperties(prefix = "spring.datasource.hikari")
Binder.bind():
maximum-pool-size → 10 (与默认值相同)
connection-timeout → 5000 (覆盖默认 30000)
idle-timeout → 300000 (覆盖默认 600000)
max-lifetime → 600000 (覆盖默认 1800000)5. HikariDataSource() 无参构造
5.1 源码
java
// HikariDataSource.java
public class HikariDataSource extends HikariConfig implements DataSource {
// 连接池实例(懒加载)
private volatile HikariPool pool;
// 是否已关闭
private volatile boolean isShutdown;
// 记录启动状态,防止重复初始化
private final AtomicBoolean poolCreated = new AtomicBoolean(false);
// 无参构造(由 Spring Boot 调用)
public HikariDataSource() {
// 只调用父类 HikariConfig 的无参构造
// 初始化默认配置:
// connectionTimeout = 30000 (30s)
// idleTimeout = 600000 (10min)
// maxLifetime = 1800000 (30min)
// maximumPoolSize = 10
// minimumIdle = 10 (与 maximumPoolSize 一致)
// poolName = "HikariPool-1" (自动生成)
super();
}
// 带配置的构造
public HikariDataSource(HikariConfig configuration) {
// 复制配置
configuration.validate();
configuration.copyStateTo(this);
// ...log poolName
}
}5.2 afterPropertiesSet() 中的初始化
java
// HikariDataSource.java
// HikariDataSource 实现了 InitializingBean(Spring 接口)
// 在 Spring Boot 中,@Bean 的 init-method 会触发此方法
@Override
public void afterPropertiesSet() throws Exception {
// 延迟初始化 HikariPool
// 只有当 pool 为 null 且未被关闭时才创建
if (this.pool == null && !this.isShutdown) {
getConnection(); // 触发连接池创建
}
}
// 第一次获取连接时也会创建 HikariPool
@Override
public Connection getConnection() throws SQLException {
// 1. 检查是否已关闭
if (isShutdown) {
throw new SQLException("HikariDataSource is closed");
}
// 2. 懒加载: 首次调用时创建 HikariPool
if (pool == null) {
// 使用 CAS 确保只创建一次
if (poolCreated.compareAndSet(false, true)) {
// 创建 HikariPool
// HikariPool 的构造器会执行完整的初始化:
// - 验证配置
// - 创建 ConcurrentBag
// - 创建 HouseKeeper(定时维护任务)
// - 填充初始连接(minimumIdle 个)
this.pool = new HikariPool(this);
}
}
// 3. 从连接池获取连接
// 委托给 HikariPool.getConnection()
return pool.getConnection();
}5.3 懒加载流程
Spring 容器启动:
│
├─ 1. HikariDataSource 无参构造
│ → 只设置 HikariConfig 默认值
│ → pool = null(尚未创建 HikariPool)
│
├─ 2. @ConfigurationProperties 绑定子属性
│ → spring.datasource.hikari.* → 覆盖配置
│
├─ 3. afterPropertiesSet() 被调用
│ → 调用 getConnection() → 创建 HikariPool
│ → 或者 getConnection() 在首次数据访问时调用
│
├─ 4. HikariPool 初始化
│ → 创建连接到数据库
│ → 填充 minimumIdle 个连接
│ → 启动 HouseKeeper 定时任务
│
└─ 5. 后续 getConnection() 从 HikariPool 获取6. HikariPool 初始化流程
6.1 源码
java
// HikariPool.java
public class HikariPool extends PoolBase
implements HikariPoolMXBean, IBagStateListener {
// ConcurrentBag — 连接容器
public final ConcurrentBag<PoolEntry> connectionBag;
// 连接超时等待器
private final SynchronousQueue<PoolEntry> handoffQueue;
// 定时维护任务
private final ScheduledExecutorService houseKeeper;
// HouseKeeper 定时任务
private final ScheduledFuture<?> houseKeeperTask;
public HikariPool(final HikariConfig config) {
// 1. 调用父类 PoolBase 构造 — 验证配置
super(config);
// 2. 验证配置有效性
this.config.validate();
// 3. 创建 ConcurrentBag(无锁并发容器)
this.connectionBag = new ConcurrentBag<>(this);
// 4. 创建 handoffQueue(用于等待连接时的线程间传递)
this.handoffQueue = new SynchronousQueue<>(true); // fair=true
// 5. 创建 HouseKeeper 线程池(单线程定时调度)
this.houseKeeper = createHouseKeeperExecutor();
// 6. 创建连接池统计信息
this.poolStats = new PoolStats(config.getMaximumPoolSize());
// 7. 填充初始连接
// 根据 minimumIdle 创建初始连接数
fillPool();
// 8. 启动 HouseKeeper 定时任务
// 周期: 根据 idleTimeout 和 maxLifetime 计算
// 任务: 检查连接有效性、移除过期连接、补充最小连接数
this.houseKeeperTask = this.houseKeeper.scheduleWithFixedDelay(
new HouseKeeper(), // 维护任务
100L, // 初始延迟
houseKeepingPeriodMs, // 执行周期
MILLISECONDS
);
}
// 从连接池获取连接
public Connection getConnection(final long hardTimeout) throws SQLException {
// 1. 检查连接池状态
// 2. 从 ConcurrentBag 借出连接
// 3. 如果无可用连接 → 创建新连接(未超过 maximumPoolSize)
// 4. 如果超过 maximumPoolSize → 等待其他线程归还
// 5. 如果在 connectionTimeout 内等待超时 → 抛出 SQLException
// 详见第 7、8 节
}
}6.2 初始化流程图示
new HikariPool(config)
│
├─ 1. PoolBase(config)
│ ├─ 保存 config 引用
│ ├─ 创建 DataSource(从 config.getDataSource() 或 DriverManager)
│ └─ 验证 config
│
├─ 2. new ConcurrentBag<>(this)
│ ├─ 创建 ThreadLocal 列表(每个线程的本地缓存)
│ ├─ 创建共享列表(CopyOnWriteArrayList)
│ └─ 创建等待队列(SynchronousQueue)
│
├─ 3. new SynchronousQueue<>(true)
│ └─ 公平模式的线程间交接队列
│
├─ 4. fillPool()
│ └─ 创建 minimumIdle 个连接
│ ├─ PoolEntry.create() → DriverManager.getConnection()
│ ├─ 包装为 ConnectionProxy(含泄漏检测)
│ └─ 添加到 ConcurrentBag
│
└─ 5. HouseKeeper 定时任务启动
└─ 周期执行:
├─ 检查连接有效性
├─ 移除超过 maxLifetime 的连接
└─ 补充到 minimumIdle7. ConcurrentBag.borrow() 的实现
7.1 源码
java
// ConcurrentBag.java
public class ConcurrentBag<T extends IConcurrentBagEntry>
implements AutoCloseable {
// 共享列表(所有连接)
private final CopyOnWriteArrayList<T> sharedList;
// 线程本地缓存(无锁)
private final ThreadLocal<List<Object>> threadLocalList;
// 等待队列(连接不足时)
private final SynchronousQueue<T> handoffQueue;
@Override
public T borrow(long timeout, final TimeUnit timeUnit)
throws InterruptedException {
// 1. 先尝试从 ThreadLocal 获取(无锁)
List<Object> list = threadLocalList.get();
for (int i = list.size() - 1; i >= 0; i--) {
final Object entry = list.remove(i);
@SuppressWarnings("unchecked")
final T bagEntry = (T) entry;
// 尝试标记为已借出
// CAS 操作: STATE_NOT_IN_USE → STATE_IN_USE
if (bagEntry.compareAndSet(
IConcurrentBagEntry.STATE_NOT_IN_USE,
IConcurrentBagEntry.STATE_IN_USE)) {
return bagEntry; // 借出成功(无锁)
}
}
// 2. ThreadLocal 没有可用连接 → 遍历共享列表
final int waiting = sharedList.size();
for (int i = 0; i < waiting; i++) {
final T bagEntry = sharedList.get(i);
// CAS 标记
if (bagEntry.compareAndSet(
IConcurrentBagEntry.STATE_NOT_IN_USE,
IConcurrentBagEntry.STATE_IN_USE)) {
// 加入线程本地缓存
list.add(bagEntry);
return bagEntry;
}
}
// 3. ThreadLocal 和共享列表都没有 → 等待
// 阻塞在 handoffQueue 上
// 超时时间由 connectionTimeout 控制
while (timeout > 0) {
final T bagEntry = handoffQueue.poll(timeout, timeUnit);
if (bagEntry == null) {
break; // 超时
}
// CAS 标记
if (bagEntry.compareAndSet(
IConcurrentBagEntry.STATE_NOT_IN_USE,
IConcurrentBagEntry.STATE_IN_USE)) {
return bagEntry; // 从等待队列借出成功
}
// CAS 失败 → 继续等待
}
// 4. 所有方式都失败 → 返回 null
return null;
}
}7.2 三个借出层次的对比
| 层次 | 数据结构 | 锁 | 速度 | 触发条件 |
|---|---|---|---|---|
| 第 1 层 | ThreadLocal<List> | 无锁 | 最快 | 线程之前借过并归还过连接 |
| 第 2 层 | CopyOnWriteArrayList | 无锁(CAS) | 中等 | ThreadLocal 中没有 |
| 第 3 层 | SynchronousQueue | 阻塞 | 最慢 | 所有连接都在使用中 |
7.3 连接状态转换
┌─────────────────────────────────────────────┐
│ STATE_NOT_IN_USE (0) │
│ (空闲,在池中) │
└──────────┬──────────────────────┬───────────┘
│ │
borrow() CAS remove()
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ STATE_IN_USE (1) │ │ 被移除连接池 │
│ (已借出) │ │ │
└────────┬─────────┘ └──────────────────┘
│
close() / 归还
│
▼
STATE_NOT_IN_USE
(归还到池中)8. connectionTimeout=30000ms 的超时实现
8.1 源码
java
// HikariPool.java
public Connection getConnection(final long hardTimeout) throws SQLException {
// 默认: hardTimeout = config.getConnectionTimeout() = 30000ms
// 1. 记录开始时间
final long startTime = currentTime();
// 2. 从 ConcurrentBag 借出连接
PoolEntry poolEntry = connectionBag.borrow(
hardTimeout, MILLISECONDS);
// 3. 检查是否超时
if (poolEntry == null) {
// borrow 返回 null → 没有可用连接
// 且等待了 connectionTimeout 时间
// 注意:borrow 返回 null 还有可能因为
// 发生了 InterruptedException
if (connectionBag.getWaitingThreadCount() > 0) {
// 还有线程在等待 → 超时
throw new SQLTimeoutException(
"Timeout after " + hardTimeout + "ms of waiting for a connection.");
}
// 没有线程等待 → 继续处理
}
// 4. 验证连接的可用性
// 检查连接是否有效,无效则关闭并重试
if (poolEntry.isEvicted() || (connectionBag.isConnectionDead(poolEntry))) {
// 连接已废弃 → 关闭并尝试获取新连接
closeConnection(poolEntry, "(connection was evicted)");
return getConnection(hardTimeout - elapsedTime); // 递归重试
}
// 5. 重置泄漏检测
poolEntry.getProxyLeakTask().cancel(); // 取消之前的泄漏检测任务
// 6. 设置泄漏检测(如果开启)
final long leakDetectionThreshold = config.getLeakDetectionThreshold();
if (leakDetectionThreshold > 0) {
// 创建新的泄漏检测定时任务
poolEntry.setProxyLeakTask(
new ProxyLeakTask(config, poolEntry));
}
// 7. 返回代理连接
return poolEntry.createProxyConnection();
}8.2 SynchronousQueue.poll() 的超时等待
java
// ConcurrentBag.borrow() 中的等待逻辑
// SynchronousQueue 的特性:
// - 容量为 0
// - put() 操作必须等待 take() 操作
// - take() 操作必须等待 put() 操作
// - 适用于线程间直接交接(handoff)
// 在 HikariCP 中的使用:
//
// 线程 A 要借连接:
// bag.borrow(timeout):
// → 遍历 ThreadLocal 和 sharedList 都没有
// → handoffQueue.poll(connectionTimeout, MILLISECONDS)
// → 线程 A 阻塞等待,最多等 30000ms
//
// 线程 B 归还连接:
// bag.return(entry):
// → handoffQueue.offer(entry)
// → 线程 B 将连接直接交接给线程 A
// → 线程 A 从 poll() 返回
//
// 如果线程 A 等满 30000ms 仍无人归还:
// → poll() 返回 null
// → borrow() 返回 null
// → getConnection() 抛出 SQLTimeoutException
// 超时间隔与等待线程数的关系:
// connectionTimeout = 30000ms
// 某时刻有 5 个线程都在等连接
// 没有线程归还 → 5 个线程都会等满 30000ms
// 各线程轮流超时,每次超时抛出异常
// 因此建议谨慎设置 connectionTimeout 不要过小9. leakDetectionThreshold=0 关闭泄漏检测
9.1 源码
java
// ProxyLeakTask.java
public class ProxyLeakTask implements Runnable {
// 默认: 0(关闭泄漏检测)
private static final long LEAK_DETECTION_THRESHOLD = 0;
// 泄漏检测任务
private ScheduledFuture<?> scheduledFuture;
// 连接条目
private PoolEntry poolEntry;
// 创建连接时的堆栈(用于定位泄漏点)
private final Exception leakException;
public ProxyLeakTask(final HikariConfig config,
final PoolEntry poolEntry) {
// 仅在连接被借出时设置
this.leakException = new Exception("Apparent connection leak detected");
this.poolEntry = poolEntry;
}
@Override
public void run() {
// 定时任务触发 → 打印泄漏警告
// 不关闭连接(防止影响正常使用)
LOGGER.warn("Connection leak detection triggered for {}",
poolEntry.toString());
// 打印创建连接时的堆栈
LOGGER.warn("Last connection stack trace:", leakException);
}
// 取消泄漏检测(连接归还时调用)
public void cancel() {
if (scheduledFuture != null) {
scheduledFuture.cancel(false);
}
}
}9.2 泄漏检测的完整流程
设置 leakDetectionThreshold = 30000 (30s) 时:
│
├─ 连接被借出:
│ └─ getConnection() → createProxyConnection()
│ └─ poolEntry.setProxyLeakTask(
│ new ProxyLeakTask(config, poolEntry))
│ └─ scheduledFuture = houseKeeper.schedule(
│ proxyLeakTask,
│ leakDetectionThreshold, // 30s 后触发
│ MILLISECONDS)
│
├─ 连接被正常归还:
│ └─ connection.close() → proxyConnection.close()
│ └─ poolEntry.getProxyLeakTask().cancel()
│ └─ scheduledFuture.cancel(false)
│ → 定时任务取消,不会触发警告
│
└─ 连接泄漏(30s 后):
└─ scheduledFuture 触发
└─ ProxyLeakTask.run()
├─ WARN: "Connection leak detected"
└─ 打印借出时的堆栈跟踪
→ 可以定位哪段代码未关闭连接9.3 配置建议
yaml
spring:
datasource:
hikari:
leak-detection-threshold: 0 # 关闭泄漏检测(默认)
# leak-detection-threshold: 30000 # 30s 未归还 → 警告
# 建议:
# - 开发环境: 设置 30000 (30s),帮助发现连接泄漏
# - 生产环境: 保持 0(关闭),避免性能开销
# 除非怀疑有连接泄漏问题10. EmbeddedDatabaseConfiguration 的 H2 自动切换
10.1 源码
java
// DataSourceAutoConfiguration.java
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(
prefix = "spring.datasource",
name = "url",
havingValue = "true",
matchIfMissing = true)
static class EmbeddedDatabaseConfiguration {
@Bean
@ConditionalOnMissingBean
DataSource dataSource(DataSourceProperties properties) {
// 1. 创建 EmbeddedDatabaseBuilder
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
// 2. 检测可用的内嵌数据库
// 按优先级检测:
// H2 (推荐) → HSQLDB → Derby
//
// 检测条件: classpath 存在对应驱动
// H2: org.h2.Driver
// HSQLDB: org.hsqldb.jdbcDriver
// Derby: org.apache.derby.jdbc.EmbeddedDriver
// 3. 设置名称和类型
return builder
.setName(properties.getName()) // 默认: testdb
.setType(EmbeddedDatabaseType.H2) // 自动检测
.build();
}
}10.2 内嵌数据库的检测逻辑
java
// EmbeddedDatabaseBuilder.java
public EmbeddedDatabaseBuilder setType(
EmbeddedDatabaseType databaseType) {
// 根据 classpath 存在的数据库决定
// 检测顺序:
try {
Class.forName("org.h2.Driver");
// H2 在 classpath → 使用 H2
this.databaseType = EmbeddedDatabaseType.H2;
} catch (ClassNotFoundException ex) {
try {
Class.forName("org.hsqldb.jdbcDriver");
// HSQLDB 在 classpath → 使用 HSQLDB
this.databaseType = EmbeddedDatabaseType.HSQLDB;
} catch (ClassNotFoundException ex2) {
try {
Class.forName("org.apache.derby.jdbc.EmbeddedDriver");
// Derby 在 classpath → 使用 Derby
this.databaseType = EmbeddedDatabaseType.DERBY;
} catch (ClassNotFoundException ex3) {
throw new IllegalStateException(
"No embedded database found. Please add H2, HSQLDB, or Derby to classpath.");
}
}
}
return this;
}10.3 使用注意
yaml
# 场景 1: 开发测试(自动使用 H2)
spring:
datasource:
url: jdbc:h2:mem:testdb # 可以不配置,自动使用内嵌
# 不配置 url、username、password
# → DataSourceAutoConfiguration.EmbeddedDatabaseConfiguration 生效
# → 自动创建 H2 内嵌数据库
# 场景 2: 显示指定内嵌数据库
spring:
datasource:
url: jdbc:h2:mem:mydb
username: sa
password:
driver-class-name: org.h2.Driver
# 场景 3: 使用 MySQL
spring:
datasource:
url: jdbc:mysql://localhost:3306/db
username: root
password: secret
driver-class-name: com.mysql.cj.jdbc.Driver
# 配置了 url → EmbeddedDatabaseConfiguration 不生效
# → PooledDataSourceConfiguration.Hikari 生效11. DataSourceInitializerInvoker 执行 schema.sql
11.1 源码
java
// DataSourceInitializerInvoker.java
public class DataSourceInitializerInvoker
implements InitializingBean {
// DataSourceInitializer — 实际执行 SQL 的组件
private DataSourceInitializer dataSourceInitializer;
@Override
public void afterPropertiesSet() {
// 1. 获取 DataSource
DataSource dataSource = this.dataSource;
// 2. 创建 DataSourceInitializer
this.dataSourceInitializer = new DataSourceInitializer(
dataSource, this.properties);
// 3. 执行 schema.sql
// 在 @PostConstruct 阶段
// 早于 @PostConstruct 标注的方法
initializeSchema();
}
// 执行 schema.sql
private void initializeSchema() {
// 1. 创建 ResourceDatabasePopulator(SQL 执行器)
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
// 2. 加载 schema.sql
// 搜索路径:
// - classpath:schema.sql
// - classpath:schema-${platform}.sql
// - spring.sql.init.schema-locations 指定的位置
Resource schemaResource = getSchemaResource();
if (schemaResource != null && schemaResource.exists()) {
populator.addScript(schemaResource);
}
// 3. 执行 SQL 脚本
DatabasePopulatorUtils.execute(populator, this.dataSource);
// 4. 日志
if (schemaResource != null) {
logger.info("Executed SQL script from " + schemaResource);
}
}
}11.2 ResourceDatabasePopulator 的执行
java
// ResourceDatabasePopulator.java
public class ResourceDatabasePopulator
implements DatabasePopulator, ResourceLoaderAware {
// 要执行的 SQL 脚本资源列表
private List<Resource> scripts = new ArrayList<>();
// 是否在出错时继续
private boolean continueOnError = false;
// SQL 语句分隔符
private String separator = ";";
@Override
public void populate(Connection connection)
throws ScriptException {
// 1. 创建 ScriptUtils
// 2. 逐个执行脚本
for (Resource script : this.scripts) {
// 读取 SQL 脚本内容
String sql = readScript(script);
// 按分隔符拆分为单条 SQL 语句
String[] statements = splitSqlScript(sql, separator);
// 逐条执行
for (String statement : statements) {
if (statement.trim().isEmpty()) {
continue;
}
try (Statement stmt = connection.createStatement()) {
stmt.execute(statement.trim());
} catch (SQLException ex) {
if (!continueOnError) {
throw new ScriptException(
"Failed to execute SQL script", ex);
}
// continueOnError = true → 忽略错误
}
}
}
}
}11.3 完整执行流程
Spring 容器启动
│
├─ DataSource 初始化完成后
│
├─ DataSourceInitializerInvoker.afterPropertiesSet()
│ │
│ ├─ 创建 DataSourceInitializer
│ │
│ └─ initializeSchema()
│ │
│ ├─ 1. 检查 spring.sql.init.mode
│ │ ├─ always → 总是执行
│ │ ├─ embedded → 仅内嵌数据库执行
│ │ └─ never → 跳过
│ │
│ ├─ 2. 加载 schema.sql 资源
│ │ ├─ classpath:schema.sql
│ │ ├─ classpath:schema-all.sql
│ │ └─ classpath:schema-${platform}.sql
│ │
│ ├─ 3. 创建 ResourceDatabasePopulator
│ │ ├─ continueOnError: 是否忽略错误
│ │ └─ separator: SQL 语句分隔符(;)
│ │
│ ├─ 4. 执行 SQL 脚本
│ │ ├─ DROP TABLE IF EXISTS ...
│ │ ├─ CREATE TABLE user (...)
│ │ └─ CREATE INDEX ...
│ │
│ └─ 5. 执行 data.sql(如果存在)
│ ├─ INSERT INTO user VALUES (...)
│ └─ ...
│
└─ Bean 初始化完成12. spring.sql.init.mode=embedded 的执行条件
12.1 源码
java
// SqlInitializationProperties.java
@ConfigurationProperties(prefix = "spring.sql.init")
public class SqlInitializationProperties {
/**
* SQL 脚本执行模式。
* - embedded: 仅对内嵌数据库执行(默认)
* - always: 始终执行(包括非内嵌)
* - never: 从不执行
*/
private DatabaseInitializationMode mode =
DatabaseInitializationMode.EMBEDDED;
}
// DataSourceInitializerInvoker 中的判断
private boolean isEnabled() {
// 1. 获取执行模式
DatabaseInitializationMode mode = this.properties.getMode();
// 2. 判断是否应执行
switch (mode) {
case ALWAYS:
return true; // 始终执行
case NEVER:
return false; // 从不执行
case EMBEDDED:
// 仅当 DataSource 是内嵌数据库时执行
return EmbeddedDatabaseConnection.isEmbedded(this.dataSource);
default:
return false;
}
}12.2 内嵌数据库的检测
java
// EmbeddedDatabaseConnection.java
public static boolean isEmbedded(DataSource dataSource) {
try {
// 1. 获取连接
try (Connection connection = dataSource.getConnection()) {
// 2. 获取数据库元数据
DatabaseMetaData meta = connection.getMetaData();
// 3. 获取数据库产品名
String productName = meta.getDatabaseProductName();
// 4. 检查是否是内嵌数据库
return isEmbedded(productName);
}
} catch (SQLException ex) {
// 连接失败 → 认为不是内嵌数据库
return false;
}
}
private static boolean isEmbedded(String productName) {
// 已知内嵌数据库的产品名:
// H2: 以 "H2" 开头
// HSQLDB: 以 "HSQL Database Engine" 开头
// Derby: 以 "Apache Derby" 开头
return productName.startsWith("H2")
|| productName.startsWith("HSQL Database Engine")
|| productName.startsWith("Apache Derby");
}12.3 配置策略
yaml
# 开发环境(内嵌数据库):
# mode=embedded(默认)→ 自动执行 schema.sql
# 不配置 mode 即可
# 测试/生产环境(MySQL):
# mode=embedded → 不会执行(DataSource 不是内嵌)
# 需要显式设置:
spring:
sql:
init:
mode: always # 强制执行 schema.sql
schema-locations: # 指定 SQL 文件路径
- classpath:sql/schema.sql
data-locations:
- classpath:sql/data.sql
continue-on-error: true # 出错继续
# 完全禁用:
spring:
sql:
init:
mode: never # 不执行任何初始化 SQL
# 按平台分离:
spring:
sql:
init:
platform: mysql # 加载 schema-mysql.sql 和 data-mysql.sql
mode: always12.4 完整配置项
yaml
spring:
sql:
init:
mode: embedded # 执行模式: always / embedded / never
schema-locations: # schema 文件路径(默认: classpath:schema.sql)
- classpath:sql/schema.sql
data-locations: # data 文件路径(默认: classpath:data.sql)
- classpath:sql/data.sql
platform: all # 平台名称,加载 schema-${platform}.sql
continue-on-error: false # SQL 执行出错时是否继续
separator: ";" # SQL 语句分隔符
encoding: UTF-8 # SQL 文件编码
username: # 执行 SQL 的用户(默认为 DataSource 用户)
password: # 执行 SQL 的密码(默认为 DataSource 密码)总结
| # | 细节点 | 核心要点 |
|---|---|---|
| ① | @ConditionalOnClass | DataSource.class(JDK 自带) + EmbeddedDatabaseType.class(spring-jdbc) |
| ② | Hikari 内部配置类 | @ConditionalOnClass + @ConditionalOnMissingBean + @ConditionalOnProperty(matchIfMissing=true) 三级条件 |
| ③ | 属性映射 | url→jdbcUrl、username→username、password→password、driverClassName→driverClassName |
| ④ | 子属性注入 | @ConfigurationProperties(prefix="spring.datasource.hikari") 通过 Binder 绑定 |
| ⑤ | 无参构造 + afterPropertiesSet() | 构造时不创建连接池,getConnection() 首次调用时通过 CAS 懒加载创建 HikariPool |
| ⑥ | HikariPool 初始化 | ConcurrentBag → PoolEntryCreator → fillPool() → HouseKeeper 定时维护 |
| ⑦ | ConcurrentBag.borrow() | ThreadLocal(最快) → CopyOnWriteArrayList(CAS) → SynchronousQueue(阻塞) 三级借出 |
| ⑧ | connectionTimeout | SynchronousQueue.poll(timeout, MILLISECONDS) 实现 30s 超时 |
| ⑨ | leakDetectionThreshold | 默认 0 关闭;开启后在连接借出时创建定时任务,超时未归还则打印堆栈 |
| ⑩ | EmbeddedDatabaseConfiguration | @ConditionalOnMissingBean(DataSource.class) → 自动检测 H2/HSQLDB/Derby |
| ⑪ | schema.sql 执行 | DataSourceInitializerInvoker.afterPropertiesSet() → ResourceDatabasePopulator.execute() |
| ⑫ | spring.sql.init.mode | embedded(仅内嵌)、always(总是)、never(从不),通过 EmbeddedDatabaseConnection.isEmbedded() 检测 |