Spring Boot 4.1.1 深度源码解析-第 23 章 · JPA / Hibernate 与持久化模块
本章所有类名、方法名、常量、注解参数均来自
spring-boot-4.1.1真实源码。 涉及第三方(Hibernate ORM、Spring Data JPA、Spring Framework ORM)的部分,凡本仓库读不到的,一律标注「未核实」并给出自测方法。
1. 引子:一个只有 1225 行的模块,凭什么撑起一整章
先把一个反直觉的事实摆在前面。整个 spring-boot-jpa 模块的主源码,加起来只有 1225 行:
383 org/springframework/boot/jpa/EntityManagerFactoryBuilder.java
82 org/springframework/boot/jpa/JpaDatabaseInitializerDetector.java
52 org/springframework/boot/jpa/JpaDependsOnDatabaseInitializationDetector.java
23 org/springframework/boot/jpa/package-info.java
69 .../jpa/autoconfigure/BootstrapExecutorRequiredException.java
50 .../jpa/autoconfigure/BootstrapExecutorRequiredFailureAnalyzer.java
37 .../jpa/autoconfigure/EntityManagerFactoryBuilderCustomizer.java
58 .../jpa/autoconfigure/EntityManagerFactoryDependsOnPostProcessor.java
284 .../jpa/autoconfigure/JpaBaseConfiguration.java
164 .../jpa/autoconfigure/JpaProperties.java
23 .../jpa/autoconfigure/package-info.java
1225 total
再把 Hibernate 侧加进来:
module/spring-boot-hibernate/src/main/java/org/springframework/boot/hibernate/
├── SpringImplicitNamingStrategy.java
├── SpringJtaPlatform.java (@Deprecated since 4.1.0)
└── autoconfigure/
├── HibernateDefaultDdlAutoProvider.java
├── HibernateJpaAutoConfiguration.java
├── HibernateJpaConfiguration.java
├── HibernateProperties.java
├── HibernatePropertiesCustomizer.java
├── HibernateSettings.java
└── metrics/HibernateMetricsAutoConfiguration.java
两个模块加起来不到 20 个文件。这与第 22 章 JDBC 模块那种"几百个类、七种连接池"的规模完全不是一个量级。
那么问题来了:为什么 Spring Boot 的 JPA 自动配置可以这么小?
答案是本章的第一个核心命题:
JPA 自动配置的复杂度,不在"配置"本身,而在"编排"——它必须把自己精确地插进数据源、事务管理器、SQL 初始化脚本、缓存、Spring Data Repository 这五条独立流水线之间的缝隙里。
JpaBaseConfiguration 里那 5 个 @Bean 方法写得极其朴素,几乎就是"new 一个对象,塞几个属性"。真正难的是:
- 事务管理器:JPA 要
JpaTransactionManager,JDBC 要DataSourceTransactionManager,两者都匹配"没有TransactionManagerbean"这个条件——谁先注册谁赢。这个胜负由@AutoConfiguration(before = ...)决定,而不是由条件注解决定。 EntityManagerFactory必须在 SQL 脚本(schema.sql/data.sql)之后创建,但spring.jpa.defer-datasource-initialization=true时要反过来。这个双向顺序靠一组BeanFactoryPostProcessor动态改写BeanDefinition.dependsOn实现,而不是靠@DependsOn静态注解。- 嵌入式数据库下
ddl-auto默认create-drop,但有 Flyway/Liquibase 时必须是none。这个判断要问一圈SchemaManagementProvider。 - 异步引导(
spring.jpa.bootstrap=async)需要一个AsyncTaskExecutor,但没有的时候不是静默降级,而是抛一个带专用FailureAnalyzer的异常。
这四件事,每一件都是"跨模块的时序问题"。Spring Boot 的解法是把它们全部收敛成极小的、可组合的 SPI:
@AutoConfiguration(after/before)—— 解决模块间顺序(第 14 章讲过排序,本章看它的实际杀伤力)DatabaseInitializerDetector/DependsOnDatabaseInitializationDetector—— 解决"谁是初始化者、谁依赖初始化"SchemaManagementProvider—— 解决"谁来管 schema"HibernatePropertiesCustomizer/EntityManagerFactoryBuilderCustomizer—— 解决用户扩展点
所以本章的读法是:先看那 5 个 @Bean(它们很简单),再看它们周围的编排机制(这才是难点)。
2. 模块拓扑:JPA 不是一个模块,是一条链
在 module/ 下,与"用 JPA 访问数据库"直接相关的模块有 6 个:
| 模块 | 职责 | 是否含自动配置 |
|---|---|---|
spring-boot-persistence | @EntityScan、EntityScanPackages、异常转换 | 1 个 |
spring-boot-sql | SQL 脚本初始化的抽象层(spring.sql.init.*)与依赖探测 SPI | 0 个 |
spring-boot-jdbc | DataSource、连接池、脚本初始化的 JDBC 实现 | 多个 |
spring-boot-jpa | JPA 通用自动配置基类、EntityManagerFactoryBuilder | 0 个(无 .imports!) |
spring-boot-hibernate | Hibernate 具体实现 | 2 个 |
spring-boot-data-jpa | Spring Data JPA Repository | 1 个 |
注意 spring-boot-jpa 这一行:它没有 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件。验证:
$ find module/spring-boot-jpa/src/main/resources -type f
module/spring-boot-jpa/src/main/resources/META-INF/additional-spring-configuration-metadata.json
module/spring-boot-jpa/src/main/resources/META-INF/spring.factories
只有 spring.factories(注册 FailureAnalyzer 和两个 Detector)和补充元数据。也就是说:
spring-boot-jpa是一个"库模块",不是一个"自动配置模块"。它提供JpaBaseConfiguration这个抽象基类,但自己不注册任何自动配置。真正被*.imports拉起的是spring-boot-hibernate的HibernateJpaAutoConfiguration。
这是第 1 章讲的"模块拓扑"原则的一个漂亮实例:抽象与实现分离到不同 artifact。spring-boot-jpa 依赖 jakarta.persistence + Spring ORM,但不依赖 Hibernate;spring-boot-hibernate 才依赖 org.hibernate。所以理论上你可以写一个 EclipseLink 的 starter,只依赖 spring-boot-jpa,继承 JpaBaseConfiguration,不碰 Hibernate。
spring.factories 的完整内容(这是本章后面 §8 的关键入口):
# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.jpa.autoconfigure.BootstrapExecutorRequiredFailureAnalyzer
# Database Initializer Detectors
org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector=\
org.springframework.boot.jpa.JpaDatabaseInitializerDetector
# Depends On Database Initialization Detectors
org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector=\
org.springframework.boot.jpa.JpaDependsOnDatabaseInitializationDetector
三个 key,三个类。注意这里用的是 spring.factories 而不是 *.imports——因为这三个 SPI 都不是 AutoConfiguration,它们是 SpringFactoriesLoader 直接加载的普通 SPI(与第 16 章讲的 FailureAnalyzer 同一套机制)。
spring-boot-sql 的 spring.factories 只有一个:
# Depends On Database Initialization Detectors
org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector=\
org.springframework.boot.sql.init.dependency.AnnotationDependsOnDatabaseInitializationDetector
spring-boot-hibernate 的 .imports:
org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration
org.springframework.boot.hibernate.autoconfigure.metrics.HibernateMetricsAutoConfiguration
spring-boot-persistence 的 .imports:
org.springframework.boot.persistence.autoconfigure.PersistenceExceptionTranslationAutoConfiguration
记住这张表,后面每个类都会回到它所属的位置。
3. HibernateJpaAutoConfiguration:一个空的类,一段沉的注解
先看这个类本体——它是空的:
@AutoConfiguration(
after = { DataSourceAutoConfiguration.class, JtaAutoConfiguration.class,
TransactionManagerCustomizationAutoConfiguration.class },
before = { TransactionAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class })
@ConditionalOnClass({ LocalContainerEntityManagerFactoryBean.class, EntityManager.class, SessionImplementor.class })
@EnableConfigurationProperties(JpaProperties.class)
@Import(HibernateJpaConfiguration.class)
public final class HibernateJpaAutoConfiguration {
}
类体里一个方法都没有。全部信息在注解里。这本身就是一个信号:这是一个纯编排节点。
3.1 为什么要 @Import 而不是直接写成配置类
HibernateJpaConfiguration 是包级私有的(class HibernateJpaConfiguration extends JpaBaseConfiguration,没有 public)。它是一个 @Configuration(proxyBeanMethods = false),但不是 @AutoConfiguration。
这个拆分有实际意义:
HibernateJpaAutoConfiguration是"入口",携带@AutoConfiguration的after/before排序元数据和@ConditionalOnClass门禁。它必须是public(*.imports里按类名反射加载),且通常是final。HibernateJpaConfiguration是"本体",携带@ConditionalOnSingleCandidate(DataSource.class)和所有@Bean。它可以是包级私有,因为它是被@Import进来的,不需要被AutoConfigurationImportSelector扫描到。
关键推论:@ConditionalOnSingleCandidate(DataSource.class) 写在 HibernateJpaConfiguration 上,而不是写在 HibernateJpaAutoConfiguration 上。这意味着——
当
DataSource不唯一(比如你配了主从两个数据源)时,HibernateJpaAutoConfiguration本身仍然会被自动配置引擎选中并排序,只是它@Import的那个配置类不生效。
这个区别在条件报告里看得出来:HibernateJpaAutoConfiguration 会显示为 "matched",而 HibernateJpaConfiguration 显示为 "did not match"。如果你在调 NoUniqueBeanDefinitionException 或 "no EntityManagerFactory" 之类的问题,看条件报告时要注意看两层。
自测方法:启动加 --debug,在 ConditionEvaluationReport 里同时搜 HibernateJpaAutoConfiguration 和 HibernateJpaConfiguration;或者在 application.properties 写 logging.level.org.springframework.boot.autoconfigure.logging=DEBUG(注:该 logger 名需以实际为准,未核实),观察两者的判定差异。
3.2 @ConditionalOnClass 的三个探针
@ConditionalOnClass({ LocalContainerEntityManagerFactoryBean.class, EntityManager.class, SessionImplementor.class })
这三个类分别来自三个不同的 jar:
| 类 | 来自 | 说明 |
|---|---|---|
LocalContainerEntityManagerFactoryBean | spring-orm | Spring 的 JPA 集成基础设施 |
EntityManager | jakarta.persistence-api | JPA 规范 API |
SessionImplementor | hibernate-core | Hibernate 内部 SPI |
第三个是关键。前两个只证明"JPA 可用",第三个才证明"Hibernate 在 classpath 上"。
为什么不用 SessionFactory 或 HibernateJpaVendorAdapter?SessionImplementor 是 org.hibernate.engine.spi 包下的接口,属于 Hibernate 的内部 SPI 而非公开 API。用它的好处是:几乎不可能被其他库"误提供",因此判定非常可靠;坏处是它是内部类,理论上 Hibernate 可以改名。
推测(未核实):选择
SessionImplementor而非更稳定的SessionFactory,可能是历史原因——HibernateJpaAutoConfiguration的@ConditionalOnClass在 Spring Boot 2.x 就是这个三元组,一直沿用。若要验证,可在git log -p中搜索该类的注解变更历史。
注意 spring-orm 的 HibernateJpaVendorAdapter 其实也在 classpath 上(因为 spring-orm 自带 Hibernate 支持的类,但它是 optional 依赖)。所以不能用"有没有 HibernateJpaVendorAdapter"来判断——spring-orm 里有这个类,但 Hibernate 本体可以不在。这是第 15 章讲 @ConditionalOnClass 时反复强调的陷阱:选探针类时要选"只有目标库才有的类"。
3.3 after / before:本章最凶险的一处编排
after = { DataSourceAutoConfiguration.class, JtaAutoConfiguration.class,
TransactionManagerCustomizationAutoConfiguration.class },
before = { TransactionAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class }
after 三项很好理解:
DataSourceAutoConfiguration—— 没有DataSource就没有EntityManagerFactory,构造HibernateJpaConfiguration需要注入DataSource。JtaAutoConfiguration—— 决定有没有JtaTransactionManager(影响isJta())。TransactionManagerCustomizationAutoConfiguration—— 提供TransactionManagerCustomizers,JpaBaseConfiguration.transactionManager()要用它。
before 两项是真正的重点:
before = { TransactionAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class }
为什么 JPA 的事务管理器必须排在 JDBC 的事务管理器之前?看两边的条件:
JpaBaseConfiguration(JPA 侧):
@Bean
@ConditionalOnMissingBean(TransactionManager.class)
public PlatformTransactionManager transactionManager(
ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(transactionManager));
return transactionManager;
}
条件是 @ConditionalOnMissingBean(TransactionManager.class)。
DataSourceTransactionManagerAutoConfiguration(JDBC 侧,第 22 章讲过):它也注册一个 DataSourceTransactionManager,条件同样是"没有 PlatformTransactionManager/TransactionManager bean"。
两边条件完全相同。 谁先被处理,谁就注册成功;后处理的那个看到"已经有了",条件不成立,自动退场。
这正是第 15 章"条件 phase 决定条件能否工作"那条定律的实战:@ConditionalOnMissingBean 是顺序敏感的,它的判定结果取决于"在我之前有没有人注册过"。因此:
- 引入
spring-boot-starter-data-jpa(含 Hibernate)→HibernateJpaAutoConfiguration排在DataSourceTransactionManagerAutoConfiguration之前 → 拿到JpaTransactionManager。 - 只引入
spring-boot-starter-jdbc→ 没有 JPA 自动配置 → 拿到DataSourceTransactionManager。 - 同时引入两者 → 仍然是
JpaTransactionManager(因为before生效)。
这是"你的 @Transactional 到底用哪个事务管理器"这个问题的唯一解释路径。很多人以为"我用了 JPA 所以自动是 JpaTransactionManager"——不对,是因为 before 让 JPA 赢了这场赛跑。
自测方法:在任意 @SpringBootTest 里注入 PlatformTransactionManager 并打印 getClass().getName()。加/去掉 spring-boot-starter-data-jpa 各跑一次,观察类名从 JpaTransactionManager 变成 DataSourceTransactionManager。
未核实:DataSourceTransactionManagerAutoConfiguration 的确切条件注解(@ConditionalOnMissingBean(TransactionManager.class) 还是 (PlatformTransactionManager.class))本章未重新打开该文件逐行核对,建议对照第 22 章的记录或直接打开 module/spring-boot-jdbc/src/main/java/org/springframework/boot/jdbc/autoconfigure/DataSourceTransactionManagerAutoConfiguration.java 确认。
3.4 @EnableConfigurationProperties(JpaProperties.class) 出现了两次
注意:HibernateJpaAutoConfiguration 上有 @EnableConfigurationProperties(JpaProperties.class),而 JpaBaseConfiguration 上也有:
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(JpaProperties.class)
public abstract class JpaBaseConfiguration {
以及 HibernateJpaConfiguration 上:
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(HibernateProperties.class)
@ConditionalOnSingleCandidate(DataSource.class)
@ImportRuntimeHints(HibernateRuntimeHints.class)
class HibernateJpaConfiguration extends JpaBaseConfiguration {
同一个 JpaProperties 被注册两次。@EnableConfigurationProperties 是幂等的(它注册的是 BeanDefinitionRegistry 里的一个 ConfigurationPropertiesBeanDefinition,重复注册同名 bean definition 会被覆盖/忽略,取决于 Spring Framework 的实现细节)。
推测(未核实):重复声明是为了让
HibernateJpaConfiguration在脱离HibernateJpaAutoConfiguration单独使用(例如被用户@Import)时也能正常工作。Spring Framework 7 对重复@EnableConfigurationProperties的处理策略未核实——若抛BeanDefinitionOverrideException,则说明 Spring Boot 依赖spring.main.allow-bean-definition-overriding或注册器的幂等性。可自测:写一个只@Import(HibernateJpaConfiguration.class)的最小配置看是否报错。
4. JpaBaseConfiguration:五个 @Bean 与两个内部类
现在进本体。JpaBaseConfiguration 是 public abstract,构造参数三个:
protected JpaBaseConfiguration(DataSource dataSource, JpaProperties properties,
ObjectProvider<JtaTransactionManager> jtaTransactionManager) {
this.dataSource = dataSource;
this.properties = properties;
this.jtaTransactionManager = jtaTransactionManager.getIfAvailable();
}
注意 DataSource 是直接注入(不是 ObjectProvider),意味着必须有;JtaTransactionManager 是 ObjectProvider,可以没有——这正是 isJta() 的判定依据:
protected final boolean isJta() {
return (this.jtaTransactionManager != null);
}
4.1 transactionManager():一个不带 @ConditionalOnClass 的 JpaTransactionManager
@Bean
@ConditionalOnMissingBean(TransactionManager.class)
public PlatformTransactionManager transactionManager(
ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(transactionManager));
return transactionManager;
}
三个细节:
-
返回类型是
PlatformTransactionManager,条件是TransactionManager.class。TransactionManager是PlatformTransactionManager和ReactiveTransactionManager的共同父接口(Spring Framework 5.2+ 引入的标记接口)。用TransactionManager.class做条件意味着:只要有任何响应式事务管理器存在,JPA 也不注册。这是一个"宽条件",比@ConditionalOnMissingBean(PlatformTransactionManager.class)更保守。 -
JpaTransactionManager没有显式设置EntityManagerFactory。这是有意的——JpaTransactionManager实现了BeanFactoryAware,它会自己在afterPropertiesSet时从BeanFactory里按类型找EntityManagerFactory。这避免了循环依赖:如果写成transactionManager(EntityManagerFactory emf),而EntityManagerFactory的创建又要用到DataSource(DataSource的创建可能又需要事务管理器做初始化),就可能成环。推测(未核实):
JpaTransactionManager的EntityManagerFactory解析逻辑在spring-orm中,本仓库读不到。推断其走BeanFactoryUtils.beanOfTypeIncludingAncestors或ObjectProvider。自测:在有两个EntityManagerFactory的应用里启动,看是否报NoUniqueBeanDefinitionException(若是,则证明它是按类型解析的)。 -
transactionManagerCustomizers.ifAvailable(...)—— 第 22 章提到的TransactionManagerCustomizers模式。它由TransactionManagerCustomizationAutoConfiguration提供(这就是 §3.3 里after要排它的原因)。用户可以实现TransactionManagerCustomizer<JpaTransactionManager>来定制。
4.2 jpaVendorAdapter():把 JpaProperties 翻译成 vendor 属性
@Bean
@ConditionalOnMissingBean
public JpaVendorAdapter jpaVendorAdapter() {
AbstractJpaVendorAdapter adapter = createJpaVendorAdapter();
adapter.setShowSql(this.properties.isShowSql());
if (this.properties.getDatabase() != null) {
adapter.setDatabase(this.properties.getDatabase());
}
if (this.properties.getDatabasePlatform() != null) {
adapter.setDatabasePlatform(this.properties.getDatabasePlatform());
}
adapter.setGenerateDdl(this.properties.isGenerateDdl());
return adapter;
}
createJpaVendorAdapter() 是抽象方法,由 HibernateJpaConfiguration 实现(§6.1)。
注意这里的条件性赋值:setDatabase 和 setDatabasePlatform 只在非 null 时调用。为什么?因为 AbstractJpaVendorAdapter 内部有一个 this.database 字段,默认值是 Database.DEFAULT(表示"自动检测")。如果你无条件调用 adapter.setDatabase(null),就会把自动检测覆盖掉。
这是第 30 定律(determineXxx / getXxx 分离)的一个变体:"只在用户真的配了时才覆盖默认值" 必须靠显式 null 检查,不能靠 setter 自己判断。
spring.jpa.database 的类型是 org.springframework.orm.jpa.vendor.Database 枚举。可选值(来自 Spring ORM,未核实完整列表)通常包括 DEFAULT, DB2, DERBY, H2, HSQL, INFORMIX, MYSQL, ORACLE, POSTGRESQL, SQL_SERVER, SYBASE。
spring.jpa.generate-ddl(默认 false)与 spring.jpa.hibernate.ddl-auto 是两个不同的开关,且语义重叠。这一点在 §7 详细展开。
4.3 entityManagerFactoryBuilder():本章最复杂的一个 @Bean
@Bean
@ConditionalOnMissingBean
public EntityManagerFactoryBuilder entityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter,
ObjectProvider<PersistenceUnitManager> persistenceUnitManager,
ObjectProvider<EntityManagerFactoryBuilderCustomizer> customizers, ListableBeanFactory beanFactory) {
EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(jpaVendorAdapter,
this::buildJpaProperties, persistenceUnitManager.getIfAvailable(), null,
() -> determineBootstrapExecutor(beanFactory));
if (this.properties.getBootstrap() == Bootstrap.ASYNC) {
builder.requireBootstrapExecutor(
() -> BootstrapExecutorRequiredException.ofProperty("spring.jpa.bootstrap", "async"));
}
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder;
}
五件事依次发生:
(1) 构造 EntityManagerFactoryBuilder,参数是 jpaVendorAdapter、一个方法引用 this::buildJpaProperties(Function<DataSource, Map<String, ?>>)、persistenceUnitManager(可能为 null)、persistenceUnitRootLocation 传 null、以及一个 lambda 形式的 fallback executor supplier。
注意 persistenceUnitRootLocation 固定传 null。这意味着:Spring Boot 自动配置的 EntityManagerFactory 不使用 persistence.xml 的 root location 机制,而是完全走 PersistenceManagedTypes 扫描(见 §4.4)。
(2) this::buildJpaProperties 是延迟求值。buildJpaProperties 的签名是:
private Map<String, ?> buildJpaProperties(DataSource dataSource) {
Map<String, Object> properties = new HashMap<>(this.properties.getProperties());
Map<String, Object> vendorProperties = getVendorProperties(dataSource);
customizeVendorProperties(vendorProperties);
properties.putAll(vendorProperties);
return properties;
}
它接收 DataSource 参数——这个 DataSource 是调用时由 Builder.build() 传进来的,不是构造时。为什么这么绕?因为 getVendorProperties(dataSource) 需要 DataSource 来判断"是不是嵌入式数据库"(决定 ddl-auto)。而 EntityManagerFactoryBuilder 可以被用来建多个 EntityManagerFactory,每个可能用不同的 DataSource。所以属性计算必须延迟到 build() 时刻。
这是第 29 定律(外部覆盖靠 BPP 排在属性绑定之后)之外又一条:当配置依赖运行时对象时,用 Function 延迟,而不是在构造期求值。
(3) 合并顺序:spring.jpa.properties.*(用户)→ getVendorProperties(...)(Hibernate 侧)→ customizeVendorProperties(...)(子类钩子)→ properties.putAll(vendorProperties)。
也就是说:vendor 属性覆盖用户写的 spring.jpa.properties.*。这是个容易踩的坑:你在 application.yml 里写
spring:
jpa:
properties:
hibernate:
hbm2ddl:
auto: update
看起来生效了,但 §7.2 会证明:HibernateProperties.determineHibernateProperties 里有一段逻辑会先读 existing.get(HBM2DDL_AUTO) 并原样返回——所以它确实生效。但其它键(比如 hibernate.physical_naming_strategy)就可能被 Naming 的默认策略覆盖。
(4) spring.jpa.bootstrap=async 时要求 executor:
if (this.properties.getBootstrap() == Bootstrap.ASYNC) {
builder.requireBootstrapExecutor(
() -> BootstrapExecutorRequiredException.ofProperty("spring.jpa.bootstrap", "async"));
}
Bootstrap 枚举只有两个值:
public enum Bootstrap {
DEFAULT,
ASYNC
}
requireBootstrapExecutor 只是把 Supplier<RuntimeException> 存起来,并不立即检查。真正的检查在 Builder.build() 里(§5.3)。
(5) customizers.orderedStream() —— EntityManagerFactoryBuilderCustomizer 是一个 @FunctionalInterface:
@FunctionalInterface
public interface EntityManagerFactoryBuilderCustomizer {
void customize(EntityManagerFactoryBuilder builder);
}
注意它不是通过 spring.factories 注册的,而是通过容器里的 bean。所以用户只要写:
@Bean
EntityManagerFactoryBuilderCustomizer myCustomizer() {
return (builder) -> builder.setBootstrapExecutor(myExecutor());
}
就能介入。orderedStream() 意味着支持 @Order / Ordered。
4.4 entityManagerFactory():@Primary + 双类型 @ConditionalOnMissingBean
@Bean
@Primary
@ConditionalOnMissingBean({ LocalContainerEntityManagerFactoryBean.class, EntityManagerFactory.class })
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder factoryBuilder,
PersistenceManagedTypes persistenceManagedTypes) {
return factoryBuilder.dataSource(this.dataSource)
.managedTypes(persistenceManagedTypes)
.mappingResources(getMappingResources())
.jta(isJta())
.build();
}
四个细节:
(1) 返回类型是 LocalContainerEntityManagerFactoryBean 而不是 EntityManagerFactory。 这是一个 FactoryBean。Spring 容器里名为 entityManagerFactory 的 bean,其 getObject() 返回 EntityManagerFactory。所以:
- 注入
EntityManagerFactory→ 拿到FactoryBean.getObject()的产物 ✓ - 注入
LocalContainerEntityManagerFactoryBean→ 要在名字前加&(&entityManagerFactory)才能拿到 FactoryBean 本身
(2) @ConditionalOnMissingBean 检查两个类型:LocalContainerEntityManagerFactoryBean.class 和 EntityManagerFactory.class。为什么要检查 EntityManagerFactory?因为用户可能自己定义了 LocalEntityManagerFactoryBean(另一个实现),或者直接用 @Bean EntityManagerFactory emf()。只检查 LocalContainerEntityManagerFactoryBean 会漏掉这些情况。
(3) @Primary —— 当有多个 EntityManagerFactory 时,自动配置的这个是首选。注意 @Primary 与 @ConditionalOnMissingBean 共存:如果只有一个,它就是唯一的;如果有多个(用户自定义的 + 自动配置的),自动配置的是 @Primary。
(4) .jta(isJta()) —— 决定调 setDataSource 还是 setJtaDataSource(见 §5.2)。
getMappingResources():
private String @Nullable [] getMappingResources() {
List<String> mappingResources = this.properties.getMappingResources();
return (!ObjectUtils.isEmpty(mappingResources) ? StringUtils.toStringArray(mappingResources) : null);
}
spring.jpa.mapping-resources 等价于 persistence.xml 里的 <mapping-file> 条目。返回 null 而不是空数组——因为 PropertyMapper 的 map.from(x).to(y) 在源为 null 时会跳过调用(PropertyMapper 的语义:null 源不触发 to)。
4.5 内部类一:PersistenceManagedTypesConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean({ LocalContainerEntityManagerFactoryBean.class, EntityManagerFactory.class })
static class PersistenceManagedTypesConfiguration {
@Bean
@Primary
@ConditionalOnMissingBean
static PersistenceManagedTypes persistenceManagedTypes(BeanFactory beanFactory, ResourceLoader resourceLoader,
ObjectProvider<ManagedClassNameFilter> managedClassNameFilter) {
String[] packagesToScan = getPackagesToScan(beanFactory);
return new PersistenceManagedTypesScanner(resourceLoader, managedClassNameFilter.getIfAvailable())
.scan(packagesToScan);
}
private static String[] getPackagesToScan(BeanFactory beanFactory) {
List<String> packages = EntityScanPackages.get(beanFactory).getPackageNames();
if (packages.isEmpty() && AutoConfigurationPackages.has(beanFactory)) {
packages = AutoConfigurationPackages.get(beanFactory);
}
return StringUtils.toStringArray(packages);
}
}
这是本章最值得记住的一段代码,因为它体现了 Spring Boot 的"包扫描包来源优先级":
1. @EntityScan 指定的包 (EntityScanPackages.get(beanFactory))
2. 自动配置包 (AutoConfigurationPackages.get(beanFactory),即 @SpringBootApplication 所在包及其子包)
3. (都没有)→ 空数组,扫不到任何实体
注意 static 修饰:persistenceManagedTypes 是 static @Bean 方法。这样 Spring 不需要先实例化 PersistenceManagedTypesConfiguration 就能调用它,属于 FactoryMethod 优化(与 proxyBeanMethods = false 配套)。同类手法在第 19/20 章的 Web 自动配置里也见过。
EntityScanPackages.get() 的实现(在 spring-boot-persistence 模块)值得一看:
public static EntityScanPackages get(BeanFactory beanFactory) {
// Currently we only store a single base package, but we return a list to
// allow this to change in the future if needed
try {
return beanFactory.getBean(BEAN, EntityScanPackages.class);
}
catch (NoSuchBeanDefinitionException ex) {
return NONE;
}
}
BEAN 是 EntityScanPackages.class.getName(),即 org.springframework.boot.persistence.autoconfigure.EntityScanPackages——用类的全限定名当 bean 名。这是一个很朴素的技巧,避免与其他 bean 重名。
NONE 是 new EntityScanPackages()(空包列表),所以 getPackageNames() 返回空 list,isEmpty() 为真,回退到 AutoConfigurationPackages。
AutoConfigurationPackages.has(beanFactory) 这个检查是必须的——因为 AutoConfigurationPackages.get() 在没有注册时会抛异常(第 13 章讲过)。这里先 has 再 get,是防御式写法。
PersistenceManagedTypesScanner 来自 Spring ORM(org.springframework.orm.jpa.persistenceunit),未核实其扫描实现(是否用 ASM 读字节码而非反射加载类)。Spring Framework 6+ 的实现一般是基于 ClassPathScanningCandidateComponentProvider 的 ASM 元数据读取,避免触发类加载——这对启动性能很关键。
ManagedClassNameFilter 是一个过滤钩子(ObjectProvider 注入,可为 null),允许你在扫描时排除某些类名。
4.6 内部类二:JpaWebConfiguration(OSIV)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(WebMvcConfigurer.class)
@ConditionalOnMissingBean({ OpenEntityManagerInViewInterceptor.class, OpenEntityManagerInViewFilter.class })
@ConditionalOnMissingFilterBean(OpenEntityManagerInViewFilter.class)
@ConditionalOnBooleanProperty(name = "spring.jpa.open-in-view", matchIfMissing = true)
protected static class JpaWebConfiguration {
五个条件层层叠加:
| 条件 | 含义 |
|---|---|
@ConditionalOnWebApplication(type = Type.SERVLET) | 必须是 Servlet Web 应用(WebFlux 不生效——响应式下没有"请求线程"可绑) |
@ConditionalOnClass(WebMvcConfigurer.class) | classpath 上有 Spring MVC |
@ConditionalOnMissingBean({OpenEntityManagerInViewInterceptor.class, OpenEntityManagerInViewFilter.class}) | 用户没自己注册过 OSIV 拦截器/过滤器 |
@ConditionalOnMissingFilterBean(OpenEntityManagerInViewFilter.class) | 没有注册过同类型的 Filter bean |
@ConditionalOnBooleanProperty(name = "spring.jpa.open-in-view", matchIfMissing = true) | 默认开启 |
第 4 条 @ConditionalOnMissingFilterBean 是第 15 章讲过的"Servlet Filter 特化条件"——因为 Filter 类型的 bean 在 Spring Boot 里有特殊注册路径(FilterRegistrationBean),普通的 @ConditionalOnMissingBean 看不到它们。
OSIV(Open Session/EntityManager In View)是 Spring Boot 里争议最大的默认行为之一。它的作用是:把一个 EntityManager 绑定到整个请求的线程上,这样在视图渲染阶段(比如 Thymeleaf 模板里访问 user.getOrders())也能触发懒加载,不会抛 LazyInitializationException。
代价是:
- 每个请求占用一个数据库连接更久
- 懒加载发生在渲染期,出现异常时堆栈很难看
- 在
@ResponseBody(REST)场景下毫无用处,纯粹浪费
Spring Boot 团队显然知道这一点,所以它在运行时打警告:
@Bean
public OpenEntityManagerInViewInterceptor openEntityManagerInViewInterceptor() {
if (this.jpaProperties.getOpenInView() == null) {
logger.warn("spring.jpa.open-in-view is enabled by default. "
+ "Therefore, database queries may be performed during view "
+ "rendering. Explicitly configure spring.jpa.open-in-view to disable this warning");
}
return new OpenEntityManagerInViewInterceptor();
}
注意条件:getOpenInView() == null。也就是说——
只要你显式写了
spring.jpa.open-in-view=true或=false,警告就消失。
matchIfMissing = true 意味着不配的时候是 true(开启),但 JpaProperties.openInView 字段是 @Nullable Boolean,不配时是 null。所以:
- 不配 → 条件匹配(因为
matchIfMissing)→ 且openInView == null→ 打警告 + 启用 - 配
true→ 条件匹配 →openInView == TRUE→ 不打警告 + 启用 - 配
false→ 条件不匹配 → 整个JpaWebConfiguration不生效 → 不打警告 + 不启用
这个"用 null 区分'用户没配'和'用户配了默认值'"的技巧,与第 22 章里的 generateUniqueName、第 20 章的若干属性是同一套设计。
生产建议:REST 应用应显式写
spring.jpa.open-in-view=false。这不只是消除警告,而是真的能省下连接占用。
注册拦截器的方式也值得一提:
@Bean
public WebMvcConfigurer openEntityManagerInViewInterceptorConfigurer(
OpenEntityManagerInViewInterceptor interceptor) {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addWebRequestInterceptor(interceptor);
}
};
}
用 addWebRequestInterceptor 而不是 addInterceptor——因为 OpenEntityManagerInViewInterceptor 实现的是 WebRequestInterceptor(不是 HandlerInterceptor)。WebMvcConfigurer.addWebRequestInterceptor 会把它包装成 WebRequestHandlerInterceptorAdapter。这是 Spring MVC 的一个细节差异(第 19 章讲过 WebMvcConfigurer 的回调体系)。
5. EntityManagerFactoryBuilder 深潜:4 个构造器、1 个内部类、1 个 4.1.1 的破坏性改动
EntityManagerFactoryBuilder 是 spring-boot-jpa 模块里唯一的 public 工具 API(383 行,最大的文件)。它的 Javadoc 第一句就说明了定位:
Convenient builder for JPA EntityManagerFactory instances. Collects common configuration when constructed and then allows you to create one or more
LocalContainerEntityManagerFactoryBeanthrough a fluent builder pattern.
关键词:one or more。这是理解它全部设计的前提。
5.1 四个构造器的演化史
| # | 签名 | @since | 状态 |
|---|---|---|---|
| 1 | (JpaVendorAdapter, Function<DataSource, Map<String,?>>, PersistenceUnitManager) | 4.0.0 | 存在,委托给 #2 |
| 2 | (..., PersistenceUnitManager, URL persistenceUnitRootLocation) | 4.0.0 | 存在,委托给 #4(() -> null) |
| 3 | (..., PersistenceUnitManager, URL, AsyncTaskExecutor fallbackBootstrapExecutor) | 4.1.0 | @Deprecated(since = "4.1.1", forRemoval = true) |
| 4 | (..., PersistenceUnitManager, URL, Supplier<? extends AsyncTaskExecutor> fallbackBootstrapExecutor) | 4.1.1 | 当前推荐 |
看 #3 的注解:
@Deprecated(since = "4.1.1", forRemoval = true)
public EntityManagerFactoryBuilder(JpaVendorAdapter jpaVendorAdapter,
Function<DataSource, Map<String, ?>> jpaPropertiesFactory,
@Nullable PersistenceUnitManager persistenceUnitManager, @Nullable URL persistenceUnitRootLocation,
@Nullable AsyncTaskExecutor fallbackBootstrapExecutor) {
this(jpaVendorAdapter, jpaPropertiesFactory, persistenceUnitManager, persistenceUnitRootLocation,
() -> fallbackBootstrapExecutor);
}
从 AsyncTaskExecutor 改成 Supplier<? extends AsyncTaskExecutor>——这是一个不到一个 minor 版本就发生的 API 破坏。
为什么要改?答案在 JpaBaseConfiguration.entityManagerFactoryBuilder() 里:
EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(jpaVendorAdapter,
this::buildJpaProperties, persistenceUnitManager.getIfAvailable(), null,
() -> determineBootstrapExecutor(beanFactory));
determineBootstrapExecutor(beanFactory) 的实现:
private @Nullable AsyncTaskExecutor determineBootstrapExecutor(ListableBeanFactory beanFactory) {
Map<String, AsyncTaskExecutor> taskExecutors = beanFactory.getBeansOfType(AsyncTaskExecutor.class);
return (taskExecutors.size() == 1) ? taskExecutors.values().iterator().next()
: taskExecutors.get(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME);
}
它调 beanFactory.getBeansOfType(...)——这是一个会触发 bean 实例化的调用(getBeansOfType 默认 allowEagerInit=true)!
如果这个调用发生在构造 EntityManagerFactoryBuilder 的时候(即 entityManagerFactoryBuilder() 这个 @Bean 方法执行时),而此时容器里还有其它 AsyncTaskExecutor 尚未就绪,就会造成过早初始化,甚至死锁。
改成 Supplier 后,determineBootstrapExecutor 延迟到 Builder.build() 被调用时才执行——那时 entityManagerFactory bean 正在创建,getBeansOfType 的时机就安全多了。
这是本章最重要的一个工程细节,也是一条可复用的定律: 第三十一定律:构造参数若会触发容器查询(getBeansOfType / getBeanNamesForType),必须包装成 Supplier 延迟求值,否则会把"配置期"变成"实例化期",制造过早初始化。
这条定律在第 22 章 JdbcConnectionDetailsBeanPostProcessor 那里也隐约出现过(它用 PriorityOrdered 解决时机问题),这里则是用 Supplier 解决。
determineBootstrapExecutor 的规则本身也值得记:
1. 如果容器里恰好有 1 个 AsyncTaskExecutor → 用它
2. 否则(0 个或多个)→ 用名为 'applicationTaskExecutor' 的那个
3. 还没有 → null
注意"多个"的情况下不用 getBeansOfType 的第一个,而是按名字精确找 applicationTaskExecutor。这是为了避免歧义。常量来自第 30 章的 TaskExecutionAutoConfiguration:
TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME
未核实:
APPLICATION_TASK_EXECUTOR_BEAN_NAME的字面值是"applicationTaskExecutor"(这是 Spring Boot 长期以来的约定),本章未打开TaskExecutionAutoConfiguration逐字确认。可自测:grep -n "APPLICATION_TASK_EXECUTOR_BEAN_NAME =" module/spring-boot-task-execution/...或直接打印该常量。
5.2 内部类 Builder:13 个 fluent 方法,1 个 build()
Builder 是 public final class(非 static 的内部类,持有外部类引用 EntityManagerFactoryBuilder.this)。
它的字段:
private final DataSource dataSource;
private @Nullable PersistenceManagedTypes managedTypes;
private String @Nullable [] packagesToScan;
private @Nullable String persistenceUnit;
private final Map<String, Object> properties = new HashMap<>();
private String @Nullable [] mappingResources;
private boolean jta;
注意 managedTypes 和 packagesToScan 是两个独立字段。 packages() 的 Javadoc 明确写了 @see #managedTypes(PersistenceManagedTypes)——说明 managedTypes 是新的推荐方式,packages 是遗留方式。
build() 方法全文:
public LocalContainerEntityManagerFactoryBean build() {
PropertyMapper map = PropertyMapper.get();
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
map.from(EntityManagerFactoryBuilder.this.persistenceUnitManager).to(factory::setPersistenceUnitManager);
map.from(this.persistenceUnit).to(factory::setPersistenceUnitName);
map.from(EntityManagerFactoryBuilder.this.jpaVendorAdapter).to(factory::setJpaVendorAdapter);
map.from(this.dataSource).to((!this.jta) ? factory::setDataSource : factory::setJtaDataSource);
map.from(this.managedTypes).to(factory::setManagedTypes);
map.from(this.packagesToScan).to(factory::setPackagesToScan);
map.from(this::jpaPropertyMap).to(factory.getJpaPropertyMap()::putAll);
map.from(this.mappingResources).whenNot(ObjectUtils::isEmpty).to(factory::setMappingResources);
map.from(EntityManagerFactoryBuilder.this.persistenceUnitRootLocation)
.as(Object::toString)
.to(factory::setPersistenceUnitRootLocation);
map.from(this::bootstrapExecutor).to(factory::setBootstrapExecutor);
map.from(EntityManagerFactoryBuilder.this.persistenceUnitPostProcessors)
.as((postProcessors) -> postProcessors.toArray(PersistenceUnitPostProcessor[]::new))
.to(factory::setPersistenceUnitPostProcessors);
return factory;
}
几个点:
(1) PropertyMapper 的 null-跳过语义。第 8 章讲过 PropertyMapper,这里全篇都在用它的"源为 null 就不调用 to"特性。这让 11 个可选配置可以用一行写完,不需要 if (x != null)。
(2) map.from(this.dataSource).to((!this.jta) ? factory::setDataSource : factory::setJtaDataSource) —— 一个三元表达式决定了数据源注册到哪个槽。JTA 环境下必须走 setJtaDataSource,否则 Hibernate 会认为这是一个本地事务数据源,从而不使用 JTA 事务管理器。
(3) map.from(this::jpaPropertyMap).to(factory.getJpaPropertyMap()::putAll) —— 注意是 putAll 到已有的 map,而不是 setJpaPropertyMap。这样如果用户先 factory.setJpaPropertyMap(...) 再 build()……不对,这里 factory 是新 new 出来的。用 putAll 是因为 getJpaPropertyMap() 返回的可能是内部懒创建的可变 map。未核实 Spring Framework 7 的 LocalContainerEntityManagerFactoryBean.getJpaPropertyMap() 是否返回内部引用;若返回副本,这行就是无效的。
(4) persistenceUnitRootLocation 用 .as(Object::toString) —— URL 转成 String。LocalContainerEntityManagerFactoryBean.setPersistenceUnitRootLocation 接受 String。
(5) map.from(this::bootstrapExecutor) —— 注意这是一个 Supplier(this::bootstrapExecutor 作为 Supplier<AsyncTaskExecutor> 传给 map.from)。PropertyMapper.from(Supplier) 会调 get() 取值。所以 bootstrapExecutor() 这个可能抛异常的方法,在这里被真正调用。
jpaPropertyMap():
private Map<String, Object> jpaPropertyMap() {
Map<String, Object> jpaPropertyMap = new LinkedHashMap<>();
jpaPropertyMap.putAll(EntityManagerFactoryBuilder.this.jpaPropertiesFactory.apply(this.dataSource));
jpaPropertyMap.putAll(this.properties);
return jpaPropertyMap;
}
三层合并,顺序固定:
第 1 层:jpaPropertiesFactory.apply(dataSource) ← spring.jpa.properties.* + Hibernate 属性 + customizer
第 2 层:this.properties ← Builder.properties(...) 显式传入的(编程式,优先级最高)
即:编程式 Builder.properties() 覆盖配置文件。
5.3 bootstrapExecutor():本章唯一一处会主动抛异常的配置逻辑
private @Nullable AsyncTaskExecutor bootstrapExecutor() {
if (EntityManagerFactoryBuilder.this.bootstrapExecutor != null) {
return EntityManagerFactoryBuilder.this.bootstrapExecutor;
}
if (EntityManagerFactoryBuilder.this.requireBootstrapExecutorExceptionSupplier != null) {
@Nullable AsyncTaskExecutor fallback = EntityManagerFactoryBuilder.this.fallbackBootstrapExecutor.get();
if (fallback != null) {
return fallback;
}
RuntimeException ex = EntityManagerFactoryBuilder.this.requireBootstrapExecutorExceptionSupplier.get();
throw (ex != null) ? ex : new IllegalStateException("A bootstrap executor is required");
}
return null;
}
四路分支,画成表:
| 场景 | bootstrapExecutor 显式设置? | requireBootstrapExecutor 调用过? | fallbackExecutor 有值? | 结果 |
|---|---|---|---|---|
| A | 是 | — | — | 返回显式设置的 |
| B | 否 | 否 | — | 返回 null(同步引导) |
| C | 否 | 是 | 是 | 返回 fallback(异步引导) |
| D | 否 | 是 | 否 | 抛异常 |
场景 D 就是 spring.jpa.bootstrap=async 但容器里没有唯一 AsyncTaskExecutor 也没有 applicationTaskExecutor 的情况。
抛的是 BootstrapExecutorRequiredException:
public class BootstrapExecutorRequiredException extends IllegalStateException {
private final @Nullable String propertyName;
private final @Nullable String propertyValue;
public static BootstrapExecutorRequiredException ofProperty(String name, String value) {
Assert.hasText(name, "'name' must not be empty");
Assert.hasText(value, "'value' must not be empty");
String message = "An EntityManagerFactoryBean bootstrap executor is required when '%s' is set to '%s'"
.formatted(name, value);
return new BootstrapExecutorRequiredException(message, name, value, null);
}
}
消息形如:
An EntityManagerFactoryBean bootstrap executor is required when 'spring.jpa.bootstrap' is set to 'async'
5.4 BootstrapExecutorRequiredFailureAnalyzer:把异常翻译成三条可执行建议
class BootstrapExecutorRequiredFailureAnalyzer extends AbstractFailureAnalyzer<BootstrapExecutorRequiredException> {
@Override
protected @Nullable FailureAnalysis analyze(Throwable rootFailure, BootstrapExecutorRequiredException cause) {
StringBuilder action = new StringBuilder();
action.append(actionPreamble(cause.getPropertyName()));
action.append("\t- With an auto-configured task executor "
+ "(you may need to set 'spring.task.execution.mode' to 'force').\n");
action.append("\t- With an AsyncTaskExecutor bean named 'applicationTaskExecutor'.\n");
action.append("\t- Using a EntityManagerFactoryBuilderCustomizer.\n");
return new FailureAnalysis(cause.getMessage(), action.toString(), cause);
}
private String actionPreamble(@Nullable String propertyName) {
return (propertyName != null)
? "Use a different '%s' value or provide a bootstrap executor using one of the following methods:\n"
.formatted(propertyName)
: "Provide a bootstrap executor using one of the following methods:\n";
}
}
这是第 16 章讲 FailureAnalyzer 的最佳范例。注意第二条建议里的 spring.task.execution.mode=force——它揭示了 TaskExecutionAutoConfiguration 的一个行为:
默认情况下,如果应用上下文里已经有自定义的
Executor/AsyncTaskExecutorbean,TaskExecutionAutoConfiguration就不再创建applicationTaskExecutor。设成force后无论如何都创建。
这正好解释了 §5.1 里 determineBootstrapExecutor 为什么在"多个 executor"的情况下会找不到 applicationTaskExecutor——因为它压根没被创建。所以官方建议第一条就是 force。
自测方法:写一个只有 @SpringBootApplication + spring-boot-starter-data-jpa + H2 的应用,加 spring.jpa.bootstrap=async,再自己定义一个 @Bean AsyncTaskExecutor foo(),启动。预期:失败分析报告里出现上述三条建议。
5.5 异步引导到底"异步"了什么
setBootstrapExecutor 传给 LocalContainerEntityManagerFactoryBean,意味着 EntityManagerFactory 的创建(Hibernate 的 SessionFactory 构建——解析实体、生成 SQL、建连接池元数据)会在后台线程执行,主线程继续完成容器刷新。首次调用 getObject() 时若还未完成则阻塞等待。
这是 Spring Boot 3.x/4.x 主打的启动加速手段之一。收益取决于实体数量和数据库延迟。
未核实:
LocalContainerEntityManagerFactoryBean.setBootstrapExecutor的具体语义(是否用Future、是否在afterSingletonsInstantiated前强制等待)在本仓库不可见,属于spring-orm。自测:写一个含 200 个实体的应用,对比spring.jpa.bootstrap=default与async的启动耗时(用StartupTimeMetrics或 Actuator 的startup端点)。
5.6 setPersistenceUnitPostProcessors 与 addPersistenceUnitPostProcessors 的 4.1.0 分裂
public void setPersistenceUnitPostProcessors(PersistenceUnitPostProcessor... persistenceUnitPostProcessors) {
this.persistenceUnitPostProcessors = new ArrayList<>(Arrays.asList(persistenceUnitPostProcessors));
}
public void addPersistenceUnitPostProcessors(PersistenceUnitPostProcessor... persistenceUnitPostProcessors) {
if (this.persistenceUnitPostProcessors != null) {
this.persistenceUnitPostProcessors.addAll(Arrays.asList(persistenceUnitPostProcessors));
}
else {
setPersistenceUnitPostProcessors(persistenceUnitPostProcessors);
}
}
add 是 @since 4.1.0。为什么需要 add?因为 EntityManagerFactoryBuilderCustomizer 是多个的(orderedStream()),如果每个 customizer 都调 set...,后面的会覆盖前面的。有了 add...,多个 customizer 可以各自追加。
这也印证了第 30 定律附近的那条:Map/集合配置用 accumulator 而非 setter。Spring Boot 4.1 在这里补了一个 add 变体,正是因为 setter 语义在多 customizer 场景下是错的。
6. HibernateJpaConfiguration:四个重写点
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(HibernateProperties.class)
@ConditionalOnSingleCandidate(DataSource.class)
@ImportRuntimeHints(HibernateRuntimeHints.class)
class HibernateJpaConfiguration extends JpaBaseConfiguration {
@ConditionalOnSingleCandidate(DataSource.class) —— 第 15 章讲过它与 @ConditionalOnBean 的区别:"单个候选"包括"有多个但其中一个是 @Primary"。所以主从数据源场景下,只要你把主库标 @Primary,JPA 仍然会自动配置并绑到主库上。
@ImportRuntimeHints(HibernateRuntimeHints.class) —— 直接挂在配置类上(而不是 @ImportRuntimeHints 挂在自动配置类上),这是一个细微差别:只有这个配置类生效时,hints 才注册。
6.1 createJpaVendorAdapter():异常翻译器的注入点
@Override
protected HibernateJpaVendorAdapter createJpaVendorAdapter() {
HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
this.sqlExceptionTranslator.ifUnique(adapter.getJpaDialect()::setJdbcExceptionTranslator);
JtaTransactionManager jtaTransactionManager = getJtaTransactionManager();
if (jtaTransactionManager != null) {
adapter.setJtaTransactionManager(jtaTransactionManager);
}
return adapter;
}
两件事:
(1) sqlExceptionTranslator.ifUnique(...) —— ObjectProvider.ifUnique 表示"容器里恰好一个 SQLExceptionTranslator 时才注入"。这类 bean 通常来自 spring-boot-jdbc 的 JdbcTemplateAutoConfiguration 或用户自定义。作用是把 Hibernate 抛的 HibernateException / JDBC SQLException 翻译成 Spring 的 DataAccessException 体系。
注意是 adapter.getJpaDialect()::setJdbcExceptionTranslator——设置到 HibernateJpaDialect 上(HibernateJpaVendorAdapter 内部持有它),而不是 adapter 本身。
(2) JTA 事务管理器。有 JtaTransactionManager 就传给 adapter。这在 4.1.0 是个改动点——SpringJtaPlatform 被废弃了:
/**
* ...
* @deprecated since 4.1.0 for removal in 4.3.0 in favor of @link
* {@link HibernateJpaVendorAdapter#setJtaTransactionManager(Object)} and
* {@link org.springframework.orm.jpa.hibernate.ConfigurableJtaPlatform}
*/
@Deprecated(since = "4.1.0", forRemoval = true)
public class SpringJtaPlatform extends AbstractJtaPlatform {
也就是说:以前 Spring Boot 自己写一个 SpringJtaPlatform(继承 Hibernate 的 AbstractJtaPlatform)把 Spring 的 JtaTransactionManager 桥接给 Hibernate;现在改由 Spring Framework 的 HibernateJpaVendorAdapter.setJtaTransactionManager 承担,并引入了新的 ConfigurableJtaPlatform 抽象。
这是一个典型的"把平台适配代码上移到 Spring Framework"的演进。注意 @deprecated 的 Javadoc 里有个笔误:in favor of @link——@link 被写成了字面量(多了个空格?实际是 of @link 而非 of {@link),导致这个链接不会渲染成超链接。属于文档瑕疵,不影响功能。
未核实:org.springframework.orm.jpa.hibernate.ConfigurableJtaPlatform 的完整签名与它在 Spring Framework 7.0.9 中的行为。本仓库无 spring-orm 源码。
6.2 getVendorProperties():HibernateSettings 的两段式配置
@Override
protected Map<String, Object> getVendorProperties(DataSource dataSource) {
Supplier<String> defaultDdlMode = () -> this.defaultDdlAutoProvider.getDefaultDdlAuto(dataSource);
return new LinkedHashMap<>(this.hibernateProperties.determineHibernateProperties(
getProperties().getProperties(), new HibernateSettings().ddlAuto(defaultDdlMode)
.hibernatePropertiesCustomizers(this.hibernatePropertiesCustomizers)));
}
HibernateSettings 是一个极简的值对象(只有两个字段 + fluent setter):
public class HibernateSettings {
private @Nullable Supplier<String> ddlAuto;
private @Nullable Collection<HibernatePropertiesCustomizer> hibernatePropertiesCustomizers;
public HibernateSettings ddlAuto(@Nullable Supplier<String> ddlAuto) { ... }
public HibernateSettings hibernatePropertiesCustomizers(Collection<HibernatePropertiesCustomizer> c) { ... }
}
注意 ddlAuto 的类型是 Supplier<String> 而不是 String。又一次延迟求值——因为 getDefaultDdlAuto(dataSource) 要连数据库探测(判断是不是嵌入式),开销不小,而且只有在用户没显式配 ddl-auto 时才需要。
这已经是本章第二次看到
Supplier延迟(第一次是 §5.1 的fallbackBootstrapExecutor)。Spring Boot 4 里Supplier在配置对象中的密度明显上升,这是为了减少启动期的无谓计算。
6.3 customizeVendorProperties() 与 provider_disables_autocommit
private static final String PROVIDER_DISABLES_AUTOCOMMIT = "hibernate.connection.provider_disables_autocommit";
@Override
protected void customizeVendorProperties(Map<String, Object> vendorProperties) {
super.customizeVendorProperties(vendorProperties);
if (!vendorProperties.containsKey(PROVIDER_DISABLES_AUTOCOMMIT)) {
configureProviderDisablesAutocommit(vendorProperties);
}
}
private void configureProviderDisablesAutocommit(Map<String, Object> vendorProperties) {
if (isDataSourceAutoCommitDisabled() && !isJta()) {
vendorProperties.put(PROVIDER_DISABLES_AUTOCOMMIT, "true");
}
}
private boolean isDataSourceAutoCommitDisabled() {
DataSourcePoolMetadata poolMetadata = this.poolMetadataProvider.getDataSourcePoolMetadata(getDataSource());
return poolMetadata != null && Boolean.FALSE.equals(poolMetadata.getDefaultAutoCommit());
}
这条链路值得完整追一遍,因为它把三个模块串起来了:
DataSourcePoolMetadataProvider(spring-boot-jdbc,第 22 章)
└─ 由 ObjectProvider<Collection<DataSourcePoolMetadataProvider>> 注入
└─ 包装成 CompositeDataSourcePoolMetadataProvider
└─ getDataSourcePoolMetadata(dataSource) → DataSourcePoolMetadata
└─ getDefaultAutoCommit() ← 连接池报告的 autoCommit 默认值
如果连接池报告的 defaultAutoCommit 是 Boolean.FALSE,就告诉 Hibernate:hibernate.connection.provider_disables_autocommit=true。
这条属性的含义(Hibernate 侧,未核实具体行为,以下为推断):告诉 Hibernate"连接池交给我的连接已经关闭了 autocommit,你不要自己去调 Connection.setAutoCommit(false)"。
为什么这是一个优化? 因为每次 setAutoCommit 都是一次 JDBC 往返(取决于驱动实现,某些驱动会真的发一条语句)。如果连接池(如 HikariCP)在连接创建时就把 autocommit 关掉,Hibernate 再调一次就是浪费。HikariCP 的默认 autoCommit 是 true,所以默认情况下不会触发这条优化;如果你配了 spring.datasource.hikari.auto-commit=false,就会触发。
!isJta() 的原因:JTA 环境下事务由 JTA 事务管理器控制,Hibernate 不应干预连接的 autocommit。
自测方法:配 spring.datasource.hikari.auto-commit=false,启动时加 -Dhibernate.show_sql 之类看不到效果——更直接的方法是打断点在 configureProviderDisablesAutocommit,或打印 EntityManagerFactory.getProperties() 看是否含该键。
6.4 命名策略的三层来源
命名策略(把 Java 的 userName 字段映射成数据库的 user_name 列名)在 Spring Boot 4 里由三层决定:
第 1 层:配置文件(HibernateProperties.Naming)
public static class Naming {
private @Nullable String implicitStrategy;
private @Nullable String physicalStrategy;
private void applyNamingStrategies(Map<String, Object> properties) {
applyNamingStrategy(properties, MappingSettings.IMPLICIT_NAMING_STRATEGY, this.implicitStrategy,
SpringImplicitNamingStrategy.class::getName);
applyNamingStrategy(properties, MappingSettings.PHYSICAL_NAMING_STRATEGY, this.physicalStrategy,
PhysicalNamingStrategySnakeCaseImpl.class::getName);
}
private void applyNamingStrategy(Map<String, Object> properties, String key, @Nullable Object strategy,
Supplier<String> defaultStrategy) {
if (strategy != null) {
properties.put(key, strategy);
}
else {
properties.computeIfAbsent(key, (k) -> defaultStrategy.get());
}
}
}
默认值:
hibernate.implicit_naming_strategy→org.springframework.boot.hibernate.SpringImplicitNamingStrategyhibernate.physical_naming_strategy→org.hibernate.boot.model.naming.PhysicalNamingStrategySnakeCaseImpl
注意这里有一个 Spring Boot 4 的破坏性变更:3.x 时代的默认 physical strategy 是 org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy(Spring Boot 自己写的),4.x 换成了 Hibernate 原生的 PhysicalNamingStrategySnakeCaseImpl。
推测(未核实):这次替换的原因可能是 Hibernate 7 自带了等价实现,Spring Boot 没必要再维护一份。但两者的行为是否完全等价未核实——尤其在某些边界情况(连续大写字母如
URL、HTTPStatus)的下划线转换规则上,历史上CamelCaseToUnderscoresNamingStrategy与 Hibernate 的实现有差异。 自测方法(强烈建议做):写一个实体含httpStatusUrl字段,分别用 3.x 与 4.x 启动并打印 Hibernate 生成的 DDL(spring.jpa.show-sql=true或jakarta.persistence.schema-generation.scripts.action=create),对比列名。
computeIfAbsent 的用法很讲究:
properties.computeIfAbsent(key, (k) -> defaultStrategy.get());
只在键不存在时才填默认值。所以如果用户在 spring.jpa.properties.hibernate.physical_naming_strategy 里写了值(因为 getAdditionalProperties 一开始就把 existing(即 spring.jpa.properties.*)复制进 result),这里就不会覆盖。
第 2 层:容器里的 PhysicalNamingStrategy / ImplicitNamingStrategy bean
HibernateJpaConfiguration(...) {
...
this.hibernatePropertiesCustomizers = determineHibernatePropertiesCustomizers(
physicalNamingStrategy.getIfAvailable(), implicitNamingStrategy.getIfAvailable(),
hibernatePropertiesCustomizers.orderedStream().toList());
}
private List<HibernatePropertiesCustomizer> determineHibernatePropertiesCustomizers(
@Nullable PhysicalNamingStrategy physicalNamingStrategy,
@Nullable ImplicitNamingStrategy implicitNamingStrategy,
List<HibernatePropertiesCustomizer> hibernatePropertiesCustomizers) {
List<HibernatePropertiesCustomizer> customizers = new ArrayList<>();
if (physicalNamingStrategy != null || implicitNamingStrategy != null) {
customizers
.add(new NamingStrategiesHibernatePropertiesCustomizer(physicalNamingStrategy, implicitNamingStrategy));
}
customizers.addAll(hibernatePropertiesCustomizers);
return customizers;
}
注意顺序:bean 形式的 customizer 排在用户 customizer 之前。而 §6.2 里 getAdditionalProperties 是 customizers.forEach(...) 依次执行,所以后面的覆盖前面的 —— 用户自己的 HibernatePropertiesCustomizer bean 会覆盖通过 bean 注入的命名策略。
NamingStrategiesHibernatePropertiesCustomizer 的实现:
@Override
public void customize(Map<String, Object> hibernateProperties) {
if (this.physicalNamingStrategy != null) {
hibernateProperties.put("hibernate.physical_naming_strategy", this.physicalNamingStrategy);
}
if (this.implicitNamingStrategy != null) {
hibernateProperties.put("hibernate.implicit_naming_strategy", this.implicitNamingStrategy);
}
}
这里是直接给字符串字面量 hibernate.physical_naming_strategy,而第 1 层用的是 MappingSettings.PHYSICAL_NAMING_STRATEGY 常量。两者必须一致——
未核实:
org.hibernate.cfg.MappingSettings.PHYSICAL_NAMING_STRATEGY的字面值是否为"hibernate.physical_naming_strategy"。若是,两层写法等价;若不是(比如 Hibernate 7 改成了hibernate.naming.physical-strategy),那这两层就是不一致的 bug。 自测方法:打印MappingSettings.PHYSICAL_NAMING_STRATEGY和MappingSettings.IMPLICIT_NAMING_STRATEGY两个常量的值,与上面的字面量比对。这是本章最值得立刻动手验证的一条。
第 3 层:SpringImplicitNamingStrategy 到底改了什么
public class SpringImplicitNamingStrategy extends ImplicitNamingStrategyJpaCompliantImpl {
@Override
public Identifier determineJoinTableName(ImplicitJoinTableNameSource source) {
String name = source.getOwningPhysicalTableName() + "_"
+ source.getAssociationOwningAttributePath().getProperty();
return toIdentifier(name, source.getBuildingContext());
}
}
只重写了一个方法:determineJoinTableName。Javadoc 说得很清楚:
Naming conventions implemented here are identical to
ImplicitNamingStrategyJpaCompliantImplwith the exception that join table names are of the form{owning_physical_table_name}_{association_owning_property_name}.
也就是说:JPA 规范默认的关联表命名是 OwnerEntity_TargetEntity(用实体名),Spring Boot 改成 owner_table_property_name(用表名 + 属性名)。
举例:
@Entity
@Table(name = "author")
class Author {
@ManyToMany
private Set<Book> books; // Book 实体对应表 book
}
- JPA 规范默认(
ImplicitNamingStrategyJpaCompliantImpl):Author_Book - Spring Boot(
SpringImplicitNamingStrategy):author_books
(注意:getAssociationOwningAttributePath().getProperty() 取的是属性名 books,所以是 author_books 而不是 author_book。)
未核实:
ImplicitJoinTableNameSource.getAssociationOwningAttributePath().getProperty()返回的是属性名还是目标实体名。以上推断基于"Javadoc 说association_owning_property_name"。自测:写上述实体,打印 Hibernate 的 DDL。
6.5 HibernateRuntimeHints:为 Native 准备的三个类型
static class HibernateRuntimeHints implements RuntimeHintsRegistrar {
private static final Consumer<Builder> INVOKE_DECLARED_CONSTRUCTORS = TypeHint
.builtWith(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
@Override
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
for (String noJtaPlatformClass : NO_JTA_PLATFORM_CLASSES) {
hints.reflection().registerType(TypeReference.of(noJtaPlatformClass), INVOKE_DECLARED_CONSTRUCTORS);
}
hints.reflection().registerType(SpringImplicitNamingStrategy.class, INVOKE_DECLARED_CONSTRUCTORS);
hints.reflection().registerType(PhysicalNamingStrategySnakeCaseImpl.class, INVOKE_DECLARED_CONSTRUCTORS);
}
}
NO_JTA_PLATFORM_CLASSES:
private static final String[] NO_JTA_PLATFORM_CLASSES = {
"org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform",
"org.hibernate.service.jta.platform.internal.NoJtaPlatform" };
两个全限定名,一个在 Hibernate 5/6 的路径,一个在 Hibernate 7 的路径。注释写 "NoJtaPlatform implementations for various Hibernate versions"。
为什么用字符串而不是 ClassUtils.isPresent + 反射?因为 registerType(TypeReference.of(String), ...) 在类不存在时是安全的(TypeReference.of 只是包装名字,不触发加载),而 TypeReference.of(Class) 需要类存在。这是一个"同时兼容多个 Hibernate 版本"的写法。
SpringImplicitNamingStrategy 和 PhysicalNamingStrategySnakeCaseImpl 是因为它们可能通过配置文件里的类名字符串被 Hibernate 反射实例化,所以必须注册构造函数。
这印证了第 28 章(AOT)要讲的核心:凡是"类名出现在配置里、运行时反射 new"的地方,都需要 RuntimeHints。
6.6 HibernatePropertiesCustomizer:唯一的公开扩展点
@FunctionalInterface
public interface HibernatePropertiesCustomizer {
void customize(Map<String, Object> hibernateProperties);
}
用法:
@Bean
HibernatePropertiesCustomizer myHibernateCustomizer() {
return (props) -> props.put("hibernate.jdbc.batch_size", "50");
}
注意 Map<String, Object> —— value 可以是对象(如 PhysicalNamingStrategy 实例),不一定是 String。
7. ddl-auto 的决策链:四级回退与一个"看不见的"探测
spring.jpa.hibernate.ddl-auto 是 Spring Boot JPA 里最容易踩坑的属性。它的默认值不是常量,而是一个四级决策的结果。
7.1 determineDdlAuto:四级回退
private @Nullable String determineDdlAuto(Map<String, String> existing, Supplier<@Nullable String> defaultDdlAuto) {
String ddlAuto = existing.get(SchemaToolingSettings.HBM2DDL_AUTO);
if (ddlAuto != null) {
return ddlAuto;
}
if (this.ddlAuto != null) {
return this.ddlAuto;
}
if (existing.get(SchemaToolingSettings.JAKARTA_HBM2DDL_DATABASE_ACTION) != null) {
return null;
}
return defaultDdlAuto.get();
}
四级,按优先级:
| 级别 | 来源 | 键 |
|---|---|---|
| 1 | spring.jpa.properties.hibernate.hbm2ddl.auto | existing.get(HBM2DDL_AUTO) |
| 2 | spring.jpa.hibernate.ddl-auto | this.ddlAuto |
| 3 | spring.jpa.properties.jakarta.persistence.schema-generation.database.action | existing.get(JAKARTA_HBM2DDL_DATABASE_ACTION) → 存在则返回 null |
| 4 | 推断默认值 | defaultDdlAuto.get() |
第 3 级是最反直觉的一级:如果它存在,返回 null(不是返回它的值),然后在 getAdditionalProperties 里:
String ddlAuto = determineDdlAuto(existing, settings::getDdlAuto);
if (StringUtils.hasText(ddlAuto) && !"none".equals(ddlAuto)) {
result.put(SchemaToolingSettings.HBM2DDL_AUTO, ddlAuto);
}
else {
result.remove(SchemaToolingSettings.HBM2DDL_AUTO);
}
null → hasText 假 → remove(HBM2DDL_AUTO)。
为什么?因为 JPA 规范定义的 jakarta.persistence.schema-generation.database.action 是 Hibernate hbm2ddl.auto 的等价物。如果用户用了规范标准的键,Spring Boot 就不应该再插手 hbm2ddl.auto——否则两个键都生效会冲突。所以第 3 级的语义是:"用户已经用标准方式表达了意图,我退出。"
SchemaToolingSettings 来自 org.hibernate.cfg(Hibernate 7 把配置项常量集中到了 SchemaToolingSettings / MappingSettings / PersistenceSettings 等类里,取代了老的环境类)。
未核实:
SchemaToolingSettings.HBM2DDL_AUTO与JAKARTA_HBM2DDL_DATABASE_ACTION的字面值(推断分别是hibernate.hbm2ddl.auto和jakarta.persistence.schema-generation.database.action)。自测:打印这两个常量。
另外注意 "none" 的特判:如果用户显式写了 ddl-auto=none,会走 else 分支 remove。也就是说 none 不是"设置成 none",而是"把键删掉"。效果上等价(Hibernate 默认不做 DDL),但语义上更干净:不往 Hibernate 属性里塞一个无意义的键。
7.2 HibernateDefaultDdlAutoProvider:嵌入式 + 无人管 schema → create-drop
第 4 级的 defaultDdlAuto 是 HibernateJpaConfiguration 里传进来的:
Supplier<String> defaultDdlMode = () -> this.defaultDdlAutoProvider.getDefaultDdlAuto(dataSource);
HibernateDefaultDdlAutoProvider 本体:
class HibernateDefaultDdlAutoProvider implements SchemaManagementProvider {
private final Iterable<SchemaManagementProvider> providers;
HibernateDefaultDdlAutoProvider(Iterable<SchemaManagementProvider> providers) {
this.providers = providers;
}
String getDefaultDdlAuto(DataSource dataSource) {
if (!EmbeddedDatabaseConnection.isEmbedded(dataSource)) {
return "none";
}
SchemaManagement schemaManagement = getSchemaManagement(dataSource);
if (SchemaManagement.MANAGED.equals(schemaManagement)) {
return "none";
}
return "create-drop";
}
@Override
public SchemaManagement getSchemaManagement(DataSource dataSource) {
return StreamSupport.stream(this.providers.spliterator(), false)
.map((provider) -> provider.getSchemaManagement(dataSource))
.filter(SchemaManagement.MANAGED::equals)
.findFirst()
.orElse(SchemaManagement.UNMANAGED);
}
}
决策表:
| 数据源类型 | 有 Flyway/Liquibase 等 schema manager? | 结果 |
|---|---|---|
| 非嵌入式(MySQL/PostgreSQL/...) | — | none |
| 嵌入式(H2/HSQL/Derby) | 有(MANAGED) | none |
| 嵌入式 | 无(UNMANAGED) | create-drop |
这是"我在本地跑得好好的,上生产表被删了"这个经典事故的技术根源吗? 不是——恰恰相反。非嵌入式数据库默认是 none,所以生产环境不会自动建表。真正的事故通常是:
- 有人在
application.yml里写了ddl-auto: update并把它带到了生产; - 或者用了嵌入式数据库(H2 内存库)跑测试,测试之间表被
create-drop重建。
create-drop 的语义:启动时建表,SessionFactory 关闭时删表。所以:
- 应用正常关闭 → 表没了 → 下次启动重建 → 数据全丢
- 应用被
kill -9→ 表还在 → 下次启动create-drop会先 drop 再 create
SchemaManagementProvider 是谁提供的? 由 ObjectProvider<SchemaManagementProvider> providers 注入(构造函数里)。Flyway 和 Liquibase 各自的自动配置会注册这个 bean。
未核实:
spring-boot-flyway/spring-boot-liquibase中SchemaManagementProviderbean 的具体实现类名,以及它们如何判定MANAGED。第 22 章提到过这两个模块,本章未重新核对。自测:grep -rn "SchemaManagementProvider" module/spring-boot-flyway module/spring-boot-liquibase。
EmbeddedDatabaseConnection.isEmbedded(dataSource) 的实现在 spring-boot-jdbc(第 22 章讲过)。它靠 JDBC URL 前缀 判断(jdbc:h2:mem: 等),需要真的打开一个连接拿元数据。
重要:这意味着第 4 级决策会真的连数据库。所以:
- 如果数据库连不上,
getDefaultDdlAuto会抛异常;- 这就是为什么它是
Supplier——只有走到第 4 级才付代价。未核实:
EmbeddedDatabaseConnection.isEmbedded在连接失败时是抛异常还是返回false。(DataSourceScriptDatabaseInitializer.isEmbeddedDatabase()里有try/catch兜底返回false,但HibernateDefaultDdlAutoProvider里没有 try/catch。)可自测:配一个连不上的 H2 URL 前缀(如jdbc:h2:tcp://localhost:9999/mem:x),启动观察异常栈。
7.3 spring.jpa.generate-ddl 与 ddl-auto 的关系
JpaProperties.generateDdl(默认 false)被传给 AbstractJpaVendorAdapter.setGenerateDdl(...):
adapter.setGenerateDdl(this.properties.isGenerateDdl());
而 HibernateProperties.ddlAuto 走的是 Hibernate 属性。这是两条独立的通路,generate-ddl 是 Spring ORM 的抽象(它会转成 vendor 对应的属性),ddl-auto 是 Hibernate 的直写。
未核实:
AbstractJpaVendorAdapter.setGenerateDdl(true)在 Hibernate 侧最终写成哪个属性键。推断是hibernate.hbm2ddl.auto=update(Spring ORM 的历史行为)。若是,则spring.jpa.generate-ddl=true与spring.jpa.hibernate.ddl-auto=create同时配置时会冲突,且谁赢取决于determineHibernateProperties的执行顺序——由于generateDdl走的是jpaVendorAdapter而非hibernateProperties,推断两者会分别写入不同的 map,最终由 Hibernate 的合并规则决定。强烈建议自测:同时配置两者,打印最终的EntityManagerFactory.getProperties()。
7.4 applyScanner:为什么要禁用 Hibernate 的扫描器
private static final String DISABLED_SCANNER_CLASS = "org.hibernate.boot.archive.scan.internal.DisabledScanner";
private void applyScanner(Map<String, Object> result) {
if (!result.containsKey(PersistenceSettings.SCANNER) && ClassUtils.isPresent(DISABLED_SCANNER_CLASS, null)) {
result.put(PersistenceSettings.SCANNER, DISABLED_SCANNER_CLASS);
}
}
hibernate.archive.scanner 设为 DisabledScanner——禁止 Hibernate 自己去扫 classpath 找实体。
为什么?因为 Spring Boot 已经通过 PersistenceManagedTypesScanner(§4.5)精确扫过了 @EntityScan 或自动配置包,并把结果通过 setManagedTypes(...) 交给了 LocalContainerEntityManagerFactoryBean。如果 Hibernate 再自己扫一遍:
- 性能:重复扫描整个 classpath,是启动慢的主要来源之一。
- 正确性:Hibernate 的扫描规则(找
persistence.xml、找@Entity)与 Spring 的不完全一致,可能扫出 Spring 不想要的实体,或扫不到 Spring 能扫到的(例如 Spring 支持自定义的ManagedClassNameFilter)。
ClassUtils.isPresent(DISABLED_SCANNER_CLASS, null) 的第二个参数是 null classLoader —— 使用默认(ClassUtils 的默认 ClassLoader)。这在不同 Hibernate 版本下安全降级:Hibernate 5 没有这个类,就不设置。
这是一个"用可选类的字符串名探测"的经典手法,与第 22 章
DataSourceBuilder.lookupPooled用字符串类名探测连接池、第 21 章 gRPC 的做法是同一个套路。它避免了编译期依赖。
8. 数据库初始化依赖编排:本章最难的一段
这一节回答第 22 章末尾留下的问题:schema.sql / data.sql 与 EntityManagerFactory 谁先谁后?
答案是:默认 EntityManagerFactory 后于脚本;spring.jpa.defer-datasource-initialization=true 时反过来。 而这个反转不是靠 @DependsOn,是靠运行时改写 BeanDefinition。
8.1 参与者全景
| 角色 | 类 | 注册方式 |
|---|---|---|
| 编排器 | DatabaseInitializationDependencyConfigurer | 被 DataSourceInitializationAutoConfiguration @Import |
| 实际工作的 BFPP | DependsOnDatabaseInitializationPostProcessor(内部类) | 由上面的 ImportBeanDefinitionRegistrar 注册 |
| 探测"谁初始化数据库" | DatabaseInitializerDetector | spring.factories |
| 探测"谁依赖初始化" | DependsOnDatabaseInitializationDetector | spring.factories |
| JPA 侧实现(初始化者) | JpaDatabaseInitializerDetector | spring-boot-jpa/spring.factories |
| JPA 侧实现(依赖者) | JpaDependsOnDatabaseInitializationDetector | 同上 |
| 注解驱动的依赖者 | AnnotationDependsOnDatabaseInitializationDetector | spring-boot-sql/spring.factories |
8.2 DatabaseInitializationDependencyConfigurer:一个 ImportBeanDefinitionRegistrar
public class DatabaseInitializationDependencyConfigurer implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
String name = DependsOnDatabaseInitializationPostProcessor.class.getName();
if (!registry.containsBeanDefinition(name)) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.rootBeanDefinition(DependsOnDatabaseInitializationPostProcessor.class);
registry.registerBeanDefinition(name, builder.getBeanDefinition());
}
}
...
}
用 ImportBeanDefinitionRegistrar 而不是 @Bean,原因有三:
BeanFactoryPostProcessor必须尽早注册。用@Bean方法的话,它所在的@Configuration类要先被解析,而@Configuration的解析发生在 BFPP 执行之后的某些阶段——会错过时机。if (!registry.containsBeanDefinition(name))做幂等去重。因为DatabaseInitializationDependencyConfigurer会被多个自动配置@Import(JDBC 的、R2DBC 的、Flyway 的、Liquibase 的……),重复注册同一个 BFPP 会导致它执行多次。- bean name 用类的全限定名(
DependsOnDatabaseInitializationPostProcessor.class.getName()),和 §4.5 的EntityScanPackages一样,避免冲突。
8.3 DependsOnDatabaseInitializationPostProcessor:分批链式依赖
核心逻辑:
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
if (AotDetector.useGeneratedArtifacts()) {
return;
}
InitializerBeanNames initializerBeanNames = detectInitializerBeanNames(beanFactory);
if (initializerBeanNames.isEmpty()) {
return;
}
Set<String> previousInitializerBeanNamesBatch = null;
for (Set<String> initializerBeanNamesBatch : initializerBeanNames.batchedBeanNames()) {
for (String initializerBeanName : initializerBeanNamesBatch) {
BeanDefinition beanDefinition = getBeanDefinition(initializerBeanName, beanFactory);
beanDefinition
.setDependsOn(merge(beanDefinition.getDependsOn(), previousInitializerBeanNamesBatch));
}
previousInitializerBeanNamesBatch = initializerBeanNamesBatch;
}
for (String dependsOnInitializationBeanNames : detectDependsOnInitializationBeanNames(beanFactory)) {
BeanDefinition beanDefinition = getBeanDefinition(dependsOnInitializationBeanNames, beanFactory);
beanDefinition.setDependsOn(merge(beanDefinition.getDependsOn(), initializerBeanNames.beanNames()));
}
}
这段代码的精妙之处在"分批"。
InitializerBeanNames 内部结构:
static class InitializerBeanNames {
private final Map<DatabaseInitializerDetector, Set<String>> byDetectorBeanNames = new LinkedHashMap<>();
private final Set<String> beanNames = new LinkedHashSet<>();
private void detected(DatabaseInitializerDetector detector, String beanName) {
this.byDetectorBeanNames.computeIfAbsent(detector, (key) -> new LinkedHashSet<>()).add(beanName);
this.beanNames.add(beanName);
}
...
private Iterable<Set<String>> batchedBeanNames() {
return this.byDetectorBeanNames.values();
}
}
按 detector 分组。然后循环里:
第 1 批(detector A 找到的 {a1, a2})→ previous == null → 不加依赖
第 2 批(detector B 找到的 {b1}) → previous == {a1,a2} → b1.dependsOn += a1, a2
第 3 批(detector C 找到的 {c1}) → previous == {b1} → c1.dependsOn += b1
形成一个链:a1,a2 → b1 → c1。
为什么要链而不是"每个都依赖前面所有"? 因为 detector 是有顺序的(DatabaseInitializerDetector extends Ordered,默认 0)。Spring Boot 期望的顺序是:Flyway/Liquibase → SQL 脚本 → JPA。让它们按 detector 顺序串起来,就能保证:
Flyway(schema 版本化) → data.sql(初始数据) → EntityManagerFactory(JPA)
如果只是"每个依赖所有其它",就可能乱序。
第二个循环:所有"依赖者"(DependsOnDatabaseInitializationDetector 找到的)依赖所有初始化者(initializerBeanNames.beanNames())。
8.4 AotDetector.useGeneratedArtifacts() 的提前返回
if (AotDetector.useGeneratedArtifacts()) {
return;
}
Native / AOT 模式下整个机制不生效。因为在 AOT 阶段,BeanDefinition 已经被固化成 Java 代码,运行时没有 BeanDefinition 可改。
这是一个重要的限制:如果你用 GraalVM Native 且依赖
defer-datasource-initialization的顺序保证,它不会生效。这与第 28 章(AOT)要讲的"运行时 BFPP 在 AOT 下失效"是同一类问题。Spring Boot 的做法是:AOT 阶段由对应的BeanFactoryInitializationAotProcessor把依赖写进生成的代码;但DatabaseInitializationDependencyConfigurer没有配套的 AOT 处理器——未核实是否由此导致 Native 下顺序不保证。自测:Native 编译一个同时有 Flyway 和data.sql的应用,观察执行顺序。
8.5 JPA 的两个 detector:同一个属性,两种相反的语义
这是全章最漂亮(也最容易看晕)的一处设计。
JpaDatabaseInitializerDetector(JPA 是"初始化者"):
@Override
protected Set<Class<?>> getDatabaseInitializerBeanTypes() {
boolean deferred = this.environment.getProperty("spring.jpa.defer-datasource-initialization", boolean.class,
false);
return deferred ? Collections.singleton(EntityManagerFactory.class) : Collections.emptySet();
}
defer=true→EntityManagerFactory是一个数据库初始化者defer=false→ 空集(JPA 不算初始化者)
JpaDependsOnDatabaseInitializationDetector(JPA 是"依赖者"):
@Override
protected Set<Class<?>> getDependsOnDatabaseInitializationBeanTypes() {
boolean postpone = this.environment.getProperty("spring.jpa.defer-datasource-initialization", boolean.class,
false);
return postpone ? Collections.emptySet()
: new HashSet<>(Arrays.asList(EntityManagerFactory.class, AbstractEntityManagerFactoryBean.class));
}
defer=true→ 空集(JPA 不依赖别人)defer=false→EntityManagerFactory+AbstractEntityManagerFactoryBean都是依赖者
两者读取的是同一个属性,结果完全相反。 这就是"反转"的实现方式:
defer=false(默认):
JPA ∈ 依赖者集合 → EntityManagerFactory.dependsOn += 所有初始化者(如 dataScriptDatabaseInitializer)
⇒ 脚本先跑,JPA 后建
defer=true:
JPA ∈ 初始化者集合 → 其它初始化者(data.sql 的)dependsOn += EntityManagerFactory
⇒ JPA 先建(Hibernate create-drop 建表),脚本后跑
defer=true 的典型用途:用 data.sql 导入初始数据,但表由 Hibernate 的 ddl-auto=create-drop 生成。默认顺序下,脚本先跑,此时表还不存在 → 报错。设 defer=true 后,Hibernate 先建表,脚本再插数据。
注意:这只在嵌入式数据库下有意义(因为
ddl-auto默认只在嵌入式下是create-drop)。生产库通常应该让 Flyway/Liquibase 管 schema。
注意依赖者类型里同时有 EntityManagerFactory 和 AbstractEntityManagerFactoryBean。为什么要两个?因为 BeansOfTypeDetector 用的是:
String[] names = beanFactory.getBeanNamesForType(type, true, false);
对于名为 entityManagerFactory 的 bean,其 FactoryBean 的 getObjectType() 是 EntityManagerFactory,所以 getBeanNamesForType(EntityManagerFactory.class) 能找到它;而 getBeanNamesForType(AbstractEntityManagerFactoryBean.class) 找到的是 FactoryBean 本身(需要 & 前缀,或 includeNonSingletons)。BeanFactoryUtils::transformedBeanName 会把 &xxx 变成 xxx。
未核实:
getBeanNamesForType(AbstractEntityManagerFactoryBean.class, true, false)是否能匹配到LocalContainerEntityManagerFactoryBean类型的 FactoryBean。getBeanNamesForType对 FactoryBean 的处理是:匹配getObjectType(),同时如果类型本身是FactoryBean的父类……这个细节较绕。自测:打断点在BeansOfTypeDetector.detect,看names的实际内容。
8.6 detectionComplete:JPA 初始化的"插队"逻辑
JpaDatabaseInitializerDetector 还重写了 detectionComplete:
@Override
public void detectionComplete(ConfigurableListableBeanFactory beanFactory, Set<String> dataSourceInitializerNames) {
configureOtherInitializersToDependOnJpaInitializers(beanFactory, dataSourceInitializerNames);
}
private void configureOtherInitializersToDependOnJpaInitializers(ConfigurableListableBeanFactory beanFactory,
Set<String> dataSourceInitializerNames) {
Set<String> jpaInitializers = new HashSet<>();
Set<String> otherInitializers = new HashSet<>(dataSourceInitializerNames);
Iterator<String> iterator = otherInitializers.iterator();
while (iterator.hasNext()) {
String initializerName = iterator.next();
BeanDefinition initializerDefinition = beanFactory.getBeanDefinition(initializerName);
if (JpaDatabaseInitializerDetector.class.getName()
.equals(initializerDefinition.getAttribute(DatabaseInitializerDetector.class.getName()))) {
iterator.remove();
jpaInitializers.add(initializerName);
}
}
for (String otherInitializerName : otherInitializers) {
BeanDefinition definition = beanFactory.getBeanDefinition(otherInitializerName);
String[] dependencies = definition.getDependsOn();
for (String dependencyName : jpaInitializers) {
dependencies = StringUtils.addStringToArray(dependencies, dependencyName);
}
definition.setDependsOn(dependencies);
}
}
它做的事:把 JPA 检测到的初始化者从"其它初始化者"里摘出来,让其它初始化者依赖它们。
识别方式是BeanDefinition 的 attribute:
beanDefinition.setAttribute(DatabaseInitializerDetector.class.getName(), detector.getClass().getName());
在 detectInitializerBeanNames 里,每个 detector 找到的 bean 都会被盖一个"章",章的内容是 detector 的类名。然后这里用 JpaDatabaseInitializerDetector.class.getName() 比对,就能认出"这是我(JPA detector)找到的"。
这是一个非常巧妙的手法:用
BeanDefinition的 attribute 做"来源标记",避免了在 detector 之间传递状态。第 14/16 章讲过BeanDefinition的 attribute 机制(ConfigurationClassPostProcessor也用它标记配置类来源),这里又是一例。
调用时机:detector.detectionComplete(...) 在 detectInitializerBeanNames 的第二个循环里,即所有 detector 都 detect 完之后。所以 JPA detector 能看到全局的 dataSourceInitializerNames。
8.7 三个 detector 基类的容错策略
AbstractBeansOfTypeDatabaseInitializerDetector:
@Override
public Set<String> detect(ConfigurableListableBeanFactory beanFactory) {
try {
Set<Class<?>> types = getDatabaseInitializerBeanTypes();
return new BeansOfTypeDetector(types).detect(beanFactory);
}
catch (Throwable ex) {
return Collections.emptySet();
}
}
catch (Throwable) —— 任何异常都返回空集。为什么这么激进?因为:
getDatabaseInitializerBeanTypes()里读Environment属性,理论上不会抛;- 但子类可能做更复杂的事(比如连接数据库判断类型);
- 一个 detector 挂掉不应该让整个应用启动失败。
BeansOfTypeDetector.detect 内部也有一层容错:
Set<String> detect(ListableBeanFactory beanFactory) {
Set<String> beanNames = new HashSet<>();
for (Class<?> type : this.types) {
try {
String[] names = beanFactory.getBeanNamesForType(type, true, false);
Arrays.stream(names).map(BeanFactoryUtils::transformedBeanName).forEach(beanNames::add);
}
catch (Throwable ex) {
// Continue
}
}
return beanNames;
}
每个类型单独 try/catch。这样即使某个类型(比如 AbstractEntityManagerFactoryBean,它属于 spring-orm,可能不在 classpath)触发 NoClassDefFoundError,也不影响其它类型。
注意
catch (Throwable)会捕获NoClassDefFoundError这类Error。这在"可选依赖"场景下是必要的——这是"可选依赖用字符串类名探测"定律之外的另一条路线:用 try/catch(Throwable) 兜住类型解析失败。
AbstractBeansOfTypeDependsOnDatabaseInitializationDetector 是同样的结构。
AnnotationDependsOnDatabaseInitializationDetector:
@Override
public Set<String> detect(ConfigurableListableBeanFactory beanFactory) {
Set<String> dependentBeans = new HashSet<>();
for (String beanName : beanFactory.getBeanDefinitionNames()) {
if (beanFactory.findAnnotationOnBean(beanName, DependsOnDatabaseInitialization.class, false) != null) {
dependentBeans.add(beanName);
}
}
return dependentBeans;
}
遍历所有 bean definition,找带 @DependsOnDatabaseInitialization 注解的。这是给用户提供的声明式手段:
@Bean
@DependsOnDatabaseInitialization
public MyCacheWarmer cacheWarmer(JdbcTemplate jdbc) { ... }
findAnnotationOnBean(name, anno, false) 的第三个参数 false 表示不在工厂方法上找?还是"不遍历接口"?
未核实:
ListableBeanFactory.findAnnotationOnBean(String, Class, boolean)第三个参数的语义。推断为allowFactoryBeanInit(是否允许初始化 FactoryBean 来确定类型),false表示不允许——这很关键,因为 BFPP 阶段不应该触发 bean 实例化。强烈建议核实。
8.8 一个完整的时序推演
假设:Spring Boot 应用,H2 内存库,classpath 有 spring-boot-starter-data-jpa,src/main/resources/data.sql 存在,spring.jpa.defer-datasource-initialization 未配置。
【阶段 1】自动配置选择
DataSourceAutoConfiguration → 注册 Hikari DataSource(嵌入式,随机名)
DataSourceInitializationAutoConfiguration → @Import(DatabaseInitializationDependencyConfigurer)
→ 注册 dataSourceScriptDatabaseInitializer bean
HibernateJpaAutoConfiguration → @Import(HibernateJpaConfiguration)
→ 注册 entityManagerFactory(LocalContainerEntityManagerFactoryBean)
→ 注册 transactionManager(JpaTransactionManager)
【阶段 2】BFPP 执行(DependsOnDatabaseInitializationPostProcessor)
detectInitializerBeanNames:
JpaDatabaseInitializerDetector.detect → defer=false → 空集
(若有 Flyway detector → 找到 flywayInitializer)
⇒ initializerBeanNames = { dataSourceScriptDatabaseInitializer }
⇒ 给每个 bean 盖 attribute 章
detectDependsOnInitializationBeanNames:
JpaDependsOnDatabaseInitializationDetector.detect → defer=false
→ 类型 = {EntityManagerFactory, AbstractEntityManagerFactoryBean}
→ 找到 bean 名 "entityManagerFactory"
AnnotationDependsOnDatabaseInitializationDetector → 无
⇒ dependentBeans = { entityManagerFactory }
应用:entityManagerFactory.dependsOn += { dataSourceScriptDatabaseInitializer }
【阶段 3】单例实例化
dataSourceScriptDatabaseInitializer 先创建
→ afterPropertiesSet() → initializeDatabase()
→ 读 data.sql → ResourceDatabasePopulator → 执行
entityManagerFactory 后创建
→ Hibernate 建 SessionFactory
→ ddl-auto 由 HibernateDefaultDdlAutoProvider 决定(嵌入式 + 无 schema manager → create-drop)
⚠️ create-drop 会 drop 掉 data.sql 刚建的表!
最后一行是重点:这解释了为什么默认配置下 data.sql + 嵌入式 + create-drop 会"数据插进去又没了"。解决办法就是 spring.jpa.defer-datasource-initialization=true。
再推演 defer=true:
【阶段 2】
detectInitializerBeanNames:
JpaDatabaseInitializerDetector.detect → defer=true → 类型 {EntityManagerFactory}
→ 找到 "entityManagerFactory" ⇒ JPA 成为初始化者!
→ 盖章(JpaDatabaseInitializerDetector)
⇒ byDetectorBeanNames = { JpaDetector → {entityManagerFactory},
其它detector → {dataSourceScriptDatabaseInitializer} }
分批循环(假设顺序:其它 detector 先,JPA detector 后):
批 1 = {dataSourceScriptDatabaseInitializer},previous=null → 无依赖
批 2 = {entityManagerFactory},previous={dataSourceScriptDatabaseInitializer}
→ entityManagerFactory.dependsOn += dataSourceScriptDatabaseInitializer ❌ 方向错了!
detectionComplete(JPA detector):
jpaInitializers = {entityManagerFactory}
otherInitializers = {dataSourceScriptDatabaseInitializer}
→ dataSourceScriptDatabaseInitializer.dependsOn += entityManagerFactory ✅
【阶段 3】
entityManagerFactory → Hibernate create-drop 建表
dataSourceScriptDatabaseInitializer → data.sql 插数据
注意:分批循环给 entityManagerFactory 加了一个反向依赖(entityManagerFactory depends on dataSourceScriptDatabaseInitializer),而 detectionComplete 又给 dataSourceScriptDatabaseInitializer 加了 entityManagerFactory 依赖——这形成了循环依赖!
这里必须诚实标注:上述推演出现了一个逻辑冲突,我未能从源码上完全确认实际执行结果。
可能的解释(均为推测,未核实):
DatabaseInitializerDetector extends Ordered,JpaDatabaseInitializerDetector不重写getOrder(),默认 0。而其它 detector(如 Flyway 的)可能设了 order,因此 JPA 批排在最前面,previous == null,不会给它加反向依赖。这样就没有循环。- 或者
SpringFactoriesLoader.load的加载顺序(按 jar 内的spring.factories顺序)恰好让 JPA 排第一。- 或者
BeanDefinition.setDependsOn的循环依赖在dependsOn层面由DefaultListableBeanFactory的dependentBeanMap处理,Spring 允许dependsOn循环(它不像构造器注入那样立即失败),只是顺序不确定。自测方法(强烈建议):写一个 H2 +
data.sql+defer=true的最小应用,在DependsOnDatabaseInitializationPostProcessor.postProcessBeanFactory返回处打断点,打印beanFactory.getBeanDefinition("entityManagerFactory").getDependsOn()和beanFactory.getBeanDefinition("dataSourceScriptDatabaseInitializer").getDependsOn(),看是否互为依赖。这个不确定点也说明了一个方法论事实:
dependsOn的双向写入是脆弱的,Spring Boot 依赖 detector 顺序来避免成环。
9. Spring Data JPA:DataJpaRepositoriesAutoConfiguration
@AutoConfiguration(after = { HibernateJpaAutoConfiguration.class, TaskExecutionAutoConfiguration.class })
@ConditionalOnBean(DataSource.class)
@ConditionalOnClass(JpaRepository.class)
@ConditionalOnMissingBean({ JpaRepositoryFactoryBean.class, JpaRepositoryConfigExtension.class })
@ConditionalOnBooleanProperty(name = "spring.data.jpa.repositories.enabled", matchIfMissing = true)
@Import(JpaRepositoriesImportSelector.class)
public final class DataJpaRepositoriesAutoConfiguration {
9.1 条件的四个层次
@ConditionalOnBean(DataSource.class)—— 有DataSource才启用(比 Hibernate 侧宽松:Hibernate 要@ConditionalOnSingleCandidate,这里只要"有")。@ConditionalOnClass(JpaRepository.class)—— Spring Data JPA 在 classpath。@ConditionalOnMissingBean({ JpaRepositoryFactoryBean.class, JpaRepositoryConfigExtension.class })—— 用户没有手动@EnableJpaRepositories。因为@EnableJpaRepositories会注册JpaRepositoryConfigExtension,一旦注册了,自动配置就该退出。这是 Spring Boot 检测"用户已手动启用"的标准手法。@ConditionalOnBooleanProperty(name = "spring.data.jpa.repositories.enabled", matchIfMissing = true)—— 总开关。
@AutoConfiguration(after = { HibernateJpaAutoConfiguration.class, TaskExecutionAutoConfiguration.class }) —— 为什么要 after TaskExecutionAutoConfiguration?因为下面的 @Bean 里可能用到 executor(bootstrap-mode=deferred)。
9.2 JpaRepositoriesImportSelector:Envers 的条件切换
static class JpaRepositoriesImportSelector implements ImportSelector {
private static final boolean ENVERS_AVAILABLE = ClassUtils.isPresent(
"org.springframework.data.envers.repository.config.EnableEnversRepositories",
JpaRepositoriesImportSelector.class.getClassLoader());
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[] { determineImport() };
}
private String determineImport() {
return ENVERS_AVAILABLE ? EnversRevisionRepositoriesRegistrar.class.getName()
: DataJpaRepositoriesRegistrar.class.getName();
}
}
这是一个 ImportSelector(不是 DeferredImportSelector),返回两个 Registrar 之一:
- classpath 有
spring-data-envers→EnversRevisionRepositoriesRegistrar(支持RevisionRepository,Hibernate Envers 审计) - 否则 →
DataJpaRepositoriesRegistrar
ENVERS_AVAILABLE 是 static final,在类加载时算一次。注意用的探测类是 EnableEnversRepositories(注解),而不是 EnversRevisionRepositoryFactoryBean——因为注解类更轻量、更稳定。
DataJpaRepositoriesRegistrar / EnversRevisionRepositoriesRegistrar 的实现(在 data-jpa 模块,各约 40 行)未逐行核实,推断它们继承 Spring Data 的 AbstractRepositoryConfigurationSourceSupport(或 RepositoryConfigurationDelegate 相关基类),并设置:
- 基础包 =
AutoConfigurationPackages.get(beanFactory) considerNestedRepositories = true
未核实:这两个 Registrar 的完整源码与它们如何确定扫描包。自测:
cat module/spring-boot-data-jpa/src/main/java/org/springframework/boot/data/jpa/autoconfigure/DataJpaRepositoriesRegistrar.java。
注意:Repository 的扫描包来自 AutoConfigurationPackages,不是 EntityScanPackages!这意味着:
@EntityScan只影响实体扫描,不影响 Repository 扫描。 如果你把实体放在包 A、Repository 放在包 B,而@SpringBootApplication在包 C,那么你需要同时用@EntityScan("A")和@EnableJpaRepositories("B")(后者会让自动配置退出)。
这是一个常见困惑点,也是 EntityScanPackages 与 AutoConfigurationPackages 分工的本质。
9.3 bootstrap-mode=deferred 的第二个触发点
@Bean
@ConditionalOnProperty(name = "spring.data.jpa.repositories.bootstrap-mode", havingValue = "deferred")
EntityManagerFactoryBuilderCustomizer entityManagerFactoryBootstrapExecutorCustomizer() {
return (builder) -> builder.requireBootstrapExecutor(() -> BootstrapExecutorRequiredException
.ofProperty("spring.data.jpa.repositories.bootstrap-mode", "deferred"));
}
这是 requireBootstrapExecutor 的第二个入口(第一个是 §4.3 的 spring.jpa.bootstrap=async)。
deferred 的语义:Repository 的创建延后到后台线程。这要求 EntityManagerFactory 也异步引导,否则 Repository 等待 EMF 就没意义了。
注意条件用的是 @ConditionalOnProperty(name=..., havingValue="deferred"),没有 matchIfMissing——所以 bootstrap-mode 默认是 default(Spring Data 的默认值),不触发。
注意:
spring.data.jpa.repositories.bootstrap-mode这个属性名属于 Spring Data JPA,不是 Spring Boot 定义的。DataJpaRepositoriesAutoConfiguration只是读它,然后把它翻译成 Spring Boot 侧的requireBootstrapExecutor。这是一个"跨项目属性桥接"的例子。未核实:Spring Data JPA 4.x 支持哪几种
bootstrap-mode(推断:DEFAULT、LAZY、DEFERRED)。本仓库无spring-data-jpa源码。
9.4 eagerJpaMetamodelCacheCleanup:一个容易被忽略的性能细节
@Bean
static LazyInitializationExcludeFilter eagerJpaMetamodelCacheCleanup() {
return (name, definition, type) -> "org.springframework.data.jpa.util.JpaMetamodelCacheCleanup".equals(name);
}
LazyInitializationExcludeFilter 是 Spring Boot 的懒加载排除机制:当 spring.main.lazy-initialization=true 时,被这个 filter 匹配的 bean 仍然会立即创建。
为什么要让 JpaMetamodelCacheCleanup 立即创建?因为它的作用是在容器所有单例创建完毕后,清空 JPA metamodel 的构建缓存(Hibernate 在解析实体时会缓存大量反射元数据,这些元数据在建完 SessionFactory 后就没用了)。如果它被懒加载,就可能永远不执行,导致内存泄漏。
注意它匹配的是 bean 名 "org.springframework.data.jpa.util.JpaMetamodelCacheCleanup"——这是一个由 Spring Data JPA 内部注册的 bean(BeanDefinitionRegistryPostProcessor 之类的),名字是全限定类名。
又是一个 static @Bean 方法。
9.5 HibernateMetricsAutoConfiguration:统计数据的桥接
@AutoConfiguration(after = HibernateJpaAutoConfiguration.class,
afterName = "org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration")
@ConditionalOnClass({ EntityManagerFactory.class, SessionFactory.class, HibernateMetrics.class, MeterRegistry.class })
@ConditionalOnBean({ EntityManagerFactory.class, MeterRegistry.class })
public final class HibernateMetricsAutoConfiguration implements SmartInitializingSingleton {
注意第二个 after 用的是 afterName(字符串形式的类名),而不是 after(Class)。为什么?因为 CompositeMeterRegistryAutoConfiguration 在 spring-boot-micrometer-metrics 模块,spring-boot-hibernate 模块不能编译期依赖它(会造成循环模块依赖)。所以用字符串。
这是第 3 章讲模块化时提到的 optional 依赖的两种处理法之一:用
afterName/@ConditionalOnClass(name="...")的字符串形式避免编译期依赖。另一种是用Class但把依赖声明为compileOnly/optional。
SmartInitializingSingleton 实现:
@Override
public void afterSingletonsInstantiated() {
bindEntityManagerFactoriesToRegistry(this.entityManagerFactories, this.meterRegistry);
}
private void bindEntityManagerFactoryToRegistry(String beanName, EntityManagerFactory entityManagerFactory,
MeterRegistry registry) {
String entityManagerFactoryName = getEntityManagerFactoryName(beanName);
try {
new HibernateMetrics(entityManagerFactory.unwrap(SessionFactory.class), entityManagerFactoryName,
Collections.emptyList())
.bindTo(registry);
}
catch (PersistenceException ex) {
// Continue
}
}
三个细节:
- 构造参数是
ConfigurableListableBeanFactory而不是Map<String, EntityManagerFactory>:
HibernateMetricsAutoConfiguration(ConfigurableListableBeanFactory beanFactory, MeterRegistry meterRegistry) {
this.entityManagerFactories = SimpleAutowireCandidateResolver.resolveAutowireCandidates(beanFactory,
EntityManagerFactory.class);
this.meterRegistry = meterRegistry;
}
用 SimpleAutowireCandidateResolver.resolveAutowireCandidates **只取"自动装配候选"**的 EntityManagerFactory bean——即排除那些 autowireCandidate=false 的。这比 getBeansOfType 更精确(后者会包含非候选 bean 并触发它们的实例化)。
-
为什么用
SmartInitializingSingleton而不是@Bean+ 直接绑定? 因为必须等所有EntityManagerFactory单例都创建完(unwrap(SessionFactory.class)需要 EMF 已初始化)才能绑定。afterSingletonsInstantiated正是这个时机。 -
catch (PersistenceException ex) { // Continue }——unwrap失败(比如不是 Hibernate 的 EMF)就跳过。
指标名前缀的处理:
private String getEntityManagerFactoryName(String beanName) {
if (beanName.length() > ENTITY_MANAGER_FACTORY_SUFFIX.length()
&& StringUtils.endsWithIgnoreCase(beanName, ENTITY_MANAGER_FACTORY_SUFFIX)) {
return beanName.substring(0, beanName.length() - ENTITY_MANAGER_FACTORY_SUFFIX.length());
}
return beanName;
}
ENTITY_MANAGER_FACTORY_SUFFIX = "entityManagerFactory"。所以 bean 名 entityManagerFactory → 名字 ""(空串,因为 length() > suffix.length() 为 false → 走 else → 返回原 bean 名?)
等等,仔细算:beanName = "entityManagerFactory",长度 20;suffix = "entityManagerFactory",长度 20。20 > 20 为 false,所以走 return beanName,即返回 "entityManagerFactory"。
如果 bean 名是 orderEntityManagerFactory(长度 25),25 > 20 为真且以 entityManagerFactory 结尾 → 返回 "order"。
所以:默认的单个 EMF 指标名就是 entityManagerFactory,多数据源时才是前缀名。这个逻辑有点微妙(默认情况没去掉后缀),但符合"默认场景下名字可预测"的目标。
未核实:
org.hibernate.orm.micrometer.HibernateMetrics的包名与构造签名(Hibernate 7 把 micrometer 集成放进了hibernate-micrometer模块,包名org.hibernate.orm.micrometer)。本仓库无 Hibernate 源码。
10. spring-boot-persistence:实体扫描与异常转换
10.1 @EntityScan 注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EntityScanPackages.Registrar.class)
public @interface EntityScan {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
}
标准 Spring 注解三件套:value / basePackages 互为别名(@AliasFor),basePackageClasses 类型安全变体。
@Import(EntityScanPackages.Registrar.class) —— 直接导入 Registrar。
10.2 EntityScanPackages.Registrar:三个来源 + 默认包回退
static class Registrar implements ImportBeanDefinitionRegistrar {
private final Environment environment;
Registrar(Environment environment) {
this.environment = environment;
}
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, getPackagesToScan(metadata));
}
private Set<String> getPackagesToScan(AnnotationMetadata metadata) {
AnnotationAttributes attributes = AnnotationAttributes
.fromMap(metadata.getAnnotationAttributes(EntityScan.class.getName()));
Assert.state(attributes != null, "'attributes' must not be null");
Set<String> packagesToScan = new LinkedHashSet<>();
for (String basePackage : attributes.getStringArray("basePackages")) {
String[] tokenized = StringUtils.tokenizeToStringArray(
this.environment.resolvePlaceholders(basePackage),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
Collections.addAll(packagesToScan, tokenized);
}
for (Class<?> basePackageClass : attributes.getClassArray("basePackageClasses")) {
packagesToScan.add(this.environment.resolvePlaceholders(ClassUtils.getPackageName(basePackageClass)));
}
if (packagesToScan.isEmpty()) {
String packageName = ClassUtils.getPackageName(metadata.getClassName());
Assert.state(StringUtils.hasLength(packageName), "@EntityScan cannot be used with the default package");
return Collections.singleton(packageName);
}
return packagesToScan;
}
}
四个要点:
basePackages支持占位符:this.environment.resolvePlaceholders(basePackage),所以可以写@EntityScan("${my.entity.packages}")。- 支持
CONFIG_LOCATION_DELIMITERS:StringUtils.tokenizeToStringArray(..., ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)。这个常量是", ; \t\n"(第 7 章讲 ConfigData 时见过),所以可以写@EntityScan("com.a,com.b;com.c")。 basePackageClasses也做占位符解析(虽然类名不长这样,但保持一致)。- 空则回退到注解所在类的包,且禁止默认包:
Assert.state(StringUtils.hasLength(packageName), "@EntityScan cannot be used with the default package")。这与第 13 章组件扫描对默认包的处理一致——默认包下扫描会扫遍整个 classpath,是灾难。
注意 Registrar 的构造器注入了 Environment——ImportBeanDefinitionRegistrar 可以声明 EnvironmentAware/BeanFactoryAware 等,Spring 会通过 AutowireCapableBeanFactory 注入,也可以直接用构造器(Spring 5.2+ 支持 ImportBeanDefinitionRegistrar 的构造器注入?)。
未核实:
ImportBeanDefinitionRegistrar实现类是否支持构造器注入Environment。Spring Framework 的ConfigurationClassParser在实例化 registrar 时用的是ParserStrategyUtils.instantiateClass(...),它支持Environment、ResourceLoader、BeanClassLoader三个参数的构造器注入。本仓库无spring-context源码,推断成立但未核实。自测:写一个自定义ImportBeanDefinitionRegistrar带Environment构造器参数,看是否被注入。
10.3 EntityScanPackagesBeanDefinition:一个累加式的 RootBeanDefinition
static class EntityScanPackagesBeanDefinition extends RootBeanDefinition {
private final Set<String> packageNames = new LinkedHashSet<>();
EntityScanPackagesBeanDefinition(Collection<String> packageNames) {
setBeanClass(EntityScanPackages.class);
setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
addPackageNames(packageNames);
}
private void addPackageNames(Collection<String> additionalPackageNames) {
this.packageNames.addAll(additionalPackageNames);
getConstructorArgumentValues().addIndexedArgumentValue(0, StringUtils.toStringArray(this.packageNames));
}
}
继承 RootBeanDefinition 是为了持有一个可变的 packageNames 集合,并在每次 add 时更新构造参数。
注册逻辑:
public static void register(BeanDefinitionRegistry registry, Collection<String> packageNames) {
Assert.notNull(registry, "'registry' must not be null");
Assert.notNull(packageNames, "'packageNames' must not be null");
if (registry.containsBeanDefinition(BEAN)) {
EntityScanPackagesBeanDefinition beanDefinition = (EntityScanPackagesBeanDefinition) registry
.getBeanDefinition(BEAN);
beanDefinition.addPackageNames(packageNames);
}
else {
registry.registerBeanDefinition(BEAN, new EntityScanPackagesBeanDefinition(packageNames));
}
}
多个 @EntityScan 会累加——如果存在已有的 bean definition,就往里加包名,而不是覆盖。这允许在多模块应用里每个模块各声明一次。
setRole(BeanDefinition.ROLE_INFRASTRUCTURE) —— 标记为基础设施 bean,在某些工具视图里会被隐藏(比如 Actuator 的 beans 端点会区分)。
注意 getPackageNames() 返回的是 Collections.unmodifiableList(...)(在 EntityScanPackages 构造器里包装的),所以外部不能改。
10.4 PersistenceExceptionTranslationAutoConfiguration:把 SQLException 变成 DataAccessException
@AutoConfiguration
@ConditionalOnClass(PersistenceExceptionTranslationPostProcessor.class)
public final class PersistenceExceptionTranslationAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnBooleanProperty(name = "spring.persistence.exceptiontranslation.enabled", matchIfMissing = true)
static PersistenceExceptionTranslationPostProcessor persistenceExceptionTranslationPostProcessor(
Environment environment) {
PersistenceExceptionTranslationPostProcessor postProcessor = new PersistenceExceptionTranslationPostProcessor();
boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class,
Boolean.TRUE);
postProcessor.setProxyTargetClass(proxyTargetClass);
return postProcessor;
}
}
PersistenceExceptionTranslationPostProcessor 是 Spring Framework 的 BeanFactoryPostProcessor/BeanPostProcessor(org.springframework.dao.annotation),它给带 @Repository 注解的 bean 加 AOP 切面,把持久化异常(如 Hibernate 的 HibernateException、JDBC 的 SQLException)翻译成 Spring 统一的 DataAccessException 层次。
三个细节:
- 没有
@ConditionalOnBean(DataSource.class)之类的门禁 —— 它只要spring-tx在 classpath 就启用。所以即使你只用 MongoDB,它也会在(对非@Repositorybean 无副作用)。 spring.aop.proxy-target-class默认true—— 即默认用 CGLIB 代理而不是 JDK 动态代理。这是 Spring Boot 2.0 以来的选择(第 19 章讲 AOP 时提过)。static @Bean—— 又一次。
推论:这解释了一个常见现象——即使你没有显式配置任何异常翻译,
@Repository类里抛出的 Hibernate 异常也会被包装成DataAccessException。如果你写了catch (HibernateException)却 catch 不到,原因就在这。
11. 全章决策表速查
11.1 事务管理器归属
| classpath | 生效的自动配置 | 得到的 PlatformTransactionManager |
|---|---|---|
只有 spring-boot-starter-jdbc | DataSourceTransactionManagerAutoConfiguration | DataSourceTransactionManager |
jdbc + data-jpa | HibernateJpaAutoConfiguration(before 生效) | JpaTransactionManager |
data-r2dbc | (响应式) | R2dbcTransactionManager(未核实) |
用户自定义 @Bean PlatformTransactionManager | — | 用户的(@ConditionalOnMissingBean(TransactionManager.class) 让所有自动配置退出) |
11.2 ddl-auto 最终值
spring.jpa.properties.hibernate.hbm2ddl.auto | spring.jpa.hibernate.ddl-auto | jakarta.persistence.schema-generation.database.action | 数据源 | 有 schema manager | 结果 |
|---|---|---|---|---|---|
| 有 | — | — | — | — | 用它的值 |
| 无 | 有 | — | — | — | 用它的值 |
| 无 | 无 | 有 | — | — | hbm2ddl.auto 被删除(交给规范键) |
| 无 | 无 | 无 | 非嵌入式 | — | none |
| 无 | 无 | 无 | 嵌入式 | 有 | none |
| 无 | 无 | 无 | 嵌入式 | 无 | create-drop |
11.3 数据库初始化顺序
spring.jpa.defer-datasource-initialization | JPA 角色 | 顺序 |
|---|---|---|
false(默认) | 依赖者 | 脚本 → EntityManagerFactory |
true | 初始化者 | EntityManagerFactory → 脚本 |
11.4 实体扫描包
| 条件 | 扫描包 |
|---|---|
有 @EntityScan("a","b") | a, b |
无 @EntityScan,有 @SpringBootApplication | 自动配置包(@SpringBootApplication 所在包及子包) |
无 @EntityScan,无自动配置包 | 空(扫不到实体) |
11.5 异步引导
spring.jpa.bootstrap | spring.data.jpa.repositories.bootstrap-mode | 唯一/命名的 executor | 结果 |
|---|---|---|---|
default | default | — | 同步 |
async | — | 有 | 异步 |
async | — | 无 | BootstrapExecutorRequiredException + 三条建议 |
| — | deferred | 有 | 异步(经 EntityManagerFactoryBuilderCustomizer) |
| — | deferred | 无 | 同上异常,属性名变成 spring.data.jpa.repositories.bootstrap-mode |
11.6 OSIV
| 应用类型 | spring.jpa.open-in-view | 结果 |
|---|---|---|
| Servlet + MVC | 未配 | 启用 + WARN |
| Servlet + MVC | true | 启用,无 WARN |
| Servlet + MVC | false | 不启用 |
| WebFlux | 任意 | 不启用(@ConditionalOnWebApplication(type=SERVLET) 不匹配) |
12. 本章新增定律
阅读 JPA 模块时浮现的方法论,补充到全系列定律表:
- 第三十一定律:容器查询必须延迟。构造参数若包含
beanFactory.getBeansOfType(...)这类会触发实例化的调用,必须包成Supplier。EntityManagerFactoryBuilder在 4.1.0→4.1.1 之间为此改了 API 并废弃了旧构造器(§5.1)。 - 第三十二定律:条件相同的两个自动配置,胜负由
before/after决定。JPA 与 JDBC 的事务管理器条件都是@ConditionalOnMissingBean(TransactionManager.class),谁赢只看排序(§3.3)。 - 第三十三定律:
null是"用户没配"的信号。JpaProperties.openInView用@Nullable Boolean区分"未配(打 WARN)"与"配了默认值(不打)"(§4.6)。 - 第三十四定律:
BeanDefinition的 attribute 可当"来源标记"。JPA detector 用 attribute 记录"这个 bean 是哪个 detector 找到的",从而在多 detector 协作时识别自己的产出(§8.6)。 - 第三十五定律:双向
dependsOn写入依赖 detector 顺序来避免成环。DatabaseInitializationDependencyConfigurer的分批链式依赖 +detectionComplete的反向写入,构成了一个对顺序敏感的脆弱平衡(§8.8,含未解决的不确定性)。
13. 本章未核实清单(务必自测)
按"最该先验证"排序:
MappingSettings.PHYSICAL_NAMING_STRATEGY与字面量"hibernate.physical_naming_strategy"是否相等(§6.4)。若不等,则 bean 形式与配置形式的命名策略写入了不同的键,是 bug。defer-datasource-initialization=true时是否产生dependsOn循环(§8.8)。打断点打印两个 bean definition 的getDependsOn()。EmbeddedDatabaseConnection.isEmbedded在数据库连不上时的行为(§7.2)。HibernateDefaultDdlAutoProvider无 try/catch。spring.jpa.generate-ddl=true与ddl-auto并存时的最终值(§7.3)。打印EntityManagerFactory.getProperties()。ImplicitJoinTableNameSource.getAssociationOwningAttributePath().getProperty()返回属性名还是实体名(§6.4)。写实体打印 DDL。PhysicalNamingStrategySnakeCaseImpl与 3.x 的CamelCaseToUnderscoresNamingStrategy行为是否等价(§6.4)。迁移应用必测。SchemaToolingSettings.HBM2DDL_AUTO/JAKARTA_HBM2DDL_DATABASE_ACTION的字面值(§7.1)。ListableBeanFactory.findAnnotationOnBean(name, anno, false)第三参语义(§8.7)。DataSourceTransactionManagerAutoConfiguration的确切条件注解(§3.3)。JpaTransactionManager如何解析EntityManagerFactory(§4.1)。多 EMF 场景必测。DataJpaRepositoriesRegistrar的完整实现(§9.2)。TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME的字面值(§5.1)。ImportBeanDefinitionRegistrar的构造器注入支持(§10.2)。- AOT/Native 下
DatabaseInitializationDependencyConfigurer被跳过后的顺序保证(§8.4)。 getBeanNamesForType(AbstractEntityManagerFactoryBean.class, true, false)是否能匹配 FactoryBean(§8.5)。
第三方源码(不在本仓库,全部未核实):LocalContainerEntityManagerFactoryBean、HibernateJpaVendorAdapter、PersistenceManagedTypesScanner、ConfigurableJtaPlatform、HibernateMetrics、SchemaToolingSettings/MappingSettings/PersistenceSettings 常量值、spring-data-jpa 的 bootstrap-mode 取值。
14. 本章小结
spring-boot-jpa + spring-boot-hibernate 加起来不到 20 个文件,却精确地把 JPA 塞进了 Spring Boot 的启动流程:
HibernateJpaAutoConfiguration是空的,全部语义在@AutoConfiguration(after/before)与@ConditionalOnClass三元组里(§3)。JpaBaseConfiguration的 5 个@Bean都很朴素,真正复杂的是它们周围的编排(§4)。EntityManagerFactoryBuilder是唯一的 public API,它的 4.1.1 改动(AsyncTaskExecutor→Supplier)暴露了"配置期不能触发容器查询"的硬约束(§5)。ddl-auto是四级回退 + 数据库探测的结果,默认create-drop只在"嵌入式且无人管 schema"时成立(§7)。- 数据库初始化顺序靠
BeanFactoryPostProcessor运行时改写dependsOn实现,spring.jpa.defer-datasource-initialization用一个属性把 JPA 从"依赖者"翻转成"初始化者"(§8)。
15. 下一章预告
第 24 章我们转向卷七(运维与可观测)的开篇:日志系统。你将看到一件与本章形成鲜明对比的事——
Spring Boot 4.1.1 里,日志没有
LoggingAutoConfiguration。
全仓库搜不到这个类。日志的初始化完全由 LoggingApplicationListener(一个注册在 spring.factories 里的 ApplicationListener)驱动,而不是由自动配置驱动。为什么?因为它必须在 Environment 就绪的第一时间(ApplicationEnvironmentPreparedEvent)就完成,而自动配置要等到 ApplicationContext 刷新阶段——太晚了。
第 24 章会拆开:LoggingSystem 抽象与三种实现(Logback / Log4j2 / JUL)的优先级、LogbackLoggingSystem 的 SUPPRESS_ALL_FILTER 抑制技巧、DefaultLogbackConfiguration 与 defaults.xml 的"两套默认配置"、以及 Spring Boot 3.4 引入、4.x 成熟的结构化日志体系(StructuredLogFormatter / ECS / GELF / Logstash)。