【Spring IN ACTION】-----REST概述及Spring对REST支持(一)


-----------------------------------笔记来自于《Spring IN ACTION》

1.简介
1.1 REST概述

REST全称是Representational State Transferm,中文意思是表述性状态转移。REST首次出现在2000年的Roy Fielding博士论文中,Roy Fielding是HTTP规范的是主要编写者之一, 他在论文中提到:“我这篇文章的写作目的,就是想在符合架构原理的前提下,理解和评估以网络为基础的应用软件的架构设计,得到一个功能强、性能好、适宜通信的架构。REST指的是一组架构约束条件和原则。” 如果一个架构符合REST的约束条件和原则,我们就称它为RESTful架构。
REST本身并没有创造新的技术、组件或服务,而隐藏在RESTful背后的理念就是使用Web的现有特征和能力, 更好地使用现有Web标准中的一些准则和约束。虽然REST本身受Web技术的影响很深, 但是理论上REST架构风格并不是绑定在HTTP上,只不过目前HTTP是唯一与REST相关的实例。 所以此处描述的REST也是通过HTTP实现的REST。

REST经常被误当做是基于URL的WEB服务(即将REST视为另外一种类型的远程过程调用机制(remote procedure callRPC),就像SOAP一样。只不过是通过简单的HTTP URL来触发,而不是使用SOAP大量的XML命名空间)。实际上REST与RPC没有任何关系,RPC是面向服务的,关注与行为和动作;而REST是面向资源的,强调描述应用程序的事物和名词。可以将REST(Representational State Transferm)拆分为不同的构成部分:

  • 表述性(Representational ):REST资源实际上可以用各种形式来进行表述,包括XMLJSONJavaScript Object Notation)甚至HTML,最适合资源使用者的任意形式;
  • 状态(State):当使用REST的时候,我们更关注资源的状态而不是对资源采取的行为。
  • 转移(Transfer):REST涉及到转移资源数据,它以某种表述性形式从一个应用转移到另一个应用。

简洁地讲,REST就是将资源的状态以最适合客户端或者服务端的形式(如Json或者XML)从服务器端转移到客户端(或反过来)。在REST中,资源通过URL来进行识别和定位。RESTful URL的结构并没有严格的规则,但是URL应该能够识别资源,而不是简单地发一条命令到服务器上。REST的行为可以通过HTTP方法来定义的。具体讲就是GETPOSTPUTDELETEPATCH以及其他的HTTP方法构成了REST中的动作。这些HTTP方法通常会匹配如下的CRUD动作:

  • Create:POST
  • Read:GET
  • Update:PUT或PATCH
  • Delete:DELETE

HTTP方法会映射为CRUD动作,但并非严格限制。有时候PUT可以用来创建资源,POST可以用来更新资源。实际上POST请求非幂等性(non-idempotent)(即无论调用多少次HTTP方法,结果都是一致的,获取资源是不变的)的特点使其成为一个非常灵活的方法,对于无法适应其他HTTP方法语义的操作,它都能够胜任。

1.2 Spring支持REST

Spring 3.0版本中,Spring对Spring MVC的一些增强功能对REST提供了良好的支持。在4.0版本后,Spring支持以下方式来创建REST资源:

  • 【方式一】:控制器可以处理所有HTTP的方法,包含四个主要的REST方法:GETPUTDELETE以及POST。Spring3.2及以上版本支持PATCH方法。
  • 【方式二】:借助@PathVariable注解,控制器能够处理参数化的URL(将变量输入作为URL的一部分)
  • 【方式三】:借助Spring的视图和视图解析器,资源能够以多种方式进行表述,包括将模型数据渲染为XMLJSONAtomRSS的view实现。
  • 【方式四】:借助@ResponseBody注解和各种HttpMethodConverter实现类可以将传入的HTTP数据转化为传入控制器处理方法的Java对象。
  • 【方式五】:借助RestTemplate,Spring应用能够方便地使用REST资源。
2.创建REST端点
2.1 协商资源表述

