Spring Boot 4.1.1 深度源码解析-第 23 章 · JPA / Hibernate 与持久化模块

0 阅读9分钟

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 一个对象,塞几个属性"。真正难的是:

  1. 事务管理器:JPA 要 JpaTransactionManager,JDBC 要 DataSourceTransactionManager,两者都匹配"没有 TransactionManager bean"这个条件——谁先注册谁赢。这个胜负由 @AutoConfiguration(before = ...) 决定,而不是由条件注解决定。
  2. EntityManagerFactory 必须在 SQL 脚本(schema.sql / data.sql)之后创建,但 spring.jpa.defer-datasource-initialization=true 时要反过来。这个双向顺序靠一组 BeanFactoryPostProcessor 动态改写 BeanDefinition.dependsOn 实现,而不是靠 @DependsOn 静态注解。
  3. 嵌入式数据库下 ddl-auto 默认 create-drop,但有 Flyway/Liquibase 时必须是 none。这个判断要问一圈 SchemaManagementProvider。
  4. 异步引导(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-sqlSQL 脚本初始化的抽象层(spring.sql.init.*)与依赖探测 SPI0 个
spring-boot-jdbcDataSource、连接池、脚本初始化的 JDBC 实现多个
spring-boot-jpaJPA 通用自动配置基类、EntityManagerFactoryBuilder0 个(无 .imports!)
spring-boot-hibernateHibernate 具体实现2 个
spring-boot-data-jpaSpring Data JPA Repository1 个

注意 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:

类来自说明
LocalContainerEntityManagerFactoryBeanspring-ormSpring 的 JPA 集成基础设施
EntityManagerjakarta.persistence-apiJPA 规范 API
SessionImplementorhibernate-coreHibernate 内部 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;
}

三个细节:

  1. 返回类型是 PlatformTransactionManager,条件是 TransactionManager.class。TransactionManager 是 PlatformTransactionManager 和 ReactiveTransactionManager 的共同父接口(Spring Framework 5.2+ 引入的标记接口)。用 TransactionManager.class 做条件意味着:只要有任何响应式事务管理器存在,JPA 也不注册。这是一个"宽条件",比 @ConditionalOnMissingBean(PlatformTransactionManager.class) 更保守。

  2. JpaTransactionManager 没有显式设置 EntityManagerFactory。这是有意的——JpaTransactionManager 实现了 BeanFactoryAware,它会自己在 afterPropertiesSet 时从 BeanFactory 里按类型找 EntityManagerFactory。这避免了循环依赖:如果写成 transactionManager(EntityManagerFactory emf),而 EntityManagerFactory 的创建又要用到 DataSource(DataSource 的创建可能又需要事务管理器做初始化),就可能成环。

    推测(未核实):JpaTransactionManager 的 EntityManagerFactory 解析逻辑在 spring-orm 中,本仓库读不到。推断其走 BeanFactoryUtils.beanOfTypeIncludingAncestors 或 ObjectProvider。自测:在有两个 EntityManagerFactory 的应用里启动,看是否报 NoUniqueBeanDefinitionException(若是,则证明它是按类型解析的)。

  3. 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 LocalContainerEntityManagerFactoryBean through 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/AsyncTaskExecutor bean,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.SpringImplicitNamingStrategy
  • hibernate.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 ImplicitNamingStrategyJpaCompliantImpl with 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();
}

四级,按优先级:

