REST风格

1. 构建REST风格网站

在HTTP协议发展的过程中,提出了很多的规则,但是这些规则有些烦琐,于是又提出了一种风格约定,它便是REST风格。实际上严格地说它不是一种标准,而是一种风格。在现今互联网的世界中这种风格已经被广泛使用起来了。尤其是现今流行的微服务中,这样的风格甚至被推荐为各个微服务系统之间用于交互的方式。首先在REST风格中,每一个资源都只是对应着一个网址,而一个代表资源网址应该是一个名词,而不存在动词,这代表对一个资源的操作。在这样的风格下对于简易参数则尽量通过网址进行传递。例如,要获取id为1的用户的URL可能就设计成http://localhost:8080/user/1。其中user是名词,它代表用户信息,1则是用户的编号,它的含义就是获取用户id为1的资源信息。为了更好地介绍这些内容,需要再进一步地学习REST风格的一些特点。

1.1 REST简述

REST这个词,是Roy Thomas Fielding在他2000年的博士论文中提出的。Fielding博士是HTTP协议(1.0版和1.1版)的主要设计者、Apache服务器软件的作者之一、Apache基金会的第一任主席。所以,他的这篇论文一经发表,就引起了关注,并且立即对互联网开发产生了深远的影响。Fielding将他对互联网软件的架构原则,命名为REST(Representational State Transfer)。如果一个架构符合REST原则,就称它为REST风格架构。

1.2 REST名词解释

本节主要针对REST做必要的名词解释,REST按其英文名称(RepresentationalState Transfer)可翻译为表现层状态转换。首先需要有资源才能表现,所以第一个名词是“资源”。有了资源也要根据需要以合适的形式表现资源,这就是第二个名词——表现层。最后是资源可以被新增、修改、删除等,也就是第三个名词“状态转换”。这就是REST风格的三个主要的名词。下面对其做进一步的阐述。

  • **资源:**它可以是系统权限用户、角色和菜单等,也可以是一些媒体类型,如文本、图片、歌曲,总之它就是一个具体存在的对象。可以用一个URI(UniformResource Identifier,统一资源定位符)指向它,每个资源对应一个特定的URI。要获取这个资源,访问它的URI即可,而在REST中每一个资源都会对应一个独一无二的URI。在REST中,URI也可以称为端点(End Point)。
  • **表现层:**有了资源还需要确定如何表现这个资源。例如,一个用户可以使用JSON、XML或者其他的形式表现出来,又如可能返回的是一幅图片。在现今的互联网开发中,JSON数据集已经是一种最常用的表现形式,所以全书也是以JSON为中心的。
  • **状态转换:**现实中资源并不是一成不变的,它是一个变化的过程,一个资源可以经历创建(create)、访问(visit)、修改(update)和删除(delete)的过程。对于HTTP协议,是一个没有状态的协议,这也意味着对于资源的状态变化就只能在服务器端保存和变化,不过好在HTTP中却存在多种动作来对应这些变化。本章后面会具体讲解这些动作和它们的使用。

有了上面的描述,下面稍微总结一下REST风格架构的特点:

  • 服务器存在一系列的资源,每一个资源通过单独唯一的URI进行标识;
  • 客户端和服务器之间可以相互传递资源,而资源会以某种表现层得以展示;
  • 客户端通过HTTP协议所定义的动作对资源进行操作,以实现资源的状态转换。

2. HTTP的动作

上面讲述了REST的关键名词,也谈到了REST风格的资源是通过HTTP的行为去操作资源的。对于资源而言,它存在创建(create)、修改(update)、访问(visit)和删除(delete)的状态转换,这样它就对应于HTTP的行为的5种动作。

  • **GET(VISIT):**访问服务器资源(一个或者多个资源)。
  • **POST(CREATE):**提交服务器资源信息,用来创建新的资源。
  • **PUT(UPDATE):**修改服务器已经存在的资源,使用PUT时需要把资源的所有属性一并提交。
  • **PATCH(UPDATE):**修改服务器已经存在的资源,使用PATCH时只需要将部分资源属性提交。目前来说这个动作并不常用也不普及,有些Java类还不能完全支持它,所以在现实中使用它需要慎重。
  • **DELETE(DELETE):**从服务器将资源删除。

以上就是本章需要重点讨论的内容,其中POST动作对应创建资源,PUT和PATCH对应更新资源,GET请求对应访问资源,DELETE对应删除资源。对于HTTP协议,还有另外两种不常用的动作行为。

  • HEAD:获取资源的元数据(Content-type)。

  • OPTIONS:提供资源可供客户端修改的属性信息。