表述(Representational)是REST中很重要的一个方面,它描述的是客户端和服务器端针对某个资源是如何通信的。任何给定的资源可以使用任意形式来进行表述。通常应用或者调用REST端点的代码可使用JSON或者XML来进行表述,例如客户端是javascript使用json来表述,因为javaScript中json数据不需要编排(marshaling)和解排(demarshaling)。如果用户在浏览器中查看资源的话,也可以使用HTML方式来展现(或者PDF,Excel及其他便于人类阅读的格式)。资源没有变化,只是它的表述方式变化了。
Spring MVC中控制器本身并不关注资源表述形式,它以java对象的方式来处理资源。控制器完成了它的工作后,资源才会被转化成最适合客户端的形式。Spring提供了两种方式将资源的java表述形式转换为发送给客户端的表述形式:

  • 内容协商(Content nogotiation):选择一个视图,它能够将模型渲染为呈现给客户端的表述形式。
  • 消息转换器(Message conversion):通过一个消息转换器将控制器所返回的对象转换为呈现给客户端的表述形式。

Spring MVC请求的简单描述:Spring MVC核心是DispatcherServletDispatcherServlet会将请求交给控制器进行处理,处理完成后,通常会返回一个逻辑视图名。如果方法不直接返回逻辑视图名(例如方法返回void),那么逻辑视图名会根据请求的URL判断得出。DispatcherServlet接下来会将视图的名字传递给一个视图解析器,要求它来帮助确定应该使用哪个视图来渲染请求结果。 当要将视图解析为能够产生资源表述的视图时,视图不仅要匹配视图名,而且所选的视图要适合客户端。如果客户端想要json,那么渲染HTML的视图就不适合了,尽管视图名可能匹配。
在这里插入图片描述
Spring的ContentNegotiatingViewResolver是一个特殊的视图解析器,它考虑了客户所需要的内容类型,其涉及到内容协商的两个步骤:

  1. 确定请求的媒体类型。
  2. 找到适合请求媒体的最佳视图。

———确定请求媒体的类型
  内容协商的第一步是确定客户端想要的内容表述的类型,不能直接通过Accept头部信息来确定,例如客户端是web浏览器,不能保证客户端需要的类型就是浏览器在Accept头部发送的值。web浏览器只接收对人类用户友好的内容类型(如text/html),所以无法指定不同类型。ContentNegotiatingViewResolver会优先查看URL的文件扩展名,如果URL在结尾处有文件扩展名,ContentNegotiatingViewResolver将会基于该扩展名确定所需的类型,如扩展名是".json",需要的内容类型是必须是application/json。如果根据文件扩展名不能得到任何媒体类型的话,Accept头部信息的值表明了客户端想要的MIME类型,如果没有Accept头部信息和无扩展名时,ContentNegotiatingViewResolver将使用"/"作为默认的内容类型,这表示客户端必须要接收服务器发送的任何形式的表述。
  一旦内容类型确定之后,ContentNegotiatingViewResolver就将逻辑视图名解析为渲染模型的view,与Spring的其他视图解析器不同,ContentNegotiatingViewResolver本身不会解析视图,而是委托给其他的视图解析器,让它们将逻辑视图名解析为视图。解析得到的每个视图都会放到一个列表中。这个列表装配完成后,ContentNegotiatingViewResolver会循环客户端请求的所有的媒体类型,在候选视图中查找能够产生对应内容类型的视图。第一个匹配的视图会用来渲染模型。

———影响媒体类型的选择
ContentNegotiatingViewResolvert通过上面这种默认策略来确定请求媒体类型,但可以通过为其设置一个ContentNegotiationManager改变它的行为。借助ContentNegotiationManager可以做到如下事情:

  1. 指定默认的内容类型,如果根据请求无法的到内容类型的话,将会使用默认值。
  2. 忽略请求的Accept头部信息。
  3. 将请求的扩展名映射为特定的媒体类型。
  4. 将JAF(Java Activation Framework)作为根据扩展名查找媒体类型的备用方法。

有三种方式配置ContentNegotiationManager

【方式一】: 直接声明一个ContentNegotiationManager类型的bean.
【方式二】: 通过ContentNegotiationManagerFactoryBean简介创建bean。
【方式三】: 继承WebMvcConfigurer接口,重载configureContentNegotiation()方法。

  直接创建ContentNegotiationManager比较复杂,推荐使用后两种更加简单方式创建ContentNegotiationManager
