4. 媒体类型

4.1. HAL – 超文本应用语言

JSON 超文本应用语言(简称 HAL)是在不讨论特定 Web 技术栈时,最简单且被广泛采用的超媒体媒体类型之一。spring-doc.cadn.net.cn

这是 Spring HATEOAS 采用的首个基于规范的媒体类型。spring-doc.cadn.net.cn

4.1.1. 构建 HAL 表示模型

自 Spring HATEOAS 1.1 起,我们提供了一个专用的HalModelBuilder,允许通过符合 HAL 惯例的 API 创建RepresentationModel实例。 以下是其基本假设:spring-doc.cadn.net.cn

  1. HAL 表示可以由任意对象(实体)支持,该对象用于构建表示中包含的领域字段。spring-doc.cadn.net.cn

  2. 该表示可以通过多种嵌入文档进行丰富,这些文档可以是任意对象,也可以是 HAL 表示本身(即包含嵌套的嵌入资源和链接)。spring-doc.cadn.net.cn

  3. 某些 HAL 特定的模式(例如预览)可以直接在 API 中使用,使得设置表示的代码读起来就像您在遵循这些惯用法描述 HAL 表示一样。spring-doc.cadn.net.cn

以下是所用 API 的示例:spring-doc.cadn.net.cn

// An order
var order = new Order(…); (1)

// The customer who placed the order
var customer = customer.findById(order.getCustomerId());

var customerLink = Link.of("/orders/{id}/customer") (2)
  .expand(order.getId())
  .withRel("customer");

var additional = …

var model = HalModelBuilder.halModelOf(order)
  .preview(new CustomerSummary(customer)) (3)
  .forLink(customerLink) (4)
  .embed(additional) (5)
  .link(Link.of(…, IanaLinkRelations.SELF));
  .build();
1 我们设置了一些领域类型。在这种情况下,订单与下订单的客户之间存在关联关系。
2 我们准备了一个指向将公开客户详细信息的资源的链接。
3 我们通过提供应在 _embeddable 子句中渲染的有效负载来开始构建预览。
4 我们通过提供目标链接来结束此预览。它会被透明地添加到_links对象中,并且其链接关系将用作上一步中所提供对象的键。
5 其他对象可以被添加以显示在 _embedded 之下。 它们所列出的键源自对象的关系设置。这些设置可通过 @Relation 或专用的 LinkRelationProvider 进行自定义(详见 使用 LinkRelationProvider API)。
{
  "_links" : {
    "self" : { "href" : "…" }, (1)
    "customer" : { "href" : "/orders/4711/customer" } (2)
  },
  "_embedded" : {
    "customer" : { … }, (3)
    "additional" : { … } (4)
  }
}
1 self 链接已明确提供。
2 customer 链接通过 ….preview(…).forLink(…) 透明地添加。
3 提供的预览对象。
4 通过显式 ….embed(…) 添加的额外元素。

在 HAL 中,_embedded 也用于表示顶层集合。 它们通常归类于从对象类型派生的链接关系下。 例如,订单列表在 HAL 中看起来如下:spring-doc.cadn.net.cn

{
  "_embedded" : {
    "order : [
      … (1)
    ]
  }
}
1 个人订单文档请放在此处。

创建这样的表示就像这样简单:spring-doc.cadn.net.cn

Collection<Order> orders = …;

HalModelBuilder.emptyHalDocument()
  .embed(orders);

也就是说,如果订单为空,则无法推导出应出现在 _embedded 内的链接关系,因此当集合为空时,文档将保持为空。spring-doc.cadn.net.cn

如果您希望显式地传达一个空集合,可以将一个类型传递给接受 Collection….embed(…) 方法重载。 如果传递给该方法的集合为空,这将导致渲染一个字段,其链接关系源自给定的类型。spring-doc.cadn.net.cn

HalModelBuilder.emptyHalModel()
  .embed(Collections.emptyList(), Order.class);
  // or
  .embed(Collections.emptyList(), LinkRelation.of("orders"));

将创建以下更明确的表示形式。spring-doc.cadn.net.cn

{
  "_embedded" : {
    "orders" : []
  }
}

4.1.2. 配置链接渲染

在 HAL 中,_links 条目是一个 JSON 对象。属性名称是 链接关系,每个值要么是 一个链接对象,要么是链接对象数组spring-doc.cadn.net.cn

对于具有两个或更多链接的给定链接关系,规范对表示方式有明确规定:spring-doc.cadn.net.cn

示例 24:包含与一个关系关联的两个链接的 HAL 文档
{
  "_links": {
    "item": [
      { "href": "https://myhost/cart/42" },
      { "href": "https://myhost/inventory/12" }
    ]
  },
  "customer": "Dave Matthews"
}

但是,如果给定关系只有一个链接,则规范是模糊的。您可以将其渲染为单个对象,也可以渲染为单元素数组。spring-doc.cadn.net.cn