对于这两个不常用的动作,不再进行更为细致的讨论,因为实用价值不是很大。下面给出几个REST风格的请求的URI,以帮助读者理解REST的概念,如代码清单11-1所示。

代码清单11-1 REST风格的URI设计

image-20210701091106365

注意,在URI中并没有出现动词,而对于参数主要通过URI设计去获取。对于参数数量超过5个的可以考虑使用传递JSON的方式来传递参数。关于JSON传递参数,在Spring MVC中已经有了详尽的阐述,这里就不再赘述。

2.1 REST风格的一些误区

在设计URI时REST风格存在一些规范,例如,一般不应该在URI中存在动词:

GET    /user/get/1

这里的get是一个动词,在REST风格是不应该存在这样的动词的,可以修改为:

GET    /user/1

这样就代表获取id为1的用户信息。

另外一个误区是加入版本号,例如:

GET    /v1/user/1

其中v1代表一个版本号,而user代表用户信息,1则代表用户编号。这是一个错误的表达,因为在REST风格中资源的URI是唯一的,如果存在版本号,可以设置HTTP请求头,使用请求头的信息进行区分。例如,设置请求头的version参数为1.0:

Accept: version = 1.0

在很多时候REST都不推荐使用类似于

PUT    users?userName=user_name&note=note

这样传递参数。这是一个更新用户的URI,按REST风格的建议是采用

PUT    users/{userName}/{note}

但是有时候会出现参数很多的情况,如果全部写入到URI中,可读性和使用就会带来很大的困扰。这时就不应该考虑使用URI传递参数,而是考虑请求体获取参数,类似10.2.4节那样处理就可以了。

3. 使用Spring MVC开发REST风格端点

3.1 Spring MVC整合REST

在10.1节中,讨论了如果使用@RequestMapping让URL映射到对应的控制器,只要把URI设计为符合REST风格规范,那么显然就已经满足REST风格了。不过为了更为便捷地支持REST风格的开发,Spring 4.3之后除了@RequestMapping外,还可以使用以下5个注解。

  • @GetMapping:对应HTTP的GET请求,获取资源。
  • @PostMapping:对应HTTP的POST请求,创建资源。
  • @PutMapping:对应HTTP的PUT请求,提交所有资源属性以修改资源。
  • @PatchMapping:对应HTTP的PATCH请求,提交资源部分修改的属性。
  • @DeleteMapping:对应HTTP的DELETE请求,删除服务器端的资源。

从描述中可以看出5个注解主要是针对HTTP的动作而言的,通过它们就能够有效地支持REST风格的规范。

在REST风格的设计中,如果是简单的参数,往往会通过URL直接传递,在SpringMVC可以使用注解@PathVariable进行获取,这样就能够满足REST风格传递参数的要求。对于那些复杂的参数,例如,你需要传递一个复杂的资源需要十几个甚至几十个字段,可以考虑使用请求体JSON的方式提交给服务器,这样就可以使用注解@RequestBody将JSON数据集转换为Java对象。

通过@RequestMapping、@GetMapping等注解就能把URI定位到对应的控制器方法上,通过注解@PathVariable就能够将URI地址的参数获取,通过@RequestBody可以将请求体为JSON的数据转化为复杂的Java对象,其他均可以依据Spring MVC的参数规则进行处理。这样就能够进入到对应的控制器,进入控制器后,就可以根据获取的参数来处理对应的逻辑。最后可以得到后台的数据,准备渲染给请求。在现今的开发中,数据转化为JSON是最常见的方式,这个时候可以考虑使用注解@ResponseBody,这样Spring MVC就会通过MappingJackson2HttpMessageConverter最终将数据转换为JSON数据集,而在Spring MVC对REST风格的设计中,甚至可以使用注解@RestController让整个控制器都默认转换为JSON数据集。这些在后续章节中也会谈到。实际上有时候还需要转变为其他的数据形式,如URI可能请求的是一幅图片、一段视频等。这显然就是REST的表现形式。为了克服这个问题,Spring提供了一个协商资源的视图解析器——ContentNegotiatingViewResolver,关于它后面会再讨论。

3.2 使用@RestController

因为现在前后端分离,所以使用JSON作为前后端交互已经十分普遍。如果每一个方法都加入@ResponseBody才能将数据模型转换为JSON,这显然有些冗余。Spring MVC在支持REST风格中还存在一个注解@RestController,通过它可以将控制器返回的对象转化为JSON数据集。