ContentNegotiationManager是在Spring3.2中加入的,在Spring3.2之前ContentNegotiatingViewResolver的很多行为通过其属性的setter方法来设置,在spring 3.2版本之后,大多setter方法被废弃,推荐通过ContentNegotiationManager来进行配置。ContentNegotiationManager所设置属性在ContentNegotiatingViewResolver都能找到对应属性。
——xml配置方式
如果使用XML配置方式配置ContentNegotiationManager,可以使用ContentNegotiationManagerFactoryBean,如设置"application/json"作为ContentNegotiationManager的默认内容类型。

<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean" 
p:defaultContentType="application/json" />

ContentNegotiationManagerFactoryBean是一个工厂Bean,能够创建一个ContentNegotiationManager bean,将这个bean注入到ContentNegotiatingViewResolvercontentNegotiationManager属性中。
——Java类配置方式
如果使用java配置的话,获取ContentNegotiationManager 最简单的方式是实现org.springframework.web.servlet.config.annotation.WebMvcConfigurer接口并重载configureContentNegotiation()方法,如下:

public class WebConfig implements WebMvcConfigurer{
  @Override
  public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
   configurer.defaultContentType(MediaType.APPLICATION_JSON);
  }
}

  其中方法configureContentNegotiation()参数是ContentNegotiationConfigurerContentNegotiationConfigurer中多数方法实际上底层是通过在ContentNegotiationManagerFactoryBean调用相应的方法来实现的。因此在ContentNegotiationConfigurer上设置属性,进而通过ContentNegotiationManager来设置内容协商相关的属性。本例中调用defaultContentType()方法将默认内容设置为"application/json"
  此时已经有了ContentNegotiationManager bean,可以将它注入到ContentNegotiatingViewResolvercontentNegotiationManager属性中,这样ContentNegotiatingViewResolver将会使用ContentNegotiationManager所定义的行为。

 @Bean
  public ViewResolver cnViewResvolver(ContentNegotiationManager cnm) {
    ContentNegotiatingViewResolver cnvr = new ContentNegotiatingViewResolver();
    cnvr.setContentNegotiationManager(cnm);
    return cnvr;
  }

需要注意的是:ContentNegotiatingViewResolver通常默认使用的是HTML视图,但是对特定的视图名称可以渲染为JSON输出。

public class WebConfig implements WebMvcConfigurer{
  @Override
  //默认为HTML表述
  public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
   configurer.defaultContentType(MediaType.TEXT_HTML);
  }
  
  @Bean
  public ViewResolver cnViewResvolver(ContentNegotiationManager cnm) {
    ContentNegotiatingViewResolver cnvr = new ContentNegotiatingViewResolver();
    cnvr.setContentNegotiationManager(cnm);
    return cnvr;
  }
  //以bean的形式查找视图
  @Bean
  public ViewResolver beanNameViewResolver() {
    return new BeanNameViewResolver();
  }
  //将"spittles"定义为JSON视图
  @Bean
  public View spittles() {
    return new MappingJackson2JsonView();
  }
}

除了以上配置外,还应该需要配置一个HTML的视图解析器(如InternalResourceViewResolver或者TilesViewResolver)。如果需要的是JSON格式,ContentNegotiatingViewResolver需要配置和查找一个能够处理JSON视图的视图解析器。
本例逻辑视图名称为"spittles",那么配置的BeanNameViewResolver将解析spittles()方法中所声明的View,这是因为bean名称匹配逻辑视图名称。如果没有匹配view的话,ContentNegotiatingViewResolver将采用默认的行为,将输出为HTML。

  • ContentNegotiatingViewResolver优势:ContentNegotiatingViewResolver最大优势在于,它在Spring MVC之上构建了REST资源表述层,同一个控制器方法既能适合人类用户产生HTML内容,也能针对非人类的客户端产生的JSON或XML。
  • ContentNegotiatingViewResolver限制:如果面向人类用户的接口与面向非人类的客户端的接口之间没有太多重叠时,ContentNegotiatingViewResolver没有太大优势。作为ViewResolver的实现类,只能决定资源如何渲染到客户端,无法控制客户端要发送控制器的表述。此外所选中的view会渲染模型给客户端,而不是资源,这是比较细微且重要的区别。客户端可能无法得到想要的JSON格式。如下客户端需要的JSON格式如下:
[
    {
        "id": 1,
        "latitude": 22.22,
        "longitude": 66.6,
        "message": "Hello World!!!",
        "time": "2019-06-20T05:42:22.963+0000"
    }
]