级别来源键
1spring.jpa.properties.hibernate.hbm2ddl.autoexisting.get(HBM2DDL_AUTO)
2spring.jpa.hibernate.ddl-autothis.ddlAuto
3spring.jpa.properties.jakarta.persistence.schema-generation.database.actionexisting.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,所以生产环境不会自动建表。真正的事故通常是:

  1. 有人在 application.yml 里写了 ddl-auto: update 并把它带到了生产;
  2. 或者用了嵌入式数据库(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 中 SchemaManagementProvider bean 的具体实现类名,以及它们如何判定 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 再自己扫一遍:

  1. 性能:重复扫描整个 classpath,是启动慢的主要来源之一。
  2. 正确性: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
实际工作的 BFPPDependsOnDatabaseInitializationPostProcessor(内部类)由上面的 ImportBeanDefinitionRegistrar 注册
探测"谁初始化数据库"DatabaseInitializerDetectorspring.factories
探测"谁依赖初始化"DependsOnDatabaseInitializationDetectorspring.factories
JPA 侧实现(初始化者)JpaDatabaseInitializerDetectorspring-boot-jpa/spring.factories
JPA 侧实现(依赖者)JpaDependsOnDatabaseInitializationDetector同上
注解驱动的依赖者AnnotationDependsOnDatabaseInitializationDetectorspring-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,原因有三:

  1. BeanFactoryPostProcessor 必须尽早注册。用 @Bean 方法的话,它所在的 @Configuration 类要先被解析,而 @Configuration 的解析发生在 BFPP 执行之后的某些阶段——会错过时机。
  2. if (!registry.containsBeanDefinition(name)) 做幂等去重。因为 DatabaseInitializationDependencyConfigurer 会被多个自动配置 @Import(JDBC 的、R2DBC 的、Flyway 的、Liquibase 的……),重复注册同一个 BFPP 会导致它执行多次。
  3. 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 依赖——这形成了循环依赖!

这里必须诚实标注:上述推演出现了一个逻辑冲突,我未能从源码上完全确认实际执行结果。

可能的解释(均为推测,未核实):

  1. DatabaseInitializerDetector extends Ordered,JpaDatabaseInitializerDetector 不重写 getOrder(),默认 0。而其它 detector(如 Flyway 的)可能设了 order,因此 JPA 批排在最前面,previous == null,不会给它加反向依赖。这样就没有循环。
  2. 或者 SpringFactoriesLoader.load 的加载顺序(按 jar 内的 spring.factories 顺序)恰好让 JPA 排第一。
  3. 或者 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
	}
}

三个细节:

  1. 构造参数是 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 并触发它们的实例化)。

  1. 为什么用 SmartInitializingSingleton 而不是 @Bean + 直接绑定? 因为必须等所有 EntityManagerFactory 单例都创建完(unwrap(SessionFactory.class) 需要 EMF 已初始化)才能绑定。afterSingletonsInstantiated 正是这个时机。

  2. 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;
	}

}

四个要点:

  1. basePackages 支持占位符:this.environment.resolvePlaceholders(basePackage),所以可以写 @EntityScan("${my.entity.packages}")。
  2. 支持 CONFIG_LOCATION_DELIMITERS:StringUtils.tokenizeToStringArray(..., ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)。这个常量是 ", ; \t\n"(第 7 章讲 ConfigData 时见过),所以可以写 @EntityScan("com.a,com.b;com.c")。
  3. basePackageClasses 也做占位符解析(虽然类名不长这样,但保持一致)。
  4. 空则回退到注解所在类的包,且禁止默认包: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 层次。

三个细节:

  1. 没有 @ConditionalOnBean(DataSource.class) 之类的门禁 —— 它只要 spring-tx 在 classpath 就启用。所以即使你只用 MongoDB,它也会在(对非 @Repository bean 无副作用)。
  2. spring.aop.proxy-target-class 默认 true —— 即默认用 CGLIB 代理而不是 JDK 动态代理。这是 Spring Boot 2.0 以来的选择(第 19 章讲 AOP 时提过)。
  3. static @Bean —— 又一次。

推论:这解释了一个常见现象——即使你没有显式配置任何异常翻译,@Repository 类里抛出的 Hibernate 异常也会被包装成 DataAccessException。如果你写了 catch (HibernateException) 却 catch 不到,原因就在这。


11. 全章决策表速查

11.1 事务管理器归属