这里采用了@RestController定义,这样对于getUser 返回的对象就会转变为JSON数据集了。为了使得JSP视图也能够被渲染成功,原本通过直接返回字符串的方式就不能再用了。这时可以参考index方法,采用ModelAndView的返回,这样就能够让Spring MVC通过视图名称找到对应的视图,将数据模型进行渲染,展示JSP视图给用户观看。

4. 渲染结果

在讲解注解@RestController时,可以看到将数据转变为JSTL或者JSON展示给客户端。但是实际上还可能有更多的资源类型的视图,如PDF、Excel等媒体类型(MediaType)。在第9章和第10章中,对于返回数据表现在流程中可能存在两种:一种是类似注解@ResponseBody那样,在返回结果后,由处理器使用已经注册在Spring IoC容器中的HttpMessageConverter接口实现类——MappingJackson2HttpMessageConverter进行直接转换,这时就不需要再使用视图解析器对数据模型进行处理;另外一种是类型是代码清单11-19那样,使用ModelAndView捆绑视图,然后让后面的视图解析器进行处理。

先讨论使用HttpMessageConverter接口实现类的方案。在Spring MVC中,IoC容器启动时注册了两个HttpMessageConverter接口的实现类,它们分别为StringHttpMessageConverter和Mapping Jackson2HttpMessageConverter。HttpMessageConverter接口中定义了一个canWrite方法:

boolean canWrite(Class<?> clazz, MediaType mediaType)

它返回一个布尔值,而MediaType则是可以传入的媒体类型,Spring MVC在执行控制器的方法后回去遍历注册的HttpMessageConverter接口的实现类使用canWrite方法去判断是否拦截控制器的返回。

在@RequestMapping、GetMapping等注解中还存在consumes和produces两个属性。其中consumes代表的是限制该方法接收什么类型的请求体(body),produces代表的是限定返回的媒体类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回。例如,如果只希望返回一个用户名称字符串,而不是JSON或者JSP页面,可以采用如代码清单11-20所示的方法。

代码清单11-20 使用字符串作为REST风格的表示层

package com.springboot.chapter11.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

import com.springboot.chapter11.enumeration.SexEnum;
import com.springboot.chapter11.pojo.User;
import com.springboot.chapter11.service.UserService;
import com.springboot.chapter11.vo.UserVo;

/**** imports ****/
@RestController
public class UserController2 {

   // 用户服务接口
   @Autowired
   private UserService userService = null;

   // 映射JSP视图
   @GetMapping(value = "/restful2")
   public ModelAndView index() {
      ModelAndView mv = new ModelAndView("restful");
      return mv;
   }

   // 获取用户
   @GetMapping(value = "/user2/{id}")
   public UserVo getUser(@PathVariable("id") Long id) {
      User user = userService.getUser(id);
      return changeToVo(user);
   }

   @GetMapping(value = "/user2/name/{id}",
         // 接受任意类型的请求体
         consumes = MediaType.ALL_VALUE,
         // 限定返回的媒体类型为文本
         produces = MediaType.TEXT_PLAIN_VALUE)
   public String getUserName(@PathVariable("id") Long id) {
      User user = userService.getUser(id);
      // 返回字符
      return user.getUserName();
   }
   
   // 转换PO变为VO
   private UserVo changeToVo(User user) {
      UserVo userVo = new UserVo();
      userVo.setId(user.getId());
      userVo.setUserName(user.getUserName());
      userVo.setSexCode(user.getSex().getCode());
      userVo.setSexName(user.getSex().getName());
      userVo.setNote(user.getNote());
      return userVo;
   }

   // 转换Vo变为PO
   private User changeToPo(UserVo userVo) {
      User user = new User();
      user.setId(userVo.getId());
      user.setUserName(userVo.getUserName());
      user.setSex(SexEnum.getSexEnum(userVo.getSexCode()));
      user.setNote(userVo.getNote());
      return user;
   }
}

这个控制器声明了@RestController,则默认会使用JSON数据集作为结果,那么它就会默认方法标注为“application/json;charset=UTF-8”。这样在控制器getUser方法结束后,Spring就会遍历注册好HttpMessageConverter接口的实现类,而其中已经注册好的MappingJackson2HttpMessageConverter的canWrite方法就会返回true,那么它就会启用MappingJackson2HttpMessageConverter将其转换为JSON数据集。

