投影与摘录
Spring Data REST 提供了你所导出的领域模型的默认视图。然而,有时你可能由于各种原因需要改变该模型的视图。本节将介绍如何定义投影和摘录,以提供资源的简化和缩减视图。
投影
考虑以下领域模型:
@Entity
public class Person {
@Id @GeneratedValue
private Long id;
private String firstName, lastName;
@OneToOne
private Address address;
…
}
在前面的示例中,Person
对象具有多个属性:
-
id
是主键。 -
firstName
和lastName
是数据属性。 -
address
是链接到另一个域对象的链接。
现在假设我们创建了一个对应的仓库,如下所示:
interface PersonRepository extends CrudRepository<Person, Long> {}
默认情况下,Spring Data REST 会导出这个领域对象,包括其所有属性。firstName
和 lastName
会被导出为普通的 data 对象。对于 address
属性,有两种处理方式。其中一种方式是为 Address
对象定义一个 repository,如下所示:
interface AddressRepository extends CrudRepository<Address, Long> {}
在这种情况下,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"
}
}
}
还有另一种方法。如果 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"
}
}
}
但如果你完全不需要 address
的详细信息呢?默认情况下,Spring Data REST 会导出所有的属性(除了 id
)。你可以通过定义一个或多个投影,为你的 REST 服务的消费者提供另一种选择。以下示例展示了一个不包含地址的投影:
@Projection(name = "noAddresses", types = { Person.class }) 1
interface NoAddresses { 2
String getFirstName(); 3
String getLastName(); 4
}
@Projection
注解将此标记为一个投影。name
属性提供了投影的名称,我们稍后会详细讨论。types
属性指定此投影仅适用于Person
对象。这是一个 Java 接口,使其具有声明性。
它导出
firstName
。它导出
lastName
。
NoAddresses
投影仅包含 firstName
和 lastName
的 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"
}
}
}
此资源有一个新选项:
{?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"
} ]
} ]
} ]
},
…
ALPS 文档的这一部分展示了关于
GET
和Person
资源的详细信息。这一部分包含了
projection
选项。这一部分包含了
noAddresses
投影。此投影提供的实际属性包括
firstName
和lastName
。
如果投影定义满足以下条件,它们将被识别并可供客户端使用:
-
使用
@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;
…
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();
}
如果创建并使用了这样的投影,它将绕过放置在 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();
}
Spring 的
@Value
注解允许你插入一个 SpEL 表达式,该表达式会获取目标对象并将其firstName
和lastName
属性拼接在一起,以生成一个只读的fullName
。
摘录
摘录(excerpt)是一个自动应用于资源集合的投影。例如,你可以按如下方式修改 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 会将 person
的 address
作为一个 URI 提供给客户端,客户端需要进一步导航才能获取。但如果消费者通常会总是获取这额外的数据,那么可以通过摘要投影(excerpt projection)将这些额外的数据内联到响应中,从而节省一次额外的 GET
请求。为此,您可以定义另一个摘要投影,如下所示:
@Projection(name = "inlineAddress", types = { Person.class }) 1
interface InlineAddress {
String getFirstName();
String getLastName();
Address getAddress(); 2
}
该投影已被命名为
inlineAddress
。该投影添加了
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" : "http://localhost:8080/persons/1"
},
"address" : { 2
"href" : "http://localhost:8080/persons/1/address"
}
}
}
address
数据直接内联包含,因此您无需导航即可获取它。仍然提供了指向
Address
资源的链接,使其仍然可以导航到其自己的资源。
请注意,前面的示例是本章之前展示的示例的混合体。你可能需要回过头阅读这些内容,以便理解最终示例的演变过程。
为仓库配置 @RepositoryRestResource(excerptProjection=…)
会改变默认行为。如果你已经发布了服务,这可能会导致服务的消费者出现破坏性变更。