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 的集成,如下例所示

QuerydslPredicateExecutor 接口
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.
}
1 查找并返回匹配 Predicate 的单个实体。
2 查找并返回匹配 Predicate 的所有实体。
3 返回匹配 Predicate 的实体数量。
4 返回是否存在匹配 Predicate 的实体。

要使用 Querydsl 支持,请在你的 repository 接口上扩展 QuerydslPredicateExecutor,如下例所示

Repository 中的 Querydsl 集成
interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}

前面的示例允许你使用 Querydsl Predicate 实例编写类型安全的查询,如下例所示

Predicate predicate = user.firstname.equalsIgnoreCase("dave")
	.and(user.lastname.startsWithIgnoreCase("mathews"));

userRepository.findAll(predicate);

Web 支持

支持 repository 编程模型的 Spring Data 模块提供多种 Web 支持。Web 相关组件要求 Spring MVC JAR 位于 classpath 中。其中一些甚至提供与 Spring HATEOAS 的集成。通常,集成支持通过在 JavaConfig 配置类中使用 @EnableSpringDataWebSupport 注解启用,如下例所示

启用 Spring Data Web 支持
  • 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 注解会注册一些组件。我们将在本节后面讨论这些组件。它还会检测 classpath 中的 Spring HATEOAS 并注册相应的集成组件(如果存在)。

基本 Web 支持

在 XML 中启用 Spring Data Web 支持

上一节中所示的配置会注册一些基本组件

  • 使用 DomainClassConverter,让 Spring MVC 从请求参数或路径变量解析 Repository 管理的域类实例。

  • HandlerMethodArgumentResolver 实现,让 Spring MVC 从请求参数解析 PageableSort 实例。

  • Jackson 模块,用于反/序列化 Point 和 Distance 等类型,或根据使用的 Spring Data 模块反/序列化特定类型。

使用 DomainClassConverter

DomainClassConverter 类允许你在 Spring MVC 控制器方法签名中直接使用域类型,这样你就不必手动通过 repository 查找实例,如下例所示

在方法签名中使用域类型的 Spring MVC 控制器
@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(…) 来解析实例。

目前,repository 必须实现 CrudRepository 才能被发现进行转换。

Pageable 和 Sort 的 HandlerMethodArgumentResolvers

上一节中所示的配置片段还注册了 PageableHandlerMethodArgumentResolver 以及 SortHandlerMethodArgumentResolver 实例。该注册启用 PageableSort 作为有效的控制器方法参数,如下例所示

将 Pageable 用作控制器方法参数
@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 实例的请求参数

page

要检索的页码。索引从 0 开始,默认为 0。

size

要检索的页面大小。默认为 20。

sort

需要按其排序的属性,格式为 property,property(,ASC|DESC)(,IgnoreCase)。默认排序方向是区分大小写的升序。如果要切换方向或大小写敏感性,可以使用多个 sort 参数,例如 ?sort=firstname&sort=lastname,asc&sort=city,ignorecase

要自定义此行为,请注册实现 PageableHandlerMethodArgumentResolverCustomizer 接口或 SortHandlerMethodArgumentResolverCustomizer 接口的 bean。将调用其 customize() 方法,允许你更改设置,如下例所示

@Bean SortHandlerMethodArgumentResolverCustomizer sortCustomizer() {
    return s -> s.setPropertyDelimiter("<-->");
}

如果设置现有 MethodArgumentResolver 的属性不足以满足你的目的,请扩展 SpringDataWebConfiguration 或支持 HATEOAS 的等效类,覆盖 pageableResolver()sortResolver() 方法,并导入你的自定义配置文件而不是使用 @Enable 注解。

如果你需要从请求中解析多个 PageableSort 实例(例如,针对多个表),可以使用 Spring 的 @Qualifier 注解来区分它们。请求参数必须以 ${qualifier}_ 为前缀。以下示例展示了最终的方法签名

String showUsers(Model model,
      @Qualifier("thing1") Pageable first,
      @Qualifier("thing2") Pageable second) { … }

你必须填充 thing1_pagething2_page 等。

传递给方法的默认 Pageable 等同于 PageRequest.of(0, 20),但你可以通过在 Pageable 参数上使用 @PageableDefault 注解来对其进行自定义。