而模型key-value组成的Map,响应结果可能如下面:

{
“spittleList”:[
       {
        "id": 1,
        "latitude": 22.22,
        "longitude": 66.6,
        "message": "Hello World!!!",
        "time": "2019-06-20T05:42:22.963+0000"
     }
   ]
}

因为这些限制,可以使用Spring消息转换功能来生成资源表述。

2.2 使用HTTP信息转换器

消息转换(message conversion)提供了一种更为直接的方式,它能将控制器产生的数据转换为服务于客户端的表述形式。当使用消息转换功能时,DispatcherServlet不再需要那么麻烦地将模型数据传送到视图中。实际上,此时没有模型和视图,只有控制器产生的数据以及消息转换器转换数据之后产生的资源表述。
  Spring自带了各种各样的转换器,这些转换器满足常见的将对象转换为表述的需要。除了部分的HTTP信息转换器外,其他都是自动注册。因此使用它们时,不需要Spring配置。但为了支持它们,需要将相应库添加到应用程序类路径下面。例如:客户端发送请求的Accept头信息表明需要"application/json"格式时,可以添加MappingJackson2HttpMessageConverter来实现JSON消息和Java对象的互相转换,但需要将Jackson JSON Processor库添加到类路径下。客户端表明需要"text/xml"格式,可以使用Jaxb2RootElementHttpMessageConverter实现XML消息和Java对象的互相转换,需要添加类库JAXB。如果信息是Atom或者RSS格式的话,AtomFeedHttpMessageConverterRssChannelHttpMessageConverter需要ROME库。