对于getUserName方法,它则与getUser方法完全不一样,因为@GetMapping的属性consumes声明为接收所有的请求体(Body),所以它可以接收任何的请求体,而对于结果则声明为普通文本类型,也就是修改了原有@RestController默认的JSON类型,同样结果也会被Spring MVC自身注册好的StringHttpMessageConverter拦截,这样就可以转变为一个简单的字符串。

对于HttpMessageConverter机制没有处理的数据模型,按Spring MVC的流程,它会流转到视图解析器(ViewResolver),正如代码清单11-19中可以看到使用了ModelAndView。在Spring对REST风格的支持中,还会提供协商视图解析器——ContentNegotiatingViewResolver。它是一个中介,在控制器返回结果找不到HttpMessageConverter解析时,就会流转到它那里,这样它就会对返回的结果进行解析。例如,返回的是ModelAndView,则它会去处理这个ModelAndView,首先是解析这个View的类型,然后根据其返回,找到最好的视图解析器去处理。就代码清单11-19来说即找到InternalResource ViewResolver进行处理,进而找到对应的JSP进行渲染。实际上Spring MVC 已经内置好了以下视图解析器。

  • BeanNameViewResolver:根据请求URI名称找到对应的视图。
  • ViewResolverComposite:视图解析器组合。
  • InternalResourceViewResolver:逻辑视图解析器,也是最常用的解析器。

一般来说,只需要使用它们就可以得到想要的对应的视图,对于JstlViewResolver也是一个InternalResourceViewResolver的子类。因此在使用ModelAndView给出视图名称后即可找到对应的JSP视图。

5. 处理HTTP状态码、异常和响应头

本章之前的内容只是讨论了能够找到数据的资源处理,而没有讨论当没有找到资源的处理或者说是在发生异常时应当如何处理。当发生资源找不到或者处理逻辑发生异常时,需要考虑的是返回给客户端的HTTP状态码和错误消息的问题。为了简化这些开发,Spring提供了实体封装类ResponseEntity和注解@ResponseStatus。ResponseEntity可以有效封装错误消息和状态码,通过@ResponseStatus可以配置指定的响应码给客户端。

在大部分情况下,后台请求成功后会返回一个200的状态码,代表请求成功。但是有时候这些还不够具体,例如,新增了用户,使用200状态码固然是没错,但使用201状态码会更加具体一些,因为201状态码代表着新增资源成功,200只是代表请求成功而已。这时就可以使用ResponseEntity类或者@ResponseStatus来标识本次请求的状态码。除了可以在HTTP响应头中加入属性响应码之外,还可以给响应头加入属性来提供成功或者失败的消息。下面修改插入用户的方法,将状态码修改为201,并且插入响应头的属性来标识这次请求的结果,如代码清单11-21所示。

