Spring Boot 4.1.1 深度源码解析-第 22 章 · 数据源、连接池与事务
卷五(Web 栈)我们看了 Boot 怎么处理"一个请求进来"的完整链路。 卷六(数据访问)要回答一个不同的问题:Boot 怎么处理"几十个可选依赖"?
module/下与数据相关的模块有二十多个:spring-boot-jdbc、spring-boot-jpa、spring-boot-hibernate、spring-boot-jooq、spring-boot-r2dbc、spring-boot-flyway、spring-boot-liquibase、spring-boot-data-*(Cassandra、Couchbase、Elasticsearch、JDBC、JPA、LDAP、 MongoDB、Neo4j、R2DBC、Redis)......本章只看其中最基础的一块 :JDBC 数据源与事务。 但它足以展示 Boot 应对"依赖矩阵"的完整手法------ 五选一的连接池判定、两级条件嵌套、BPP 覆盖绑定值、以及一条贯穿始终的
JdbcConnectionDetails抽象。
0. 引子:一条 spring.datasource.url 引发的十步推断
最小配置:
yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: secret
java
@Autowired DataSource dataSource; // 拿到的到底是什么?
答案是 HikariDataSource------但 Boot 为得出这个结论走了十步:
markdown
1. `spring-boot-starter-jdbc` 引入了 HikariCP(间接)
2. `DataSourceAutoConfiguration` 检查:DataSource + EmbeddedDatabaseType 在 classpath?
3. 检查:没有 R2DBC 的 ConnectionFactory(`@ConditionalOnMissingBean(type=...)`)
4. PooledDataSourceCondition:spring.datasource.type 配了?或者能找到连接池?
5. DataSourceBuilder.findType(classLoader) 按固定顺序试 6 种连接池
6. Hikari 胜出 → @Import 顺序里 Hikari 排第一
7. 但 url 已经配了 → EmbeddedDatabaseCondition 不匹配(不用嵌入式数据库)
8. 创建 PropertiesJdbcConnectionDetails(把 DataSourceProperties 适配成接口)
9. HikariDataSource bean 用 @ConfigurationProperties("spring.datasource.hikari") 二次绑定
10. HikariJdbcConnectionDetailsBeanPostProcessor 用 JdbcConnectionDetails 覆盖 url/user/password
注意第 9 与第 10 步的微妙关系 : dataSource(...) 方法里已经用 connectionDetails 设了 url/username/password(第 10 步的一部分), 然后 @ConfigurationProperties 绑定会再应用 spring.datasource.hikari.*, 最后 BPP 再按 JdbcConnectionDetails 覆盖一次。
为什么要在最后再覆盖一次? 因为 JdbcConnectionDetails 可能来自 Docker Compose / Testcontainers 等外部源 (§5 展开),那些值必须能覆盖配置文件里的值。 这是本章的核心设计。
1. 模块版图:9 条 imports 的分工
按第一定律,先看 spring-boot-jdbc 的 .imports:
org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration
org.springframework.boot.jdbc.autoconfigure.DataSourceInitializationAutoConfiguration
org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration
org.springframework.boot.jdbc.autoconfigure.JdbcClientAutoConfiguration
org.springframework.boot.jdbc.autoconfigure.JdbcTemplateAutoConfiguration
org.springframework.boot.jdbc.autoconfigure.JndiDataSourceAutoConfiguration
org.springframework.boot.jdbc.autoconfigure.XADataSourceAutoConfiguration
org.springframework.boot.jdbc.autoconfigure.health.DataSourceHealthContributorAutoConfiguration
org.springframework.boot.jdbc.autoconfigure.metrics.DataSourcePoolMetricsAutoConfiguration
9 条 ,可以按"是否创建 DataSource"分成两组:
| 组 | 自动配置 | 关系 |
|---|---|---|
| 创建 DataSource 的 | DataSourceAutoConfiguration、JndiDataSourceAutoConfiguration、XADataSourceAutoConfiguration |
三者互斥(§4 详述) |
| 消费 DataSource 的 | DataSourceTransactionManagerAutoConfiguration、JdbcTemplateAutoConfiguration、JdbcClientAutoConfiguration、DataSourceInitializationAutoConfiguration、health.*、metrics.* |
都要求"已有 DataSource" |
消费方一律用 @ConditionalOnSingleCandidate(DataSource.class) ------ 与第 21 章 gRPC client 的 @ConditionalOnSingleCandidate(GrpcChannelFactory.class) 是同一手法:"只有一个候选时才自动配置",有多个就交给用户决定。
1.1 三条"创建"路径的优先级
kotlin
JndiDataSourceAutoConfiguration @ConditionalOnProperty("spring.datasource.jndi-name")
XADataSourceAutoConfiguration @ConditionalOnBean(XADataSourceWrapper.class) + @ConditionalOnMissingBean(DataSource)
DataSourceAutoConfiguration 兜底
JndiDataSourceAutoConfiguration 上有 @AutoConfiguration(before = { XADataSourceAutoConfiguration.class, DataSourceAutoConfiguration.class }) ------JNDI 排最前。
XADataSourceAutoConfiguration 上是 @AutoConfiguration(before = DataSourceAutoConfiguration.class, after = JtaAutoConfiguration.class)。
于是完整顺序:
JTA → JNDI → XA → DataSourceAutoConfiguration
JNDI 为什么最优先? 因为它靠属性 触发(显式配了 jndi-name), 而 XA 靠 bean 触发(有 XADataSourceWrapper)。 显式属性 > 隐式 bean ------ 这与第 17 章讲的"属性覆盖编程式设置"是同一精神。
2. DataSourceAutoConfiguration:两个内部配置 + 三个自定义 Condition
java
@AutoConfiguration(before = DataSourceInitializationAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceCheckpointRestoreConfiguration.class })
public final class DataSourceAutoConfiguration {
2.1 ★ @ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
这是全章最值得记住的一行。
spring-boot-jdbc 不能 引用 io.r2dbc.spi.ConnectionFactory 的类字面量 (R2DBC 是独立的模块,不是 JDBC 的依赖)。所以用 type = "..." 字符串形式。
语义 :如果应用里已经有 R2DBC 的 ConnectionFactory,就不要自动配置 JDBC 数据源。
为什么? 因为 spring-boot-starter-data-r2dbc 与 spring-boot-starter-jdbc 可能同时在 classpath 上(比如你既用 R2DBC 又用 Flyway 的 JDBC 迁移)。 此时如果两边都创建数据源,会:
- 浪费资源(两个连接池)
- 语义混乱(
@Autowired DataSource与ConnectionFactory指向不同的库?)
Boot 的策略:R2DBC 优先,JDBC 让路。
对称的设计 :
R2dbcAutoConfiguration那边应该也有对应的@ConditionalOnMissingBean(type = "javax.sql.DataSource")。 ⚠️ 未核实 :我没有读R2dbcAutoConfiguration的类级注解(第 23 章会读)。 如果两边都写"对方存在就让路",会形成死锁(都不创建)。 推断是只有一边写了 (JDBC 让路),或者两边的其它条件不同。 自测方法:同时引入两个 starter,看创建了哪个。
2.2 两个内部配置的互斥
java
@Configuration(proxyBeanMethods = false)
@Conditional(EmbeddedDatabaseCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import(EmbeddedDataSourceConfiguration.class)
protected static class EmbeddedDatabaseConfiguration {
}
@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class,
LazyConnectionDataSourceConfiguration.class })
protected static class PooledDataSourceConfiguration {
@Bean
@ConditionalOnMissingBean(JdbcConnectionDetails.class)
PropertiesJdbcConnectionDetails jdbcConnectionDetails(DataSourceProperties properties) {
return new PropertiesJdbcConnectionDetails(properties);
}
}
两个都用 @ConditionalOnMissingBean({DataSource, XADataSource}) ------ 用户自己定义了数据源,两个都失效。
真正的互斥靠各自的 @Conditional:
| 内部配置 | 条件 | 含义 |
|---|---|---|
EmbeddedDatabaseConfiguration |
EmbeddedDatabaseCondition |
没有 url 且没有连接池且有嵌入式数据库 |
PooledDataSourceConfiguration |
PooledDataSourceCondition |
配了 type 或能找到连接池 |
2.3 EmbeddedDatabaseCondition:Javadoc 里写明优先级
java
/**
* {@link Condition} to detect when an embedded {@link DataSource} type can be used.
* If a pooled {@link DataSource} is available, it will always be preferred to an
* {@code EmbeddedDatabase}.
*/
static class EmbeddedDatabaseCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("EmbeddedDataSource");
if (hasDataSourceUrlProperty(context)) {
return ConditionOutcome.noMatch(message.because(DATASOURCE_URL_PROPERTY + " is set"));
}
if (anyMatches(context, metadata, this.pooledCondition)) {
return ConditionOutcome.noMatch(message.foundExactly("supported pooled data source"));
}
if (!ClassUtils.isPresent(EMBEDDED_DATABASE_TYPE, context.getClassLoader())) {
return ConditionOutcome.noMatch(message.didNotFind("required class").items(Style.QUOTE, EMBEDDED_DATABASE_TYPE));
}
EmbeddedDatabaseType type = EmbeddedDatabaseConnection.get(context.getClassLoader()).getType();
if (type == null) {
return ConditionOutcome.noMatch(message.didNotFind("embedded database").atAll());
}
return ConditionOutcome.match(message.found("embedded database").items(type));
}
★ 这是"第四定律"(顺序语义常藏在 Javadoc 里)的教科书例证 : "If a pooled DataSource is available, it will always be preferred to an EmbeddedDatabase" ------这句话只写在 Javadoc 里,代码里只能看到"如果有连接池就 noMatch"。
四个否决条件(按顺序):
css
① 配了 spring.datasource.url(且有文本内容) → 不用嵌入式
② 能找到连接池 → 不用嵌入式(★ Javadoc 声明的优先级)
③ spring-jdbc 的 EmbeddedDatabaseType 不在 classpath → 没法用嵌入式
④ classpath 上找不到 H2/HSQL/Derby → 没得用
① 的 hasDataSourceUrlProperty 有个细节:
java
private boolean hasDataSourceUrlProperty(ConditionContext context) {
Environment environment = context.getEnvironment();
if (environment.containsProperty(DATASOURCE_URL_PROPERTY)) {
try {
return StringUtils.hasText(environment.getProperty(DATASOURCE_URL_PROPERTY));
}
catch (IllegalArgumentException ex) {
// NOTE: This should be PlaceholderResolutionException
// Ignore unresolvable placeholder errors
}
}
return false;
}
为什么要 try-catch? 如果 spring.datasource.url=${DB_URL} 而 DB_URL 未定义, getProperty 会抛 IllegalArgumentException(占位符无法解析)。
Boot 的选择:忽略,当作"没配 url" → 于是走嵌入式数据库分支。
这个 catch 很危险也很重要。 危险:你以为配了 URL,实际没生效,应用静默用上了 H2 内存库。 重要:如果直接抛出,启动会失败在一个很难懂的地方。 Boot 选择"降级到嵌入式" + 后面的
DataSourceProperties会给出清晰的失败分析(§7)。注释
// NOTE: This should be PlaceholderResolutionException是开发者留下的"技术债标记"------理想情况下应该只捕获PlaceholderResolutionException, 而不是宽泛的IllegalArgumentException。 (Framework 4.x 应该还没有这个异常类型,或者 Boot 不想依赖它。)
2.4 PooledDataSourceCondition:AnyNestedCondition 的 OR 语义
java
static class PooledDataSourceCondition extends AnyNestedCondition {
PooledDataSourceCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@ConditionalOnProperty("spring.datasource.type")
static class ExplicitType {
}
@Conditional(PooledDataSourceAvailableCondition.class)
static class PooledDataSourceAvailable {
}
}
OR 语义:
- 显式配了
spring.datasource.type(用户指定了实现类) - 或 classpath 上能找到支持的连接池
phase 是 PARSE_CONFIGURATION ------ 注意与第 21 章 gRPC 的 MissingNetworkGrpcServerCondition(REGISTER_BEAN)不同。
为什么这里可以用 PARSE_CONFIGURATION? 因为它的两个分支问的是属性和 classpath,都不是 bean:
@ConditionalOnProperty问 EnvironmentPooledDataSourceAvailableCondition问DataSourceBuilder.findType(纯 classpath 探测)
第二十五定律的验证:条件问什么,phase 就配什么。
PooledDataSourceAvailableCondition 是手写的 SpringBootCondition:
java
static class PooledDataSourceAvailableCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("PooledDataSource");
if (DataSourceBuilder.findType(context.getClassLoader()) != null) {
return ConditionOutcome.match(message.foundExactly("supported DataSource"));
}
return ConditionOutcome.noMatch(message.didNotFind("supported DataSource").atAll());
}
}
它只是把 DataSourceBuilder.findType 包装成条件 ------ 把"能不能找到连接池"这个业务判断 委托给 DataSourceBuilder,条件层只做适配。
这是 Boot 的分层习惯 : 判断逻辑放在能独立测试的类里(
DataSourceBuilder.findType是 public static),Condition只负责把结果转成ConditionOutcome(带诊断消息)。
3. ★ DataSourceBuilder:六个连接池的固定顺序
DataSourceBuilder 是 @since 2.0.0 的公共 API(不是自动配置内部类), 用户可以自己用。它的核心是 findType。
3.1 findType 的两级查找
java
public static @Nullable Class<? extends DataSource> findType(@Nullable ClassLoader classLoader) {
MappedDataSourceProperties<?> mappings = MappedDataSourceProperties.forType(classLoader, null);
return (mappings != null) ? mappings.getDataSourceInstanceType() : null;
}
forType(classLoader, null) 里 type 是 null,所以走"随便哪个都行"的分支:
java
static <T extends DataSource> @Nullable MappedDataSourceProperties<T> lookupPooled(
@Nullable ClassLoader classLoader, @Nullable Class<T> type) {
MappedDataSourceProperties<T> result = null;
result = lookup(classLoader, type, result, "com.zaxxer.hikari.HikariDataSource",
HikariDataSourceProperties::new);
result = lookup(classLoader, type, result, "org.apache.tomcat.jdbc.pool.DataSource",
TomcatPoolDataSourceProperties::new);
result = lookup(classLoader, type, result, "org.apache.commons.dbcp2.BasicDataSource",
MappedDbcp2DataSource::new);
result = lookup(classLoader, type, result, "oracle.ucp.jdbc.PoolDataSourceImpl",
OraclePoolDataSourceProperties::new, "oracle.jdbc.OracleConnection");
result = lookup(classLoader, type, result, "com.mchange.v2.c3p0.ComboPooledDataSource",
ComboPooledDataSourceProperties::new);
result = lookup(classLoader, type, result, "org.vibur.dbcp.ViburDBCPDataSource",
ViburDataSourceProperties::new);
return result;
}
★ 六种连接池的固定优先级:
| 顺序 | 连接池 | 类名 | 额外要求 |
|---|---|---|---|
| 1 | HikariCP | com.zaxxer.hikari.HikariDataSource |
--- |
| 2 | Tomcat JDBC Pool | org.apache.tomcat.jdbc.pool.DataSource |
--- |
| 3 | Commons DBCP2 | org.apache.commons.dbcp2.BasicDataSource |
--- |
| 4 | Oracle UCP | oracle.ucp.jdbc.PoolDataSourceImpl |
还需 oracle.jdbc.OracleConnection |
| 5 | C3P0 | com.mchange.v2.c3p0.ComboPooledDataSource |
--- |
| 6 | Vibur DBCP | org.vibur.dbcp.ViburDBCPDataSource |
--- |
lookup 的短路逻辑:
java
private static <T extends DataSource> @Nullable MappedDataSourceProperties<T> lookup(
@Nullable ClassLoader classLoader, @Nullable Class<T> dataSourceType,
@Nullable MappedDataSourceProperties<T> existing, String dataSourceClassName,
Supplier<MappedDataSourceProperties<?>> propertyMappingsSupplier, String... requiredClassNames) {
if (existing != null || !allPresent(classLoader, dataSourceClassName, requiredClassNames)) {
return existing; // ★ 已有结果 or 类不全 → 返回 existing
}
MappedDataSourceProperties<?> propertyMappings = propertyMappingsSupplier.get();
return (dataSourceType == null
|| propertyMappings.getDataSourceInstanceType().isAssignableFrom(dataSourceType))
? (MappedDataSourceProperties<T>) propertyMappings : null;
}
existing != null 就短路 ------ 所以第一个匹配到的胜出。
requiredClassNames 的作用 :Oracle UCP 需要两个类 都在 (PoolDataSourceImpl + oracle.jdbc.OracleConnection)。 因为 UCP 通常随 ojdbc 一起出现,单独有 UCP 没驱动是没用的。
这是"第二十六定律"(可选依赖用字符串类名探测)的批量版 : 六个候选、七个类名,全部用
ClassUtils.isPresent探测,没有任何编译期依赖。spring-boot-jdbc的build.gradle里这些连接池都是 optional。
3.2 还有一级:lookupBasic
java
private static <T extends DataSource> @Nullable MappedDataSourceProperties<T> lookupBasic(
@Nullable ClassLoader classLoader, Class<T> dataSourceType) {
MappedDataSourceProperties<T> result = null;
result = lookup(classLoader, dataSourceType, result,
"org.springframework.jdbc.datasource.SimpleDriverDataSource", SimpleDataSourceProperties::new);
result = lookup(classLoader, dataSourceType, result, "oracle.jdbc.datasource.OracleDataSource",
OracleDataSourceProperties::new);
result = lookup(classLoader, dataSourceType, result, "org.h2.jdbcx.JdbcDataSource",
H2DataSourceProperties::new);
result = lookup(classLoader, dataSourceType, result, "org.postgresql.ds.PGSimpleDataSource",
PostgresDataSourceProperties::new);
return result;
}
"Basic" = 非连接池的直接数据源 。四个: SimpleDriverDataSource、OracleDataSource、H2 JdbcDataSource、PGSimpleDataSource。
什么时候用 basic 而不是 pooled? forType 的逻辑是"先试 pooled,找不到再试 basic"(推断,因为 findType 只调 pooled; basic 应该是在显式指定 type 时用)。
⚠️ 未核实 :
MappedDataSourceProperties.forType的完整分支逻辑 (我只读了lookupPooled和lookupBasic两个方法,没读forType的 if-else)。 推断:type == null→ 先 pooled 后 basic;type != null→ 找该 type 对应的映射。
3.3 属性名映射:DataSourceProperty 枚举
DataSourceBuilder 要往任意 DataSource 实现上设 url/username/password, 但不同实现的 setter 名不同:
java
private enum DataSourceProperty {
URL(false, "url", "URL"),
DRIVER_CLASS_NAME(true, "driverClassName"),
USERNAME(false, "username", "user"),
PASSWORD(false, "password");
private final boolean optional;
private final String[] names;
@Nullable Method findSetter(Class<?> type) {
return findMethod("set", type, String.class);
}
private @Nullable Method findMethod(String prefix, Class<?> type, Class<?>... paramTypes) {
for (String name : this.names) {
String candidate = prefix + StringUtils.capitalize(name);
Method method = ReflectionUtils.findMethod(type, candidate, paramTypes);
if (method != null) {
return method;
}
}
return null;
}
}
每个属性有若干候选名:
- URL →
setUrl或setURL - USERNAME →
setUsername或setUser - PASSWORD →
setPassword
按顺序试,第一个找到的用 。这解释了为什么 HikariDataSource (有 setJdbcUrl 而非 setUrl)需要专门的映射类:
java
// HikariDataSourceProperties
add(DataSourceProperty.URL, HikariDataSource::getJdbcUrl, HikariDataSource::setJdbcUrl);
每个连接池一个 MappedDataSourceProperties 子类,用方法引用声明 setter/getter。 这比反射"猜"更可靠,也比写六个 if-else 优雅。
DRIVER_CLASS_NAME(true, ...) 的 optional = true ------ driver class 是唯一可选的(可以不设,靠 URL 推断)。
3.4 derivedFrom:DataSourceUnwrapper.unwrapRoot
java
public static DataSourceBuilder<?> derivedFrom(DataSource dataSource) {
return new DataSourceBuilder<>(DataSourceUnwrapper.unwrapRoot(dataSource));
}
DataSourceInitializationAutoConfiguration 里用到了它(§6.2)。
unwrapRoot 的作用是"剥掉所有代理包装,拿到最里层的真数据源"。 因为数据源可能被 LazyConnectionDataSourceProxy、DelegatingDataSource、 事务代理等层层包裹,derivedFrom 需要拿到原始配置才能复制。
⚠️ 未核实 :
DataSourceUnwrapper的实现细节(它在本仓库,但我没读)。 从名字和用途推断是循环unwrap。
4. DataSourceConfiguration:五个内部配置类的"抢椅子"
DataSourceAutoConfiguration.PooledDataSourceConfiguration 导入了 5 个:
java
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class,
LazyConnectionDataSourceConfiguration.class })
顺序:Hikari → Tomcat → Dbcp2 → OracleUcp → Generic。
每个都有 @ConditionalOnMissingBean(DataSource.class)------ 先注册的赢 (第 14 章讲过:@ConditionalOnMissingBean 的判定依赖注册顺序)。
4.1 四个"具体"配置的共同结构
以 Hikari 为例:
java
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
matchIfMissing = true)
static class Hikari {
@Bean
static HikariJdbcConnectionDetailsBeanPostProcessor jdbcConnectionDetailsHikariBeanPostProcessor(
ObjectProvider<JdbcConnectionDetails> connectionDetailsProvider) {
return new HikariJdbcConnectionDetailsBeanPostProcessor(connectionDetailsProvider);
}
@Bean
@ConfigurationProperties("spring.datasource.hikari")
HikariDataSource dataSource(DataSourceProperties properties, JdbcConnectionDetails connectionDetails,
Environment environment) {
String dataSourceClassName = environment.getProperty("spring.datasource.hikari.data-source-class-name");
HikariDataSource dataSource = createDataSource(connectionDetails, HikariDataSource.class,
properties.getClassLoader(), dataSourceClassName == null);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
}
三重条件:
| 条件 | 含义 |
|---|---|
@ConditionalOnClass(HikariDataSource.class) |
HikariCP 在 classpath |
@ConditionalOnMissingBean(DataSource.class) |
还没有人创建 |
@ConditionalOnProperty(..., havingValue = "com.zaxxer.hikari.HikariDataSource", matchIfMissing = true) |
要么没配 type,要么 type 正好是 Hikari |
★ 第三个条件是精妙之处:
matchIfMissing = true → 没配 spring.datasource.type 时通过。 配了的话,havingValue 必须完全等于 com.zaxxer.hikari.HikariDataSource。
所以:
- 不配 type → Hikari 通过(如果 classpath 有它)
- 配
type=com.zaxxer.hikari.HikariDataSource→ Hikari 通过,其它不通过 - 配
type=org.apache.commons.dbcp2.BasicDataSource→ Hikari 不通过,Dbcp2 通过
havingValue 是精确字符串匹配 ------所以写 HikariDataSource(不带包名)不生效。 ⚠️ 这是一个很常见的踩坑点。
4.2 Generic:兜底
java
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type")
static class Generic {
@Bean
DataSource dataSource(DataSourceProperties properties, JdbcConnectionDetails connectionDetails) {
return createDataSource(connectionDetails, properties.getType(), properties.getClassLoader());
}
}
唯一没有 matchIfMissing 的 ------必须显式 配 spring.datasource.type。
它的存在意义:spring.datasource.type 指向一个Boot 不认识 的实现类 (比如自定义的、或 C3P0/Vibur 这种 DataSourceConfiguration 里没列的)。
注意 C3P0 和 Vibur 的"半支持"状态 : DataSourceBuilder.lookupPooled 认识 它们(所以 findType 能返回它们, PooledDataSourceCondition 会匹配), 但 DataSourceConfiguration 没有 对应的内部配置类 (所以真正创建时落到 Generic)。
这是一个有意思的不对称 :
DataSourceBuilder支持 6 种,DataSourceConfiguration只支持 4 种 + Generic。 差距的那 2 种(C3P0、Vibur)靠Generic+DataSourceBuilder的映射能力兜底。 我不知道这是否有意(可能是"C3P0/Vibur 用得少,不值得单独写配置类")。 ⚠️ 未核实 :C3P0 场景下@ConfigurationProperties("spring.datasource.c3p0")是否有效 (Generic的dataSource()没有@ConfigurationProperties注解!)。 这意味着用 C3P0 时,spring.datasource.c3p0.*不会被绑定。 这是一个真实的能力缺失,值得记一笔。
4.3 ★ createDataSource 的 applyDriverClassName 参数
java
private static <T> T createDataSource(JdbcConnectionDetails connectionDetails,
@Nullable Class<? extends DataSource> type, ClassLoader classLoader, boolean applyDriverClassName) {
DataSourceBuilder<? extends DataSource> builder = DataSourceBuilder.create(classLoader).type(type);
if (applyDriverClassName) {
builder.driverClassName(connectionDetails.getDriverClassName());
}
return (T) builder.url(connectionDetails.getJdbcUrl())
.username(connectionDetails.getUsername())
.password(connectionDetails.getPassword())
.build();
}
只有 Hikari 传了非默认的 false:
java
String dataSourceClassName = environment.getProperty("spring.datasource.hikari.data-source-class-name");
HikariDataSource dataSource = createDataSource(connectionDetails, HikariDataSource.class,
properties.getClassLoader(), dataSourceClassName == null);
为什么 Hikari 特殊?
因为 Hikari 有两种指定数据库的方式:
jdbcUrl+driverClassName(传统)dataSourceClassName(Hikari 推荐,性能更好)
两者互斥 :Hikari 的 HikariConfig 里,如果同时设了 dataSourceClassName 和 driverClassName,会抛异常(或至少语义冲突)。
所以 Boot 的逻辑是: 如果用户配了 spring.datasource.hikari.data-source-class-name,就不设 driverClassName。
这是一个"用户选择优先"的处理 : Boot 不阻止你用
dataSourceClassName(那是 Hikari 的高级用法), 但会自动避开冲突。 ⚠️ 未核实 :dataSourceClassName与jdbcUrl是否也冲突 (从代码看,url 总是设的,所以应该不冲突;Hikari 内部可能优先用 dataSourceClassName)。
4.4 Tomcat 的额外处理:validationQuery
java
org.apache.tomcat.jdbc.pool.DataSource dataSource(DataSourceProperties properties,
JdbcConnectionDetails connectionDetails) {
Class<? extends DataSource> dataSourceType = org.apache.tomcat.jdbc.pool.DataSource.class;
org.apache.tomcat.jdbc.pool.DataSource dataSource = createDataSource(connectionDetails, dataSourceType,
properties.getClassLoader());
String validationQuery;
DatabaseDriver databaseDriver = DatabaseDriver.fromJdbcUrl(connectionDetails.getJdbcUrl());
validationQuery = databaseDriver.getValidationQuery();
if (validationQuery != null) {
dataSource.setTestOnBorrow(true);
dataSource.setValidationQuery(validationQuery);
}
return dataSource;
}
只有 Tomcat 池做了这件事 :根据数据库类型推断 validationQuery 并开启 testOnBorrow。
为什么 Hikari 不需要? 因为 Hikari 有更现代的连接检测机制 (connectionTestQuery 可选,默认用 JDBC4 isValid())。 Tomcat 池较老,需要显式配置。
又一次"多实现的差异点 = 底层 API 能力差"(第十八定律)。
4.5 poolName 的设置:Hikari 与 Oracle UCP 有,Tomcat/Dbcp2 没有
java
// Hikari
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
// OracleUcp
if (StringUtils.hasText(properties.getName())) {
dataSource.setConnectionPoolName(properties.getName());
}
spring.datasource.name 只在这两个池上生效。 ⚠️ 未核实:Tomcat/Dbcp2 是否有对应的 API(可能有但 Boot 没设)。
4.6 一个不对称:BPP 的 @ConditionalOnMissingBean
| 配置类 | BPP bean 的条件 |
|---|---|
| Tomcat | @ConditionalOnMissingBean(PropertiesJdbcConnectionDetails.class) |
| Hikari | 无 |
| Dbcp2 | 无 |
| OracleUcp | 无 |
只有 Tomcat 的 BPP 有 @ConditionalOnMissingBean(PropertiesJdbcConnectionDetails.class)。
语义 :如果 JdbcConnectionDetails 是"来自属性"的 (PropertiesJdbcConnectionDetails),就不需要 BPP------ 因为属性值已经在创建时应用过了。
**但只有 Tomcat 写了这个优化。**其它三个的 BPP 无条件注册, 靠 JdbcConnectionDetailsBeanPostProcessor 内部 的 if (!(connectionDetails instanceof PropertiesJdbcConnectionDetails)) 判断跳过。
所以 Tomcat 的那个条件注解是冗余的 (内部已经判断了)。 ⚠️ 未核实 :是否有什么场景让这个条件有意义。 推断是遗留代码(早期版本的写法,后来把判断移到了基类里)。
5. JdbcConnectionDetails:本章的核心抽象
这是一个 @since 4.0.0 的接口,是 Boot 4.x 数据访问改造的关键。
5.1 接口定义
java
public interface JdbcConnectionDetails extends ConnectionDetails {
@Nullable String getUsername();
@Nullable String getPassword();
String getJdbcUrl();
default String getDriverClassName() {
String driverClassName = DatabaseDriver.fromJdbcUrl(getJdbcUrl()).getDriverClassName();
Assert.state(driverClassName != null, "'driverClassName' must not be null");
return driverClassName;
}
default @Nullable String getXaDataSourceClassName() {
return DatabaseDriver.fromJdbcUrl(getJdbcUrl()).getXaDataSourceClassName();
}
}
四个方法,两个有默认实现 (靠 DatabaseDriver.fromJdbcUrl 推断)。
它 extends ConnectionDetails ------ 这是 Boot 4.x 的通用"连接信息"抽象 (R2DBC、MongoDB、Redis、Elasticsearch 等各有自己的子接口)。 第 30 章讲 Docker Compose / Testcontainers 时会再遇到它。
5.2 为什么需要它:三种"连接信息"来源
| 来源 | 实现 | 优先级 |
|---|---|---|
spring.datasource.* 属性 |
PropertiesJdbcConnectionDetails |
低 |
| Docker Compose | *DockerComposeConnectionDetailsFactory 产生的实现 |
高 |
| Testcontainers | *ContainerConnectionDetailsFactory 产生的实现 |
高 |
关键需求 :当 Docker Compose / Testcontainers 提供了连接信息时, 它必须覆盖 application.yaml 里写的值。
spring-boot-jdbc 里有 7 个 Docker Compose 工厂 (ClickHouse、MariaDB、MySQL、Oracle Free、Oracle XE、Postgres、SQL Server) 和 1 个 Testcontainers 工厂。
5.3 覆盖机制:JdbcConnectionDetailsBeanPostProcessor
java
abstract class JdbcConnectionDetailsBeanPostProcessor<T> implements BeanPostProcessor, PriorityOrdered {
private final Class<T> dataSourceClass;
private final ObjectProvider<JdbcConnectionDetails> connectionDetailsProvider;
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (this.dataSourceClass.isInstance(bean) && "dataSource".equals(beanName)) {
JdbcConnectionDetails connectionDetails = this.connectionDetailsProvider.getObject();
if (!(connectionDetails instanceof PropertiesJdbcConnectionDetails)) {
return processDataSource((T) bean, connectionDetails);
}
}
return bean;
}
protected abstract Object processDataSource(T dataSource, JdbcConnectionDetails connectionDetails);
@Override
public int getOrder() {
// Runs after ConfigurationPropertiesBindingPostProcessor
return Ordered.HIGHEST_PRECEDENCE + 2;
}
}
五个设计点:
① postProcessBeforeInitialization(不是 After) 在 bean 初始化之前 改属性------因为连接池可能在 afterPropertiesSet 里建连接。
② 三个判定条件
java
this.dataSourceClass.isInstance(bean) // 类型匹配
&& "dataSource".equals(beanName) // ★ 名字必须是 "dataSource"
&& !(connectionDetails instanceof PropertiesJdbcConnectionDetails) // 不是属性来源
按 bean 名判定 ------第 19 章讲过(第十九定律): Boot 约定数据源 bean 名为 dataSource,BPP 也按这个名字找。 用户自定义的数据源(名字不是 dataSource)不会被 BPP 改。
③ PriorityOrdered + getOrder() = HIGHEST_PRECEDENCE + 2
注释写明 "Runs after ConfigurationPropertiesBindingPostProcessor"。
这是本章最精妙的一处时序设计:
sql
① @Bean 方法创建 HikariDataSource(此时 url 来自 PropertiesJdbcConnectionDetails)
② ConfigurationPropertiesBindingPostProcessor 绑定 spring.datasource.hikari.*
(order = HIGHEST_PRECEDENCE + 1,推测)
③ JdbcConnectionDetailsBeanPostProcessor 覆盖 url/user/password(HIGHEST_PRECEDENCE + 2)
BPP 排在属性绑定之后 → 外部连接信息最终胜出。
⚠️ 未核实 :
ConfigurationPropertiesBindingPostProcessor的确切 order。 从 Boot 的源码惯例推断是HIGHEST_PRECEDENCE + 1。 若真是如此,+2就精确地"刚好在其后"。 自测方法:打印两个 BPP 的 order。
④ 为什么要 PriorityOrdered 而不是 Ordered?
PriorityOrdered 是 Spring 的一个更高优先级层级 : PostProcessorRegistrationDelegate 先注册所有 PriorityOrdered,再注册 Ordered。 所以 BPP 一定在普通 Ordered BPP 之前。
⑤ 四个子类的差异
| BPP | 目标类型 | 设置方式 |
|---|---|---|
HikariJdbcConnectionDetailsBeanPostProcessor |
HikariDataSource |
setJdbcUrl / setUsername / setPassword / setDriverClassName(判空) |
TomcatJdbcConnectionDetailsBeanPostProcessor |
DataSource(org.apache.tomcat.jdbc.pool.DataSource) |
setUrl / setUsername / setPassword / setDriverClassName |
Dbcp2JdbcConnectionDetailsBeanPostProcessor |
BasicDataSource |
setUrl / ... |
OracleUcpJdbcConnectionDetailsBeanPostProcessor |
PoolDataSourceImpl |
setURL / setUser / setPassword / setConnectionFactoryClassName(driver) |
Oracle UCP 的两个特殊点:
setURL(全大写)+setUser(不是setUsername)- 方法抛
SQLException→ 被 catch 成RuntimeException
java
try {
dataSource.setURL(connectionDetails.getJdbcUrl());
...
return dataSource;
}
catch (SQLException ex) {
throw new RuntimeException("Failed to set URL / user / password of datasource", ex);
}
只有 Hikari 对 driverClassName 判空:
java
String driverClassName = connectionDetails.getDriverClassName();
if (driverClassName != null) {
dataSource.setDriverClassName(driverClassName);
}
因为 JdbcConnectionDetails.getDriverClassName() 的返回类型是 String(非 @Nullable),但 PropertiesJdbcConnectionDetails 之外的实现 可能返回 null?不,接口声明是 String......
实际上这个判空是防御性的 :接口签名是非 null,但
Assert.state在默认实现里。 自定义实现可能返回 null。Hikari 的 BPP 判空,其它三个不判(直接传给 setter, null 可能被 setter 接受或拒绝)。不一致,但无害。
5.4 PropertiesJdbcConnectionDetails:适配器
java
final class PropertiesJdbcConnectionDetails implements JdbcConnectionDetails {
private final DataSourceProperties properties;
@Override public @Nullable String getUsername() { return this.properties.determineUsername(); }
@Override public @Nullable String getPassword() { return this.properties.determinePassword(); }
@Override public String getJdbcUrl() { return this.properties.determineUrl(); }
@Override public String getDriverClassName() { return this.properties.determineDriverClassName(); }
@Override
public @Nullable String getXaDataSourceClassName() {
return (this.properties.getXa().getDataSourceClassName() != null)
? this.properties.getXa().getDataSourceClassName()
: JdbcConnectionDetails.super.getXaDataSourceClassName();
}
}
它把 DataSourceProperties 的 determineXxx() 方法适配成接口。
determineXxx vs getXxx 的区分 (DataSourceProperties 里的惯例):
getUrl()------ 用户配了什么就返回什么(可能为 null)determineUrl()------ 加上推断逻辑(嵌入式数据库、唯一名生成等),不会返回 null(失败则抛异常)
PropertiesJdbcConnectionDetails 用的是 determineXxx------正确。
getXaDataSourceClassName 的 JdbcConnectionDetails.super.xxx() : 调用接口默认方法 的语法。当 properties.getXa().getDataSourceClassName() 为 null 时, 回退到接口默认实现(从 URL 推断)。
这是 Java 8 的一个不太常见的语法:
InterfaceName.super.method()。 在实现类里调用父接口的默认方法。
5.5 注册点:两处
java
// DataSourceAutoConfiguration.PooledDataSourceConfiguration
@Bean
@ConditionalOnMissingBean(JdbcConnectionDetails.class)
PropertiesJdbcConnectionDetails jdbcConnectionDetails(DataSourceProperties properties) {
return new PropertiesJdbcConnectionDetails(properties);
}
java
// XADataSourceAutoConfiguration(同样的代码)
@Bean
@ConditionalOnMissingBean(JdbcConnectionDetails.class)
PropertiesJdbcConnectionDetails jdbcConnectionDetails(DataSourceProperties properties) {
return new PropertiesJdbcConnectionDetails(properties);
}
@ConditionalOnMissingBean(JdbcConnectionDetails.class) ------ 如果 Docker Compose / Testcontainers 已经提供了实现,Boot 的就不注册。
这就是"外部连接信息优先"的完整机制:
dart
① Boot 尝试注册 PropertiesJdbcConnectionDetails(属性来源)
但只有【没有其它 JdbcConnectionDetails bean】时才注册
② Docker Compose 的 factory 注册了实现 → Boot 的不注册
③ 数据源 bean 创建时用 JdbcConnectionDetails(此时是 Docker Compose 的)
④ BPP 发现不是 PropertiesJdbcConnectionDetails → 再覆盖一次(双保险)
为什么 ③ 和 ④ 都要做? ③ 是主路径(创建时就用对的值)。 ④ 是保险(防止 @ConfigurationProperties 绑定在中间把值改回去)。 两道防线。
5.6 Docker Compose 与 Testcontainers:两种"外部来源"的实现
§5.2 说过 JdbcConnectionDetails 有三种来源。这里看两种外部来源的具体实现------ 它们是 Boot 4.x 的**服务连接(Service Connection)**能力,第 29 章讲测试时会再展开。
5.6.1 七个 Docker Compose 工厂的共性
spring-boot-jdbc 有 7 个 *JdbcDockerComposeConnectionDetailsFactory:
| 服务 | 工厂类 | 协议 | 容器端口 |
|---|---|---|---|
| ClickHouse | ClickHouseJdbcDockerComposeConnectionDetailsFactory |
--- | --- |
| MariaDB | MariaDbJdbcDockerComposeConnectionDetailsFactory |
--- | --- |
| MySQL | MySqlJdbcDockerComposeConnectionDetailsFactory |
mysql |
3306 |
| Oracle Free | OracleFreeJdbcDockerComposeConnectionDetailsFactory |
--- | --- |
| Oracle XE | OracleXeJdbcDockerComposeConnectionDetailsFactory |
--- | --- |
| Postgres | PostgresJdbcDockerComposeConnectionDetailsFactory |
postgresql |
5432 |
| SQL Server | SqlServerJdbcDockerComposeConnectionDetailsFactory |
--- | --- |
(⚠️ 未核实:我只读了 MySQL 与 Postgres 两个,其余五个的协议/端口是推断。)
以 MySQL 为例:
java
class MySqlJdbcDockerComposeConnectionDetailsFactory
extends DockerComposeConnectionDetailsFactory<JdbcConnectionDetails> {
protected MySqlJdbcDockerComposeConnectionDetailsFactory() {
super("mysql"); // ★ 匹配 docker-compose.yml 里的服务名
}
@Override
protected JdbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
return new MySqlJdbcDockerComposeConnectionDetails(source.getRunningService());
}
static class MySqlJdbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails
implements JdbcConnectionDetails {
private static final JdbcUrlBuilder jdbcUrlBuilder = new JdbcUrlBuilder("mysql", 3306);
private final MySqlEnvironment environment;
private final String jdbcUrl;
MySqlJdbcDockerComposeConnectionDetails(RunningService service) {
super(service);
this.environment = new MySqlEnvironment(service.env());
this.jdbcUrl = jdbcUrlBuilder.build(service, this.environment.getDatabase());
}
@Override public String getUsername() { return this.environment.getUsername(); }
@Override public String getPassword() { return this.environment.getPassword(); }
@Override public String getJdbcUrl() { return this.jdbcUrl; }
}
}
四个结构要点:
① super("mysql") ------ 用服务名 匹配 docker-compose.yml 里的服务。 Boot 扫描正在运行的 compose 服务,按名字找对应的工厂。
② jdbcUrlBuilder 是 static final ------ 每个服务类的协议/端口是固定的, 做成静态常量(对比第 21 章 TestGrpcTransportAutoConfiguration 的 static address, 那里是"共享",这里是"常量")。
③ URL 在构造器里构建一次 ------ this.jdbcUrl = jdbcUrlBuilder.build(...)。 不是每次 getJdbcUrl() 都重建。因为 URL 依赖的是启动时的容器状态,不会变。
对比
PropertiesJdbcConnectionDetails:它每次调getJdbcUrl()都走properties.determineUrl()(含 UUID 缓存等逻辑)。 一个"算一次存起来",一个"每次都算" ------差异来自"值会不会变": compose 服务的端口在启动时确定,之后不变; 而DataSourceProperties的 url 可能受 profile/占位符影响。
④ 环境变量由 *Environment 类解析
MySqlEnvironment(service.env()) ------ 从容器的环境变量里读 MYSQL_USER、MYSQL_PASSWORD、MYSQL_DATABASE 等。 每个数据库的环境变量名不同 ,所以每个服务有自己的 Environment 类 (MySqlEnvironment、PostgresEnvironment、OracleEnvironment......)。
5.6.2 JdbcUrlBuilder:端口映射的关键
java
private String urlFor(RunningService service, @Nullable String database) {
Assert.notNull(service, "'service' must not be null");
StringBuilder url = new StringBuilder("jdbc:%s://%s:%d".formatted(this.driverProtocol, service.host(),
service.ports().get(this.containerPort)));
if (StringUtils.hasLength(database)) {
url.append("/");
url.append(database);
}
String parameters = getParameters(service);
if (StringUtils.hasLength(parameters)) {
appendParameters(url, parameters);
}
return url.toString();
}
★ service.ports().get(this.containerPort) ------ 这是核心。
Docker Compose 里的容器端口(如 MySQL 的 3306)会被映射到一个随机的宿主机端口 (如 32768)。JdbcUrlBuilder 用"容器端口 → 实际映射端口"的查询, 拼出宿主机能连的 URL。
这就是为什么你不能手写 jdbc:mysql://localhost:3306/... ------容器里的 3306 不一定映射到宿主机的 3306。
getParameters(service) 的扩展点:
java
private static final String PARAMETERS_LABEL = "org.springframework.boot.jdbc.parameters";
private @Nullable String getParameters(RunningService service) {
return service.labels().get(PARAMETERS_LABEL);
}
从 Docker label 读额外的 JDBC 参数 。在 docker-compose.yml 里:
yaml
services:
postgres:
image: postgres:16
labels:
org.springframework.boot.jdbc.parameters: "?sslmode=disable&connectTimeout=10"
这是 Boot 给"JDBC 参数"留的后门------因为 URL 是 Boot 生成的, 用户没法往里加参数,于是用 label。
appendParameters 是 protected ------ 可以被子类覆写:
java
/**
* The default implementation appends a {@code ?} followed by the {@code parameters}.
*/
protected void appendParameters(StringBuilder url, String parameters) {
url.append("?").append(parameters);
}
为什么要留这个扩展点? 因为不是所有数据库都用 ? 做参数分隔符 。 (比如某些数据库用 ; 或者要求参数在特定位置。) ⚠️ 未核实:哪些子类覆写了它(我只读了基类)。
5.6.3 Postgres 的 ApplicationName 特殊处理
java
private static String addApplicationNameIfNecessary(String jdbcUrl, Environment environment) {
if (jdbcUrl.contains("&ApplicationName=") || jdbcUrl.contains("?ApplicationName=")) {
return jdbcUrl; // 已经有了就不加
}
String applicationName = environment.getProperty("spring.application.name");
// ... 拼接
}
Postgres 独有 :自动把 spring.application.name 作为 ApplicationName 参数加到 URL 里。
作用 :DBA 在 pg_stat_activity 里能看到是哪个应用占着连接。 这对排查连接泄漏极有价值。
注意它先检查"是否已有" ------尊重用户通过 label 设置的 ApplicationName。 又是"不覆盖用户显式值"(第 17 章讲过的模式)。
5.6.4 Testcontainers:更简单的实现
java
class JdbcContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<JdbcDatabaseContainer<?>, JdbcConnectionDetails> {
@Override
protected JdbcConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<JdbcDatabaseContainer<?>> source) {
return new JdbcContainerConnectionDetails(source);
}
private static final class JdbcContainerConnectionDetails
extends ContainerConnectionDetails<JdbcDatabaseContainer<?>> implements JdbcConnectionDetails {
@Override public String getUsername() { return getContainer().getUsername(); }
@Override public String getPassword() { return getContainer().getPassword(); }
@Override public String getJdbcUrl() { return getContainer().getJdbcUrl(); }
@Override
public String getDriverClassName() {
return getContainer().getDriverClassName();
}
}
}
比 Docker Compose 版简单得多 ------ 因为 Testcontainers 的 JdbcDatabaseContainer 自己就能提供 getJdbcUrl() / getUsername() / ...
所以 Boot 这侧只是转发 ,不需要 JdbcUrlBuilder(端口映射由 Testcontainers 做了)。
它是唯一覆写 getDriverClassName() 的实现------ 因为容器知道自己的驱动类,不需要从 URL 推断(接口默认方法做的事)。
对比 Docker Compose 版 :都没有覆写 getDriverClassName(), 所以走接口默认实现(DatabaseDriver.fromJdbcUrl(url).getDriverClassName())。
这是一个"能力差异决定代码量"的例子 : Testcontainers 的容器 API 已经提供了所有信息 → Boot 写 4 行转发; Docker Compose 只给原始 service 信息 → Boot 要写
JdbcUrlBuilder
*Environment+ URL 拼接。
6. DataSourceProperties:determineXxx 体系与"三个默认"
DataSourceProperties 是 @since 4.0.0(4.x 包迁移后的新类),409 行。
6.1 getXxx vs determineXxx:一个贯穿全类的区分
| 方法对 | getXxx |
determineXxx |
|---|---|---|
| URL | 返回配置值(可能 null) | 配置值 → 否则用嵌入式 URL → 否则抛异常 |
| DriverClassName | 返回配置值(可能 null) | 配置值 → 从 URL 推断 → 嵌入式驱动 → 否则抛异常 |
| Username / Password | 返回配置值 | 配置值 → 嵌入式的默认值 |
| DatabaseName | --- | generateUniqueName → UUID;name → 用它;嵌入式 → "testdb" |
规则:
getXxx= "用户说了什么"(忠实反映配置,null 就是 null)determineXxx= "最终应该用什么"(含推断与兜底,失败则抛明确异常)
这个区分很重要 ,因为失败分析(§9)需要知道"用户到底配了没有"。 看 DataSourceBeanCreationFailureAnalyzer:
java
if (!StringUtils.hasText(cause.getProperties().getUrl())) { // ★ getUrl,不是 determineUrl
description.append("'url' attribute is not specified and ");
}
用 getUrl() ------ 因为要判断"用户没配"。 如果用 determineUrl(),永远不为 null(或已抛异常),就没法诊断了。
6.2 determineDatabaseName:三分支
java
public @Nullable String determineDatabaseName() {
if (this.generateUniqueName) {
if (this.uniqueName == null) {
this.uniqueName = UUID.randomUUID().toString();
}
return this.uniqueName;
}
if (StringUtils.hasLength(this.name)) {
return this.name;
}
if (this.embeddedDatabaseConnection != EmbeddedDatabaseConnection.NONE) {
return "testdb";
}
return null;
}
generateUniqueName 默认 true ------ 这是 Boot 的一个经典行为:
每个嵌入式数据库默认有一个随机唯一的名字 (UUID)。 所以同一 JVM 里跑多个测试,各自拿到独立的内存数据库,互不干扰。
这是"测试隔离"的基石 : @SpringBootTest 不配 spring.datasource.url 时, 每次都得到一个全新的 H2 内存库。
代价 :数据库名是 UUID,不好认。想要固定名字就设 spring.datasource.generate-unique-name=false + spring.datasource.name=mydb (或默认 testdb)。
uniqueName 被缓存 (if (this.uniqueName == null))------ 多次调 determineDatabaseName() 返回同一个值。
6.3 findDriverClassName 的三级回退
java
@Nullable String findDriverClassName() {
if (StringUtils.hasText(this.driverClassName)) {
Assert.state(driverClassIsLoadable(this.driverClassName),
() -> "Cannot load driver class: " + this.driverClassName);
return this.driverClassName;
}
String driverClassName = null;
if (StringUtils.hasText(this.url)) {
driverClassName = DatabaseDriver.fromJdbcUrl(this.url).getDriverClassName();
}
if (!StringUtils.hasText(driverClassName)) {
driverClassName = this.embeddedDatabaseConnection.getDriverClassName();
}
return driverClassName;
}
scss
① spring.datasource.driver-class-name (显式,且必须能加载)
② DatabaseDriver.fromJdbcUrl(url).getDriverClassName() (从 URL 推断)
③ embeddedDatabaseConnection.getDriverClassName() (嵌入式默认)
① 的 Assert.state(driverClassIsLoadable(...)) : 显式配了驱动类却加载不了 → 立即失败,不会静默回退到推断。
driverClassIsLoadable 的一个细节:
java
private boolean driverClassIsLoadable(String driverClassName) {
try {
ClassUtils.forName(driverClassName, null);
return true;
}
catch (UnsupportedClassVersionError ex) {
// Driver library has been compiled with a later JDK, propagate error
throw ex;
}
catch (Throwable ex) {
return false;
}
}
UnsupportedClassVersionError 被单独抛出 ,其它 Throwable 返回 false。
为什么? UnsupportedClassVersionError 意味着 驱动是用更高版本的 JDK 编译的 ------这不是"类找不到", 而是环境不匹配 。如果返回 false,Boot 会走推断路径, 然后可能报一个完全无关的错("Failed to determine a suitable driver class"), 掩盖了真正的问题。
★ 这是一个很好的错误处理范式 : 区分"找不到"(可降级)与"找到了但用不了"(必须暴露) 。 第 18 章
PortInUseException靠消息区分"端口占用"与"地址无法分配"是同一思路。
6.4 determineUrl 与嵌入式 URL
java
public String determineUrl() {
if (StringUtils.hasText(this.url)) {
return this.url;
}
String databaseName = determineDatabaseName();
String url = (databaseName != null) ? this.embeddedDatabaseConnection.getUrl(databaseName) : null;
if (!StringUtils.hasText(url)) {
throw new DataSourceBeanCreationException("Failed to determine suitable jdbc url", this,
this.embeddedDatabaseConnection);
}
return url;
}
EmbeddedDatabaseConnection.getUrl(databaseName) 用 %s 占位符:
java
public enum EmbeddedDatabaseConnection {
NONE(null),
H2("jdbc:h2:mem:%s;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"),
@Deprecated(since = "4.1.0", forRemoval = true)
DERBY("jdbc:derby:memory:%s;create=true"),
HSQLDB("org.hsqldb.jdbcDriver", "jdbc:hsqldb:mem:%s");
★ H2 的 URL 里有两个关键参数:
vbnet
jdbc:h2:mem:{UUID};DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
| 参数 | 含义 | 为什么必要 |
|---|---|---|
DB_CLOSE_DELAY=-1 |
最后一个连接关闭后不销毁数据库 | 否则连接池空闲一段时间后库就没了 |
DB_CLOSE_ON_EXIT=FALSE |
JVM 退出时不关闭 | 配合上面 |
这两个参数是 H2 内存库能在连接池下工作的前提。 如果你自己手写 jdbc:h2:mem:testdb 而漏了 DB_CLOSE_DELAY=-1, 会遇到"连上后库是空的"这种诡异问题------连接池建连接、用完后关闭, H2 就把库销毁了,下次连接是全新的空库。
EmbeddedDatabaseConnection 只有 3 种 (NONE、H2、DERBY、HSQLDB), 且 Derby 已 @Deprecated(forRemoval):
java
/**
* Derby Database Connection.
* @deprecated since 4.1.0 for removal in 4.3.0 as Derby is EOL.
*/
Derby 已 EOL(End of Life) ,Boot 4.1 标记废弃,4.3 移除。 DatabaseDriver.DERBY 同样标记了。
这是一个可以确定的"未来变更": Boot 4.3 会删掉 Derby 支持。如果你在用 Derby,现在就该迁移。
6.5 Xa 内部类的存在意义
java
private Xa xa = new Xa();
spring.datasource.xa.* ------ 只在 XADataSourceAutoConfiguration 里用到 (PropertiesJdbcConnectionDetails.getXaDataSourceClassName())。
⚠️ 未核实 :Xa 的字段列表(我没读)。从用法看至少有 dataSourceClassName properties 等。第 23 章讲 JPA/XA 时如果涉及会补。
6.6 afterPropertiesSet 与 BeanClassLoaderAware
java
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
public void afterPropertiesSet() throws Exception {
if (this.embeddedDatabaseConnection == null) {
this.embeddedDatabaseConnection = EmbeddedDatabaseConnection.get(this.classLoader);
}
}
}
EmbeddedDatabaseConnection.get(classLoader) 在 afterPropertiesSet 里做------ 即绑定完成后才探测 classpath 上有哪些嵌入式数据库。
为什么要在这里而不是构造时? 因为要用到 classLoader(由 BeanClassLoaderAware 注入,晚于构造)。
它必须早于 determineXxx() 被调用 ------ @ConfigurationProperties bean 的 afterPropertiesSet 由 Spring 在属性绑定后调用, 所以没问题。
7. DatabaseDriver:346 行的数据库知识库
@since 1.4.0 的公共枚举。它编码了每种数据库的:
| 信息 | 用途 |
|---|---|
| productName | 从 DatabaseMetaData.getDatabaseProductName() 反查 |
| driverClassName | 驱动类 |
| xaDataSourceClassName | XA 数据源类 |
| validationQuery | 连接有效性检测 SQL |
| urlPrefixes | 从 JDBC URL 反查 |
7.1 枚举常量(部分)
java
public enum DatabaseDriver {
UNKNOWN(null, null),
@Deprecated(since = "4.1.0", forRemoval = true)
DERBY("Apache Derby", "org.apache.derby.jdbc.EmbeddedDriver", "org.apache.derby.jdbc.EmbeddedXADataSource",
"SELECT 1 FROM SYSIBM.SYSDUMMY1"),
H2("H2", "org.h2.Driver", "org.h2.jdbcx.JdbcDataSource", "SELECT 1"),
HSQLDB("HSQL Database Engine", "org.hsqldb.jdbc.JDBCDriver", "org.hsqldb.jdbc.pool.JDBCXADataSource",
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.SYSTEM_USERS"),
SQLITE("SQLite", "org.sqlite.JDBC"),
MYSQL("MySQL", "com.mysql.cj.jdbc.Driver", "com.mysql.cj.jdbc.MysqlXADataSource", "/* ping */ SELECT 1"),
MARIADB("MariaDB", "org.mariadb.jdbc.Driver", "org.mariadb.jdbc.MariaDbDataSource", "SELECT 1"),
ORACLE("Oracle", "oracle.jdbc.OracleDriver", "oracle.jdbc.xa.client.OracleXADataSource",
"SELECT 'Hello' from DUAL"),
POSTGRESQL("PostgreSQL", "org.postgresql.Driver", "org.postgresql.xa.PGXADataSource", "SELECT 1"),
...
几个有意思的 validationQuery:
| 数据库 | validationQuery | 为什么这样写 |
|---|---|---|
| H2 | SELECT 1 |
最简单 |
| HSQLDB | SELECT COUNT(*) FROM INFORMATION_SCHEMA.SYSTEM_USERS |
HSQLDB 早期版本不支持无 FROM 的 SELECT |
| MySQL | /* ping */ SELECT 1 |
/* ping */ 是 MySQL 的特殊优化:带这个注释的查询走"轻量 ping"路径,不进查询缓存、不记慢日志 |
| Oracle | SELECT 'Hello' from DUAL |
Oracle 要求 FROM,用 DUAL 伪表 |
| Derby | SELECT 1 FROM SYSIBM.SYSDUMMY1 |
同 Oracle,伪表名不同 |
| SQL Server | SELECT 1 |
--- |
★ /* ping */ 是本章最有价值的一个冷知识。 MySQL Connector/J 会识别这个注释,把查询当作 ping 处理------ 这是连接池做连接检测时的标准优化 。手写 SELECT 1 也行,但会走完整查询路径。
7.2 fromJdbcUrl:前缀匹配的细节
java
public static DatabaseDriver fromJdbcUrl(@Nullable String url) {
if (StringUtils.hasLength(url)) {
Assert.isTrue(url.startsWith("jdbc"), "'url' must start with \"jdbc\"");
String urlWithoutPrefix = url.substring("jdbc".length()).toLowerCase(Locale.ENGLISH);
for (DatabaseDriver driver : values()) {
for (String urlPrefix : driver.getUrlPrefixes()) {
String prefix = ":" + urlPrefix + ":";
if (driver != UNKNOWN && urlWithoutPrefix.startsWith(prefix)) {
return driver;
}
}
}
}
return UNKNOWN;
}
三个细节:
① Assert.isTrue(url.startsWith("jdbc")) URL 必须以 jdbc 开头,否则抛异常 。 但注意:这个检查只在 StringUtils.hasLength(url) 为真时执行------ 空 URL 直接返回 UNKNOWN。
潜在陷阱 :
spring.datasource.url=myhost:3306/db(漏了jdbc:前缀) 会在启动时抛IllegalArgumentException: 'url' must start with "jdbc"。 这个错误信息比较直白,还算好排查。
② toLowerCase(Locale.ENGLISH) 大小写不敏感,且显式指定 Locale ------避免在土耳其语 locale 下 I → ı 的转换问题(经典的 Locale bug)。
③ ":" + urlPrefix + ":" 的前后冒号
jdbc:mysql://... → 去掉 jdbc 后是 mysql://... → 但比较用的是 :mysql:。 所以 mysql:// 不匹配 :mysql:?
不对 ------让我重新推: jdbc:mysql://localhost 去掉前 3 个字符(jdbc)→ :mysql://localhost。 比较 startsWith(":mysql:") → :mysql: ✓ 匹配。
所以 getUrlPrefixes() 返回的是 mysql 这种不带冒号的形式, 比较时前后各加一个冒号。 前后冒号保证完整匹配(不会把 mysql2 误判为 mysql)。
④ driver != UNKNOWN 的跳过 UNKNOWN 的 urlPrefixes 是空(构造参数是 null, null), 所以这个判断是防御性的(正常情况下遍历不到它,或它的 prefix 列表为空)。
⑤ values() 的顺序即优先级 Enum.values() 按声明顺序返回。 DatabaseDriver 的声明顺序里 Derby 在 H2 之前------ 如果两个驱动的 URL 前缀能匹配同一个 URL(不太可能),声明靠前的胜出。
⚠️ 未核实 :是否存在 URL 前缀重叠的数据库对。 (比如 jtds 与 sqlserver------JTDS 驱动可以连 SQL Server。 从代码看 JTDS 覆写了 getUrlPrefixes(),但它的前缀是啥我没读。)
7.3 fromProductName:另一条推断路径
java
public static DatabaseDriver fromProductName(@Nullable String productName) {
if (StringUtils.hasLength(productName)) {
for (DatabaseDriver candidate : values()) {
if (candidate.getProductName() != null && candidate.getProductName().equalsIgnoreCase(productName)) {
return candidate;
}
}
}
return UNKNOWN;
}
从 DatabaseMetaData.getDatabaseProductName() 反查------ 用在不知道 URL 但能连上数据库的场景(比如 Actuator 健康检查、 SchemaManagementProvider)。
JTDS 的 productName 是 null:
java
/**
* jTDS. As it can be used for several databases, there isn't a single product name we
* could rely on.
*/
JTDS(null, "net.sourceforge.jtds.jdbc.Driver"),
Javadoc 解释了原因:jTDS 能连多种数据库, 所以 getDatabaseProductName() 返回的是真实数据库 的名字,不是 "jTDS"。 因此 jTDS 无法从 productName 反查。
这是"元数据缺失"的诚实处理 : 与其猜,不如返回 null 让
fromProductName跳过它。
7.4 HANA 覆写 getUrlPrefixes
java
HANA("HDB", "com.sap.db.jdbc.Driver", "com.sap.db.jdbcext.XADataSourceSAP", "SELECT 1 FROM SYS.DUMMY") {
@Override
protected Collection<String> getUrlPrefixes() {
return Collections.singleton("sap");
}
},
枚举常量可以带方法体 (匿名子类)。 HANA 的 URL 是 jdbc:sap://...,所以前缀是 sap。
为什么它需要覆写而其它不需要? 因为默认实现大概是"从 productName 或某种约定推导"。 ⚠️ 未核实 :getUrlPrefixes() 的默认实现(没读)。 从 HANA 和 SQLSERVER 都覆写了来看,默认实现可能不够用。
8. 三个"特殊"数据源配置
8.1 LazyConnectionDataSourceConfiguration:用 BPP 替换 bean
java
@Configuration
@ConditionalOnBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.connection-fetch", havingValue = "lazy")
class LazyConnectionDataSourceConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static LazyConnectionDataSourceBeanPostProcessor lazyConnectionDataSourceBeanPostProcessor(
ObjectProvider<MBeanExporter> mbeanExporter) {
return new LazyConnectionDataSourceBeanPostProcessor(mbeanExporter);
}
static class LazyConnectionDataSourceBeanPostProcessor implements BeanPostProcessor, Ordered {
@Override
public @Nullable Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (beanName.equals("dataSource") && bean instanceof DataSource dataSource) {
this.mbeanExporter.ifAvailable((exporter) -> {
if (JmxUtils.isMBean(dataSource.getClass())) {
exporter.registerManagedResource(dataSource, beanName);
}
});
return new LazyConnectionDataSourceProxy(dataSource);
}
return bean;
}
@Override
public int getOrder() {
return 0;
}
}
}
spring.datasource.connection-fetch=lazy ------ 一个不常被提到的配置项。
作用 :把 dataSource bean 换成 LazyConnectionDataSourceProxy, 延迟到真正执行 SQL 时才从池里取连接。
解决什么问题? 考虑这个场景:
java
@Transactional(readOnly = true)
public List<User> findAll() {
// 如果没查缓存就直接返回了,这次调用根本不需要数据库连接
}
默认情况下,@Transactional 的拦截器会立即从连接池取连接(开启事务)。 如果方法体里很多分支不需要查库,就白白占用了连接。
LazyConnectionDataSourceProxy 让"取连接"推迟到第一次真正用 Connection。
三个实现细节:
① postProcessAfterInitialization(与 §5.3 的 Before 相反) 因为要替换整个 bean 对象(return new LazyConnectionDataSourceProxy(...)), 必须在 bean 完全初始化之后 。替换 = After,改属性 = Before。
② 用 postProcessAfterInitialization 替换 bean 是有风险的 如果这个 bean 已经被其它 bean 注入了,替换就不会生效 (其它 bean 拿到的是原对象)。 BPP 在 bean 初始化后立刻执行,早于 它被注入给别人,所以安全。 但如果有人用 @DependsOn 或者提前 getBean,就会拿到未包装的版本。
③ 顺手做了 JMX 注册
java
this.mbeanExporter.ifAvailable((exporter) -> {
if (JmxUtils.isMBean(dataSource.getClass())) {
exporter.registerManagedResource(dataSource, beanName);
}
});
为什么? 因为替换后,容器里的 dataSource bean 是 LazyConnectionDataSourceProxy, 它本身不是 MBean ,JMX 导出的会是代理对象(没有有用的指标)。 所以在替换之前 ,先把真实的数据源注册到 JMX。
这是一个"副作用补偿" :替换 bean 会破坏原有的 JMX 机制, 所以手动补上。注释里写得很清楚(类 Javadoc: "Also make sure to register the target DataSource in the JMX domain if necessary")。
对比 JndiDataSourceAutoConfiguration 里的反向操作:
java
private void excludeMBeanIfNecessary(Object candidate, String beanName, ApplicationContext context) {
for (MBeanExporter mbeanExporter : context.getBeansOfType(MBeanExporter.class).values()) {
if (JmxUtils.isMBean(candidate.getClass())) {
mbeanExporter.addExcludedBean(beanName);
}
}
}
JNDI 数据源要排除 JMX(因为容器可能已经注册过了,重复注册会冲突)。
一个注册、一个排除------都是为了让 JMX 与数据源的实际形态对齐。
8.2 DataSourceJmxConfiguration
被 PooledDataSourceConfiguration 导入。
⚠️ 未核实 :它的内容(没读)。从名字看是把支持 JMX 的连接池 (Tomcat、Hikari 等)注册到 MBeanExporter。 第 25 章讲 Actuator 时如果涉及 JMX 会补。
8.3 DataSourceCheckpointRestoreConfiguration 与 HikariCheckpointRestoreLifecycle
被 DataSourceAutoConfiguration 类级 @Import(无条件导入)。
spring-boot-jdbc 里有一个 HikariCheckpointRestoreLifecycle 类------ CRaC(Coordinated Restore at Checkpoint) 支持。 第 12 章讲过 CRaC:JVM 做 checkpoint 后恢复时, 数据库连接必须重建(socket 已经失效了)。
⚠️ 未核实 :这两个类的具体实现。 推断:HikariCheckpointRestoreLifecycle 在 onCheckpoint 时关闭池、 onRestore 时重建。自测方法:读这两个类(它们在本仓库,第 12 章未展开)。
9. 事务:DataSourceTransactionManagerAutoConfiguration
java
@AutoConfiguration(before = TransactionAutoConfiguration.class,
after = { DataSourceAutoConfiguration.class, TransactionManagerCustomizationAutoConfiguration.class })
@ConditionalOnClass({ DataSource.class, JdbcTemplate.class, TransactionManager.class })
@AutoConfigureOrder(Ordered.LOWEST_PRECEDENCE)
public final class DataSourceTransactionManagerAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnSingleCandidate(DataSource.class)
static class JdbcTransactionManagerConfiguration {
@Bean
@ConditionalOnMissingBean(TransactionManager.class)
DataSourceTransactionManager transactionManager(Environment environment, DataSource dataSource,
ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
DataSourceTransactionManager transactionManager = createTransactionManager(environment, dataSource);
transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(transactionManager));
return transactionManager;
}
private DataSourceTransactionManager createTransactionManager(Environment environment, DataSource dataSource) {
return environment.getProperty("spring.dao.exceptiontranslation.enabled", Boolean.class, Boolean.TRUE)
? new JdbcTransactionManager(dataSource) : new DataSourceTransactionManager(dataSource);
}
}
}
9.1 JdbcTransactionManager vs DataSourceTransactionManager
java
environment.getProperty("spring.dao.exceptiontranslation.enabled", Boolean.class, Boolean.TRUE)
? new JdbcTransactionManager(dataSource) : new DataSourceTransactionManager(dataSource);
默认用 JdbcTransactionManager (spring.dao.exceptiontranslation.enabled 默认 true)。
两者的区别:
DataSourceTransactionManager:Spring 的经典实现JdbcTransactionManager(@since 5.3):子类,额外提供异常转译
JdbcTransactionManager 实现 SQLExceptionTranslator 接口, 能把 SQLException 转成 Spring 的 DataAccessException 体系 (DuplicateKeyException、DataIntegrityViolationException 等)。
为什么这很重要? 因为在 @Transactional 方法里, 异常转译默认发生在 DAO 层 (JdbcTemplate 内部)。 但如果你直接使用 Connection(比如 JOOQ、MyBatis 的某些用法), 异常转译需要事务管理器来做。
实践含义 : 用
JdbcTemplate时,spring.dao.exceptiontranslation.enabled影响不大 (JdbcTemplate自己会转译)。 但用 JOOQ / MyBatis / 裸 JDBC 时,把它设成false会让你拿到原始SQLException而非 Spring 的DataAccessException。
⚠️ 未核实 :JdbcTransactionManager 的异常转译具体在哪个环节生效 (它在 Framework 里)。我只能确认 Boot 的选择逻辑。
9.2 @AutoConfigureOrder(Ordered.LOWEST_PRECEDENCE)
自动配置类级别的 @AutoConfigureOrder ,不是 @Order。
第 14 章讲过:自动配置类的排序用 @AutoConfigureOrder / @AutoConfigureBefore/After, 普通 bean 的排序用 @Order。
LOWEST_PRECEDENCE = 排在最后。
为什么事务管理器要排最后? 因为它要求 @ConditionalOnSingleCandidate(DataSource.class)------ 必须等所有可能创建 DataSource 的自动配置都跑完, 才能确定"是否只有一个候选"。
9.3 @ConditionalOnSingleCandidate(DataSource.class)
第 21 章 gRPC client 用过同一个条件。这里是它的经典用途:
| DataSource bean 数量 | 结果 |
|---|---|
| 0 | 不匹配(没有数据源,不需要事务管理器) |
| 1 | 匹配 ✓ |
2+,其中一个是 @Primary |
匹配(用 @Primary 那个) |
2+,无 @Primary |
不匹配(Boot 不替你猜) |
"多个数据源时不自动配置事务管理器" ------ 这是正确的: 多数据源场景下,用户必须自己指定每个事务管理器绑哪个数据源。
9.4 TransactionManagerCustomizers
java
transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(transactionManager));
一个定制器链(第 19/20 章反复见到的模式)。 来自 TransactionManagerCustomizationAutoConfiguration (本自动配置 after 它)。
⚠️ 未核实 :TransactionManagerCustomizers 的定义与内容 (在 spring-boot-tx 或类似模块,本章未读)。
10. 数据库初始化:DataSourceInitializationAutoConfiguration
java
@AutoConfiguration
@ConditionalOnMissingBean(ApplicationScriptDatabaseInitializer.class)
@ConditionalOnSingleCandidate(DataSource.class)
@ConditionalOnClass(DatabasePopulator.class)
@Import(DatabaseInitializationDependencyConfigurer.class)
@EnableConfigurationProperties(SqlInitializationProperties.class)
@ConditionalOnSqlInitialization
public final class DataSourceInitializationAutoConfiguration {
@Bean
ApplicationDataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource,
SqlInitializationProperties properties) {
return new ApplicationDataSourceScriptDatabaseInitializer(
determineDataSource(dataSource, properties.getUsername(), properties.getPassword()), properties);
}
private static DataSource determineDataSource(DataSource dataSource, @Nullable String username,
@Nullable String password) {
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
return DataSourceBuilder.derivedFrom(dataSource)
.username(username)
.password(password)
.type(SimpleDriverDataSource.class)
.build();
}
return dataSource;
}
}
10.1 ★ derivedFrom + SimpleDriverDataSource:一个精巧的"降级"
java
DataSourceBuilder.derivedFrom(dataSource) // ① 复制原数据源的配置
.username(username).password(password) // ② 换成 sql.init 的凭据
.type(SimpleDriverDataSource.class) // ③ 换成【非连接池】实现
.build();
为什么要用 SimpleDriverDataSource(非连接池)?
因为初始化脚本只需要一个连接,用完就关。 如果用连接池:
- 初始化时机很早(可能连接池还没完全就绪)
- 白占一个连接
- 初始化完连接归还,池里多一个"已用过"的连接
用一个一次性数据源更干净。
为什么用 derivedFrom 而不是新建? 因为要继承原数据源的 url / driverClassName 。 derivedFrom 用 DataSourceUnwrapper.unwrapRoot 拿到最内层数据源, 再读它的属性(§3.4)。
10.2 凭据分离:sql.init.username / sql.init.password
yaml
spring:
datasource:
username: app_user # 应用运行时用(受限权限)
password: ...
sql:
init:
username: admin # 执行 DDL 用(高权限)
password: ...
mode: always
这是一个安全最佳实践: 应用运行用的账号只有 DML 权限, schema 变更用另一个高权限账号(只在启动时用一次)。
Boot 支持这个模式 ,且只有同时配了 username 和 password 才启用:
java
if (StringUtils.hasText(username) && StringUtils.hasText(password)) { ... }
只配 username 不生效 ------ 因为空密码在某些数据库上是合法的, Boot 不能假设"没配密码"等于"密码为空"。
10.3 @ConditionalOnSqlInitialization
一个自定义条件注解(@ConditionalOnXxx 家族)。 ⚠️ 未核实 :它的实现(应该在 spring-boot-sql 模块)。 从名字推断:检查 spring.sql.init.* 相关的脚本是否存在/启用。
10.4 DatabaseInitializationDependencyConfigurer
@Import(DatabaseInitializationDependencyConfigurer.class) ------ 名字里有 "DependencyConfigurer"。
它的作用 (推断):让其它需要"数据库已初始化"的 bean (JPA 的 EntityManagerFactory、JOOQ 的 DSLContext、Spring Batch 等) 依赖于 DatabaseInitializer。
spring-boot-jdbc 里有 SpringJdbcDependsOnDatabaseInitializationDetector、 spring-boot-jooq 里有 JooqDependsOnDatabaseInitializationDetector、 spring-boot-jpa 里有 JpaDependsOnDatabaseInitializationDetector------ 三个模块各有一个 "Detector",它们构成一个 SPI。
⚠️ 未核实 :
DatabaseInitializationDependencyConfigurer与这些 Detector 的协作机制 (在spring-boot-sql模块)。 这是 Boot 3.x 引入的"数据库初始化依赖排序"机制,第 23 章讲 JPA 时会再遇到。 自测方法 :读spring-boot-sql模块的DatabaseInitializationDependencyConfigurer。
11. JNDI 与 XA:两条"非主流"路径
11.1 JndiDataSourceAutoConfiguration
java
@AutoConfiguration(before = { XADataSourceAutoConfiguration.class, DataSourceAutoConfiguration.class })
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnProperty("spring.datasource.jndi-name")
@EnableConfigurationProperties(DataSourceProperties.class)
public final class JndiDataSourceAutoConfiguration {
@Bean(destroyMethod = "")
@ConditionalOnMissingBean
DataSource dataSource(DataSourceProperties properties, ApplicationContext context) {
JndiDataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
String jndiName = properties.getJndiName();
Assert.state(jndiName != null, "'jndiName' must not be null");
DataSource dataSource = dataSourceLookup.getDataSource(jndiName);
excludeMBeanIfNecessary(dataSource, "dataSource", context);
return dataSource;
}
@Bean(destroyMethod = "") ------ 空字符串!
第 13 章讲过 @Bean 的 destroyMethod:
- 默认
AbstractBeanDefinition.INFER_METHOD→ 自动推断close/shutdown - 设为
""→ 禁用 推断
为什么? 因为 JNDI 数据源归容器管 ------ 应用关闭时不能 调用它的 close(), 否则会关掉容器(如 Tomcat)自己管理的数据源, 其它应用或下一次部署会受影响。
这是本章最容易被忽略但很重要的一行 : "你不拥有它,就不要关闭它。" 第 18 章
WebServer的destroy()是@since 3.2.0且默认空实现, 也是同样的"所有权"考量。
Assert.state(jndiName != null) ------ 但外层已经有 @ConditionalOnProperty("spring.datasource.jndi-name"), 所以理论上不会是 null。这是给 NullAway/IDE 看的冗余断言 (Boot 大量使用这种写法来通过静态分析)。
11.2 XADataSourceAutoConfiguration
java
@AutoConfiguration(before = DataSourceAutoConfiguration.class, after = JtaAutoConfiguration.class)
@EnableConfigurationProperties(DataSourceProperties.class)
@ConditionalOnClass({ DataSource.class, TransactionManager.class, EmbeddedDatabaseType.class })
@ConditionalOnBean(XADataSourceWrapper.class)
@ConditionalOnMissingBean(DataSource.class)
public final class XADataSourceAutoConfiguration implements BeanClassLoaderAware {
@Bean
@ConditionalOnMissingBean(JdbcConnectionDetails.class)
PropertiesJdbcConnectionDetails jdbcConnectionDetails(DataSourceProperties properties) {
return new PropertiesJdbcConnectionDetails(properties);
}
@Bean
DataSource dataSource(XADataSourceWrapper wrapper, DataSourceProperties properties,
JdbcConnectionDetails connectionDetails, ObjectProvider<XADataSource> xaDataSource) throws Exception {
return wrapper... // 未读完
}
}
触发条件是 @ConditionalOnBean(XADataSourceWrapper.class) ------ XA 数据源需要一个包装器 (把 XADataSource 包装成普通 DataSource 并接入 JTA 事务管理器)。
XADataSourceWrapper 是一个接口(在 spring-boot-jdbc):
java
public interface XADataSourceWrapper {
DataSource wrap(XADataSource dataSource) throws ...;
}
谁来提供实现? 推测是 Atomikos / Bitronix / Narayana 这类 JTA 实现。 ⚠️ 未核实 :Boot 4.1 里是否还有这些实现(本仓库没看到)。 从 after = JtaAutoConfiguration.class 看,JTA 是配套的。
注意它也注册 PropertiesJdbcConnectionDetails ------ 与 DataSourceAutoConfiguration.PooledDataSourceConfiguration 里完全一样的代码。 两处重复,因为两个路径都可能走到。
这是"复制而非抽取"的一个例子 : 把 3 行代码抽成一个公共方法或基类,收益不大, 反而引入耦合。Boot 在这类情况下选择复制 。 第 19 章
SpringBeanHandlerInstantiator复制 Framework 的类也是同类决策。
12. 失败分析:两个 FailureAnalyzer
第 16 章讲过 Boot 有 37 个 FailureAnalyzer。spring-boot-jdbc 贡献了至少 2 个:
12.1 DataSourceBeanCreationFailureAnalyzer:Boot 最著名的错误提示
java
private String getDescription(DataSourceBeanCreationException cause) {
StringBuilder description = new StringBuilder();
description.append("Failed to configure a DataSource: ");
if (!StringUtils.hasText(cause.getProperties().getUrl())) {
description.append("'url' attribute is not specified and ");
}
description.append(String.format("no embedded datasource could be configured.%n"));
description.append(String.format("%nReason: %s%n", cause.getMessage()));
return description.toString();
}
private String getAction(DataSourceBeanCreationException cause) {
StringBuilder action = new StringBuilder();
action.append(String.format("Consider the following:%n"));
if (EmbeddedDatabaseConnection.NONE == cause.getConnection()) {
action.append(String.format("\tIf you want an embedded database (H2, HSQL or Derby), please put it on the classpath.%n"));
}
else {
action.append(String.format("\tReview the configuration of %s%n.", cause.getConnection()));
}
action.append("\tIf you have database settings to be loaded from a particular "
+ "profile you may need to activate it").append(getActiveProfiles());
return action.toString();
}
这就是那句每个 Spring Boot 初学者都见过的错误:
vbnet
Failed to configure a DataSource: 'url' attribute is not specified and no embedded
datasource could be configured.
Reason: Failed to determine a suitable driver class
Action:
Consider the following:
If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
If you have database settings to be loaded from a particular profile you may need to
activate it (no profiles are currently active).
三个组成部分:
- 判断 url 是否配了 (用
getUrl()而非determineUrl(),见 §6.1) - 判断 classpath 上有没有嵌入式数据库 (
EmbeddedDatabaseConnection.NONE) - 列出当前激活的 profile(这是最有用的诊断信息!)
第 3 点 是关键:很多人把数据库配置写在 application-prod.yaml 里, 却忘了激活 profile。这条提示直接告诉你"当前没有激活的 profile"。
这是"诊断能力提前布局"的最佳范例之一 : 一个可能让人困惑一小时的启动失败,被压缩成 8 行可操作的提示。 而且它的构造只需要 Environment (
DataSourceBeanCreationFailureAnalyzer(Environment)) ------FailureAnalyzer可以拿到Environment,这是第 16 章讲过的机制。
12.2 HikariDriverConfigurationFailureAnalyzer
⚠️ 未核实 :这个类的内容(没读)。 从名字推断:处理 Hikari 的 driver 配置冲突 (比如同时配了 dataSourceClassName 和 driverClassName,见 §4.3)。 自测方法:读该类,或故意配冲突看启动输出。
13. DataSourcePoolMetadata:跨连接池的统一抽象
spring-boot-jdbc 支持 4+ 种连接池,每种的监控 API 完全不同:
| 连接池 | 监控入口 |
|---|---|
| Hikari | HikariDataSource 实现 HikariConfigMXBean / HikariPoolMXBean |
| Tomcat | org.apache.tomcat.jdbc.pool.DataSource 实现 ConnectionPoolMBean |
| DBCP2 | BasicDataSource 实现 BasicDataSourceMXBean |
| Oracle UCP | PoolDataSource 有自己的 API |
Boot 的做法 :定义统一的 DataSourcePoolMetadata 接口,为每个池写一个适配器。
java
public interface DataSourcePoolMetadata {
@Nullable Float getUsage(); // 使用率
@Nullable Integer getActive(); // 活跃连接数
default @Nullable Integer getIdle() { // 空闲连接数(有默认实现)
// ...
}
@Nullable Integer getMax(); // 最大连接数
@Nullable Integer getMin(); // 最小连接数
@Nullable String getValidationQuery();
@Nullable Boolean getDefaultAutoCommit();
}
★ 所有方法都返回 @Nullable ------ 这是关键设计。
为什么? 因为不是每个连接池都能提供所有指标。 比如某些池没有"最小连接数"的概念。
"缺失只降性能不降正确性"(前面章节提过的定律)在这里再次体现: 拿不到的指标返回 null,消费者(Actuator 端点、Micrometer binder)跳过即可, 不会因为"某个池不支持某指标"而整体失败。
getIdle() 有默认实现 ------ 大概是从 max - active 推算。 ⚠️ 未核实:默认实现的公式(没读方法体)。
13.1 DataSourcePoolMetadataProvider:lambda 形式的策略
java
@Bean
DataSourcePoolMetadataProvider hikariPoolDataSourceMetadataProvider() {
return (dataSource) -> {
HikariDataSource hikariDataSource = DataSourceUnwrapper.unwrap(dataSource, HikariConfigMXBean.class,
HikariDataSource.class);
if (hikariDataSource != null) {
return new HikariDataSourcePoolMetadata(hikariDataSource);
}
return null;
};
}
它是一个函数式接口 (DataSourcePoolMetadataProvider), 实现是 "能处理就处理,不能就返回 null"。
DataSourceUnwrapper.unwrap(dataSource, HikariConfigMXBean.class, HikariDataSource.class) ------ §3.4 见过 unwrapRoot,这里是有目标的 unwrap: 剥掉代理,直到找到实现了 HikariConfigMXBean 且是 HikariDataSource 的那一层。
为什么要"直到找到某接口"? 因为数据源可能被 LazyConnectionDataSourceProxy(§8.1)或 DelegatingDataSource 包装, 直接 instanceof HikariDataSource 会失败。
13.2 CompositeDataSourcePoolMetadataProvider
java
DataSourceHealthContributorAutoConfiguration(ObjectProvider<DataSourcePoolMetadataProvider> metadataProviders) {
this.metadataProviders = metadataProviders.orderedStream().toList();
}
@Override
public void afterPropertiesSet() {
this.poolMetadataProvider = new CompositeDataSourcePoolMetadataProvider(this.metadataProviders);
}
"Composite + 挨个试":多个 provider 排好序,第一个返回非 null 的胜出。
这与 gRPC 的 CompositeGrpcChannelFactory 是同一模式(第 21 章), 但更简单:这里是"找到第一个能处理的",那里是"按 target 委派"。
14. 健康与指标:两个 @ConditionalOnBean(DataSource.class)
14.1 健康:DataSourceHealthContributorAutoConfiguration
java
@AutoConfiguration(after = DataSourceAutoConfiguration.class)
@ConditionalOnClass({ JdbcTemplate.class, AbstractRoutingDataSource.class, ConditionalOnEnabledHealthIndicator.class })
@ConditionalOnBean(DataSource.class)
@ConditionalOnEnabledHealthIndicator("db")
@EnableConfigurationProperties(DataSourceHealthIndicatorProperties.class)
public final class DataSourceHealthContributorAutoConfiguration implements InitializingBean {
@ConditionalOnEnabledHealthIndicator("db") ------ Boot 的健康指示器统一开关(management.health.db.enabled)。 第 25 章讲 Actuator 时展开。
★ 它处理了"多个数据源"与"路由数据源"两种情况:
java
private HealthContributor createContributor(Map<String, DataSource> beans) {
Assert.notEmpty(beans, "'beans' must not be empty");
if (beans.size() == 1) {
return createContributor(beans.values().iterator().next());
}
return CompositeHealthContributor.fromMap(beans, this::createContributor);
}
private HealthContributor createContributor(DataSource source) {
if (isRoutingDataSource(source)) {
return new RoutingDataSourceHealthContributor(extractRoutingDataSource(source), this::createContributor);
}
return new DataSourceHealthIndicator(source, getValidationQuery(source));
}
三种形态:
| 场景 | 结果 |
|---|---|
| 1 个普通数据源 | DataSourceHealthIndicator |
| N 个数据源 | CompositeHealthContributor(每个一个子节点) |
AbstractRoutingDataSource |
RoutingDataSourceHealthContributor(递归检查它路由到的所有目标) |
RoutingDataSourceHealthContributor 递归 很巧妙: 路由数据源本身连不上任何库,要检查的是它背后的每个目标数据源。
SimpleAutowireCandidateResolver.resolveAutowireCandidates(beanFactory, DataSource.class, false, true) ------ 用"自动装配候选解析"来找出所有数据源 bean, 这样只返回那些可被注入的(排除非候选的、内部的)。
ignoreRoutingDataSources 配置:
java
if (dataSourceHealthIndicatorProperties.isIgnoreRoutingDataSources()) {
Map<String, DataSource> filteredDatasources = dataSources.entrySet().stream()
.filter((e) -> !isRoutingDataSource(e.getValue()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
return createContributor(filteredDatasources);
}
management.health.db.ignore-routing-data-sources ------ 默认不忽略(即会递归检查路由目标)。
14.2 指标:DataSourcePoolMetricsAutoConfiguration
java
@AutoConfiguration(after = DataSourceAutoConfiguration.class,
afterName = "org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration")
@ConditionalOnClass({ DataSource.class, MeterRegistry.class })
@ConditionalOnBean({ DataSource.class, MeterRegistry.class })
public final class DataSourcePoolMetricsAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(DataSourcePoolMetadataProvider.class)
static class DataSourcePoolMetadataMetricsConfiguration {
private static final String DATASOURCE_SUFFIX = "dataSource";
@Bean
DataSourcePoolMetadataMeterBinder dataSourcePoolMetadataMeterBinder(
ConfigurableListableBeanFactory beanFactory,
ObjectProvider<DataSourcePoolMetadataProvider> metadataProviders) {
return new DataSourcePoolMetadataMeterBinder(
SimpleAutowireCandidateResolver.resolveAutowireCandidates(beanFactory, DataSource.class, false, true),
metadataProviders);
}
}
}
内层的 @ConditionalOnBean(DataSourcePoolMetadataProvider.class) : 如果没有任何一个连接池的元数据提供者,就不注册指标 binder。
这很重要:如果你用的是 Boot 不认识的数据源(比如自定义池), DataSourcePoolMetadataProvider 一个都没有 → 不注册指标(而不是注册一个空的)。
这是"第二十四定律"的反面应用: Boot 只在"能力可用"时才注册"能力组件"。 元数据提供者是"能力检测",指标 binder 是"能力组件"。
DATASOURCE_SUFFIX = "dataSource" ------ 指标名后缀, 最终指标大概是 hikaricp.connections.active 之类。 ⚠️ 未核实 :完整的指标命名(在 DataSourcePoolMetrics 里,第 26 章讲 Micrometer 时会展开)。
15. 实践配方
基于本章源码,几个可操作的配置建议。
15.1 切换连接池
默认:classpath 上有谁用谁(Hikari > Tomcat > DBCP2 > OracleUCP)。
强制指定:
yaml
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource # ★ 必须写全限定名
⚠️
havingValue是精确匹配 ,写HikariDataSource不生效。
注意 :spring.datasource.type 只是选择 , 真正的池参数在 spring.datasource.hikari.* / .tomcat.* / .dbcp2.* / .oracleucp.*。
★ 用 C3P0/Vibur 时的陷阱 (§4.2 推断): Generic 配置类的 dataSource() 没有 @ConfigurationProperties, 所以 spring.datasource.c3p0.* 不会被绑定 。 如果需要池参数,得自己定义 DataSource bean。
15.2 多数据源
Boot 不会自动配置多数据源。标准做法:
java
@Configuration
public class MultiDataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("app.datasource.primary")
DataSource primaryDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("app.datasource.secondary")
DataSource secondaryDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
}
三个必须注意的点(都来自本章源码):
① bean 名不能叫 dataSource 因为 JdbcConnectionDetailsBeanPostProcessor 只处理名为 dataSource 的 bean(§5.3)。 你自己的多数据源不会受影响------但如果你给其中一个起名叫 dataSource, 它会被 BPP 覆盖。
② 用户定义 DataSource bean 后,Boot 的自动配置全部失效 @ConditionalOnMissingBean(DataSource.class) 在 EmbeddedDatabaseConfiguration / PooledDataSourceConfiguration / DataSourceConfiguration.* 上都有。
③ 事务管理器不会自动配置 @ConditionalOnSingleCandidate(DataSource.class)(§9.3)------ 两个数据源且没 @Primary 时不匹配。 有 @Primary 时会 匹配(用 primary 那个), 但第二个数据源就需要你自己定义 DataSourceTransactionManager。 DataSourceTransactionManagerAutoConfiguration 上的 @ConditionalOnMissingBean(TransactionManager.class) 意味着 你定义了任何一个 TransactionManager,Boot 的就不注册。
15.3 生产/开发切换
yaml
# application.yaml(通用)
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: ${DB_USER}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 20
yaml
# application-test.yaml(测试)
spring:
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driver-class-name: org.h2.Driver
或者干脆不配 url,让 Boot 用嵌入式数据库(§2.3、§6.2):
- 需要 H2/HSQL 在 classpath
- 会得到一个 UUID 名字的独立库(测试隔离)
★ 一个陷阱 (§2.3 的 catch): 如果 spring.datasource.url=${DB_URL} 而 DB_URL 未定义, hasDataSourceUrlProperty 会吞掉异常 ,当成"没配 url" → 静默使用嵌入式数据库。
自测方法:注释掉环境变量,看启动日志里的数据源类型。
15.4 延迟取连接
yaml
spring:
datasource:
connection-fetch: lazy
只在"很多 @Transactional 方法实际不查库"的场景有价值(§8.1)。
副作用 :dataSource bean 变成 LazyConnectionDataSourceProxy, instanceof HikariDataSource 会失败。 Boot 的元数据提供者用 DataSourceUnwrapper 解决了这个(§13.1), 但你自己代码里的 instanceof 要小心。
15.5 SQL 初始化的权限分离
yaml
spring:
datasource:
username: app_user # 运行时(只给 DML 权限)
password: ${APP_PWD}
sql:
init:
username: migrate_user # 只在启动时执行 schema.sql(有 DDL 权限)
password: ${MIGRATE_PWD}
mode: always
必须同时配 username 和 password(§10.2)。
16. 事实清单
本章从源码直接确认的事实(挑最容易被忽略的):
spring-boot-jdbc有 9 条.imports,分成"创建 DataSource"(3 条,互斥) 与"消费 DataSource"(6 条,都用@ConditionalOnSingleCandidate)两组。- JNDI > XA > 普通 ,由
@AutoConfiguration(before = ...)表达; JNDI 靠属性 触发、XA 靠 bean 触发。 DataSourceAutoConfiguration上有@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")------ 有 R2DBC 就不配 JDBC(用字符串类名,无编译期依赖)。EmbeddedDatabaseCondition的 Javadoc 写明 "pooled DataSource always preferred to EmbeddedDatabase" ------ 代码里只有noMatch(...),优先级信息只在 Javadoc(第四定律)。EmbeddedDatabaseCondition对无法解析的占位符静默降级 为"没配 url" (catchIllegalArgumentException,注释说应该用PlaceholderResolutionException)。DataSourceBuilder.lookupPooled的六个池顺序: Hikari → Tomcat → DBCP2 → OracleUCP → C3P0 → Vibur ; OracleUCP 额外要求oracle.jdbc.OracleConnection。DataSourceBuilder还有lookupBasic四个非池实现 (SimpleDriverDataSource、Oracle、H2、Postgres)。DataSourceProperty枚举给每个属性多个候选 setter 名 (url/URL、username/user),按顺序试。DataSourceConfiguration里 5 个内部配置靠@Import顺序 +@ConditionalOnMissingBean互斥,havingValue是精确全限定名匹配。Generic是唯一没有matchIfMissing的(必须显式配spring.datasource.type), 且它的dataSource()没有@ConfigurationProperties。- Hikari 是唯一传
applyDriverClassName=false的------ 因为配了data-source-class-name时不能同时设 driverClassName。 - Tomcat 池是唯一设置
validationQuery+testOnBorrow的; Hikari 与 OracleUCP 是唯一设置 pool name 的。 JdbcConnectionDetailsBeanPostProcessor是PriorityOrdered,getOrder() = HIGHEST_PRECEDENCE + 2,注释说"在属性绑定 BPP 之后"。- BPP 只处理 bean 名为
dataSource的 bean, 且跳过PropertiesJdbcConnectionDetails(属性来源无需覆盖)。 - 四个池的 BPP setter 名各不相同:
setJdbcUrl/setUrl/setURL+setUser。 PropertiesJdbcConnectionDetails用determineXxx()而非getXxx()。DataSourceProperties.generateUniqueName默认 true → 嵌入式库名是 UUID(测试隔离的基石)。- H2 的 URL 带
DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE------ 手工写 URL 时漏了会导致"库是空的"。 - Derby 已 EOL :
DatabaseDriver.DERBY与EmbeddedDatabaseConnection.DERBY都是@Deprecated(since = "4.1.0", forRemoval = true),4.3.0 移除。 driverClassIsLoadable单独抛出UnsupportedClassVersionError, 其它 Throwable 返回 false------区分"找不到"与"用不了"。- MySQL 的 validationQuery 是
/* ping */ SELECT 1------注释让驱动走轻量 ping 路径。 DatabaseDriver.fromJdbcUrl用":" + prefix + ":"前后加冒号做完整匹配, 且toLowerCase(Locale.ENGLISH)(避免土耳其语 locale bug)。JTDS的 productName 是null(Javadoc 说明原因:它能连多种库)。LazyConnectionDataSourceConfiguration用postProcessAfterInitialization替换 bean,并顺手把真实数据源注册到 JMX(补偿)。JndiDataSourceAutoConfiguration用@Bean(destroyMethod = "")禁用销毁推断 ------JNDI 数据源归容器管,应用不能关。- 事务管理器默认用
JdbcTransactionManager(spring.dao.exceptiontranslation.enabled默认 true),它在DataSourceTransactionManager基础上加了异常转译。 DataSourceTransactionManagerAutoConfiguration有@AutoConfigureOrder(LOWEST_PRECEDENCE)------等所有数据源配置跑完再决定。- SQL 初始化用
derivedFrom+SimpleDriverDataSource建一次性 数据源, 且必须同时配sql.init.username和password才启用凭据分离。 DataSourcePoolMetadata的所有方法返回@Nullable------ 不是每个池都能提供所有指标。DataSourceHealthContributorAutoConfiguration处理三种形态: 单个、Composite、RoutingDataSource(递归检查所有路由目标)。DataSourcePoolMetricsAutoConfiguration内层还有@ConditionalOnBean(DataSourcePoolMetadataProvider.class)------ 没有元数据提供者就不注册指标 binder。DataSourceBeanCreationFailureAnalyzer用getUrl()(不是determineUrl()) 判断"用户是否配了",并列出当前激活的 profile。
17. 本章未逐行核实 / 待你实测的项
以下条目我没有从源码确认(按验证成本排序):
本仓库内但未读 : DataSourcePoolMetadata.getIdle() 的默认实现公式; DataSourcePoolMetadataProvidersConfiguration 的 OracleUcp 分支(读了一半); DataSourceJmxConfiguration 的内容; DataSourceCheckpointRestoreConfiguration 与 HikariCheckpointRestoreLifecycle 的实现; HikariDriverConfigurationFailureAnalyzer 的内容; DataSourceHealthIndicatorProperties 的字段(ignoreRoutingDataSources 的默认值); getValidationQuery(source) 的方法体; DataSourcePoolMetrics 的指标命名; DataSourceProperties.Xa 的字段; DataSourceBuilder.MappedDataSourceProperties.forType 的完整分支; DatabaseDriver.getUrlPrefixes() 的默认实现; DataSourceUnwrapper 的实现。
其它模块(本章范围外) : R2dbcAutoConfiguration 是否也有对应的 @ConditionalOnMissingBean(type="javax.sql.DataSource") (§2.1 的对称性问题); TransactionManagerCustomizers 的定义; ConditionalOnSqlInitialization 与 DatabaseInitializationDependencyConfigurer 的实现 (应在 spring-boot-sql 模块); XADataSourceWrapper 的实现提供方(Atomikos/Bitronix/Narayana?Boot 4.1 是否还有); JtaAutoConfiguration 的位置; ConfigurationPropertiesBindingPostProcessor 的确切 order(推断 HIGHEST_PRECEDENCE + 1)。
推断但未验证的行为 : 用 C3P0/Vibur 时 spring.datasource.c3p0.* 是否真的不绑定(§4.2); spring.datasource.url 占位符未解析时是否真的静默降级到嵌入式(§2.3); MBeanExporter 相关的两处 JMX 处理是否覆盖所有场景。
遗留未解决 : 第 17--21 章累积的 WAR 部署 WebApplicationContextInitializer 两次调用问题; 第 19 章的 Jackson 3 WRITE_DATES_AS_TIMESTAMPS 默认值; 第 20 章的 WebClient.Builder 自动配置位置。
18. 方法论:本章沉淀的三条定律
18.1 第二十八定律:getXxx 与 determineXxx 的分离 = 诊断能力的地基
当一个配置值既有"用户配置"又有"推断兜底"两种来源时, Boot 会拆成两个方法:
getXxx(忠实返回配置,可 null)与determineXxx(含推断,失败则抛明确异常)。 失败分析器用getXxx判断"用户到底配了没有"。
DataSourceProperties 里四组方法都这样拆(§6.1), DataSourceBeanCreationFailureAnalyzer 靠 getUrl() 才能说出 "'url' attribute is not specified" 这句精准的诊断。
推广:任何"有默认值/推断"的配置,都要保留"原始值"的访问途径, 否则诊断时无法区分"用户没配"与"用户配了一个等于默认值的东西"。
18.2 第二十九定律:外部信息覆盖内部配置 = BPP 排在属性绑定之后
当"外部来源"(Docker Compose、Testcontainers、云环境)需要覆盖 "配置文件"时,Boot 的固定做法是一个
PriorityOrdered的BeanPostProcessor,order 恰好排在ConfigurationPropertiesBindingPostProcessor之后。
JdbcConnectionDetailsBeanPostProcessor.getOrder() = HIGHEST_PRECEDENCE + 2 (注释明说 "Runs after ConfigurationPropertiesBindingPostProcessor")。
为什么必须在之后? 因为属性绑定会重新把配置文件的值写进 bean。 如果 BPP 在之前,覆盖会被绑定冲掉。
完整的三层时序:
css
① @Bean 方法创建(用 JdbcConnectionDetails,已是外部值)
② ConfigurationPropertiesBindingPostProcessor 绑定 spring.datasource.hikari.*
③ JdbcConnectionDetailsBeanPostProcessor 再覆盖一次(双保险)
为什么 ③ 与 ① 都做? ① 是主路径,③ 防 ② 覆盖。 两道防线------这不是冗余,是"属性绑定可能改写任何字段"的必然结果。
18.3 第三十定律:destroyMethod = "" 标记"所有权边界"
@Bean(destroyMethod = "")的语义是"这个 bean 不是我创建的,我不负责关它"。 看到这一行,就该意识到该资源的所有权在别处(容器、外部系统)。
JndiDataSourceAutoConfiguration 的 @Bean(destroyMethod = "") 是标准例子: JNDI 数据源归应用服务器管,应用关闭它会影响其它应用。
对称的另一端 :GrpcServerHealthAutoConfiguration 的 @Bean(destroyMethod = "enterTerminalState")(第 21 章 §9.1) 是显式指定 销毁方法------因为 HealthStatusManager 的清理方法不叫 close/shutdown, Boot 的自动推断找不到它。
三种 destroyMethod:
| 写法 | 语义 |
|---|---|
| 默认(不写) | 自动推断 close/shutdown |
"" |
禁用销毁(所有权在外) |
"methodName" |
显式指定(方法名不符合约定) |
19. 下一章预告
第 23 章:JPA / Hibernate 与持久化模块。
本章讲的是连接 ,下一章讲对象映射 。 spring-boot-jpa + spring-boot-hibernate 只有 2 条 .imports (HibernateJpaAutoConfiguration、HibernateMetricsAutoConfiguration), 但背后的 JpaBaseConfiguration 是 Boot 里最"厚"的自动配置基类之一。
第 22 章留下的伏笔会在那里得到答案:
DatabaseInitializationDependencyConfigurer与三个DependsOnDatabaseInitializationDetector的协作机制(§10.4)------JPA 是最典型的"必须等数据库初始化完"的消费者SchemaManagementProvider与spring.jpa.hibernate.ddl-auto的推断 (HibernateDefaultDdlAutoProvider已经躺在spring-boot-hibernate里了)spring-boot-hibernate里还有SpringImplicitNamingStrategy与SpringJtaPlatform------两个"Boot 替 Hibernate 补的 Spring 集成件"
另外,spring-boot-data-* 那十几个模块(Cassandra、Couchbase、Elasticsearch、 MongoDB、Neo4j、Redis、R2DBC......)会留到后续章节, 那时会看到本章的 JdbcConnectionDetails 模式如何被推广到每一种 数据存储 ------ConnectionDetails 是 Boot 4.x 数据访问层的统一语言。