默认情况下,Spring HATEOAS 使用最简洁的方式,并将单链接关系渲染为如下形式:spring-doc.cadn.net.cn

示例 25. 渲染为对象的单个链接的 HAL 文档
{
  "_links": {
    "item": { "href": "https://myhost/inventory/12" }
  },
  "customer": "Dave Matthews"
}

有些用户在使用 HAL 时,不希望在使用数组和对象之间切换。他们更倾向于这种渲染方式:spring-doc.cadn.net.cn

示例 26. 渲染为数组的单个链接的 HAL
{
  "_links": {
    "item": [{ "href": "https://myhost/inventory/12" }]
  },
  "customer": "Dave Matthews"
}

如果您希望自定义此策略,只需将一个 HalConfiguration Bean 注入到您的应用程序配置中即可。 有多种选择。spring-doc.cadn.net.cn

示例 27. 全局 HAL 单链接渲染策略
@Bean
public HalConfiguration globalPolicy() {
  return new HalConfiguration() //
      .withRenderSingleLinks(RenderSingleLinks.AS_ARRAY); (1)
}
1 通过将所有单链接关系渲染为数组,来覆盖 Spring HATEOAS 的默认行为。

如果您希望仅覆盖某些特定的链接关系,可以像这样创建一个 HalConfiguration bean:spring-doc.cadn.net.cn

示例 28. 基于链接关系的 HAL 单链接渲染策略
@Bean
public HalConfiguration linkRelationBasedPolicy() {
  return new HalConfiguration() //
      .withRenderSingleLinksFor( //
          IanaLinkRelations.ITEM, RenderSingleLinks.AS_ARRAY) (1)
      .withRenderSingleLinksFor( //
          LinkRelation.of("prev"), RenderSingleLinks.AS_SINGLE); (2)
}
1 始终将 item 链接关系渲染为数组。
2 当只有一个链接时,将 prev 个链接关系渲染为一个对象。

如果以上两种都不符合您的需求,您可以使用 Ant 风格的路径模式:spring-doc.cadn.net.cn

示例 29. 基于模式的 HAL 单链接渲染策略
@Bean
public HalConfiguration patternBasedPolicy() {
  return new HalConfiguration() //
      .withRenderSingleLinksFor( //
          "http*", RenderSingleLinks.AS_ARRAY); (1)
}
1 将所有以 http 开头的链接关系渲染为数组。
基于模式的方法使用 Spring 的 AntPathMatcher

所有这些 HalConfiguration 个限制条件可以组合成一项全面的策略。请务必对您的 API 进行广泛测试,以避免意外情况。spring-doc.cadn.net.cn

4.1.3. 链接标题国际化

HAL 为其链接对象定义了一个 title 属性。 这些标题可以通过使用 Spring 的资源包抽象机制以及一个名为 rest-messages 的资源包进行填充,以便客户端能够直接在它们的用户界面中使用它们。 该资源包将自动设置,并在 HAL 链接序列化过程中使用。spring-doc.cadn.net.cn

要为链接定义标题,请使用密钥模板 _links.$relationName.title,如下所示:spring-doc.cadn.net.cn

示例 30. 一个示例 rest-messages.properties
_links.cancel.title=Cancel order
_links.payment.title=Proceed to checkout

这将生成以下 HAL 表示形式:spring-doc.cadn.net.cn

示例 31. 定义了链接标题的 HAL 文档示例
{
  "_links" : {
    "cancel" : {
      "href" : "…"
      "title" : "Cancel order"
    },
    "payment" : {
      "href" : "…"
      "title" : "Proceed to checkout"
    }
  }
}

4.1.4. 使用CurieProviderAPI

Web Linking RFC 描述了已注册和扩展的链接关系类型。已注册的关系是与 IANA 链接关系类型注册表 注册的众所周知的字符串。不希望注册关系类型的应用程序可以使用扩展 rel URI。每个 URI 都唯一标识一种关系类型。rel URI 可以序列化为紧凑 URI 或 Curie。例如,如果 ex 被定义为 example.com/rels/{rel},那么 ex:persons 的 Curie 就代表链接关系类型 example.com/rels/persons。如果使用 Curie,则基础 URI 必须存在于响应作用域中。spring-doc.cadn.net.cn

默认 RelProvider 创建的 rel 值是扩展关系类型,因此必须是 URI,这可能会带来大量开销。CurieProvider API 解决了这一问题:它允许您将基础 URI 定义为 URI 模板,并定义一个代表该基础 URI 的前缀。如果存在 CurieProvider,则 RelProvider 会在所有 rel 值前添加 CURIE 前缀。此外,还会自动向 HAL 资源添加一个 curies 链接。spring-doc.cadn.net.cn

以下配置定义了一个默认的 curie 提供者:spring-doc.cadn.net.cn

@Configuration
@EnableWebMvc
@EnableHypermediaSupport(type= {HypermediaType.HAL})
public class Config {