classpath生效的自动配置得到的 PlatformTransactionManager
只有 spring-boot-starter-jdbcDataSourceTransactionManagerAutoConfigurationDataSourceTransactionManager
jdbc + data-jpaHibernateJpaAutoConfiguration(before 生效)JpaTransactionManager
data-r2dbc(响应式)R2dbcTransactionManager(未核实)
用户自定义 @Bean PlatformTransactionManager—用户的(@ConditionalOnMissingBean(TransactionManager.class) 让所有自动配置退出)

11.2 ddl-auto 最终值

spring.jpa.properties.hibernate.hbm2ddl.autospring.jpa.hibernate.ddl-autojakarta.persistence.schema-generation.database.action数据源有 schema manager结果
有————用它的值
无有———用它的值
无无有——hbm2ddl.auto 被删除(交给规范键)
无无无非嵌入式—none
无无无嵌入式有none
无无无嵌入式无create-drop

11.3 数据库初始化顺序

spring.jpa.defer-datasource-initializationJPA 角色顺序
false(默认)依赖者脚本 → EntityManagerFactory
true初始化者EntityManagerFactory → 脚本

11.4 实体扫描包

条件扫描包
有 @EntityScan("a","b")a, b
无 @EntityScan,有 @SpringBootApplication自动配置包(@SpringBootApplication 所在包及子包)
无 @EntityScan,无自动配置包空(扫不到实体)

11.5 异步引导

spring.jpa.bootstrapspring.data.jpa.repositories.bootstrap-mode唯一/命名的 executor结果
defaultdefault—同步
async—有异步
async—无BootstrapExecutorRequiredException + 三条建议
—deferred有异步(经 EntityManagerFactoryBuilderCustomizer)
—deferred无同上异常,属性名变成 spring.data.jpa.repositories.bootstrap-mode

11.6 OSIV

应用类型spring.jpa.open-in-view结果
Servlet + MVC未配启用 + WARN
Servlet + MVCtrue启用,无 WARN
Servlet + MVCfalse不启用
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. 本章未核实清单(务必自测)

按"最该先验证"排序:

  1. MappingSettings.PHYSICAL_NAMING_STRATEGY 与字面量 "hibernate.physical_naming_strategy" 是否相等(§6.4)。若不等,则 bean 形式与配置形式的命名策略写入了不同的键,是 bug。
  2. defer-datasource-initialization=true 时是否产生 dependsOn 循环(§8.8)。打断点打印两个 bean definition 的 getDependsOn()。
  3. EmbeddedDatabaseConnection.isEmbedded 在数据库连不上时的行为(§7.2)。HibernateDefaultDdlAutoProvider 无 try/catch。
  4. spring.jpa.generate-ddl=true 与 ddl-auto 并存时的最终值(§7.3)。打印 EntityManagerFactory.getProperties()。
  5. ImplicitJoinTableNameSource.getAssociationOwningAttributePath().getProperty() 返回属性名还是实体名(§6.4)。写实体打印 DDL。
  6. PhysicalNamingStrategySnakeCaseImpl 与 3.x 的 CamelCaseToUnderscoresNamingStrategy 行为是否等价(§6.4)。迁移应用必测。
  7. SchemaToolingSettings.HBM2DDL_AUTO / JAKARTA_HBM2DDL_DATABASE_ACTION 的字面值(§7.1)。
  8. ListableBeanFactory.findAnnotationOnBean(name, anno, false) 第三参语义(§8.7)。
  9. DataSourceTransactionManagerAutoConfiguration 的确切条件注解(§3.3)。
  10. JpaTransactionManager 如何解析 EntityManagerFactory(§4.1)。多 EMF 场景必测。
  11. DataJpaRepositoriesRegistrar 的完整实现(§9.2)。
  12. TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME 的字面值(§5.1)。
  13. ImportBeanDefinitionRegistrar 的构造器注入支持(§10.2)。
  14. AOT/Native 下 DatabaseInitializationDependencyConfigurer 被跳过后的顺序保证(§8.4)。
  15. 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)。