创建 Page 的 JSON 表示

Spring MVC 控制器通常会尝试最终向客户端渲染 Spring Data 页面的表示。虽然可以直接从处理器方法返回 Page 实例,让 Jackson 原样渲染,但我们强烈不推荐这样做,因为底层的实现类 PageImpl 是一个域类型。这意味着我们可能出于无关的原因想要或必须更改其 API,而这些更改可能会以破坏性的方式改变最终的 JSON 表示。

从 Spring Data 3.1 开始,我们通过发出一个描述此问题的警告日志来提示该问题。我们仍然最终推荐利用与 Spring HATEOAS 的集成,以一种完全稳定且支持超媒体的方式渲染页面,使客户端能够轻松导航。但从版本 3.3 开始,Spring Data 提供了一种方便使用的页面渲染机制,且无需包含 Spring HATEOAS。

使用 Spring Data 的 PagedModel

其核心在于,该支持包含 Spring HATEOAS 的 PagedModel 的简化版本(Spring Data 的版本位于 org.springframework.data.web 包中)。它可用于封装 Page 实例,并生成反映 Spring HATEOAS 建立的结构但省略导航链接的简化表示。

import org.springframework.data.web.PagedModel;

@Controller
class MyController {

  private final MyRepository repository;

  // Constructor ommitted

  @GetMapping("/page")
  PagedModel<?> page(Pageable pageable) {
    return new PagedModel<>(repository.findAll(pageable)); (1)
  }
}
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 ommitted

  @GetMapping("/page")
  Page<?> page(Pageable pageable) {
    return repository.findAll(pageable);
  }
}

PageSlice 的超媒体支持

Spring HATEOAS 提供一个表示模型类(PagedModel/SlicedModel),它允许丰富 PageSlice 实例的内容,包含必要的 Page/Slice 元数据以及链接,以便客户端轻松导航页面。将 Page 转换为 PagedModel 由一个名为 PagedResourcesAssembler 的 Spring HATEOAS RepresentationModelAssembler 接口实现完成。类似地,Slice 实例可以使用 SlicedResourcesAssembler 转换为 SlicedModel。以下示例展示如何将 PagedResourcesAssembler 用作控制器方法参数,SlicedResourcesAssembler 的工作方式完全相同

将 PagedResourcesAssembler 用作控制器方法参数
@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 的信息。

  • 根据 Page 的状态,PagedModel 可能会附带 prevnext 链接。这些链接指向方法映射到的 URI。添加到方法的分页参数与 PageableHandlerMethodArgumentResolver 的设置匹配,以确保链接稍后可以解析。

