跳到主要内容

AOT(提前编译)优化

Doubao 中英对照 Ahead of Time Optimizations

本章介绍 Spring Data 的 AOT(提前编译)优化,这些优化构建在 Spring 的 AOT(提前编译)优化 之上。

最佳实践​

为领域类型添加注解​

在应用程序启动期间,Spring 会扫描 classpath 上的领域类,以便对实体进行早期处理。通过用 Spring Data 专用的 @Table、@Document 或 @Entity 注解标注您的领域类型,您可以辅助初始实体扫描,并确保这些类型作为运行时提示(Runtime Hints)注册到 ManagedTypes 中。在原生映像的安排中无法进行 classpath 扫描,因此 Spring 必须使用 ManagedTypes 来确定初始实体集合。

运行时提示​

与常规 JVM 运行时相比,以原生映像方式运行应用程序需要额外的信息。Spring Data 会在 AOT 处理期间为原生映像的使用贡献运行时提示。这些提示尤其针对:

  • 审计

  • 用于捕获 classpath 扫描结果的 ManagedTypes

  • 仓库

    • 针对实体、返回类型和 Spring Data 注解的反射提示

    • 仓库片段

    • Querydsl Q 类

    • Kotlin 协程支持

  • Web 支持(针对 PagedModel 的 Jackson 提示)

AOT 仓库​

AOT 仓库是 AOT 处理的一种扩展,它通过预先生成符合条件的查询方法实现来完成。对于开发者而言,查询方法在底层执行的查询是不透明的。AOT 仓库基于构建时已知的派生查询、注解查询和命名查询来贡献查询方法实现。这项优化将查询方法的处理从运行时移到构建时,由于查询方法无需在每次应用程序启动时通过反射进行分析,因此可以带来显著的性能提升。

生成的 AOT 仓库片段遵循 <Repository FQCN>Impl__Aot 的命名方案,并放置在与仓库接口相同的包中。您可以在生成的仓库查询方法中找到所有查询的字符串形式。

备注

请将 AOT 仓库类视为内部优化。不要在您的代码中直接使用它们,因为生成和实现细节在未来版本中可能会发生变化。

使用 AOT 仓库运行​

AOT 是将 Spring 应用程序转换为原生可执行文件的必经步骤,因此在这种模式下运行时会自动启用。当 AOT 启用时(无论是为了原生编译还是通过设置 spring.aot.enabled=true),AOT 仓库也会被生成。

您可以完全禁用 AOT 仓库生成,或仅禁用 JDBC AOT 仓库:

  • 设置 spring.aot.repositories.enabled=false 属性,可为所有 Spring Data 模块禁用生成的仓库。

  • 设置 spring.aot.jdbc.repositories.enabled=false 属性,可仅禁用 JDBC AOT 仓库。

AOT 仓库会对实际的仓库 bean 注册贡献配置变更,以注册生成的仓库片段。

备注

当包含 AOT 优化时,一些在构建时所做的决策会被硬编码到应用程序设置中。例如,在构建时启用的 profile 在运行时也会自动启用。此外,实现某个仓库的 Spring Data 模块是固定的。更改实现需要重新进行 AOT 处理。

备注

请提供 JdbcDialect,以避免因方言检测而导致的早期数据库访问。

符合条件的方法​

AOT 仓库会筛选符合 AOT 处理条件的方法。这些方法通常是所有未由实现片段支持的查询方法。

支持的功能

  • 派生查询方法、@Query 注解查询和命名查询方法

  • 返回 void、int 和 long 的 @Modifying 方法

  • 分页、Slice、Stream 和 Optional 返回类型

  • DTO 投影和接口投影

  • 值表达式

限制

  • 接受 ScrollPosition 的方法(例如 Keyset 分页)尚未受支持

被排除的方法

  • CrudRepository、Querydsl、按示例查询以及其他基础接口方法,因为它们的实现由基类对应的片段提供

  • 实现过于复杂的方法

仓库元数据​

AOT 处理会内省查询方法,并收集有关仓库查询的元数据。Spring Data JDBC 将这些元数据存储在 JSON 文件中,文件以仓库接口命名,并与仓库接口放在一起(即在同一个包中)。仓库 JSON 元数据包含有关查询和片段的详细信息。以下面的仓库为例:

interface UserRepository extends CrudRepository<User, Integer> {

List<User> findUserNoArgumentsBy(); 1

Page<User> findPageOfUsersByLastnameStartingWith(String lastname, Pageable page); 2

@Query("select * from User u where username = ?1")
User findAnnotatedQueryByEmailAddress(String username); 3

User findByEmailAddress(String emailAddress); 4
}
java
  • 不带参数的派生查询。

  • 使用分页的派生查询。

  • 注解查询。

  • 命名查询。虽然存储过程方法包含在 JSON 元数据中,但它们的方法代码块不会在 AOT 仓库中生成。

{
"name": "com.acme.UserRepository",
"module": "JDBC",
"type": "IMPERATIVE",
"methods": [
{
"name": "findUserNoArgumentsBy",
"signature": "public abstract java.util.List<com.acme.User> com.acme.UserRepository.findUserNoArgumentsBy()",
"query": {
"query": "SELECT * FROM User"
}
},
{
"name": "findPageOfUsersByLastnameStartingWith",
"signature": "public abstract org.springframework.data.domain.Page<com.acme.User> com.acme.UserRepository.findPageOfUsersByLastnameStartingWith(java.lang.String,org.springframework.data.domain.Pageable)",
"query": {
"query": "SELECT * FROM User u WHERE lastname LIKE :lastname",
"count-query": "SELECT COUNT(*) FROM User WHERE lastname LIKE :lastname"
}
},
{
"name": "findAnnotatedQueryByEmailAddress",
"signature": "public abstract com.acme.User com.acme.UserRepository.findAnnotatedQueryByEmailAddress(java.lang.String)",
"query": {
"query": "select * from User where emailAddress = ?1"
}
},
{
"name": "findByEmailAddress",
"signature": "public abstract com.acme.User com.acme.UserRepository.findByEmailAddress(java.lang.String)",
"query": {
"name": "User.findByEmailAddress",
"query": "SELECT * FROM User WHERE emailAddress = ?1"
}
},
{
"name": "count",
"signature": "public abstract long org.springframework.data.repository.CrudRepository.count()",
"fragment": {
"fragment": "org.springframework.data.jdbc.repository.support.SimpleJdbcRepository"
}
}
]
}
json

查询可能包含以下字段:

  • query:如果该方法是查询方法,则为查询描述符。

    • name:如果是命名查询,则为命名查询的名称。

    • query:用于从 EntityManager 获取查询方法结果的查询。

    • count-name:如果计数查询是命名查询,则为命名计数查询的名称。

    • count-query:用于为使用分页的查询方法获取计数的计数查询。

  • fragment:如果方法调用被委托给某个存储(仓库基类、功能片段如 Querydsl)或用户片段,则为目标片段。如果没有进一步的接口,片段仅用 fragment 描述;如果存在接口(例如 Querydsl 或用户声明的片段接口),则以 interface 和 fragment 元组的形式描述。

备注

规范化查询形式

对查询的静态分析只能表示运行时查询行为的有限部分。查询以其规范化形式(预解析并重写后的形式)表示:

  • 值表达式被替换为绑定标记。

  • 查询元数据不反映绑定值的处理。StartingWith/EndingWith 查询会在实际绑定值前添加/追加通配符 %。

  • 运行时排序信息无法纳入查询字符串本身,因为该细节在构建时是未知的。