Spring HTTP信息转换器描述
AtomFeedHttpMessageConverterRome Feed对象和Atom feed(媒体类型是application/atom+xml) 之间互相转换,如果ROME包在类路径下将进行注册
BufferedImageHttpMessageConverterBufferedImages与图片二进制数据之间的互相转换
ByteArrayHttpMessageConverter读取/写入字节数组。支持从所有媒体类型中(*/*)读取,并以application/octet-stream格式写入
FormHttpMessageConverter将application/x-www-form-urlencoded内容读入到MultiValueMap<String,String>中,也会将MultiValueMap<String,String>写入到application/x-www-form-urlencoded中或将MultiValueMap<String,Object>写入到multipart/form-data中
Jaxb2RootElementHttpMessageConverter在xml(text/xml或application/xml)和使用JAXB2注解(@XmlRootElement和@XmlType)的对象间互相读取和写入,如果JAXB v2库在类路径下,将进行注册
MappingJackson2HttpMessageConverter在JSON和类型化的对象或者非类型化的HashMap间互相读取和写入。如果Jackson JSON库在类路径下,将进行在注册
MarshallingHttpMessageConverter使用构造器或setter方法注入编排器和解排器(marshaller和unmarshaller)来读入和写入XML。支持的编排器和解排器包括Castor,JAXB2,JIBX,XMLBeans以及XStream
ResourceHttpMessageConverter支持所有媒体类型,读取和写入Resource
RssChannelHttpMessageConverter在RSS feed和Rome Channel对象间互相读取或写入。如果Rome库在类路径下,将进行注册
SourceHttpMessageConverter在XML和javax.xml.transform.Source对象间互相读取和写入。默认注册
StringHttpMessageConverter从所有媒体类型(/)读取为String,将String写入为text/plain
AllEncompassingFormHttpMessageConverterFormHttpMessageConverter的扩展,支持基于XML和JSON的部分

为了支持消息转换,需要Spring MVC中编程模型进行小调整。

———在响应体中返回资源状态
正常情况下,当处理方法返回Java对象(除String外或者View的实现以外)时,这个对象会放在模型中并在视图中渲染使用。但是如果使用了消息转换功能时,需要告诉Spring跳过正常的模型/视图模型,并使用消息转换器。最简单的方式是添加@ResponseBody注解。如下为方法spittles()添加注解@ResponseBody注解,Spring会将方法返回的List<Spittle>转换为响应体。

  @RequestMapping(method = RequestMethod.GET, produces = "application/json")
  public @ResponseBody List<Spittle> spittles(@RequestParam(value = "max", defaultValue = "20") long max,
      @RequestParam(value = "count", defaultValue = "20") int count) {
    return spittleRespository.findSpittles(max, count);
  }

  @ResponseBody注解会告知Spring将返回的对象作为资源发送给客户端,并将转换为客户端可接受的表述形式。具体来将就是DispatcherServlet将考虑到请求中的Accept头部信息,并查找能够为客户端提供所需要表述形式的消息转换器。如客户端Accept头部信息表明它接收“application/json”,并且jackson JSON库在应用类路径下,将选择MappingJackson2HttpMessageConverter消息转换器将控制器返回的Spittle列表转换为JSON格式,并写入到响应体中。默认情况下,Jackson JSON库会将返回的对象转换为JSON资源表述时,会使用反射。对于简单的表述内容,不会有问题。但如果重构了Java类型,如添加,移除或者重命名属性,那么产生JSON也将发生变化,这可能导致客户端调用出现问题。因此可以在Java类型上使用jackson映射注解,改变JSON的行为,避免影响到API或者客户端调用。此文不叙述jackson映射注解。

———在请求体中接收资源状态
Rest端点可以提供客户端需要的表述形式,也可以接收来自客户端的资源表述。@ResponseBody告诉Spring在发送数据到客户端时,使用消息转换器。与之类似@RequestBody能告诉Spring查找一个消息转换器,将来自客户端的资源表述转换为对象

@RestController
@RequestMapping("/spittles")
public class SpittleService {
  
  //省略代码
  ...
  @RequestMapping(method=RequestMethod.POST,consumes="application/json")
  public @ResponseBody Spittle saveSpittle(@RequestBody Spittle spittle) {
    return spittleRepository.save(spittle);
  }
}

    在SpittleService类和saveSpittle()方法上添加了@RequestMapping注解并设置属性,表明这个这个方法处理的是一个"/spittles"的post请求,在Spittle上添加了注解@RequestBody注解,因此Spring会查看请求中的Content-Type头部信息,并查找能够将请求体转换为Spittle的消息转换器。例如客户端发送的是Spittle数据是JSON表述形式,那么Content-Type头部信息可能就会是"application/json",在这种情况下,DispatcherServlet会查找能够将JSON转换成为Java对象的消息转换器。如果Jackson2库在类路径中,那么会使用MappingJackson2HttpMessageConverter将JSON表述转换为Spittle,然后传递到saveSpittle()方法中。此方法还使用了注解@ResponseBody,因此方法返回的是Spittle对象将会转换为某种资源表述,发送个客户端。
    值的注意的是@RequestMapping有一个consumers属性,其值设置为"application/json"consumes属性的工作方法是类似于produces,不过它关注请求的Content-Type头部信息。它告诉Spring这个方法只会处理"/spittles"的POST请求,并且要求请求的Content-Type头部信息为"application/json"。如果无法满足这条件,会由其他合适的方法来处理请求。

———为控制器设置消息转换
当处理请求时,使用@ResponseBody@RequestBody来启用消息转换方式比较简洁和强大。但如果编写的控制器有多个方法时,并且每个方法都需要信息转换功能时,这些注解就有一定程序的复杂性。Spring 4.0版本引入了@RestController注入,如果使用@RestController注解而不是@Controller时,Spring将会为该控制器的所有处理方法应用消息转换功能。不必为每个方法都添加@ResponseBody注解。如下。

@RestController
@RequestMapping("/spittles")
public class SpittleController {

  private static final String MAX_LONG_AS_STRING = "9223372066854775807";

  private SpittleRepository spittleRepository;

  @Autowired
  public SpittleRepository getSpittleRepository() {
    return spittleRepository;
  }

  public void setSpittleRepository(SpittleRepository spittleRepository) {
    this.spittleRepository = spittleRepository;
  }

  @RequestMapping(method = RequestMethod.GET)
  public List<Spittle> spittles(@RequestParam(value = "max", defaultValue = MAX_LONG_AS_STRING) long max,
      @RequestParam(value = "count", defaultValue = "20") int count) {
    return spittleRepository.findSpittles(max, count);
  }

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

其中spittles()方法和saveSpittle()两个处理器方法都没有使用@ResponseBody注解,因为控制器使用了@RestController注解,所以它的方法返回的对象将会通过消息转换机制,产生客户端所需要的资源表述。

3.提供资源外的其他内容

@ResponseBody提供了一种很有用的方式,能够将控制器返回的Java对象转换为发送到客户端的资源表述。实际上,将资源表述发送到客户端只是整个过程的一部分。一个好的REST API不仅能够在客户端和服务器之间传递资源。它能够给客户端提供额外的元数据,帮助客户端理解资源或者在请求中出现了什么情况。

3.1 发送错误信息到客户端

如下SpittleController提供了一个根据id来查找单个Spittle对象的方法:

@RequestMapping("/spittles")
@RestController
public class SpittleController {
  
  ...//省略代码
  @RequestMapping(value="/{id}",method=RequestMethod.GET)
  public @ResponseBody Spittle spittleById(@PathVariable long id) {
    return spittleRespository.findOne(id);
  }
}

    spittleById()方法通过传入一个参数id,然后调用Repository的findOne()方法,来查找一个Spittle对象。如果没有某个对象Spittle与给定的id匹配,那么spittleById()方法返回的结果可能是null,响应体为空,不会返回任何有用的数据给客户端。同时响应中默认的HTTP状态码是200(OK),表示所有的事情运行正常。但对于客户端来说,它没有获取到Spittle对象也没有收到消息表明出现了错误。服务器实际上是在说"这是一个没有用的响应,但是能够告诉你一切正常!"。因此对于这种场景,考虑返回的状态码不应该是是200,而应该是404(Not Found),告诉客户端没有找到相应的内容。如果响应体中能够包含错误信息而不是空的话就更好了。
Spring提供了多种方式来处理这样的场景。
【方式一】:使用@ResponseStatus注解来指定状态码。
【方式二】:控制器方法可以返回ResponseEntity对象,该对象包含更多响应相关的元数据(如状态码)。
【方式三】:异常处理器能够应对错误场景,这样处理方法就能关注正常的状态。
当然不存在唯一正确的处理方式,Spring在这方面提供了灵活处理方式。

———使用ResponseEntity
作为@ResponseBody的替代方案,控制器可以返回一个ResponseEntity对象,ResponseEntity对象包含了响应相关的元数据(如头部信息和状态码)以及转换成资源表述的对象。ResponseEntity允许指定响应的状态,因此当无法找到Spittle时,可以返回HTTP 404错误,如下:

  @RequestMapping(value="/{id}",method=RequestMethod.GET)
  public ResponseEntity<Spittle> spittleById(@PathVariable long id) {
    Spittle spittle = spittleRespository.findOne(id);
    HttpStatus status = spittle!=null?HttpStatus.OK:HttpStatus.NOT_FOUND;
    return new ResponseEntity<Spittle>(spittle,status);
  }

根据id来找一个Spittle对象,如果找到时,状态会像之前默认那样,设置为HTTPStatus.OK。但如果返回的是null时,状态将会设置为HttpStatus.NOT_FOUND,这会转换成HTTP 404。最终ResponseEntity对象会将Spittle和状态码一起传送到客户端。
值的注意的是spittleById()方法并没有使用@ResponseBody注解。除了包含响应头信息,状态码以及负载以外,ResponseEntity还包含了@ResponseBody的语义。因此负载部分将会渲染到响应体中,就像之前在方法上使用@ResponseBody注解一样。如果使用ResponseEntity类,就没有必要再使用@ResponseBody注解了。

———代码优化,包含错误信息
虽然上面的代码能给客户端返回一个合适的状态码,但如果返回的Spittle对象null时,响应体也是空。客户端可能期待在响应体中包含一些错误信息。首先定义一个包含错误信息的Error对象:

public class Error {
  private int code;
  private String message;
  
  public Error(int code, String message) {
    super();
    this.code = code;
    this.message = message;
  }
  public int getCode() {
    return code;
  }
  public String getMessage() {
    return message;
  }
  
}

其次修改spittleById()方法,让它返回Error信息。

@RequestMapping(value="/{id}",method=RequestMethod.GET)
public ResponseEntity<Spittle> spittleById(@PathVariable long id) {
  Spittle spittle = spittleRespository.findOne(id);
  if(spittle==null) {
    Error error = new Error(4,"Spittlt ["+id+"] not Found");
    return new ResponseEntity<Error>(error,HttpStatus.NOT_FOUND);
  }
  return new ResponseEntity<Spittle>(spittle,HttpStatus.OK);
}

现在,根据id查找到Spittle对象时,就将找到的Spittle对象同200(OK)的状态码一起封装到ResponseEntity中。另一方面,如果findOne()方法返回了null时,将创建Error对象,并与404(Not Found)状态码一起封装到ResponseEntity中,然后返回。

———代码优化,使用错误处理器
虽然上面代码中当查找对象为null时,返回正确的错误描述。但此时可以发现代码更加复杂,涉及到更多的逻辑,包含条件语句。另外方法返回ResponseEntity<?>使用泛型为它解析或出现错误留下了太多的空间。因此可以借助错误处理器来修正问题。spittleById()方法中if代码块是处理错误的,这是控制器中错误处理器(Error handler)所擅长的领域。错误处理器能够处理导致问题的场景,这样常规的处理器方法就只需要关心正常的逻辑处理。
首先,定义一个名称为SpittleNotFoundException的错误处理器。

  @ExceptionHandler(SpittleNotFoundException.class);
  public ResponseEntity<Error> spittleNotFound(SpittleNotFoundException e){
    long spittleId= e.getSpittleId();
    Error error = new Error(4,"Spittlt ["+id+"] not Found");
    return new ResponseEntity<Error>(error,HttpStatus.NOT_FOUND);
  }
public class SpittleNotFoundException extends RuntimeException {
  private long spittleId;
  public SpittleNotFoundException(long SpittleId) {
    this.spittleId= spittleId;
  }
  public long getSpittleId() {
    return spittleId;
  }
}

@ExceptionHandler注解能够用到控制器方法上,用来处理特定的异常。这里表明如果在控制器的任意处理方法中抛出SpittleNotFoundException异常,就会调用SpittleNotFound()方法来处理异常。此时就可以移除掉spittleById()方法中大多数的错误处理代码:

 @RequestMapping(value="/{id}",method=RequestMethod.GET)
  public ResponseEntity<Spittle> spittleById(@PathVariable long id) {
    Spittle spittle = spittleRespository.findOne(id);
    if(spittle==null) {
      throw new SpittleNotFoundException(id);
    }
    return new ResponseEntity<Spittle>(spittle,HttpStatus.OK);
  }

此版本比较干净,除了对返回值为null进行了检查,完全关注成功的场景,也就是能够找到请求的Spittle的情况。同时再返回类型中,可以移除掉泛型了。不过此时也知道spittleById()方法将会返回Spittle并且HTTP状态码始终会是200(OK),因此也不需要使用ResponseEntity,而是使用@ResponseBody

  @RequestMapping(value="/{id}",method=RequestMethod.GET)
  public @ResponseBody spittleById(@PathVariable long id) {
    Spittle spittle = spittleRespository.findOne(id);
    if(spittle==null) {
      throw new SpittleNotFoundException(id);
    }
    return spittle;
  }

如果类上面使用了@RestController注解,甚至不再需要@ResponseBody注解

 @RequestMapping(value="/{id}",method=RequestMethod.GET)
  public Spittle spittleById(@PathVariable long id) {
    Spittle spittle = spittleRespository.findOne(id);
    if(spittle==null) {
      throw new SpittleNotFoundException(id);
    }
    return spittle;
  }

同样对于错误处理器的方法而言,始终会返回Error。并且HTTP状态码为404(Not Found),由于使用ResponseEntity的原因是能够设置状态码。但在spittleNotFound()方法上添加@ResponseStatus(HttpStatus.NOT_FOUND)注解,可以达到相同的效果。因此可以不再使用ResponseEntity了。修改后的代码如下:

@ExceptionHandler(SpittleNotFoundException.class);
  @ResponseStatus(HttpStatus.NOT_FOUND)
  public @ResponseBody Error spittleNotFound(SpittleNotFoundException e){
    long spittleId= e.getSpittleId();
    return new Error(4,"Spittlt ["+id+"] not Found");
  }

如果控制器类上使用了@RestController注解,可以直接去掉@ResponseBody注解。

 @ExceptionHandler(SpittleNotFoundException.class);
  @ResponseStatus(HttpStatus.NOT_FOUND)
  public Error spittleNotFound(SpittleNotFoundException e){
    long spittleId= e.getSpittleId();
    return new Error(4,"Spittlt ["+id+"] not Found");
  }

小结:上面为了设置响应状态码,首先使用了ResponseEntity类,随后借助错误处理器和@ResponseStatus注解,避免使用ResponseEntity来使得代码更加简洁。虽然经过优化,我们不再需要ResponseEntity。但有种场景ResponseEntity能够很好地完成,其他的注解或异常处理器却做不到,即在响应中设置头部信息。

3.2 在响应体中设置头部信息

前面通过设置状态码和java对象返回了服务端调用结果信息。例如SpittleController中的saveSpittle()方法处理请求后,服务器在响应体中包含了Spittle的表述以及HTTP状态码200(OK),将其返回给客户端。但这里存在一个问题就是成功创建了资源,状态可以视为OK。但除了表明客户端请求成功,还可以描述服务端创建了新资源。因此HTTP 201(Created)更加适合这种情况的状态码表述。如下

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

现在状态码能够精确告诉客户端创建了资源,此问题得以解决。还存在另外一个问题,如果客户端想知道新创建资源的位置URL,服务器端以怎么样方式告诉客户端?当创建新资源的时候,可以将资源的URL放在响应的Location头部信息中,并返回给客户端是一种很好的方式。因此需要一种方式来填充响应头部信息,此时ResponseEntity就能提供帮助了。如下面返回一个ResponseEntity时,在响应中设置头部信息。

  @RequestMapping(method=RequestMethod.POST,consumes="application/json")
  @ResponseStatus(HttpStatus.CREATED)
  public ResponseEntity<Spittle> saveSpittle(@RequestBody Spittle spittle) {
    //获取spittle
    Spittle spittle = spittleRepository.save(spittle);
    //设置location头部信息
    HttpHeaders header = new HttpHeaders();
    URI locationUri = URI.create("http://localhost:8080/spittr/spittles/"+spittle.getId());
    header.setLocation(locationUri);
    //创建ResponseEntity
    ResponseEntity<Spittle> responseEntity = new ResponseEntity<>(spittle,header,HttpStatus.CREATED);
    return responseEntity;
  }

    上面代码创建了一个HttpHeaders实例,用来存放希望在响应中包含的头部信息值。HttpHeadersMultiValueMap<String,String>的特殊实现,它有一些便利的Setter方法(如setLocation()),用来设置常见的HTTP头部信息。在得到新创建的Spittle资源的URL之后,接下来使用这个头部信息创建了ResponseEntity。值的关注的是代码中使用了硬编码的方式构建了Location头部信息,尤其是URL中"localhost"以及"8080"这两个部分。如果将应用部署到其他地方,而不是本地运行时,它们就不适用了。
    Spring提供的URLComponentsBuilder类可以通过指定URL中的各种组成部分(如host端口路径以及查询)来构建一个UriComponents实例。借助URLComponentsBuilder创建的UriComponents对象,就能获得设置给Location头部信息的适合的URI。为了使用UriComponentsBuilder,需要在处理器方法中将其作为一个参数,如下:

  @RequestMapping(method=RequestMethod.POST,consumes="application/json")
  @ResponseStatus(HttpStatus.CREATED)
  public ResponseEntity<Spittle> saveSpittle(@RequestBody Spittle spittle,UriComponentsBuilder ucb) {
    //获取spittle
    Spittle spittle = spittleRepository.save(spittle);
    //设置location头部信息
    HttpHeaders header = new HttpHeaders();
    //构建localhostUri
    URI locationUri = ucb.path("/spittles/").path(String.valueOf(spittle.getId())).build().toUri();
    header.setLocation(locationUri);
    //创建ResponseEntity
    ResponseEntity<Spittle> responseEntity = new ResponseEntity<>(spittle,header,HttpStatus.CREATED);
    return responseEntity;
  }

在处理器方法中得到的UriComponentsBuilder中,会预先设置已知的信息如host端口以及Servlet内容。它会从处理器方法所对应的请求中获取这些基础信息。基于这些信息,代码会通过设置路径的方式构建UriComponentsBuilder其余内容。路径构建分为两部分。第一步调用path()方法,将其设置为"/spittles/",也就是这个控制器所能处理的基础路径。然后在第二次调用path()时,使用已经保存的Spittle的id,每次调用path()都会基于上次调用结果。在路径设置完成后,调用build()方法来构建UriComponents对象,根据这个对象调用toUri()就能得到新创建的Spittle的URI

4.参考

《Spring IN ACTION》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值