Spring Data 扩展

本节介绍一组 Spring Data 扩展,这些扩展可以在各种环境中启用 Spring Data 的使用。目前,大多数集成都针对 Spring MVC。

Querydsl 扩展

Querydsl是一个框架,它通过其流畅的 API 启用构建静态类型的类似 SQL 的查询。

几个 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 支持,请在您的资源库接口上扩展QuerydslPredicateExecutor,如下例所示

资源库上的 Querydsl 集成
interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}

前面的示例允许您通过使用 QuerydslPredicate实例来编写类型安全的查询,如下例所示

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

userRepository.findAll(predicate);

Web 支持

支持资源库编程模型的 Spring Data 模块附带各种 Web 支持。Web 相关组件需要 Spring MVC JAR 文件位于类路径中。其中一些甚至提供了与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注解注册了一些组件。我们稍后在本节中讨论这些组件。它还会检测类路径上的 Spring HATEOAS 并注册其集成组件(如果存在)。

基本 Web 支持

在 XML 中启用 Spring Data Web 支持

上一节中显示的配置注册了一些基本组件

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

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

  • Jackson 模块用于反序列化/序列化诸如PointDistance之类的类型,或者根据所使用的 Spring Data 模块存储特定类型。

使用DomainClassConverter

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

在方法签名中使用域类型的 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类型,并最终通过调用为域类型注册的资源库实例上的findById(…)来解析该实例。

目前,资源库必须实现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 的简化版本(位于 org.springframework.data.web 包中的 Spring Data 版本)。它可以用来包装 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),它允许使用必要的 Page/Slice 元数据以及链接来丰富 PageSlice 实例的内容,以便客户端轻松导航页面。将 Page 转换为 PagedModel 是通过 Spring HATEOAS RepresentationModelAssembler 接口的实现完成的,称为 PagedResourcesAssembler。类似地,可以使用 SlicedResourcesAssemblerSlice 实例转换为 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 的信息填充它。

  • PagedModel 可以附加 prevnext 链接,具体取决于页面的状态。链接指向方法映射到的 URI。添加到方法的分页参数与 PageableHandlerMethodArgumentResolver 的设置相匹配,以确保稍后可以解析链接。

假设数据库中有 30 个 Person 实例。您现在可以触发请求 (GET localhost:8080/people) 并查看类似于以下内容的输出:

{ "links" : [
    { "rel" : "next", "href" : "https://127.0.0.1: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 参考文档

组装程序生成了正确的 URI,并拾取了默认配置以将参数解析为即将到来的请求的 Pageable。这意味着,如果您更改该配置,链接会自动遵循更改。默认情况下,组装程序指向调用它的控制器方法,但您可以通过传递自定义 Link 来定制它,该 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

公共基础设施注册以下域类型的 Data binding mixin。

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 投影(在 Projections 中描述)通过使用 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。这样,可以轻松地减轻源文档结构的更改,而无需客户端调用公开的方法(通常是基于类有效负载绑定的缺点)。

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

对于 Spring MVC,一旦 @EnableSpringDataWebSupport 处于活动状态并且类路径上提供了所需的依赖项,就会自动注册必要的转换器。对于与 RestTemplate 的用法,请手动注册 ProjectingJackson2HttpMessageConverter (JSON) 或 XmlBeamHttpMessageConverter

有关更多信息,请参阅规范 Spring Data Examples 存储库 中的 web 投影示例

Querydsl Web 支持

对于具有 QueryDSL 集成的存储,您可以从 Request 查询字符串中包含的属性派生查询。

考虑以下查询字符串:

?firstname=Dave&lastname=Matthews

给定前面示例中的 User 对象,您可以使用 QuerydslPredicateArgumentResolver 将查询字符串解析为以下值:

QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))
当在类路径中找到 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 将查询字符串参数解析为匹配的 UserPredicate

默认绑定如下:

  • 简单属性上的 Object 作为 eq

  • 集合属性上的 Object 作为 contains

  • 简单属性上的 Collection 作为 in

您可以通过 @QuerydslPredicatebindings 属性自定义这些绑定,或者通过使用 Java 8 的 default methods 并向存储库接口添加 QuerydslBinderCustomizer 方法来进行自定义,如下所示:

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 在存储库接口上定义的 QuerydslBinderCustomizer 将自动拾取并快捷方式 @QuerydslPredicate(bindings=…​)
3 定义 username 属性的绑定为简单的 contains 绑定。
4 定义 String 属性的默认绑定为不区分大小写的 contains 匹配。
5 Predicate 解析中排除 password 属性。
您可以注册一个 QuerydslBinderCustomizerDefaults bean,在应用存储库或 @QuerydslPredicate 的特定绑定之前,该 bean 存储默认的 Querydsl 绑定。

存储库填充器

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

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

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

您可以使用 Spring Data Commons 中提供的存储库命名空间的填充器元素来填充存储库。要将前面数据填充到您的 PersonRepository,请声明类似于以下内容的填充器:

声明 Jackson 存储库填充器
<?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 编组器选项之一。有关详细信息,请参阅 Spring 参考文档。以下示例显示了如何使用 JAXB 反序列化存储库填充器:

声明反序列化存储库填充器(使用 JAXB)
<?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>