此版本仍在开发中,尚未被视为稳定版本。如需最新稳定版本,请使用 Spring Data REST 5.0.4spring-doc.cadn.net.cn

投影与摘录

Spring Data REST 会为你导出的领域模型提供一个默认视图。然而,有时你可能出于各种原因需要修改该模型的视图。本节将介绍如何定义投影(Projections)和摘录(Excerpts),以提供资源的简化和精简视图。spring-doc.cadn.net.cn

投影

考虑以下领域模型:spring-doc.cadn.net.cn

@Entity
public class Person {

  @Id @GeneratedValue
  private Long id;
  private String firstName, lastName;

  @OneToOne
  private Address address;
  …
}

前面示例中的 Person 对象具有多个属性:spring-doc.cadn.net.cn

现在假设我们创建一个相应的仓库,如下所示:spring-doc.cadn.net.cn

interface PersonRepository extends CrudRepository<Person, Long> {}

默认情况下,Spring Data REST 会导出该领域对象及其所有属性。firstNamelastName 会以其原始数据对象的形式被导出。对于 address 属性,有两种处理方式。其中一种是为 Address 对象也定义一个仓库,如下所示:spring-doc.cadn.net.cn

interface AddressRepository extends CrudRepository<Address, Long> {}

在这种情况下,Person 资源会将 address 属性渲染为指向其对应的 Address 资源的 URI。如果我们尝试在系统中查找 “Frodo”,可以预期看到类似如下的 HAL 文档:spring-doc.cadn.net.cn

{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/persons/1"
    },
    "address" : {
      "href" : "http://localhost:8080/persons/1/address"
    }
  }
}

还有另一种方式。如果 Address 领域对象没有自己的仓库定义,Spring Data REST 会将数据字段直接包含在 Person 资源中,如下例所示:spring-doc.cadn.net.cn

{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "address" : {
    "street": "Bag End",
    "state": "The Shire",
    "country": "Middle Earth"
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/persons/1"
    }
  }
}

但如果你完全不想要 address 详细信息呢?同样,默认情况下,Spring Data REST 会导出其所有属性(除了 id)。你可以通过定义一个或多个投影(projection),为你的 REST 服务使用者提供替代方案。以下示例展示了一个不包含地址信息的投影:spring-doc.cadn.net.cn

@Projection(name = "noAddresses", types = { Person.class }) (1)
interface NoAddresses { (2)

  String getFirstName(); (3)

  String getLastName(); (4)
}
1 @Projection 注解用于将此类标记为投影。name 属性指定了该投影的名称,我们稍后将更详细地介绍。types 属性指定此投影仅适用于 Person 对象。
2 它是一个 Java 接口,因此是声明式的。
3 它导出了 firstName
4 它导出了 lastName

NoAddresses 投影仅包含 firstNamelastName 的 getter 方法,这意味着它不提供任何地址信息。假设你有一个单独的 Address 资源仓库,那么 Spring Data REST 的默认视图与之前的表示形式略有不同,如下例所示:spring-doc.cadn.net.cn

{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/persons/1{?projection}", (1)
      "templated" : true (2)
    },
    "address" : {
      "href" : "http://localhost:8080/persons/1/address"
    }
  }
}
1 此资源有一个新选项:{?projection}
2 self URI 是一个 URI 模板。
提供给 projection 查询参数的值与在 @Projection(name = "noAddress") 中指定的值相同。它与投影接口的名称无关。

你可以拥有多个投影。spring-doc.cadn.net.cn

参见投影(Projections)以查看一个示例项目。我们鼓励您动手尝试。

Spring Data REST 按如下方式查找投影定义:spring-doc.cadn.net.cn

  • 任何与您的实体定义位于同一包(或其子包之一)中的 @Projection 接口都会被注册。spring-doc.cadn.net.cn

  • 你可以通过使用 RepositoryRestConfiguration.getProjectionConfiguration().addProjection(…) 手动注册一个投影。spring-doc.cadn.net.cn

无论哪种情况,投影接口都必须带有 @Projection 注解。spring-doc.cadn.net.cn

查找现有投影

Spring Data REST 会公开 应用级 Profile 语义(ALPS) 文档,这是一种微型元数据格式。要查看 ALPS 元数据,请访问根资源中暴露的 profile 链接。如果你导航到 Person 资源的 ALPS 文档(即 /alps/persons),就能找到关于 Person 资源的许多详细信息。投影(Projections)会被列出,同时还会包含有关 GET REST 转换的详细信息,其格式类似于以下示例中的代码块:spring-doc.cadn.net.cn

{ …
  "id" : "get-person", (1)
  "name" : "person",
  "type" : "SAFE",
  "rt" : "#person-representation",
  "descriptors" : [ {
    "name" : "projection", (2)
    "doc" : {
      "value" : "The projection that shall be applied when rendering the response. Acceptable values available in nested descriptors.",
      "format" : "TEXT"
    },
    "type" : "SEMANTIC",
    "descriptors" : [ {
      "name" : "noAddresses", (3)
      "type" : "SEMANTIC",
      "descriptors" : [ {
        "name" : "firstName", (4)
        "type" : "SEMANTIC"
      }, {
        "name" : "lastName", (4)
        "type" : "SEMANTIC"
      } ]
    } ]
  } ]
},
…
1 ALPS 文档的这一部分展示了关于 GETPerson 资源的详细信息。
2 此部分包含 projection 选项。
3 此部分包含 noAddresses 投影。
4 此投影实际提供的属性包括 firstNamelastName