  @Bean
  public CurieProvider curieProvider() {
    return new DefaultCurieProvider("ex", new UriTemplate("https://www.example.com/rels/{rel}"));
  }
}

请注意,现在所有未向 IANA 注册的 rel 值前都会自动添加 ex: 前缀,如 ex:orders 所示。客户端可以使用 curies 链接将 CURIE 解析为其完整形式。 以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

{
  "_links": {
    "self": {
      "href": "https://myhost/person/1"
    },
    "curies": {
      "name": "ex",
      "href": "https://example.com/rels/{rel}",
      "templated": true
    },
    "ex:orders": {
      "href": "https://myhost/person/1/orders"
    }
  },
  "firstname": "Dave",
  "lastname": "Matthews"
}

由于 CurieProvider API 的目的是允许自动创建 curie,因此每个应用程序作用域中只能定义一个 CurieProvider bean。spring-doc.cadn.net.cn

4.2. HAL-FORMS

HAL-FORMS 旨在为 HAL 媒体类型 添加运行时表单支持。spring-doc.cadn.net.cn

HAL-FORMS“看起来像 HALspring-doc.cadn.net.cn

— 迈克·阿蒙森
HAL-FORMS 规范

要启用此媒体类型,请在您的代码中添加以下配置:spring-doc.cadn.net.cn

示例 32. 启用 HAL-FORMS 的应用程序
@Configuration
@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)
public class HalFormsApplication {

}

每当客户端提供带有 application/prs.hal-forms+jsonAccept 标头时,您可以期待类似以下内容:spring-doc.cadn.net.cn

示例 33. HAL-FORMS 示例文档
{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "role" : "ring bearer",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/employees/1"
    }
  },
  "_templates" : {
    "default" : {
      "method" : "put",
      "properties" : [ {
        "name" : "firstName",
        "required" : true
      }, {
        "name" : "lastName",
        "required" : true
      }, {
        "name" : "role",
        "required" : true
      } ]
    },
    "partiallyUpdateEmployee" : {
      "method" : "patch",
      "properties" : [ {
        "name" : "firstName",
        "required" : false
      }, {
        "name" : "lastName",
        "required" : false
      }, {
        "name" : "role",
        "required" : false
      } ]
    }
  }
}

查看 HAL-FORMS 规范 以了解 _templates 属性的详细信息。 阅读关于 Affordances API 的内容,以便使用此额外元数据增强您的控制器。spring-doc.cadn.net.cn

对于单项(EntityModel)和聚合根集合(CollectionModel),Spring HATEOAS 的渲染方式与 HAL 文档 完全相同。spring-doc.cadn.net.cn

4.2.1. 定义 HAL-FORMS 元数据

HAL-FORMS 允许描述每个表单字段的标准。 Spring HATEOAS 允许通过塑造输入和输出类型的模型类型并在其上使用注解来自定义这些标准。spring-doc.cadn.net.cn

每个模板都将获得以下定义的属性:spring-doc.cadn.net.cn

表 1. 模板属性
属性 描述

contentTypespring-doc.cadn.net.cn

服务器预期接收的媒体类型。仅当指向的控制器方法暴露了 @RequestMapping(consumes = "…") 属性,或在设置功能时显式定义了媒体类型时,才会包含此项。spring-doc.cadn.net.cn

methodspring-doc.cadn.net.cn

提交模板时要使用的 HTTP 方法。spring-doc.cadn.net.cn

targetspring-doc.cadn.net.cn

提交表单的目标 URI。仅当功能目标与其声明所在的链接不同时才会渲染。spring-doc.cadn.net.cn

titlespring-doc.cadn.net.cn

显示模板时的人类可读标题。spring-doc.cadn.net.cn

propertiesspring-doc.cadn.net.cn

所有要通过表单提交的属性(见下文)。spring-doc.cadn.net.cn

每个属性都将获得以下定义的属性:spring-doc.cadn.net.cn

表 2. 属性特性
属性 描述

readOnlyspring-doc.cadn.net.cn

如果属性没有 setter 方法,则设置为 true。如果存在该方法,请在访问器或字段上显式使用 Jackson 的 @JsonProperty(Access.READ_ONLY)。默认不渲染,因此默认为 falsespring-doc.cadn.net.cn

regexspring-doc.cadn.net.cn

可以通过在字段或类型上使用 JSR-303 的 @Pattern 注解来自定义。如果是后者,该模式将用于声明为该特定类型的每个属性。默认情况下不渲染。spring-doc.cadn.net.cn

requiredspring-doc.cadn.net.cn

可以通过使用 JSR-303 的 @NotNull 进行自定义。默认情况下不渲染,因此默认为 false。使用 PATCH 作为方法的模板将自动把所有属性设置为非必填。spring-doc.cadn.net.cn

maxspring-doc.cadn.net.cn

