跳到主要内容

投影与摘录

DeepSeek V3 中英对照 Projections and Excerpts

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

投影

考虑以下领域模型:

@Entity
public class Person {

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

@OneToOne
private Address address;

}
java

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

  • id 是主键。

  • firstNamelastName 是数据属性。

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

现在假设我们创建了一个对应的仓库,如下所示:

interface PersonRepository extends CrudRepository<Person, Long> {}
java

默认情况下,Spring Data REST 会导出这个领域对象,包括其所有属性。firstNamelastName 会被导出为普通的 data 对象。对于 address 属性,有两种处理方式。其中一种方式是为 Address 对象定义一个 repository,如下所示:

interface AddressRepository extends CrudRepository<Address, Long> {}
java

在这种情况下,Person 资源会将 address 属性渲染为其对应的 Address 资源的 URI。如果我们在系统中查找 “Frodo”,我们可能会看到如下的 HAL 文档:

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

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

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

但如果你完全不需要 address 的详细信息呢?默认情况下,Spring Data REST 会导出所有的属性(除了 id)。你可以通过定义一个或多个投影,为你的 REST 服务的消费者提供另一种选择。以下示例展示了一个不包含地址的投影:

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

String getFirstName(); 3

String getLastName(); 4
}
java
  • @Projection 注解将此标记为一个投影。name 属性提供了投影的名称,我们稍后会详细讨论。types 属性指定此投影仅适用于 Person 对象。

  • 这是一个 Java 接口,使其具有声明性。

  • 它导出 firstName

  • 它导出 lastName

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

{
"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"
}
}
}
javascript
  • 此资源有一个新选项:{?projection}

  • self URI 是一个 URI 模板。

要查看资源的投影,请访问 [localhost:8080/persons/1?projection=noAddresses](http://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 资源的许多详细信息。投影(Projections)以及关于 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"
} ]
} ]
} ]
},

javascript
  • ALPS 文档的这一部分展示了关于 GETPerson 资源的详细信息。

  • 这一部分包含了 projection 选项。

  • 这一部分包含了 noAddresses 投影。

  • 此投影提供的实际属性包括 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;

java
  • Jackson 的 @JsonIgnore 用于防止 password 字段被序列化为 JSON。

在前面的示例中,User 类可用于存储用户信息,并与 Spring Security 集成。如果你创建了一个 UserRepository,通常情况下 password 字段会被导出,这是不安全的。在前面的示例中,我们通过在 password 字段上应用 Jackson 的 @JsonIgnore 来防止这种情况发生。

备注

如果字段对应的 getter 函数上有 @JsonIgnore 注解,Jackson 也不会将该字段序列化为 JSON。

然而,投影引入了一种仍然可以服务该字段的能力。可以创建以下投影:

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

String getPassword();
}
java

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

important

这个例子可能看起来有点人为,但在一个更丰富的领域模型和多个投影的情况下,意外泄露这些细节是有可能的。由于 Spring Data REST 无法辨别此类数据的敏感性,因此需要你自己避免这种情况的发生。

投影也可以生成虚拟数据。假设你有以下实体定义:

@Entity
public class Person {

...
private String firstName;
private String lastName;

...
}
java

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

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

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

}
java
  • Spring 的 @Value 注解允许你插入一个 SpEL 表达式,该表达式会获取目标对象并将其 firstNamelastName 属性拼接在一起,以生成一个只读的 fullName

摘录

摘录(excerpt)是一个自动应用于资源集合的投影。例如,你可以按如下方式修改 PersonRepository

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

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

备注

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

除了改变默认渲染方式外,摘要还具有其他渲染选项,如下一节所示。

摘录常用访问数据

在使用 REST 服务时,组合领域对象是一个常见的情况。例如,Person 存储在一个表中,而与之相关的 Address 存储在另一个表中。默认情况下,Spring Data REST 会将 personaddress 作为一个 URI 提供给客户端,客户端需要进一步导航才能获取。但如果消费者通常会总是获取这额外的数据,那么可以通过摘要投影(excerpt projection)将这些额外的数据内联到响应中,从而节省一次额外的 GET 请求。为此,您可以定义另一个摘要投影,如下所示:

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

String getFirstName();

String getLastName();

Address getAddress(); 2
}
java
  • 该投影已被命名为 inlineAddress

  • 该投影添加了 getAddress,它返回 Address 字段。当在投影内部使用时,它会导致信息被内联包含。

你可以将其插入到 PersonRepository 定义中,如下所示:

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

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

{
"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"
}
}
}
javascript
  • address 数据直接内联包含,因此您无需导航即可获取它。

  • 仍然提供了指向 Address 资源的链接,使其仍然可以导航到其自己的资源。

请注意,前面的示例是本章之前展示的示例的混合体。你可能需要回过头阅读这些内容,以便理解最终示例的演变过程。

注意

为仓库配置 @RepositoryRestResource(excerptProjection=…​) 会改变默认行为。如果你已经发布了服务,这可能会导致服务的消费者出现破坏性变更。