Spring Data 扩展
本节介绍了一组 Spring Data 扩展,使 Spring Data 能够在多种上下文中使用。目前,大多数集成主要面向 Spring MVC。
Querydsl 扩展
Querydsl 是一个通过其链式 API 构建静态类型的类 SQL 查询的框架。
Querydsl 的维护速度已经放缓到社区在 OpenFeign 下 fork 了该项目的程度,地址为 github.com/OpenFeign/querydsl (groupId io.github.openfeign.querydsl)。Spring Data 以尽力而为的方式支持该 fork。
多个 Spring Data 模块通过 QuerydslPredicateExecutor 提供与 Querydsl 的集成,如下例所示:
public interface QuerydslPredicateExecutor<T> {
Optional<T> findById(Predicate predicate); 1
Iterable<T> findAll(Predicate predicate); 2
long count(Predicate predicate); 3
boolean exists(Predicate predicate); 4
// … more functionality omitted.
}
查找并返回匹配
Predicate的单个实体。查找并返回所有匹配
Predicate的实体。返回匹配
Predicate的实体数量。返回是否存在匹配
Predicate的实体。
要使用 Querydsl 支持,请在仓储接口中扩展 QuerydslPredicateExecutor,如下例所示:
interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}
前面的示例使你能够使用 Querydsl Predicate 实例编写类型安全的查询,如下例所示:
Predicate predicate = user.firstname.equalsIgnoreCase("dave")
.and(user.lastname.startsWithIgnoreCase("mathews"));
userRepository.findAll(predicate);
使用 Querydsl 的类型安全查询方法
LDAP 仓库对应实现与 Querydsl 项目集成,该项目提供了一种执行类型安全查询的方式。
查询不再以内联字符串形式编写,也不再外部化到 XML 文件中,而是通过 fluent API 构建。
— Querydsl 团队
它提供了以下功能:
-
在 IDE 中进行代码补全(所有属性、方法和操作都可以在你常用的 Java IDE 中展开)。
-
几乎不允许语法无效的查询(在所有层级上都是类型安全的)。
-
可以安全地引用领域类型和属性 — 不涉及字符串!
-
更好地适应领域类型中的重构变更。
-
增量查询定义更容易。
查看 Querydsl 文档,了解如何使用 Maven 或 Ant 为基于 APT 的代码生成引导环境。
Querydsl 允许你编写类似下面的查询:
QPerson person = QPerson.person;
List<Person> result = repository.findAll(person.address.zipCode.eq("C0123"));
Page<Person> page = repository.findAll(person.lastname.contains("a"),
PageRequest.of(0, 2, Direction.ASC, "lastname"));
QPerson 是一个由 Java annotation processor 生成的类。请参阅 Setting up Annotation Processing,了解如何在你的 Build System 中设置 Annotation Processing。它是一个 Predicate,可让你编写类型安全的查询。请注意,查询中除了 C0123 值之外没有字符串。
可以通过使用 QuerydslPredicateExecutor 接口来使用生成的 Predicate 类,以下清单展示了这一点:
public interface QuerydslPredicateExecutor<T> {
Optional<T> findOne(Predicate predicate);
List<T> findAll(Predicate predicate);
List<T> findAll(Predicate predicate, Sort sort);
List<T> findAll(Predicate predicate, OrderSpecifier<?>... orders);
Page<T> findAll(Predicate predicate, Pageable pageable);
List<T> findAll(OrderSpecifier<?>... orders);
long count(Predicate predicate);
boolean exists(Predicate predicate);
<S extends T, R> R findBy(Predicate predicate, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);
}
要在你的仓库实现中使用它,请将其添加到你的接口所继承的仓库接口列表中,如下例所示:
interface PersonRepository extends LdapRepository<Person>, QuerydslPredicateExecutor<Person> {
// additional query methods go here
}
设置 Annotation Processing
若要使用 Querydsl 与 Spring Data LDAP,你需要在构建系统中设置注解处理,以生成 Q 类。虽然你可以手写 Q 类,但建议使用 Querydsl 注解处理器为你生成它们,以保持 Q 类与领域模型同步。
Spring Data LDAP 包含一个注解处理器 LdapAnnotationProcessor,默认情况下未注册。通常,注解处理器通过 Java 的服务加载器经由 META-INF/services/javax.annotation.processing.Processor 进行注册;一旦它们位于类路径上,该机制也会启用这些处理器。大多数 Spring Data 用户并不使用 Querydsl,因此为无法从 Querydsl 中受益的项目要求额外的强制依赖并不合理。因此,你需要在构建系统中启用注解处理。
以下示例展示了如何通过在 Maven 和 Gradle 中声明依赖项和编译器配置变更来设置注解处理:
- Maven
- Gradle
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-ldap</artifactId>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-core</artifactId>
<version>${querydslVersion}</version>
<classifier>jakarta</classifier>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydslVersion}</version>
<classifier>jakarta</classifier>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessors>
<annotationProcessor>
org.springframework.data.ldap.repository.support.LdapAnnotationProcessor
</annotationProcessor>
</annotationProcessors>
<!-- Recommended: Some IDE's might require this configuration to include generated sources for IDE usage -->
<generatedTestSourcesDirectory>target/generated-test-sources</generatedTestSourcesDirectory>
<generatedSourcesDirectory>target/generated-sources</generatedSourcesDirectory>
</configuration>
</plugin>
</plugins>
</build>
dependencies {
implementation 'com.querydsl:querydsl-core:${querydslVersion}:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:${querydslVersion}:jakarta'
annotationProcessor 'org.springframework.data:spring-data-ldap'
testAnnotationProcessor 'com.querydsl:querydsl-apt:${querydslVersion}:jakarta'
testAnnotationProcessor 'org.springframework.data:spring-data-ldap'
}
tasks.withType(JavaCompile).configureEach {
options.compilerArgs += [
"-processor",
"org.springframework.data.ldap.repository.support.LdapAnnotationProcessor"]
}
请注意,上述设置仅展示了最简单的用法,省略了项目可能需要的其他选项或依赖项。
Web 支持
支持 repository 编程模型的 Spring Data 模块自带多种 web 支持。与 web 相关的组件需要 Spring MVC JAR 位于 classpath 中。其中一些甚至提供与 Spring HATEOAS 的集成。通常,集成支持通过在 JavaConfig 配置类中使用 @EnableSpringDataWebSupport 注解启用,如下例所示:
- Java
- XML
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}
<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />
<!-- If you use Spring HATEOAS, register this one *instead* of the former -->
<bean class="org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration" />
@EnableSpringDataWebSupport 注解会注册一些组件。我们将在本节后面讨论这些组件。它还会检测类路径上的 Spring HATEOAS,并为其注册集成组件(如果存在)。
基础 Web 支持
上一节中展示的配置注册了一些基本组件:
-
一个 使用 DomainClassConverter 类,用于让 Spring MVC 从请求参数或路径变量解析由 Repository 管理的领域类实例。
-
HandlerMethodArgumentResolver 实现,用于让 Spring MVC 从请求参数解析
Pageable和Sort实例。 -
Jackson Modules 用于对
Point和Distance等类型进行反序列化 / 序列化,或根据所使用的 Spring Data Module 存储特定类型。
使用 DomainClassConverter 类
DomainClassConverter 类允许你在 Spring MVC controller 方法签名中直接使用 domain 类型,因此无需通过 repository 手动查找实例,如下例所示:
@Controller
@RequestMapping("/users")
class UserController {
@RequestMapping("/{id}")
String showUserForm(@PathVariable("id") User user, Model model) {
model.addAttribute("user", user);
return "userForm";
}
}
该方法直接接收一个 User 实例,无需进一步查找。该实例可以通过让 Spring MVC 先将路径变量转换为领域类的 id 类型,并最终通过调用为该领域类型注册的 repository 实例上的 findById(…) 来解析。
目前,该仓库必须实现 CrudRepository 才能被识别用于转换。
HandlerMethodArgumentResolvers 用于 Pageable 和 Sort
在上一节中展示的配置片段还注册了 PageableHandlerMethodArgumentResolver 以及一个 SortHandlerMethodArgumentResolver 实例。该注册使得 Pageable 和 Sort 成为有效的 controller 方法参数,如下示例所示:
@Controller
@RequestMapping("/users")
class UserController {
private final UserRepository repository;
UserController(UserRepository repository) {
this.repository = repository;
}
@RequestMapping
String showUsers(Model model, Pageable pageable) {
model.addAttribute("users", repository.findAll(pageable));
return "users";
}
}
上述方法签名会使 Spring MVC 尝试从请求参数中推导一个 Pageable 实例,并采用以下默认配置:
表 1. 为 Pageable 实例评估的请求参数
从请求参数解析出的 Sort.Order 实例是普通的传输对象,会原样携带属性名称,不会针对领域模型进行任何校验。应用需要负责验证这些值,以防止非预期的排序。
要自定义此行为,请分别注册实现 PageableHandlerMethodArgumentResolverCustomizer 接口或 SortHandlerMethodArgumentResolverCustomizer 接口的 bean。其 customize() 方法会被调用,从而让你更改设置,如下面的示例所示:
@Bean SortHandlerMethodArgumentResolverCustomizer sortCustomizer() {
return s -> s.setPropertyDelimiter("<-->");
}
如果为现有 MethodArgumentResolver 设置属性无法满足你的需求,可以扩展 SpringDataWebConfiguration 或启用 HATEOAS 的等价配置类,重写 pageableResolver() 或 sortResolver() 方法,并导入自定义的配置文件,而不是使用 @Enable 注解。
如果需要从请求中解析多个 Pageable 或 Sort 实例(例如用于多个表格),可以使用 Spring 的 @Qualifier 注解来区分它们。此时请求参数需要以 ${qualifier}_ 作为前缀。下面的示例展示了生成的方法签名:
String showUsers(Model model,
@Qualifier("thing1") Pageable first,
@Qualifier("thing2") Pageable second) { … }
你必须填充 thing1_page、thing2_page 等。
传入该方法的默认 Pageable 等价于 PageRequest.of(0, 20),但你可以通过在 Pageable 参数上使用 @PageableDefault 注解来自定义它。
为 Page 创建 JSON 表示
It’s common for Spring MVC controllers to try to ultimately render a representation of a Spring Data page to clients. While one could simply return Page instances from handler methods to let Jackson render them as is, we strongly recommend against this as the underlying implementation class PageImpl is a domain type. This means we might want or have to change its API for unrelated reasons, and such changes might alter the resulting JSON representation in a breaking way.
在 Spring Data 3.1 中,我们开始通过输出描述该问题的警告日志来提示该问题。我们仍然最终建议利用 与 Spring HATEOAS 的集成 来获得一种完全稳定且支持超媒体的页面渲染方式,使客户端能够轻松导航这些页面。但从 3.3 版本起,Spring Data 提供了一种使用方便但无需引入 Spring HATEOAS 的页面渲染机制。
使用 Spring Data 的 PagedModel
其核心是,该支持由 Spring HATEOAS 中 PagedModel 的简化版本(位于 org.springframework.data.web 包中的 Spring Data 版本)组成。它可以用于包装 Page 实例,并生成一种简化表示,该表示反映了由 Spring HATEOAS 建立的结构,但省略了导航链接。
import org.springframework.data.web.PagedModel;
@Controller
class MyController {
private final MyRepository repository;
// Constructor omitted
@GetMapping("/page")
PagedModel<?> page(Pageable pageable) {
return new PagedModel<>(repository.findAll(pageable)); 1
}
}
将
Page实例包装为PagedModel。
这将生成如下所示的 JSON 结构:
{
"content" : [
… // Page content rendered here
],
"page" : {
"size" : 20,
"totalElements" : 30,
"totalPages" : 2,
"number" : 0
}
}
注意文档中包含一个 page 字段,用于暴露基本的分页元数据。
全局启用简化的 Page 渲染
如果你不想修改所有现有控制器以添加映射步骤,使其返回 PagedModel 而不是 Page,你可以通过调整 @EnableSpringDataWebSupport 来启用将 PageImpl 实例自动转换为 PagedModel,如下所示:
@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)
class MyConfiguration { }
这将允许你的控制器仍然返回 Page 实例,并且它们会自动渲染为简化表示:
@Controller
class MyController {
private final MyRepository repository;
// Constructor omitted
@GetMapping("/page")
Page<?> page(Pageable pageable) {
return repository.findAll(pageable);
}
}
Page 和 Slice 的 Hypermedia 支持
Spring HATEOAS 提供了一个表示模型类(PagedModel/SlicedModel),该模型允许使用必要的 Page/Slice 元数据以及链接来丰富 Page 或 Slice 实例的内容,以便客户端轻松导航页面。Page 到 PagedModel 的转换由 Spring HATEOAS RepresentationModelAssembler 接口的一个实现完成,该实现称为 PagedResourcesAssembler。类似地,可以使用 SlicedResourcesAssembler 将 Slice 实例转换为 SlicedModel。以下示例展示了如何将 PagedResourcesAssembler 用作控制器方法参数,因为 SlicedResourcesAssembler 的工作方式完全相同:
@Controller
class PersonController {
private final PersonRepository repository;
// Constructor omitted
@GetMapping("/people")
HttpEntity<PagedModel<Person>> people(Pageable pageable,
PagedResourcesAssembler assembler) {
Page<Person> people = repository.findAll(pageable);
return ResponseEntity.ok(assembler.toModel(people));
}
}
启用如前述示例所示的配置后,即可将 PagedResourcesAssembler 用作控制器方法参数。对其调用 toModel(…) 会产生以下效果:
-
Page的内容成为PagedModel实例的内容。 -
PagedModel对象会附加一个PageMetadata实例,并使用来自Page和底层Pageable的信息进行填充。 -
PagedModel可能会附加prev和next链接,具体取决于页面的状态。这些链接指向该方法映射到的 URI。添加到该方法的分页参数与PageableHandlerMethodArgumentResolver的设置相匹配,以确保这些链接稍后可以被解析。
假设数据库中有 30 个 Person 实例。您现在可以触发一个请求(GET [localhost:8080/people](http://localhost:8080/people)),并查看类似以下内容的输出:
{ "links" : [
{ "rel" : "next", "href" : "http://localhost:8080/persons?page=1&size=20" }
],
"content" : [
… // 20 Person instances rendered here
],
"page" : {
"size" : 20,
"totalElements" : 30,
"totalPages" : 2,
"number" : 0
}
}
此处显示的 JSON envelope 格式未遵循任何正式规范的结构,也不保证稳定,我们可能会随时更改。强烈建议启用由 Spring HATEOAS 支持的超媒体启用的官方媒体类型渲染,例如 HAL。可以使用其 @EnableHypermediaSupport 注解来启用这些功能。更多信息请参见 Spring HATEOAS reference documentation。
该 assembler 生成了正确的 URI,并采用了默认配置,将参数解析为用于后续请求的 Pageable。这意味着,如果你更改了该配置,链接会自动遵循该变更。默认情况下,该 assembler 指向其被调用所在的 controller method,但你可以通过传入自定义的 Link 作为构建分页链接的基础来自定义该行为,这会重载 PagedResourcesAssembler.toModel(…) 方法。
Spring Data Jackson 模块
核心模块,以及某些特定于存储的模块,随附一组用于 Spring Data 域所使用的类型的 Jackson Modules,例如 org.springframework.data.geo.Distance 和 org.springframework.data.geo.Point。
一旦启用 Web 支持 并且 tools.jackson.databind.ObjectMapper(Jackson 3)在类路径上可用,这些模块就会被导入。
tools.jackson.databind 是 Jackson 3 包前缀。Jackson 2 使用的是 com.fasterxml.jackson.databind。Jackson 2 支持已弃用,并将在未来版本中移除;请参阅 Jackson 3 migration guide 获取升级说明。
在初始化期间,SpringDataJackson3Modules 与 SpringDataJackson3Configuration 一样,会被基础设施拾取,从而使声明的 tools.jackson.databind.JacksonModule 实例可供 Jackson ObjectMapper 使用。
以下领域类型的数据绑定 mixins 由通用基础设施注册。
org.springframework.data.geo.Distance
org.springframework.data.geo.Point
org.springframework.data.geo.Box
org.springframework.data.geo.Circle
org.springframework.data.geo.Polygon
单个模块可能会提供额外的 SpringDataJackson3Modules.
请参考特定于 store 的章节以了解更多详情。
Web 数据绑定支持
您可以使用 Spring Data projections(在 Projections 中介绍),通过 JSONPath 表达式(需要 Jayway JsonPath)或 XPath 表达式(需要 XmlBeam)来绑定传入的请求载荷,如下例所示:
@ProjectedPayload
public interface UserPayload {
@XBRead("//firstname")
@JsonPath("$..firstname")
String getFirstname();
@XBRead("/lastname")
@JsonPath({ "$.lastname", "$.user.lastname" })
String getLastname();
}
您可以将前述示例中显示的类型用作 Spring MVC 处理程序方法参数,也可以在 RestTemplate 的某个方法中使用 ParameterizedTypeReference。前述方法声明会尝试在给定文档中查找 firstname。lastname 的 XML 查找会在传入文档的顶层执行。该查找的 JSON 变体会先尝试顶层 lastname,但如果前者未返回值,也会尝试查找嵌套在 user 子文档中的 lastname。这样,无需让客户端调用暴露的方法(这通常是基于类的负载绑定的一个缺点),即可轻松缓解源文档结构变化带来的影响。
嵌套投影的支持方式如 Projections 中所述。如果方法返回复杂的非接口类型,则使用 Jackson ObjectMapper 来映射最终值。
对于 Spring MVC,一旦 @EnableSpringDataWebSupport 处于激活状态并且所需依赖在 classpath 上可用,必要的转换器便会自动注册。若配合 RestTemplate 使用,请手动注册 ProjectingJacksonHttpMessageConverter(JSON)或 XmlBeamHttpMessageConverter。
有关更多信息,请参阅官方 Spring Data Examples 仓库 中的 Web Projection 示例。
默认情况下,绑定类集合属性的上限为 1024 个条目。你可以通过 spring.properties 文件或系统属性设置 spring.data.web.projection.collection-limit 属性来覆盖此限制。
Querydsl Web 支持
对于这些具有 Querydsl 集成的存储库,你可以从 Request 查询字符串中包含的属性派生查询。
考虑以下查询字符串:
?firstname=Dave&lastname=Matthews
给定上一示例中的 User 对象,你可以使用 QuerydslPredicateArgumentResolver 将查询字符串解析为以下值,如下所示:
QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))
此功能在 classpath 上发现 Querydsl 时,会与 @EnableSpringDataWebSupport 一起自动启用。
将 @QuerydslPredicate 添加到方法签名中即可提供一个可直接使用的 Predicate,你可以使用 QuerydslPredicateExecutor 来运行它。
类型信息通常从方法的返回类型中解析得到。由于该信息不一定与域类型匹配,因此使用 QuerydslPredicate 的 root 属性可能是一个好主意。
下面的示例展示了如何在方法签名中使用 @QuerydslPredicate:
@Controller
class UserController {
@Autowired UserRepository repository;
@RequestMapping(value = "/", method = RequestMethod.GET)
String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate, 1
Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {
model.addAttribute("users", repository.findAll(predicate, pageable));
return "index";
}
}
解析 query string 参数,使其匹配
User的Predicate。
默认绑定如下:
-
对于简单属性,
Object对应eq。 -
对于集合类属性,
Object对应contains。 -
对于简单属性,
Collection对应in。
您可以通过 @QuerydslPredicate 的 bindings 属性来自定义这些绑定,也可以利用 Java 8 default methods,并将 QuerydslBinderCustomizer 方法添加到 repository 接口中,如下所示:
interface UserRepository extends CrudRepository<User, String>,
QuerydslPredicateExecutor<User>, 1
QuerydslBinderCustomizer<QUser> { 2
@Override
default void customize(QuerydslBindings bindings, QUser user) {
bindings.bind(user.username).first((path, value) -> path.contains(value)) 3
bindings.bind(String.class)
.first((StringPath path, String value) -> path.containsIgnoreCase(value)); 4
bindings.excluding(user.password); 5
}
}
QuerydslPredicateExecutor为Predicate提供对特定 finder 方法的访问。定义在 repository 接口上的
QuerydslBinderCustomizer会被自动识别,并替代@QuerydslPredicate(bindings=…)。将
username属性的绑定定义为简单的contains绑定。将
String属性的默认绑定定义为不区分大小写的contains匹配。将
password属性从Predicate解析中排除。
你可以在应用来自 repository 或 @QuerydslPredicate 的特定绑定之前,注册一个持有默认 Querydsl 绑定的 QuerydslBinderCustomizerDefaults bean。
Repository Populators
如果你使用 Spring JDBC 模块,你可能已经熟悉使用 SQL 脚本填充 DataSource 的支持。在仓储(repositories)级别也有类似的抽象,不过它不使用 SQL 作为数据定义语言,因为它必须与存储无关。因此,填充器(populators)支持 XML(通过 Spring’s OXM 抽象)和 JSON(通过 Jackson)来定义用于填充仓储的数据。
假设你有一个名为 data.json 的文件,其内容如下:
[ { "_class" : "com.acme.Person",
"firstname" : "Dave",
"lastname" : "Matthews" },
{ "_class" : "com.acme.Person",
"firstname" : "Carter",
"lastname" : "Beauford" } ]
你可以使用 Spring Data Commons 提供的 repository 命名空间中的 populator 元素来填充你的仓库。要将前面的数据填充到你的 PersonRepository 中,请声明一个类似以下的 populator:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:repository="http://www.springframework.org/schema/data/repository"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/repository
https://www.springframework.org/schema/data/repository/spring-repository.xsd">
<repository:jackson2-populator locations="classpath:data.json" />
</beans>
上述声明会使 data.json 文件被 Jackson ObjectMapper 读取并反序列化。
JSON 对象反序列化到的类型由检查 JSON 文档的 _class 属性确定。基础设施最终会选择适当的仓库来处理被反序列化的对象。
若要改用 XML 来定义应填充到仓库中的数据,可以使用 unmarshaller-populator 元素。将其配置为使用 Spring OXM 中可用的某个 XML marshaller 选项。有关详细信息,请参阅 Spring 参考文档。以下示例展示了如何使用 JAXB 反序列化 repository populator:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:repository="http://www.springframework.org/schema/data/repository"
xmlns:oxm="http://www.springframework.org/schema/oxm"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/repository
https://www.springframework.org/schema/data/repository/spring-repository.xsd
http://www.springframework.org/schema/oxm
https://www.springframework.org/schema/oxm/spring-oxm.xsd">
<repository:unmarshaller-populator locations="classpath:data.json"
unmarshaller-ref="unmarshaller" />
<oxm:jaxb2-marshaller contextPath="com.acme" />
</beans>