该属性允许的最大值。派生自 JSR-303 的 @Size、Hibernate Validator 的 @Range,或 JSR-303 的 @Max@DecimalMax 注解。spring-doc.cadn.net.cn

maxLengthspring-doc.cadn.net.cn

属性允许的最大长度值。源自 Hibernate Validator 的 @Length 注解。spring-doc.cadn.net.cn

minspring-doc.cadn.net.cn

该属性允许的最小值。源自 JSR-303 的 @Size、Hibernate Validator 的 @Range,或 JSR-303 的 @Min@DecimalMin 注解。spring-doc.cadn.net.cn

minLengthspring-doc.cadn.net.cn

属性允许的最小长度值。源自 Hibernate Validator 的 @Length 注解。spring-doc.cadn.net.cn

optionsspring-doc.cadn.net.cn

提交表单时用于选择值的选项。详细信息请参阅 为属性定义 HAL-FORMS 选项spring-doc.cadn.net.cn

promptspring-doc.cadn.net.cn

渲染表单输入时使用的用户可读提示。详细信息,请参阅 属性提示spring-doc.cadn.net.cn

placeholderspring-doc.cadn.net.cn

一个用户可读的占位符,用于提供预期格式的示例。其定义方式遵循 属性提示,但使用后缀 _placeholderspring-doc.cadn.net.cn

typespring-doc.cadn.net.cn

HTML 输入类型源自显式的 @InputType 注解、JSR-303 验证注解或属性的类型。spring-doc.cadn.net.cn

对于无法手动添加注解的类型,您可以通过应用程序上下文中存在的 HalFormsConfiguration Bean 注册自定义模式。spring-doc.cadn.net.cn

@Configuration
class CustomConfiguration {

  @Bean
  HalFormsConfiguration halFormsConfiguration() {

    HalFormsConfiguration configuration = new HalFormsConfiguration();
    configuration.registerPatternFor(CreditCardNumber.class, "[0-9]{16}");
  }
}

此配置将导致类型为 CreditCardNumber 的表示模型属性的 HAL-FORMS 模板属性声明一个值为 [0-9]{16}regex 字段。spring-doc.cadn.net.cn

为属性定义 HAL-FORMS 选项

对于其值应匹配某个特定超集的属性,HAL-FORMS 在属性定义内定义了 options 子文档。 特定属性可用的选项可以通过 HalFormsConfigurationwithOptions(…) 来描述,它接受一个指向类型属性的指针和一个创建函数,用于将 PropertyMetadata 转换为 HalFormsOptions 实例。spring-doc.cadn.net.cn

@Configuration
class CustomConfiguration {

  @Bean
  HalFormsConfiguration halFormsConfiguration() {

    HalFormsConfiguration configuration = new HalFormsConfiguration();
    configuration.withOptions(Order.class, "shippingMethod" metadata ->
      HalFormsOptions.inline("FedEx", "DHL"));
  }
}

查看我们如何将选项值 FedExDHL 设置为供 Order.shippingMethod 属性选择的选项。 或者,HalFormsOptions.remote(…) 可以指向一个远程资源以动态提供值。 有关选项设置的更多约束,请参阅 规范HalFormsOptions 的 Javadoc。spring-doc.cadn.net.cn

4.2.2. 表单属性的国际化

HAL-FORMS 包含旨在供人类解读的属性,例如模板的标题或属性提示。 这些可以通过 Spring 的资源包支持以及 Spring HATEOAS 默认配置的 rest-messages 资源包进行定义和国际化。spring-doc.cadn.net.cn

模板标题

要定义模板标题,请使用以下模式:_templates.$affordanceName.title。请注意,在 HAL-FORMS 中,如果模板只有一个,则其名称为 default。 这意味着您通常需要使用供能(affordance)所描述的本地或完全限定输入类型名称来限定该键。spring-doc.cadn.net.cn

示例 34. 定义 HAL-FORMS 模板标题
_templates.default.title=Some title (1)
_templates.putEmployee.title=Create employee (2)
Employee._templates.default.title=Create employee (3)
com.acme.Employee._templates.default.title=Create employee (4)
1 使用 default 作为键的全局标题定义。
2 使用实际的功能名称作为键的全局标题定义。除非在创建功能时显式定义,否则默认指向创建功能时所关联的方法名称。
3 一个本地定义的标题,将应用于所有名为 Employee 的类型。
4 使用完全限定类型名的标题定义。
使用实际功能名称的键优先于默认键。
属性提示

属性提示也可以通过 Spring HATEOAS 自动配置的 rest-messages 资源包进行解析。 键可以全局、局部或完全限定地定义,并且需要将与实际属性键拼接的 ._promptspring-doc.cadn.net.cn

示例 35. 为 email 属性定义提示
firstName._prompt=Firstname (1)
Employee.firstName._prompt=Firstname (2)
com.acme.Employee.firstName._prompt=Firstname (3)
1 所有名为 firstName 的属性都将渲染为
2 在名为 Employee 的类型中,firstName 属性将提示为“名字”。
3 firstNamecom.acme.Employee 属性将被分配一个提示值“名字”。

