投影和摘录

Spring Data REST 提供了您导出域模型的默认视图。但是,有时您可能需要出于各种原因更改该模型的视图。本节介绍如何定义投影和摘录以提供资源的简化和缩减视图。

投影

考虑以下域模型

@Entity
public class Person {

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

  @OneToOne
  private Address address;
  …
}

前面的示例中的 Person 对象具有多个属性

  • id 是主键。

  • firstNamelastName 是数据属性。

  • address 是指向另一个域对象的链接。

现在假设我们创建一个相应的存储库,如下所示

interface PersonRepository extends CrudRepository<Person, Long> {}

默认情况下,Spring Data REST 导出此域对象,包括其所有属性。firstNamelastName 作为它们本身的纯数据对象导出。关于 address 属性有两个选项。一种选择是为 Address 对象定义一个存储库,如下所示

interface AddressRepository extends CrudRepository<Address, Long> {}

在这种情况下,Person 资源将 address 属性呈现为指向其相应 Address 资源的 URI。如果我们要在系统中查找“Frodo”,我们可以期望看到类似这样的 HAL 文档

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

还有另一种方法。如果 Address 域对象没有自己的存储库定义,Spring Data REST 会将数据字段包含在 Person 资源中,如下例所示

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

但是,如果您根本不想要 address 详细信息呢?同样,默认情况下,Spring Data REST 会导出其所有属性(id 除外)。您可以通过定义一个或多个投影来为 REST 服务的使用者提供替代方案。以下示例显示了一个不包含地址的投影

@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 的默认视图与之前的表示略有不同,如下例所示

{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "_links" : {
    "self" : {
      "href" : "https://127.0.0.1:8080/persons/1{?projection}", (1)
      "templated" : true (2)
    },
    "address" : {
      "href" : "https://127.0.0.1:8080/persons/1/address"
    }
  }
}
1 此资源有一个新选项:{?projection}
2 self URI 是一个 URI 模板。

要查看资源的投影,请查找 localhost:8080/persons/1?projection=noAddresses

提供给 projection 查询参数的值与 @Projection(name = "noAddress") 中指定的值相同。它与投影接口的名称无关。

您可以有多个投影。

请参阅 投影 以查看示例项目。我们鼓励您尝试一下。

Spring Data REST 如下查找投影定义

  • 在您的实体定义(或其子包之一)的同一包中找到的任何 @Projection 接口都会被注册。

  • 您可以使用 RepositoryRestConfiguration.getProjectionConfiguration().addProjection(…) 手动注册投影。

无论哪种情况,投影接口都必须具有 @Projection 注解。

查找现有投影

Spring Data REST 公开 应用程序级配置文件语义 (ALPS) 文档,这是一种微型元数据格式。要查看 ALPS 元数据,请按照根资源公开的 profile 链接进行操作。如果您导航到 Person 资源的 ALPS 文档(即 /alps/persons),您可以在其中找到有关 Person 资源的许多详细信息。投影与有关 GET REST 转换的详细信息一起列出,类似于以下示例中的块

{ …
  "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

如果投影定义

  • 使用 @Projection 注解标记并位于域类型的同一包(或子包)中,或者

  • 使用 RepositoryRestConfiguration.getProjectionConfiguration().addProjection(…) 手动注册,则它们会被提取并提供给客户端。

引入隐藏数据

到目前为止,在本节中,我们已经介绍了如何使用投影来减少呈现给用户的信息。投影还可以引入通常不可见的数据。例如,Spring Data REST 会忽略使用 @JsonIgnore 注解标记的字段或 getter。请考虑以下域对象

@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 字段通常会被导出,这不是好事。在前面的示例中,我们通过在 password 字段上应用 Jackson 的 @JsonIgnore 来阻止这种情况发生。

如果 @JsonIgnore 位于字段的相应 getter 函数上,Jackson 也不会将该字段序列化为 JSON。

然而,投影引入了仍然可以服务于此字段的能力。可以创建以下投影

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

  String getPassword();
}

如果创建并使用这种投影,它会绕过放在User.password上的@JsonIgnore指令。

这个例子可能看起来有点牵强,但有可能,在一个更丰富的领域模型和许多投影中,会意外地泄露这些细节。由于 Spring Data REST 无法识别此类数据的敏感性,因此您有责任避免此类情况。

投影还可以生成虚拟数据。假设您有以下实体定义

@Entity
public class Person {

  ...
  private String firstName;
  private String lastName;

  ...
}

您可以创建一个投影,将前面示例中的两个数据字段组合在一起,如下所示

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

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

}
1 Spring 的@Value注解允许您插入一个 SpEL 表达式,该表达式接受目标对象并将它的firstNamelastName属性拼接在一起,以呈现一个只读的fullName

摘录

摘录是一个自动应用于资源集合的投影。例如,您可以更改PersonRepository如下

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

前面的示例指示 Spring Data REST 在将Person资源嵌入到集合或相关资源中时使用NoAddresses投影。

摘录投影不会自动应用于单个资源。它们必须被有意地应用。摘录投影旨在提供集合数据的默认预览,而不是在获取单个资源时。请参阅为什么摘录投影不会自动应用于 Spring Data REST 项目资源?以了解有关此主题的讨论。

除了更改默认渲染之外,摘录还有其他渲染选项,如下一节所示。

摘录常用数据

在 REST 服务中,当您组合域对象时,会遇到一种常见情况。例如,Person存储在一个表中,而他们相关的Address存储在另一个表中。默认情况下,Spring Data REST 会将人的address作为客户端必须导航的 URI 提供。但是,如果消费者经常需要获取这部分额外数据,摘录投影可以将这部分额外数据内联,从而节省您一个额外的GET请求。为此,您可以定义另一个摘录投影,如下所示

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

  String getFirstName();

  String getLastName();

  Address getAddress(); (2)
}
1 此投影被命名为inlineAddress
2 此投影添加了getAddress,它返回Address字段。当在投影中使用时,它会导致信息被内联包含。

您可以将其插入PersonRepository定义中,如下所示

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

这样做会导致 HAL 文档如下所示

{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "address" : { (1)
    "street": "Bag End",
    "state": "The Shire",
    "country": "Middle Earth"
  },
  "_links" : {
    "self" : {
      "href" : "https://127.0.0.1:8080/persons/1"
    },
    "address" : { (2)
      "href" : "https://127.0.0.1:8080/persons/1/address"
    }
  }
}
1 address数据直接内联包含,因此您不必导航即可获取它。
2 仍然提供了指向Address资源的链接,因此仍然可以导航到其自身的资源。

请注意,前面的示例是本章前面显示的示例的混合。您可能需要重新阅读它们以跟踪到最终示例的进展。

为存储库配置@RepositoryRestResource(excerptProjection=…​)会更改默认行为。如果您已经发布了版本,这可能会对您的服务的使用者造成重大更改。