Spring Boot 4 搭配的 Spring Data JPA 会在构建期把派生查询和 @Query 方法直接生成成 Java 源码,只要 Maven 跑了 process-aot,再用 -Dspring.aot.enabled=true 启动,这部分实现就会接管仓库方法。我在 Boot 4.1.1(Spring Data JPA 4.1.1、Hibernate 7.4.5.Final、JDK 21)上测了一个 10 个查询方法的仓库,9 次启动取中位数,AOT 模式下打开仓库生成是 2.659 秒,关掉仓库生成是 3.191 秒,普通 JVM 启动是 3.309 秒。也就是说,同样是 AOT 模式,仓库生成让启动中位数少了约 0.53 秒,比普通 JVM 少约 0.65 秒;动态投影和 ScrollPosition 两个方法没有生成代码,运行时还是走老路。
这个特性在 Spring Data 2025.1(也就是 4.0 这一代)GA 时正式上线,本文用的 4.1.1 是之后的版本,官方博客 Spring Data Ahead of Time Repositories - Part 2 里有完整介绍。前阵子写 Spring Data 2026.1.0-M2 时关注的是 JPA 4.0 兼容,这次回到当前稳定版,看看 AOT 仓库到底生成了什么。它和 JDK 的 AOT cache(之前聊过 JEP 544)是两回事,这里说的是 Spring 自己在构建期生成源码。
项目和构建配置
依赖只有 spring-boot-starter-data-jpa 和 H2。AOT 处理靠 Spring Boot Maven 插件的 process-aot goal。spring-boot-starter-parent 只在 native 这个 profile 里挂了它,JVM 上用得自己加:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>process-aot</id>
<goals>
<goal>process-aot</goal>
</goals>
</execution>
</executions>
</plugin>
mvn package 时能看到这一步:
[INFO] --- spring-boot:4.1.1:process-aot (process-aot) @ jpa-aot-repository-demo ---
仓库接口里放了 7 个派生查询、1 个 @Query,再加两个我预计生成不了的方法:
public interface BookRepository extends JpaRepository<Book, Long> {
Optional<Book> findByIsbn(String isbn);
List<Book> findByAuthorOrderByPublishedDesc(String author);
List<Book> findByTitleContainingIgnoreCase(String keyword);
Page<Book> findByPriceLessThan(BigDecimal price, Pageable pageable);
List<BookTitle> findByPublishedAfter(LocalDate date);
long countByAuthor(String author);
boolean existsByIsbn(String isbn);
@Query("select b from Book b where b.author = :author and b.price between :min and :max")
List<Book> findByAuthorInPriceRange(String author, BigDecimal min, BigDecimal max);
<T> List<T> findByAuthor(String author, Class<T> type);
Window<Book> findFirst2ByOrderByIdAsc(ScrollPosition position);
}
BookTitle 是一个只有 getTitle()、getAuthor() 的接口投影。
target 下生成了什么
构建完,和这个仓库相关的文件:
$ find target/spring-aot -name "BookRepository*" | sort
target/spring-aot/main/classes/com/example/books/BookRepositoryImpl__AotRepository.class
target/spring-aot/main/classes/com/example/books/BookRepository__BeanDefinitions$1.class
target/spring-aot/main/classes/com/example/books/BookRepository__BeanDefinitions.class
target/spring-aot/main/resources/com/example/books/BookRepository.json
target/spring-aot/main/sources/com/example/books/BookRepositoryImpl__AotRepository.java
target/spring-aot/main/sources/com/example/books/BookRepository__BeanDefinitions.java
BookRepositoryImpl__AotRepository.java 就是生成的仓库片段,和接口同包,命名规则是接口全名加 Impl__AotRepository。打开看,构造器拿到 EntityManager,每个方法里 JPQL 都已经是字符串常量:
@Generated
public class BookRepositoryImpl__AotRepository extends AotRepositoryFragmentSupport {
private final RepositoryFactoryBeanSupport.FragmentCreationContext context;
private final EntityManager entityManager;
public BookRepositoryImpl__AotRepository(EntityManager entityManager,
RepositoryFactoryBeanSupport.FragmentCreationContext context) {
super(QueryEnhancerSelector.DEFAULT_SELECTOR, context);
this.entityManager = entityManager;
this.context = context;
}
/**
* AOT generated implementation of {@link BookRepository#countByAuthor(java.lang.String)}.
*/
public long countByAuthor(String author) {
String queryString = "SELECT COUNT(b) FROM Book b WHERE b.author = :author";
Query query = this.entityManager.createQuery(queryString);
query.setParameter("author", author);
return (Long) convertOne(query.getSingleResultOrNull(), false, Long.class);
}
/**
* AOT generated implementation of {@link BookRepository#existsByIsbn(java.lang.String)}.
*/
public boolean existsByIsbn(String isbn) {
String queryString = "SELECT b.id FROM Book b WHERE b.isbn = :isbn";
Query query = this.entityManager.createQuery(queryString);
query.setParameter("isbn", isbn);
query.setMaxResults(1);
return !query.getResultList().isEmpty();
}
existsBy 被翻成了查 id 加 setMaxResults(1)。@Query 方法就是把你写的 JPQL 原样抄进来:
public List<Book> findByAuthorInPriceRange(String author, BigDecimal min, BigDecimal max) {
String queryString = "select b from Book b where b.author = :author and b.price between :min and :max";
Query query = this.entityManager.createQuery(queryString);
query.setParameter("author", author);
query.setParameter("min", min);
query.setParameter("max", max);
return (List<Book>) query.getResultList();
}
分页方法会连 count 查询一起生成,排序在运行时再改写:
public Page<Book> findByPriceLessThan(BigDecimal price, Pageable pageable) {
String queryString = "SELECT b FROM Book b WHERE b.price < :price";
String countQueryString = "SELECT COUNT(b) FROM Book b WHERE b.price < :price";
Pageable pageable_1 = pageable != null ? pageable : Pageable.unpaged();
if (pageable_1.getSort().isSorted()) {
DeclaredQuery declaredQuery = DeclaredQuery.jpqlQuery(queryString);
queryString = rewriteQuery(declaredQuery, pageable_1.getSort(), Book.class);
}
接口投影只查需要的两列,用 Tuple 接结果:
public List<BookTitle> findByPublishedAfter(LocalDate date) {
String queryString = "SELECT b.title AS title, b.author AS author FROM Book b WHERE b.published > :date";
Query query = this.entityManager.createQuery(queryString, Tuple.class);
query.setParameter("date", date);
ContainingIgnoreCase 生成的是 UPPER(...) LIKE UPPER(...),通配符在 Java 里拼好:
String queryString = "SELECT b FROM Book b WHERE UPPER(b.title) LIKE UPPER(:keyword) ESCAPE '\\'";
Query query = this.entityManager.createQuery(queryString);
query.setParameter("keyword", "%%%s%%".formatted(keyword != null ? keyword.toUpperCase() : keyword));
整份文件里一共 8 个方法,findByAuthor(String, Class<T>) 和 findFirst2ByOrderByIdAsc(ScrollPosition) 没有出现。
哪些方法回退到运行时
光看 JSON 元数据容易误判。BookRepository.json 里这两个方法照样有条目,查询语句也写着:
"name": "findByAuthor",
"signature": "com.example.books.BookRepository",
"query": {
"query": "SELECT b FROM Book b WHERE b.author = :author"
}
但 Java 源码里没有它们。要确认运行时走的是哪条路,我把 org.springframework.data 的日志开到 DEBUG。Spring Data 在运行时为一个派生查询建 query 对象之前,会先去找同名的命名查询。普通 JVM 启动时,9 个派生方法挨个查了一遍:
Looking up named query 'Book.findByAuthor'
Looking up named query 'Book.findFirst2ByOrderByIdAsc'
Looking up named query 'Book.findByIsbn'
Looking up named query 'Book.findByTitleContainingIgnoreCase'
Looking up named query 'Book.findByPriceLessThan'
Looking up named query 'Book.findByPublishedAfter'
Looking up named query 'Book.countByAuthor'
Looking up named query 'Book.existsByIsbn'
Looking up named query 'Book.findByAuthorOrderByPublishedDesc'
用 -Dspring.aot.enabled=true 启动同一个 jar,日志开头变成 Starting AOT-processed BooksApplication,命名查询的查找只剩两条:
2026-10-16T08:11:42.091+08:00 DEBUG 993808 --- [jpa-aot-repository-demo] [ main] o.s.d.jpa.repository.query.NamedQuery : Looking up named query 'Book.findByAuthor'
2026-10-16T08:11:42.165+08:00 DEBUG 993808 --- [jpa-aot-repository-demo] [ main] o.s.d.jpa.repository.query.NamedQuery : Looking up named query 'Book.findFirst2ByOrderByIdAsc'
正好是没生成代码的那两个。官方文档的限制列表里也写了,动态投影和接受 ScrollPosition 的方法暂不支持。回退的方法功能上没问题,结果照常返回:
findByAuthor(dynamic) -> Spring Boot Up and Running
findFirst2ByOrderByIdAsc -> 2
JpaRepository 继承来的 save、findById 这些方法本来就由 SimpleJpaRepository 实现,不在生成范围内,JSON 里它们标的是 fragment。
控制开关:属性名和放在哪
文档里给的开关有这几个:spring.aot.repositories.enabled 控制所有模块,spring.aot.[module-name].repositories.enabled 控制单个模块,JPA 就是 spring.aot.jpa.repositories.enabled,spring.aot.repositories.metadata.enabled 只管 JSON 元数据。按 Spring Data 博客的说法,只要做了 Spring AOT 构建,仓库生成默认就是开的。
这些属性是在构建期生效的,要传给 process-aot。Maven 插件可以用 spring-boot.aot.jvmArguments 传:
mvn package -Dspring-boot.aot.jvmArguments="-Dspring.aot.jpa.repositories.enabled=false"
结果里只剩 Bean 定义,没有仓库片段和 JSON:
$ find target/spring-aot -name "BookRepository*" | sort
target/spring-aot/main/classes/com/example/books/BookRepository__BeanDefinitions.class
target/spring-aot/main/sources/com/example/books/BookRepository__BeanDefinitions.java
关掉 spring.aot.repositories.metadata.enabled 则是源码照常生成,只是少了 BookRepository.json:
target/spring-aot/main/classes/com/example/books/BookRepositoryImpl__AotRepository.class
target/spring-aot/main/classes/com/example/books/BookRepository__BeanDefinitions$1.class
target/spring-aot/main/classes/com/example/books/BookRepository__BeanDefinitions.class
target/spring-aot/main/sources/com/example/books/BookRepositoryImpl__AotRepository.java
target/spring-aot/main/sources/com/example/books/BookRepository__BeanDefinitions.java
我还试了把 spring.aot.repositories.enabled=false 直接写进 application.properties,不加任何命令行参数构建,同样没有生成仓库片段。process-aot 会按应用的配置启动一遍上下文,所以配置文件里的值也读得到。
运行时那一端是 Boot 的 spring.aot.enabled。不带它,jar 里就算有生成的类也不会用,日志里没有 AOT-processed 字样,上面那 9 条命名查询查找一条不少。
启动快了多少
三种组合各跑 9 次,交替执行,取 Started BooksApplication in X seconds 里的数字:普通 JVM 启动、AOT 模式加仓库生成、AOT 模式但构建时关掉仓库生成。机器是 8 核,H2 内存库。
== jvm
3.092 3.253 3.258 3.264 3.309 3.385 3.414 3.634 3.831
median 3.309
== aot-repos
2.441 2.53 2.564 2.626 2.659 2.688 2.731 2.807 2.825
median 2.659
== aot-no-repos
2.907 2.961 2.974 3.028 3.191 3.296 3.308 3.316 3.487
median 3.191
只看仓库生成这一项,要比后两组:同样是 AOT 模式,中位数从 3.191 秒降到 2.659 秒,少了大约 0.53 秒;和普通 JVM 的 3.309 秒比,少了约 0.65 秒。AOT 模式本身(Bean 定义预生成)对比普通 JVM 只省了 0.118 秒。
DEBUG 日志里能看到时间花在哪。从 Initializing repository instance for com.example.books.BookRepository… 到 Finished creation of repository instance 这一段,单次运行里 AOT 加仓库生成是 42.030 到 42.173,约 143 毫秒;关掉仓库生成的 AOT 模式是 00.781 到 01.366,约 585 毫秒;普通 JVM 是 49.804 到 50.527,约 723 毫秒。没有生成代码时,每个查询方法都要在启动时解析方法名、建查询。
2026-10-16T08:11:42.030+08:00 DEBUG 993808 --- [jpa-aot-repository-demo] [ main] o.s.d.r.c.s.RepositoryFactorySupport : Initializing repository instance for com.example.books.BookRepository…
2026-10-16T08:11:42.173+08:00 DEBUG 993808 --- [jpa-aot-repository-demo] [ main] o.s.d.r.c.s.RepositoryFactorySupport : Finished creation of repository instance for com.example.books.BookRepository.
踩过的坑
只加依赖不会生成任何东西
JVM 上不在插件里加 process-aot 这个 execution,target/spring-aot 根本不会出现。starter parent 默认只在 native profile 里做这一步。
jar 里有产物,运行时却没用上
AOT 产物打进了 jar,java -jar 直接跑照样是老路径,启动不会报任何错。只能靠日志里有没有 Starting AOT-processed 来判断,或者像上面那样看命名查询的查找日志。
JSON 元数据和源码对不上
动态投影和 ScrollPosition 方法在 JSON 里有完整的查询语句,Java 源码里却没有实现。判断哪些方法生成了,要看 Impl__AotRepository.java。
生成结果取决于构建时的配置
官方文档写明,JPA 的 AOT 处理会在构建期起一个内存里的 Hibernate 来收集元数据,不连数据库。本文只在 JVM 上用 H2 测试,没有测试 GraalVM Native Image 和其他数据库。application.properties 里的开关在构建期就生效了,改了配置要重新构建,光重启应用没用。
资料来源是 Spring Data JPA 与 Spring Data Commons 的 JPA AOT 文档和 Commons AOT 文档、Spring Boot JVM AOT 文档,以及 spring.io 博客 AOT Repositories Part 2 和 Spring Data 2025.1 GA。
一起交流
分享你的思考,让讨论更进一步。