4.2.3. 完整示例

让我们来看一些示例代码,这些代码结合了上述所有定义和自定义属性。 一个客户的 RepresentationModel 可能看起来像这样:spring-doc.cadn.net.cn

class CustomerRepresentation
  extends RepresentationModel<CustomerRepresentation> {

  String name;
  LocalDate birthdate; (1)
  @Pattern(regex = "[0-9]{16}") String ccn; (2)
  @Email String email; (3)
}
1 我们定义一个类型为 LocalDatebirthdate 属性。
2 我们期望 ccn 符合正则表达式。
3 我们将 email 定义为使用 JSR-303 @Email 注解的电子邮件。

请注意,此类型并非领域类型。 它被特意设计用于捕获各种可能无效的输入,以便能够一次性拒绝字段中潜在的错误值。spring-doc.cadn.net.cn

让我们继续看看控制器如何使用该模型:spring-doc.cadn.net.cn

@Controller
class CustomerController {

  @PostMapping("/customers")
  EntityModel<?> createCustomer(@RequestBody CustomerRepresentation payload) { (1)
    // …
  }

  @GetMapping("/customers")
  CollectionModel<?> getCustomers() {

    CollectionModel<?> model = …;

    CustomerController controller = methodOn(CustomerController.class);

    model.add(linkTo(controller.getCustomers()).withSelfRel() (2)
      .andAfford(controller.createCustomer(null)));

    return ResponseEntity.ok(model);
  }
}
1 如果向 /customers 发出 POST,则声明控制器方法以使用上述表示模型将请求体绑定到它。
2 /customersGET请求会准备一个模型,向其中添加一个self链接,并额外在该链接上声明一个指向映射到POST的控制器方法的功能(affordance)。 这将导致构建一个功能模型,该模型根据最终要渲染的媒体类型,会被转换为特定于该媒体类型的格式。

接下来,让我们添加一些额外的元数据,使表单对人类用户更加友好:spring-doc.cadn.net.cn

rest-messages.properties 中声明的附加属性。
CustomerRepresentation._template.createCustomer.title=Create customer (1)
CustomerRepresentation.ccn._prompt=Credit card number (2)
CustomerRepresentation.ccn._placeholder=1234123412341234 (2)
1 我们通过指向 createCustomer(…) 方法,为创建的模板定义了一个明确的标题。
2 我们为 CustomerRepresentation 模型的 ccn 属性明确提供了提示和占位符。

如果客户端现在使用值为 application/prs.hal-forms+jsonAccept 标头向 /customers 发出 GET 请求,则响应 HAL 文档将扩展为 HAL-FORMS 文档,以包含以下 _templates 定义:spring-doc.cadn.net.cn

{
  …,
  "_templates" : {
    "default" : { (1)
      "title" : "Create customer", (2)
      "method" : "post", (3)
      "properties" : [ {
        "name" : "name",
        "required" : true,
        "type" : "text" (4)
      } , {
        "name" : "birthdate",
        "required" : true,
        "type" : "date" (4)
      } , {
        "name" : "ccn",
        "prompt" : "Credit card number", (5)
        "placeholder" : "1234123412341234" (5)
        "required" : true,
        "regex" : "[0-9]{16}", (6)
        "type" : "text"
      } , {
        "name" : "email",
        "prompt" : "Email",
        "required" : true,
        "type" : "email" (7)
      } ]
    }
  }
}
1 一个名为 default 的模板已暴露。其名称为 default,因为它是唯一已定义的模板,且规范要求使用该名称。 如果附加了多个模板(通过声明额外的功能),它们将分别以其所指向的方法命名。
2 模板标题源自资源包中定义的值。请注意,根据请求中发送的 Accept-Language 头信息以及可用性的不同,可能会返回不同的值。
3 method 属性的值源自该功能所对应方法的映射。
4 type 属性的值 text 源自该属性的类型 String。 同样的规则也适用于 birthdate 属性,但其结果为 date
5 ccn 属性的提示和占位符也源自资源包。
6 @Pattern 声明用于 ccn 属性,并作为模板属性的 regex 属性暴露出来。
7 @Email 注解在 email 属性上已被转换为相应的 type 值。

HAL-FORMS 模板会被例如 HAL Explorer 等工具解析,从而自动根据这些描述渲染出 HTML 表单。spring-doc.cadn.net.cn

4.3. HTTP 问题详情

HTTP API 的问题详情是一种媒体类型,用于在 HTTP 响应中携带机器可读的错误详情,从而避免为 HTTP API 定义新的错误响应格式。spring-doc.cadn.net.cn

HTTP 问题详情定义了一组 JSON 属性,用于携带额外信息以向 HTTP 客户端描述错误详情。 有关这些属性的更多详细信息,请参阅 RFC 文档 的相关部分。spring-doc.cadn.net.cn

