1.提供资源之外的其他内容
@ResponseBody提供了一种很有用的方式,能够将控制器返回的Java对象转换为发送到客户端的资源表述。实际上,将资源表述发送给客户端只是整个过程的一部分。一个好的REST API不仅能够在客户端和服务器之间传递资源,它还能够给客户端提供额外的元数据,帮助客户端理解资源或者在请求中出现了什么情况。
发送错误信息到客户端
如果根据给定的ID,无法找到某个Spittle对象的ID属性能够与之匹配,findOne()方法返回null的时候,你觉得会发生什么呢?结果就是spittleById()方法会返回null,响应体为空,不会返回任何有用的数据给客户端。同时,响应中默认的HTTP状态码是200(OK),表示所有的事情运行正常
Spring提供了多种方式来处理这样的场景:
- 使用@ResponseStatus注解可以指定状态码;
- 控制器方法可以返回ResponseEntity对象,该对象能够包含更多响应相关的元数据;
- 异常处理器能够应对错误场景,这样处理器方法就能关注于正常的状况。
使用ResponseEntity
作为@ResponseBody的替代方案,控制器方法可以返回一个ResponseEntity对象。ResponseEntity中可以包含响应相关的元数据(如头部信息和状态码)以及要转换成资源表述的对象。因为ResponseEntity允许我们指定响应的状态码,所以当无法找到Spittle的时候,我们可以返回HTTP 404错误。如下是新版本的spittleById(),它会返回ResponseEntity:
我们重构一下代码来使用错误处理器。首先,定义能够对应SpittleNotFound-Exception的错误处理器
@ExceptionHandler注解能够用到控制器方法中,用来处理特定的异常。这里,它表明如果在控制器的任意处理方法中抛出SpittleNotFoundException异常,就会调用spittleNotFound()方法来处理异常。至于SpittleNotFoundException,它是一个很简单异常类:
在响应中设置头部信息
在saveSpittle()处理完请求之后,服务器在响应体中包含了Spittle的表述以及HTTP状态码200(OK),将其返回给客户端。这里没有什么大问题,但是还不是完全准确。当然,假设处理请求的过程中成功创建了资源,状态可以视为OK。但是,我们不仅仅需要说“OK”。我们创建了新的内容,HTTP状态码也将这种情况告诉给了客户端。不过,HTTP 201不仅能够表明请求成功完成,而且还能描述创建了新资源。如果我们希望完整准确地与客户端交流,那么响应是不是应该为201(Created),而不仅仅是200(OK)呢?根据我们目前所学到的知识,这个问题解决起来很容易。我们需要做的就是为saveSpittle()方法添加@ResponseStatus注解,如下所示:
但这只是问题的一部分。客户端知道新创建了资源,你觉得客户端会不会感兴趣新创建的资源在哪里呢?毕竟,这是一个新创建的资源,会有一个新的URL与之关联。难道客户端只能猜测新创建资源的URL是什么吗?我们能不能以某种方式将其告诉客户端?当创建新资源的时候,将资源的URL放在响应的Location头部信息中,并返回给客户端是一种很好的方式。因此,我们需要有一种方式来填充响应头部信息,此时我们的老朋友ResponseEntity就能提供帮助了。如下的程序清单展现了一个新版本的saveSpittle(),它会返回ResponseEntity用来告诉客户端新创建的资源
我们其实没有必要手动构建URL,Spring提供了UriComponentsBuilder,可以给我们一些帮助。它是一个构建类,通过逐步指定URL中的各种组成部分(如host、端口、路径以及查询),我们能够使用它来构建UriComponents实例。借助UriComponentsBuilder所构建的UriComponents对象,我们就能获得适合设置给Location头部信息的URI。为了使用UriComponentsBuilder,我们需要做的就是在处理器方法中将其作为一个参数,如下面的程序清单所示。
2.编写REST客户端
了解RestTemplate的操作
RestTemplate定义了36个与REST资源交互的方法,其中的大多数都对应于HTTP的方法。但是,在本章中我没有足够的篇幅涵盖所有的36个方法。其实,这里面只有11个独立的方法,其中有十个有三种重载形式,而第十一个则重载了六次,这样一共形成了36个方法。表16.2描述了RestTemplate所提供的11个独立方法。除了TRACE以外,RestTemplate涵盖了所有的HTTP动作。除此之外,execute()和exchange()提供了较低层次的通用方法来使用任意的HTTP方法
表16.2中的大多数操作都以三种方法的形式进行了重载:
- 一个使用java.net.URI作为URL格式,不支持参数化URL;
- 一个使用String作为URL格式,并使用Map指明URL参数;
- 一个使用String作为URL格式,并使用可变参数列表指明URL参数。
明确了RestTemplate所提供的11个操作以及各个变种如何工作之后,你就能以自己的方式编写使用REST资源的客户端了。我们通过对四个主要HTTP方法的支持(也就是GET、PUT、DELETE和POST)来研究RestTemplate的操作。我们从GET方法的getForObject()和getForEntity()开始
GET资源
你可能意识到在表16.2中列出了两种执行GET请求的方法:getForObject()和getForEntity()。正如之前所描述的,每个方法又有三种形式的重载
除了返回类型,getForEntity()方法就是getForObject()方法的镜像。实际上,它们的工作方式大同小异。它们都执行根据URL检索资源的GET请求。它们都将资源根据responseType参数匹配为一定的类型。唯一的区别在于getForObject()只返回所请求类型的对象,而getForEntity()方法会返回请求的对象以及响应相关的额外信息。让我们首先看一下稍微简单的getForObject()方法。然后再看看如何使用getForEntity()方法来从GET响应中获取更多的信息
检索资源
getForObject()方法是检索资源的合适选择。我们请求一个资源并按照所选择的Java类型接收该资源。作为getForObject()能够做什么的一个简单示例,让我们看一
下fetchFacebookProfile()的另一个实现:
我们没有使用字符串连接来构建URL,而是利用了RestTemplate可以接受参数化URL这一功能。URL中的{id}占位符最终将会用方法的id参数来填充。getForObject()方法的最后一个参数是大小可变的参数列表,每个参数都会按出现顺序插入到指定URL的占位符中。另外一种替代方案是将id参数放到Map中,并以id作为key,然后将这个Map作为最后一个参数传递给getForObject():
这里没有任何形式的JSON解析和对象映射。在表面之下,getForObject()为我们将响应体转换为对象。它实现这些需要依赖表16.1中所列的HTTP消息转换器,与带有@ResponseBody注解的Spring MVC处理方法所使用的一样。这个方法也没有任何异常处理。这不是因为getForObject()不能抛出异常,而是因为它抛出的异常都是非检查型的。如果在getForObject()中有错误,将抛出非检查型RestClientException异常(或者它的一些子类)。如果愿意的话,你可以捕获它——但编译器不会强制你捕获它
类似的方法
RESTful架构使用Web标准来集成应用程序,使得交互变得简单自然。系统中的资源采用URL进行标识,使用HTTP方法进行管理并且会以一种或多种适合客户端的方式来进行表述。在本章中,我们看到了如何编写响应RESTful资源管理请求的SpringMVC控制器。借助参数化的URL模式并将控制器处理方法与特定的HTTP方法关联,控制器能够响应对资源的GET、POST、PUT以及DELETE请求。为了响应这些请求,Spring能够将资源背后的数据以最适合客户端的形式展现。对于基于视图的响应,ContentNegotiatingViewResolver能够在多个视图解析器产生的视图中选择出最适合客户端期望内容类型的那一个。或者,控制器的处理方法可以借助@ResponseBody注解完全绕过视图解析,并使用信息转换器将返回值转换为客户端的响应。REST API为客户端暴露了应用的功能,它们暴露功能的方式恐怕最原始的API设计者做梦都想不到。REST API的客户端通常是移动应用或运行在Web浏览器中的JavaScript。但是,Spring应用也可以借助RestTemplate来使用这些API。