如果投影定义满足以下条件,它们将被自动识别并提供给客户端使用:spring-doc.cadn.net.cn

  • 使用 @Projection 注解标记,并位于领域类型所在的相同包(或其子包)中,或者spring-doc.cadn.net.cn

  • 通过使用 RepositoryRestConfiguration.getProjectionConfiguration().addProjection(…) 手动注册。spring-doc.cadn.net.cn

引入隐藏数据

到目前为止,在本节中我们已经介绍了如何使用投影(projections)来减少呈现给用户的信息。投影还可以引入通常不可见的数据。例如,Spring Data REST 会忽略带有 @JsonIgnore 注解的字段或 getter 方法。请考虑以下领域对象:spring-doc.cadn.net.cn

@Entity
public class User {

	@Id @GeneratedValue
	private Long id;
	private String name;

	@JsonIgnore private String password; (1)

	private String[] roles;
  …
1 Jackson 的 @JsonIgnore 注解用于防止 password 字段被序列化为 JSON。

前面示例中的 User 类可用于存储用户信息以及与 Spring Security 集成。如果您创建了一个 UserRepositorypassword 字段通常会被导出,这是不安全的。在前面的示例中,我们通过在 @JsonIgnore 字段上应用 Jackson 的 password 注解来防止这种情况发生。spring-doc.cadn.net.cn

如果字段对应的 getter 方法上标注了 @JsonIgnore,Jackson 也不会将该字段序列化为 JSON。

然而,投影(projections)引入了仍然可以提供该字段的能力。可以创建如下所示的投影:spring-doc.cadn.net.cn

@Projection(name = "passwords", types = { User.class })
interface PasswordProjection {

  String getPassword();
}

如果创建并使用了这样的投影,就会绕过放置在 @JsonIgnore 上的 User.password 指令。spring-doc.cadn.net.cn

这个例子看起来可能有点牵强,但在拥有更丰富的领域模型和大量投影的情况下,确实有可能意外泄露此类细节。由于 Spring Data REST 无法判断这些数据的敏感性,因此避免此类情况的责任在于你自己。

投影也可以生成虚拟数据。假设你有以下实体定义:spring-doc.cadn.net.cn

@Entity
public class Person {

  ...
  private String firstName;
  private String lastName;

  ...
}

你可以创建一个投影,将前面示例中的两个数据字段组合在一起,如下所示:spring-doc.cadn.net.cn

@Projection(name = "virtual", types = { Person.class })
public interface VirtualProjection {

  @Value("#{target.firstName} #{target.lastName}") (1)
  String getFullName();

}
1 Spring 的 @Value 注解允许你插入一个 SpEL 表达式,该表达式获取目标对象,并将其 firstNamelastName 属性拼接起来,以渲染一个只读的 fullName

摘要

摘录(excerpt)是一种自动应用于资源集合的投影。例如,您可以按如下方式修改 PersonRepositoryspring-doc.cadn.net.cn

@RepositoryRestResource(excerptProjection = NoAddresses.class)
interface PersonRepository extends CrudRepository<Person, Long> {}

前面的示例指示 Spring Data REST 在将 NoAddresses 资源嵌入到集合或关联资源中时,使用 Person 投影。spring-doc.cadn.net.cn

摘录投影(excerpt projections)不会自动应用于单个资源。它们必须显式地应用。摘录投影旨在为集合数据提供默认的预览视图,但在获取单个资源时则不适用。关于此问题的讨论,请参见为什么 Spring Data REST 的单项资源不会自动应用摘录投影?

除了更改默认渲染方式外,摘要还有其他渲染选项,如下一节所示。spring-doc.cadn.net.cn

提取常用访问数据

在 REST 服务中,一种常见的情况是组合领域对象。例如,Person 存储在一个表中,而其关联的 Address 则存储在另一个表中。默认情况下,Spring Data REST 会将该人员的 address 以 URI 的形式提供,客户端必须通过该 URI 进行导航才能获取地址信息。但如果消费者通常都需要获取这一额外的数据,那么可以使用摘要投影(excerpt projection)将该额外数据内联返回,从而省去一次额外的 GET 请求。为此,您可以定义另一个摘要投影,如下所示:spring-doc.cadn.net.cn

@Projection(name = "inlineAddress", types = { Person.class }) (1)
interface InlineAddress {

  String getFirstName();

  String getLastName();

  Address getAddress(); (2)
}
1 此投影已被命名为 inlineAddress
2 此投影添加了 getAddress 方法,该方法返回 Address 字段。当在投影内部使用时,它会使得相关信息被内联包含。

您可以将其插入到 PersonRepository 的定义中,如下所示:spring-doc.cadn.net.cn

@RepositoryRestResource(excerptProjection = InlineAddress.class)
interface PersonRepository extends CrudRepository<Person, Long> {}

这样做会使 HAL 文档呈现如下形式:spring-doc.cadn.net.cn

{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "address" : { (1)
    "street": "Bag End",
    "state": "The Shire",
    "country": "Middle Earth"
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/persons/1"
    },
    "address" : { (2)
      "href" : "http://localhost:8080/persons/1/address"
    }
  }
}
1 address 数据直接以内联方式包含,因此您无需导航即可获取它。
2 Address 资源的链接仍然被提供,因此依然可以导航至其自身的资源。

请注意,前面的示例是本章 earlier 所示多个示例的混合。您可能需要回过头去阅读这些示例,以了解如何逐步演进到最终的示例。spring-doc.cadn.net.cn

为仓库配置 @RepositoryRestResource(excerptProjection=…​) 会改变默认行为。如果您已经发布了服务,这可能会对服务的使用者造成破坏性变更。