您可以在 Spring MVC 控制器中使用 Problem 媒体类型领域类型来创建这样的 JSON 响应:spring-doc.cadn.net.cn

使用 Spring HATEOAS 的 Problem 类型报告问题详情
@RestController
class PaymentController {

  @PutMapping
  ResponseEntity<?> issuePayment(@RequestBody PaymentRequest request) {

    PaymentResult result = payments.issuePayment(request.orderId, request.amount);

    if (result.isSuccess()) {
      return ResponseEntity.ok(result);
    }

    String title = messages.getMessage("payment.out-of-credit");
    String detail = messages.getMessage("payment.out-of-credit.details", //
        new Object[] { result.getBalance(), result.getCost() });

    Problem problem = Problem.create() (1)
        .withType(OUT_OF_CREDIT_URI) //
        .withTitle(title) (2)
        .withDetail(detail) //
        .withInstance(PAYMENT_ERROR_INSTANCE.expand(result.getPaymentId())) //
        .withProperties(map -> { (3)
          map.put("balance", result.getBalance());
          map.put("accounts", Arrays.asList( //
              ACCOUNTS.expand(result.getSourceAccountId()), //
              ACCOUNTS.expand(result.getTargetAccountId()) //
          ));
        });

    return ResponseEntity.status(HttpStatus.FORBIDDEN) //
        .body(problem);
  }
}
1 首先,使用公开的工厂方法创建 Problem 的实例。
2 您可以使用 Spring 的国际化功能(见上文)为媒体类型定义的默认属性(例如类型 URI、标题和详细信息)设置值。
3 自定义属性可以通过 Map 或显式对象添加(见下文)。

要使用专用对象来处理自定义属性,请声明一个类型,创建并填充该类型的实例,然后通过 Problem 或是在实例创建时通过 Problem.create(…) 将其传入 ….withProperties(…) 实例。spring-doc.cadn.net.cn

使用专用类型来捕获扩展的问题属性
class AccountDetails {
  int balance;
  List<URI> accounts;
}

problem.withProperties(result.getDetails());

// or

Problem.create(result.getDetails());

这将产生如下所示的响应:spring-doc.cadn.net.cn

HTTP 问题详情响应示例
{
  "type": "https://example.com/probs/out-of-credit",
  "title": "You do not have enough credit.",
  "detail": "Your current balance is 30, but that costs 50.",
  "instance": "/account/12345/msgs/abc",
  "balance": 30,
  "accounts": ["/account/12345",
               "/account/67890"]
}

4.4. Collection+JSON

Collection+JSON 是一项已向 IANA 注册的 JSON 规范,其媒体类型为 application/vnd.collection+jsonspring-doc.cadn.net.cn

Collection+JSON 是一种基于 JSON 的读写超媒体类型,旨在支持简单集合的管理和查询。spring-doc.cadn.net.cn

— 迈克·阿蒙森
Collection+JSON 规范

Collection+JSON 提供了一种统一的方式来表示单个资源项以及集合。 要启用此媒体类型,请在您的代码中添加以下配置:spring-doc.cadn.net.cn

示例 36. 启用 Collection+JSON 的应用程序
@Configuration
@EnableHypermediaSupport(type = HypermediaType.COLLECTION_JSON)
public class CollectionJsonApplication {

}

此配置将使您的应用程序响应具有 Accept 标头为 application/vnd.collection+json 的请求,如下所示。spring-doc.cadn.net.cn

规范中的以下示例展示了一个单项:spring-doc.cadn.net.cn

示例 37. Collection+JSON 单项示例
{
  "collection": {
    "version": "1.0",
    "href": "https://example.org/friends/", (1)
    "links": [   (2)
      {
        "rel": "feed",
        "href": "https://example.org/friends/rss"
      },
      {
        "rel": "queries",
        "href": "https://example.org/friends/?queries"
      },
      {
        "rel": "template",
        "href": "https://example.org/friends/?template"
      }
    ],
    "items": [  (3)
      {
        "href": "https://example.org/friends/jdoe",
        "data": [  (4)
          {
            "name": "fullname",
            "value": "J. Doe",
            "prompt": "Full Name"
          },
          {
            "name": "email",
            "value": "[email protected]",
            "prompt": "Email"
          }
        ],
        "links": [ (5)
          {
            "rel": "blog",
            "href": "https://examples.org/blogs/jdoe",
            "prompt": "Blog"
          },
          {
            "rel": "avatar",
            "href": "https://examples.org/images/jdoe",
            "prompt": "Avatar",
            "render": "image"
          }
        ]
      }
    ]
  }
}
1 self 链接存储在文档的 href 属性中。
2 文档的顶部 links 部分包含集合级别的链接(不包括 self 链接)。
3 items 部分包含一个数据集合。由于这是一个单项文档,因此它只有一个条目。
4 data 部分包含实际内容。它由属性组成。
5 该项目的独立 links

