16.3.2 在响应中设置头部信息

在 saveSpittle() 方法中,我们在处理 POST 请求的过程中创建了一个新的 Spittle 资源。但是,按照目前的写法(参考程序清单 16.3),我们无法准确地与客户端交流。

在 saveSpittle() 处理完请求之后,服务器在响应体中包含了 Spittle 的表述以及 HTTP 状态码 200(OK),将其返回给客户端。 这里没有什么大问题,但是还不是完全准确。

当然,假设处理请求的过程中成功创建了资源,状态可以视为 OK。 但是,我们不仅仅需要说 “OK”。我们创建了新的内容,HTTP 状态码也将这种情况告诉给了客户端。不过,HTTP 201 不仅能够表明请求成功完成,而且还能描述创建了新资源。如果我们希望完整准确地与客户端交流,那么响应是不是应该为 201(Created),而不仅仅是 200(OK)呢?

根据我们目前所学到的知识,这个问题解决起来很容易。我们需要做的就是为 saveSpittle() 方法添加 @ResponseStatus 注解,如下所示:

@RequestMapping(method=RequestMethod.POST, consumes="application/json")
@ResponseStatus(HttpStatus.CREATED)
public Spittle saveSpittle(@RequestBody Spittle spittle) {
  return spittleRepository.save(spittle);
}

这应该能够完成我们的任务,现在状态码能够精确反应发生了什么情况。它告诉客户端我们新创建了资源。问题已经得以解决!

但这只是问题的一部分。客户端知道新创建了资源,你觉得客户端会不会感兴趣新创建的资源在哪里呢?毕竟,这是一个新创建的资源,会有一个新的 URL 与之关联。难道客户端只能猜测新创建资源的 URL 是什么吗?我们能不能以某种方式将其告诉客户端?

当创建新资源的时候,将资源的 URL 放在响应的 Location 头部信息中,并返回给客户端是一种很好的方式。因此,我们需要有一种方式来填充响应头部信息,此时我们的老朋友 ResponseEntity 就能提供帮助了。

如下的程序清单展现了一个新版本的 saveSpittle(),它会返回 ResponseEntity 用来告诉客户端新创建的资源。

程序清单 16.4 当返回 ResponseEntity 时,在响应中设置头部信息
@RequestMapping(method=RequestMethod.POST, consumes="application/json")
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<Spittle> saveSpittle(@RequestBody Spittle spittle) {
    Spittle spittle = spittleRepository.save(spittle);
    
    HttpHeaders headers = new HttpHeaders();
    URI locationUri = URI.create("http://localhost:8080/spittr/spittles" + spittle.getId());
    headers.setLocation(locationUri);
    
    ResponseEntity<Spittle> responseEntity = new ResponseEntity<Spittle>(spittle, headers, HttpStatus.CREATED);
    return responseEntity;
  }

在这个新的版本中,我们创建了一个 HttpHeaders 实例,用来存放希望在响应中包含的头部信息值。HttpHeaders 是 MultiValueMap 的特殊实现,它有一些便利的 Setter 方法(如 setLocation()),用来设置常见的 HTTP 头部信息。在得到新创建 Spittle 资源的 URL 之后,接下来使用这个头部信息来创建 ResponseEntity。

哇!原本简单的 saveSpittle() 方法瞬间变得臃肿了。但是,更值得关注的是,它使用硬编码值的方式来构建 Location 头部信息。URL 中 “localhost” 以及 “8080” 这两个部分尤其需要注意,因为如果我们将应用部署到其他地方,而不是在本地运行的话,它们就不适用了。

我们其实没有必要手动构建 URL,Spring 提供了 UriComponentsBuilder,可以给我们一些帮助。它是一个构建类,通过逐步指定 URL 中的各种组成部分(如 host、端口、路径以及查询),我们能够使用它来构建 UriComponents 实例。借助 UriComponentsBuilder 所构建的 UriComponents 对象,我们就能获得适合设置给 Location 头部信息的 URI。

为了使用 UriComponentsBuilder,我们需要做的就是在处理器方法中将其作为一个参数,如下面的程序清单所示。

程序清单 16.5 使用 UriComponentsBuilder 来构建 Location URI
@RequestMapping(method=RequestMethod.POST, consumes="application/json")
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<Spittle> saveSpittle(@RequestBody Spittle spittle, UriComponentsBuilder ucb) {
  Spittle saved = spittleRepository.save(spittle);
    
  HttpHeaders headers = new HttpHeaders();
  URI locationUri = ucb.path("/spittles/")
      .path(String.valueOf(saved.getId()))
      .build()
      .toUri();
  headers.setLocation(locationUri);
    
  ResponseEntity<Spittle> responseEntity = new ResponseEntity<Spittle>(saved, headers, HttpStatus.CREATED);
  return responseEntity;
}

在处理器方法所得到的 UriComponentsBuilder 中,会预先配置已知的信息如 host、端口以及 Servlet 内容。它会从处理器方法所对应的请求中获取这些基础信息。基于这些信息,代码会通过设置路径的方式构建 UriComponents 其余的部分。

注意,路径的构建分为两步。第一步调用 path() 方法,将其设置为 “/spittles/”,也就是这个控制器所能处理的基础路径。然后,在第二次调用 path() 的时候,使用了已保存 Spittle 的 ID。我们可以推断出来,每次调用 path() 都会基于上次调用的结果。

在路径设置完成之后,调用 build() 方法来构建 UriComponents 对象,根据这个对象调用 toUri() 就能得到新创建 Spittle 的 URI。

在 REST API 中暴露资源只代表了会话的一端。如果发布的 API 没有人关心和使用的话,那也没有什么价值。通常来讲,移动或 JavaScript 应用会是 REST API 的客户端,但是 Spring 应用也完全可以使用这些资源。我们换个方向,看一下如何编写 Spring 代码实现 RESTful 交互的客户端。

Last updated