假设数据库中有 30 个 Person 实例。你现在可以触发一个请求(GET 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 封装格式不遵循任何正式指定的结构,并且不保证稳定,我们可能随时更改它。强烈建议启用渲染为超媒体支持的官方媒体类型(由 Spring HATEOAS 支持),例如 HAL。可以通过使用其 @EnableHypermediaSupport 注解激活这些类型。更多信息请参见 Spring HATEOAS 参考文档

assembler 生成了正确的 URI,并且也使用了默认配置来解析参数到 Pageable 以供后续请求使用。这意味着,如果你更改了该配置,链接会自动遵循更改。默认情况下,assembler 指向调用它的控制器方法,但你可以通过传递自定义的 Link 作为构建分页链接的基础来定制此行为,这会重载 PagedResourcesAssembler.toModel(…) 方法。

Spring Data Jackson 模块

核心模块以及一些特定存储模块附带了一组 Jackson 模块,用于 Spring Data 域中使用的类型,例如 org.springframework.data.geo.Distanceorg.springframework.data.geo.Point
一旦启用 Web 支持并且 com.fasterxml.jackson.databind.ObjectMapper 可用,这些模块就会被导入。

在初始化期间,SpringDataJacksonModules(如 SpringDataJacksonConfiguration)会被基础设施获取,以便声明的 com.fasterxml.jackson.databind.Module 可用于 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

各个模块可能会提供额外的 SpringDataJacksonModules
请参考特定存储部分获取更多详细信息。

Web 数据绑定支持

你可以使用 Spring Data 投影(在 投影 中描述)来通过使用 JSONPath 表达式(需要 Jayway JsonPath)或 XPath 表达式(需要 XmlBeam)绑定传入的请求负载,如下例所示

使用 JSONPath 或 XPath 表达式进行 HTTP 负载绑定
@ProjectedPayload
public interface UserPayload {

  @XBRead("//firstname")
  @JsonPath("$..firstname")
  String getFirstname();

  @XBRead("/lastname")
  @JsonPath({ "$.lastname", "$.user.lastname" })
  String getLastname();
}

你可以将前面示例中所示的类型用作 Spring MVC 处理程序方法参数,或通过在 RestTemplate 的方法上使用 ParameterizedTypeReference。前面的方法声明将尝试在给定文档中的任何位置查找 firstnamelastname 的 XML 查找在传入文档的顶级执行。JSON 变体首先尝试顶级的 lastname,如果前者未返回值,则还会尝试嵌套在 user 子文档中的 lastname。通过这种方式,可以轻松缓解源文档结构的变化,而无需客户端调用暴露的方法(这通常是基于类负载绑定的缺点)。

支持嵌套投影,如 投影 中所述。如果方法返回一个复杂的、非接口类型,则使用 Jackson ObjectMapper 来映射最终值。

对于 Spring MVC,一旦 @EnableSpringDataWebSupport 处于活动状态且所需的依赖项位于 classpath 中,就会自动注册必要的转换器。对于与 RestTemplate 一起使用,请手动注册 ProjectingJackson2HttpMessageConverter (JSON) 或 XmlBeamHttpMessageConverter

有关更多信息,请参见规范 Spring Data Examples repository 中的web 投影示例

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 运行它。

类型信息通常从方法的返回类型解析。由于该信息不一定匹配域类型,因此最好使用 @QuerydslPredicateroot 属性。

以下示例展示如何在方法签名中使用 @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";
  }
}
1 将查询字符串参数解析为匹配 User 的 Predicate。

默认绑定如下

  • 简单属性上的 Object 作为 eq

  • 类似集合的属性上的 Object 作为 contains

  • 简单属性上的 Collection 作为 in

你可以通过 @QuerydslPredicatebindings 属性或利用 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)
  }
}
1 QuerydslPredicateExecutor 提供对 Predicate 特定查找方法的访问。
2 在 repository 接口上定义的 QuerydslBinderCustomizer 会自动获取,并快捷方式 @QuerydslPredicate(bindings=…​)
3 username 属性定义绑定为简单的 contains 绑定。
4 String 属性定义默认绑定为不区分大小写的 contains 匹配。
5 Predicate 解析中排除 password 属性。
你可以注册一个 QuerydslBinderCustomizerDefaults bean,该 bean 包含默认的 Querydsl 绑定,然后在应用来自 repository 或 @QuerydslPredicate 的特定绑定之前使用这些默认绑定。

Repository Populator

如果你使用 Spring JDBC 模块,你可能熟悉用 SQL 脚本填充 DataSource 的支持。在 repository 级别也有类似的抽象,尽管它不使用 SQL 作为数据定义语言,因为它必须独立于存储。因此,populator 支持 XML(通过 Spring 的 OXM 抽象)和 JSON(通过 Jackson)来定义用于填充 repository 的数据。

假设你有一个名为 data.json 的文件,其内容如下

JSON 中定义的数据
[ { "_class" : "com.acme.Person",
 "firstname" : "Dave",
  "lastname" : "Matthews" },
  { "_class" : "com.acme.Person",
 "firstname" : "Carter",
  "lastname" : "Beauford" } ]

你可以使用 Spring Data Commons 中提供的 repository 命名空间的 populator 元素来填充 repository。要将前面的数据填充到 PersonRepository 中,请声明一个类似于以下的 populator

声明 Jackson 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"
  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 属性来确定。基础设施最终会选择适当的 repository 来处理反序列化后的对象。

要改用 XML 定义要填充 repository 的数据,可以使用 unmarshaller-populator 元素。你将其配置为使用 Spring OXM 中提供的 XML marshaller 选项之一。详细信息请参见Spring 参考文档。以下示例展示如何使用 JAXB 对 repository populator 进行反序列化

声明一个使用 JAXB 的 unmarshalling 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>