上一个片段摘自规范。当 Spring HATEOAS 渲染一个 EntityModel 时,它将:spring-doc.cadn.net.cn

在渲染资源集合时,文档几乎相同,只不过items JSON 数组内部将包含多个条目,每个条目对应一个资源。spring-doc.cadn.net.cn

Spring HATEOAS 更具体地将:spring-doc.cadn.net.cn

  • 将整个集合的 self 链接放入顶层的 href 属性中。spring-doc.cadn.net.cn

  • CollectionModel 个链接(减去 self)将被放入顶层的 links 中。spring-doc.cadn.net.cn

  • 每个项目级别的 href 将包含来自 CollectionModel.content 集合的每个条目对应的 self 链接。spring-doc.cadn.net.cn

  • 每个条目级别的 links 将包含从 CollectionModel.content 开始的每个条目的所有其他链接。spring-doc.cadn.net.cn

4.5. UBER - 交换表示的统一基础

UBER 是一个实验性的 JSON 规范spring-doc.cadn.net.cn

UBER 文档格式是一种极简的读写超媒体类型,旨在支持简单的状态传输和临时的基于超媒体的转换。spring-doc.cadn.net.cn

— 迈克·阿蒙森
UBER 规范

UBER 提供了一种统一的方式来表示单个资源项以及资源集合。要启用此媒体类型,请在您的代码中添加以下配置:spring-doc.cadn.net.cn

示例 38. 启用 UBER+JSON 的应用程序
@Configuration
@EnableHypermediaSupport(type = HypermediaType.UBER)
public class UberApplication {

}

此配置将使您的应用程序使用 Accept 标头 application/vnd.amundsen-uber+json 响应请求,如下所示:spring-doc.cadn.net.cn

示例 39. UBER 示例文档
{
  "uber" : {
    "version" : "1.0",
    "data" : [ {
      "rel" : [ "self" ],
      "url" : "/employees/1"
    }, {
      "name" : "employee",
      "data" : [ {
        "name" : "role",
        "value" : "ring bearer"
      }, {
        "name" : "name",
        "value" : "Frodo"
      } ]
    } ]
  }
}

此媒体类型仍在开发中,规范本身也是如此。如果您在使用过程中遇到问题,欢迎 提交工单spring-doc.cadn.net.cn

UBER 媒体类型与共享出行公司 Uber Technologies Inc. 没有任何关联。

4.6. ALPS - 应用级配置语义

ALPS 是一种媒体类型,用于提供关于其他资源的基于配置文件的元数据。spring-doc.cadn.net.cn

ALPS 文档可用作配置文件,用于解释具有与应用无关的媒体类型(如 HTML、HAL、Collection+JSON、Siren 等)的文档的应用语义。这提高了配置文件文档在不同媒体类型间的可复用性。spring-doc.cadn.net.cn

— 迈克·阿蒙森
ALPS 规范

ALPS 无需特殊激活。相反,您可以“构建”一个 Alps 记录,并将其从 Spring MVC 或 Spring WebFlux 的 Web 方法中返回,如下所示:spring-doc.cadn.net.cn

示例 40.构建一个Alps记录
@GetMapping(value = "/profile", produces = ALPS_JSON_VALUE)
Alps profile() {

  return Alps.alps() //
      .doc(doc() //
          .href("https://example.org/samples/full/doc.html") //
          .value("value goes here") //
          .format(Format.TEXT) //
          .build()) //
      .descriptor(getExposedProperties(Employee.class).stream() //
          .map(property -> Descriptor.builder() //
              .id("class field [" + property.getName() + "]") //
              .name(property.getName()) //
              .type(Type.SEMANTIC) //
              .ext(Ext.builder() //
                  .id("ext [" + property.getName() + "]") //
                  .href("https://example.org/samples/ext/" + property.getName()) //
                  .value("value goes here") //
                  .build()) //
              .rt("rt for [" + property.getName() + "]") //
              .descriptor(Collections.singletonList(Descriptor.builder().id("embedded").build())) //
              .build()) //
          .collect(Collectors.toList()))
      .build();
}
  • 此示例利用 PropertyUtils.getExposedProperties() 提取领域对象属性的元数据。spring-doc.cadn.net.cn

此片段已插入测试数据。它将生成如下 JSON:spring-doc.cadn.net.cn

示例 41. ALPS JSON
{
  "version": "1.0",
  "doc": {
    "format": "TEXT",
    "href": "https://example.org/samples/full/doc.html",
    "value": "value goes here"
  },
  "descriptor": [
    {
      "id": "class field [name]",
      "name": "name",
      "type": "SEMANTIC",
      "descriptor": [
        {
          "id": "embedded"
        }
      ],
      "ext": {
        "id": "ext [name]",
        "href": "https://example.org/samples/ext/name",
        "value": "value goes here"
      },
      "rt": "rt for [name]"
    },
    {
      "id": "class field [role]",
      "name": "role",
      "type": "SEMANTIC",
      "descriptor": [
        {
          "id": "embedded"
        }
      ],
      "ext": {
        "id": "ext [role]",
        "href": "https://example.org/samples/ext/role",
        "value": "value goes here"
      },
      "rt": "rt for [role]"
    }
  ]
}