@PostMapping(value = "/user2/entity")    public ResponseEntity<UserVo> insertUserEntity(            @RequestBody UserVo userVo) {        User user = this.changeToPo(userVo);        userService.insertUser(user);        UserVo result = this.changeToVo(user);        HttpHeaders headers = new HttpHeaders();        String success =             (result == null || result.getId() == null) ? "false" : "true";        // 设置响应头,比较常用的方式        headers.add("success", success);        // 下面是使用集合(List)方式,不是太常用        // headers.put("success", Arrays.asList(success));        // 返回创建成功的状态码        return new ResponseEntity<UserVo>(result, headers, HttpStatus.CREATED);}    @PostMapping(value = "/user2/annotation")    // 指定状态码为201(资源已经创建)    @ResponseStatus(HttpStatus.CREATED)    public UserVo insertUserAnnotation(@RequestBody UserVo userVo) {        User user = this.changeToPo(userVo);        userService.insertUser(user);        UserVo result = this.changeToVo(user);        return result;    }

在这段代码中,insertUserEntity方法中定义返回为一个ResponseEntity的对象,这里还生成了响应头(HttpHeaders对象),并且添加了属性success来表示请求是否成功,在最后返回的时刻生成了一个ResponseEntity对象,然后将查询到的用户对象和响应头捆绑上,并且指定状态码为201(创建资源成功)。在insertUserAnnotation方法上则使用了@ResponseStatus注解将HTTP的响应码标注为201(创建资源成功),所以在方法正常返回时Spring就会将响应码设置为201。为了测试这些代码,还可以开发一段JavaScript脚本进行测试,如代码清单11-22所示。

5.1 异常处理

但是有时候会出现一些异常,例如,按照编号(id)查找用户,可能查找不到数据,这个时候就不能以正常返回去处理了,又或者在执行的过程中产生了异常,这也是需要我们进行处理的。回到10.10.4节中曾经学过的@ControllerAdvice和@ExceptionHandler注解的使用,其中注解@ControllerAdvice是用来定义控制器通知的,@ExceptionHandler则是指定异常发生的处理方法。利用这些知识就能够处理异常了,不过在此之前先定义查找失败的异常,如代码清单11-23所示。

package com.springboot.chapter11.exception;public class NotFoundException extends RuntimeException {   private static final long serialVersionUID = 1L;   // 异常编码   private Long code;   // 异常自定义信息   private String customMsg;   public NotFoundException() {   }   public NotFoundException(Long code, String customMsg) {      super();      this.code = code;      this.customMsg = customMsg;   }   public Long getCode() {      return code;   }   public void setCode(Long code) {      this.code = code;   }   public String getCustomMsg() {      return customMsg;   }   public void setCustomMsg(String customMsg) {      this.customMsg = customMsg;   }}

这里自定义了异常类,它继承了RuntimeException,所以可以在找不到用户的时刻抛出该异常。而在控制器抛出异常后,则可以在控制器通知(@ControllerAdvice)中来处理这些异常,这个时候就需要使用注解@ExceptionHandler了。实际上,在Spring Boot的机制里早已准备好了org.springframework. boot.autoconfigure.web.BasicErrorController对象去处理发生的异常,当然这并不是很友好,有时候你可能希望得到友好的页面,这就需要你自定义控制器通知了。下面定义一个自定义的控制器通知,如代码清单11-24所示。

代码清单11-24 定义控制器通知来处理异常

package com.springboot.chapter11.exception;import java.util.HashMap;import java.util.Map;import javax.servlet.http.HttpServletRequest;import org.springframework.http.HttpStatus;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.bind.annotation.ResponseStatus;import org.springframework.web.bind.annotation.RestController;//控制器通知@ControllerAdvice(      // 指定拦截包的控制器      basePackages = { "com.springboot.chapter11.controller.*" },      // 限定被标注为@Controller或者@RestController的类才被拦截      annotations = { Controller.class, RestController.class })public class VoControllerAdvice {   // 异常处理,可以定义异常类型进行拦截处理   @ExceptionHandler(value = NotFoundException.class)   // 以JSON表达方式响应   @ResponseBody   // 定义为服务器错误状态码   @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)   public Map<String, Object> exception(HttpServletRequest request, NotFoundException ex) {      Map<String, Object> msgMap = new HashMap<>();      // 获取异常信息      msgMap.put("code", ex.getCode());      msgMap.put("message", ex.getCustomMsg());      return msgMap;   }}

这里使用了@ControllerAdvice来标注类,说明在定义一个控制器通知。配置了它所拦截的包,限定了拦截的那些被标注为注解@Controller和@RestController的控制器,按照其定义就能够拦截本章开发的控制器(UserController和UserController2)了。这里的@ExceptionHandler定义了拦截NotFoundException的异常,@ResponseBody定义了响应的信息以JSON格式表达,@ResponseStatus定义了状态码为500(服务器内部错误),这样就会把这个状态码传达给请求者。

为了测试这个控制器通知对异常的处理,这里在控制器(UserController)加入代码清单11-25所示的代码。

代码清单11-25 测试控制器通知异常处理

// 响应成功@ResponseStatus(HttpStatus.OK)@ResponseBodypublic UserVo getUserForExp(@PathVariable("id") Long id) {    User user = userService.getUser(id);    // 如果找不到用户,则抛出异常,进入控制器通知    if (user == null) {        throw new NotFoundException(1L, "找不到用户【" + id +"】信息");    }    UserVo userVo = changeToVo(user);    return userVo;}

这里可以看到方法标注响应码为200(请求成功),如果查找用户失败,那么就抛出NotFoundException异常。一旦这个异常抛出,就会被控制器通知所拦截,最终经由@ExceptionHandler定义的方法所处理,这样就能够对异常进行处理了。图11-2所示是我测试查找不到用户时出现的场景。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j8ltDKFG-1625122941247)(/Users/apple/Desktop/笔记 (2)]/JavaWeb/SpringBoot/Rest/REST.assets/image-20210701095628013.png)

从图11-2可以看出,请求了编号为986的用户信息,这个用户信息不存在就跳转到控制器通知对应的方法里。该方法处理后返回错误消息,并将状态码修改为500(服务器错误)。通过上述便能够处理那些发生异常的请求。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值