您可以选择手动编写每个字段,而不是将它们“自动”链接到领域对象的字段。此外,还可以使用 Spring Framework 的消息资源包和 MessageSource 接口。这使您能够将这些值委托给特定区域设置的消息资源包,甚至实现元数据的国际化。spring-doc.cadn.net.cn

4.7. 社区驱动的媒体类型

得益于创建自定义媒体类型的能力,社区已发起多项努力以构建更多的媒体类型。spring-doc.cadn.net.cn

4.7.1. JSON:API

Maven 坐标
<dependency>
    <groupId>com.toedter</groupId>
    <artifactId>spring-hateoas-jsonapi</artifactId>
    <version>{see project page for current version}</version>
</dependency>
Gradle 坐标
implementation 'com.toedter:spring-hateoas-jsonapi:{see project page for current version}'

如果您想要快照版本,请访问项目页面获取更多详情。spring-doc.cadn.net.cn

4.7.2. 塞壬

Maven 坐标
<dependency>
    <groupId>de.ingogriebsch.hateoas</groupId>
    <artifactId>spring-hateoas-siren</artifactId>
    <version>{see project page for current version}</version>
    <scope>compile</scope>
</dependency>
Gradle 坐标
implementation 'de.ingogriebsch.hateoas:spring-hateoas-siren:{see project page for current version}'

4.8. 注册自定义媒体类型

Spring HATEOAS 允许您通过 SPI 集成自定义媒体类型。 此类实现的构建模块包括:spring-doc.cadn.net.cn

  1. 某种形式的 Jackson ObjectMapper 自定义。在最简单的情况下,那是一个 Jackson Module 实现。spring-doc.cadn.net.cn

  2. 一个 LinkDiscoverer 实现,以便客户端支持能够检测表示中的链接。spring-doc.cadn.net.cn

  3. 一小部分基础设施配置,将使 Spring HATEOAS 能够找到并加载自定义实现。spring-doc.cadn.net.cn

4.8.1. 自定义媒体类型配置

Spring HATEOAS 通过扫描应用上下文中所有 HypermediaMappingInformation 接口的实现来自动发现自定义媒体类型实现。 每种媒体类型都必须实现此接口,以便:spring-doc.cadn.net.cn

定义您自己的媒体类型可能看起来如此简单:spring-doc.cadn.net.cn

@Configuration
public class MyMediaTypeConfiguration implements HypermediaMappingInformation {

  @Override
  public List<MediaType> getMediaTypes() {
    return Collections.singletonList(MediaType.parseMediaType("application/vnd-acme-media-type")); (1)
  }

  @Override
  public Module getJacksonModule() {
    return new Jackson2MyMediaTypeModule(); (2)
  }

  @Bean
  MyLinkDiscoverer myLinkDiscoverer() {
    return new MyLinkDiscoverer(); (3)
  }
}
1 配置类返回其支持的媒体类型。这适用于服务器端和客户端场景。
2 它重写 getJacksonModule() 以提供自定义序列化器,从而创建特定于媒体类型的表示形式。
3 它还声明了一个自定义的 LinkDiscoverer 实现,以提供进一步的客户端支持。

Jackson 模块通常为表示模型类型 RepresentationModelEntityModelCollectionModelPagedModel 声明 SerializerDeserializer 个实现。 如果您需要对 Jackson ObjectMapper 进行进一步定制(例如自定义 HandlerInstantiator),您也可以选择重写 configureObjectMapper(…)spring-doc.cadn.net.cn

早期版本的参考文档曾提到需要实现 MediaTypeConfigurationProvider 接口并将其注册到 spring.factories。 这并非必要。 该 SPI 仅用于 Spring HATEOAS 提供的开箱即用媒体类型。 只需实现 HypermediaMappingInformation 接口并将其注册为 Spring Bean 即可。spring-doc.cadn.net.cn

4.8.2. 建议

实现媒体类型表示的首选方式是提供一个与预期格式匹配的类型层次结构,并可直接由 Jackson 进行序列化。 在为 RepresentationModel 注册的 SerializerDeserializer 实现中,需将实例转换为特定于媒体类型的模型类型,然后查找这些类型对应的 Jackson 序列化器。spring-doc.cadn.net.cn

默认支持的媒体类型使用与第三方实现相同的配置机制。 因此,值得研究 mediatype 中的实现。 请注意,内置的媒体类型实现将其配置类设置为包私有,因为它们是通过 @EnableHypermediaSupport 激活的。 自定义实现可能应该将这些类改为公共的,以确保用户可以从其应用程序包中导入这些配置类。spring-doc.cadn.net.cn