一、REST和RESTful

1.1 Resource

  • 所谓"资源",就是网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的实在。你可以用一个URI(统一资源标识符)指向它,每种资源对应一个特定的URI。要获取这个资源,访问它的URI就可以,因此URI就成了每一个资源的地址或独一无二的识别符。(URL是URI的子集,即具体实现)

    • http://rukihuang.xyz/index.php/archives/92/

1.2 Representation

  • "资源"是一种信息实体,它可以有多种外在表现形式。我们把"资源"具体呈现出来的形式,叫做它的"表现层"(Representation)。
  • 比如,文本可以用txt格式表现,也可以用HTML格式、XML格式、JSON格式表现,甚至可以采用二进制格式;图片可以用JPG格式表现,也可以用PNG格式表现。
  • URI只代表资源的实体,不代表它的形式。它的具体表现形式,应该在HTTP请求的头信息中用AcceptContent-Type字段指定,这两个字段才是对"表现层"的描述

    • accept:application/json
    • content-type:application/json

1.3 State Transfer

  • 访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化。
  • 互联网通信协议HTTP协议,是一个无状态协议。这意味着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生"状态转化"(State Transfer)。而这种转化是建立在表现层之上的,所以就是"表现层状态转化"。

    • 改变:(服务器端)资源的状态
    • 新增:从无到有状态的变化
    • 更新:从某个状态变成另外一种状态的转化
    • 删除:从有到无状态的变化

1.4 Uniform Interface

  • 统一接口:REST要求,必须通过统一的接口来对资源执行各种操作。对于每个资源只能执行一组有限的操作。
  • HTTP1.1协议为例:

    • 7个HTTP方法:GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS
    • HTTP头信息(可自定义)
    • HTTP响应状态代码(可自定义)
  • REST还要求,对于资源执行的操作,其操作语义必须由HTTP消息体之前的部分完全表达,不能将操作语义封装在HTTP消息体内部。

    • 以前:

      • localhost:8080/employee/saveOrUpdate.do?id=1&name=xx
    • 现在:

      • localhost:8080/employees

二、关于应用接口

  • 系统的接口:很多情况下,需要把系统的功能作为服务暴露给外部的其他应用使用或者给移动端使用,就需要把系统中的服务作为接口暴露出去,一般分为公共接口(发短信,天气服务)和私用接口(公司内部使用的)。

三、RESTful设计

3.1 资源设计

3.1.1 路径

  • 使用以前的方式完成需求:

    • 1,帖子列表,支持过滤和分页;

      • http://bbs.example.cn/bbs_list.do?keyword=xx&currentPage=1
    • 2,查看一篇帖子;

      • http://bbs.example.cn/bbs_view.do?id=xx
    • 3,查看一篇帖子的所有回帖;

      • http://bbs.example.cn/bbs_replay_list.do?id=xx
    • 4,回帖;

      • http://bbs.example.cn/bbs_replay.do?id=xx&content=xxx
    • 5,发布一篇帖子;

      • http://bbs.example.cn/bbs_post.do?title=xx&content=xxx
    • 6,修改一篇帖子;

      • http://bbs.example.cn/bbs_update.do?title=xx&content=xxx&id=xx
    • 7,删除一篇帖子;

      • http://bbs.example.cn/bbs_delete.do?id=xx
    • 这种方式的问题:大量的接口方法,URL地址设计复杂,需要在URL里面表示出资源及其操作;路径又称"终点"(endpoint),表示API的具体网址。
  • 在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以API中的名词也应该使用复数。

    • https://api.example.com/v1/zoos:动物园资源
    • https://api.example.com/v1/animals:动物资源
    • https://api.example.com/v1/employees:饲养员资源
  • 参考例子

    • https://api.github.com/
    • http://docs.jiguang.cn/jmessage/server/rest_api_im/
  • 总结:

    • 每个url代表一种资源. uri上不能有动词, 只能是名词,并且一般都和数据库的表名相同,并且是使用复数

      • /sessions post /sessions delete
      • 上传图片的接口 /images post /images delete

3.2 动作设计

3.2.1 HTTP动作

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性【补丁】)。
  • DELETE(DELETE):从服务器删除资源。
  • HEAD:获得一个资源的元数据,比如一个资源的hash值或者最后修改日期;
  • OPTIONS:获得客户端针对一个资源能够实施的操作;(获取该资源的api(能够对资源做什么操作的描述))

3.2.2 动作示例

  • GET /zoos:列出所有动物园
  • POST /zoos:新建一个动物园
  • GET /zoos/ID:获取某个指定动物园的信息
  • PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
  • PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)
  • DELETE /zoos/ID:删除某个动物园
  • GET /zoos/ID/animals:列出某个指定动物园的所有动物 /zoos/1/animals

获取某个部门的所有员工

3.3 返回结果

3.3.1 返回值类型

  • GET /zoos:返回资源对象的列表(数组/集合)
  • GET /zoos/1:返回单个资源对象
  • POST /collection:返回新生成的资源对象
  • PUT /collection/resource:返回完整的资源对象
  • PATCH /collection/resource:返回完整的资源对象
  • DELETE /collection/resource:返回一个空文档

3.3.2 常见状态码

状态码描述备注
200OK - [GET]服务器成功返回用户请求的数据。
201CREATED - [POST/PUT/PATCH]用户新建或修改数据成功。
202Accepted - [*]表示一个请求已经进入后台排队(异步任务)
204NO CONTENT - [DELETE]用户删除数据成功。
400INVALID REQUEST - [POST/PUT/PATCH]用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
401Unauthorized - [*]表示用户没有权限(令牌、用户名、密码错误)。
403Forbidden - [*]表示用户得到授权(与401错误相对),但是访问是被禁止的。
404NOT FOUND - [*]用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
406Not Acceptable - [GET]用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
410Gone -[GET]用户请求的资源被永久删除,且不会再得到的。
422Unprocesable entity - [POST/PUT/PATCH]当创建一个对象时,发生一个验证错误。

3.3.3 Content-Type

  • 一个API可以允许返回JSON,Xml甚至HTML等文档格式;建议使用json;
  • 以前通过URL来规定获取得格式类型,比如:

    • https://api.example.com/employee.json
    • https://api.example.com/employee.html
  • 但是更建议使用Accept这个请求头;
  • Accept与Content-Type的区别:

    1. Accept属于请求头, Content-Type属于实体头。 Http报头分为通用报头,请求报头,响应报头和实体报头。

      1. 请求方的http报头结构:通用报头|请求报头|实体报头
      2. 响应方的http报头结构:通用报头|响应报头|实体报头
    2. Accept代表发送端(客户端)希望接受的数据类型。 比如:

      1. Accept:application/json 代表客户端希望接受的数据类型是json类型,后台返回json数据
      2. Content-Type代表发送端(客户端|服务器)发送的实体数据的数据类型。
      3. 比如:Content-Type:application/json;代表发送端发送的数据格式是json, 后台就要以这种格式来接收前端发过来的数据。
      4. 二者合起来, Accept:application/jsonContent-Type:application/json;即代表希望接受的数据类型是json格式,本次请求发送的数据的数据格式也是json格式。

四、RESTful服务开发

4.1 Java中常见的RESTful开发框架

4.1.1 jersey

  • Jersey RESTful 框架是开源的RESTful框架, 实现了JAX-RS (JSR 311 & JSR 339) 规范。它扩展了JAX-RS 参考实现, 提供了更多的特性和工具, 可以进一步地简化 RESTful service 和 client 开发。尽管相对年轻,它已经是一个产品级的 RESTful service 和 client 框架。

    • 优点:

      • 优秀的文档和例子
      • 快速
      • 平滑的 JUnit 集成
      • 就个人而言, 当开发 RESTful service 时, JAX-RS(使用RESTful 风格来开发web service服务的规范) 实现要好于 MVC 框架。
      • 可以集成到其它库/框架 (Grizzly, Netty). 这也可能是很多产品使用它的原因。
      • 支持异步链接
      • 不喜欢 servlet container? 使用Jersey的时候可以不用它们。
      • WADL, XML/JSON support包含在Glassfish中
    • 缺点:

      • Jersey 2.0+使用了有些复杂的依赖注入实现 一大堆第三方库只支持 Jersey 1.X, 在 Jersey 2.X 不可用

4.1.2 play

  • Play Framework:使用Play Framework 很容易地创建,构建和发布 web 应用程序,支持 Java & Scala。它使用Akka, 基于一个轻量级的无状态的架构。它应该应用于大规模地低CPU和内存消耗的应用。

    • 优点

      • 易于开发
      • 快,但是没有其它的一些框架快
      • 基于 Netty, 支持非阻塞的 I/O. 并行处理远程调用的时候很优秀
      • 社区很大
      • 快速的项目构建和启动
      • 模块化
      • MVC
      • REST, JSON/XML, Web Sockets, non-blocking I/O
      • 只需刷新浏览器就可以看到最新的改变
      • 支持Async
      • 有出版的书
    • 缺点

      • 版本2.0 是最有争议的Java框架。 切换至Scala会比较头痛.
      • 不向后兼容; Play 2.X 重写了
      • 号称轻量级,但有些臃肿
      • SBT构建工具. 号称 Maven 杀手, 但是从没有优秀到替换它。
      • 难以学习和配置非 servlet

4.1.3 SpringMVC

4.2 使用SpringMVC开发RESTful服务

4.2.1 API接口测试——Postman

4.2.2 注解详解

@RequestMapping参数化

  • @RequestMapping(path = "/employees", method = RequestMethod.GET)等价于@GetMapping("/employees")
/**
 * 员工资源控制器
 */
@Controller
public class EmployeeController {

    /**
     * 获取所有员工
     * 1. 确定资源 /employees
     * 2. 确定请求方式 GET
     * 3. 确定返回结果(类型、头信息、状态码), 员工集合, content-type=application/json, 200
     * @return
     */
    @RequestMapping(path = "/employees", method = RequestMethod.GET)
    //@GetMapping("/employees")
    @ResponseBody
    public List<Employee> findAll() {
        ArrayList<Employee> list = new ArrayList<>();
        list.add(new Employee(1L, "张三"));
        list.add(new Employee(2L, "李四"));
        return list;
    }
}
  • @RequestMapping参数

    • path等价于value,为路径
    • method为请求方法

      • RequestMethod.GET
      • RequestMethod.POST ...
    • params:规定请求时必须指定的参数名和参数值
    • headers:规定请求时必须带有指定的头信息
    • consumes:消费,相当于配置了 headers = "content-type/html",服务器要求客户端请求必须要有的请求头信息
    • produces:生产,相当于配置了headers="accept=text/html",客户端要求服务器必须生产出固定格式的内容
package com.ruki.restful.controller;

import com.ruki.restful.domain.Employee;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("test")
public class TestController {
    /**
     * params是规定请求时必须指定的参数名和参数值
     * 代码例子:代表请求时,必须带有name参数,并且值也必须是admin
     */
    @RequestMapping(params = "name=admin")
    public void test() {
        System.out.println("test方法");
    }

    /**
     * @RequestParam 注解,规定请求时必须带有指定的参数,但是参数值不规定
     *
     * @param username
     * @param password
     */
    @RequestMapping("/login")
    public void login(@RequestParam String username, @RequestParam String password) {
        System.out.println("login方法");
    }

    /**
     * headers规定,请求时必须带有指定的头信息
     */
    @RequestMapping(value = "/header", headers = "content-type=text/html")
    public void header1() {
        System.out.println("headers1...html");
    }

    @RequestMapping(value = "/header", headers = "content-type=text/xml")
    public void header2() {
        System.out.println("header2...xml");
    }

    /**
     * 消费
     * 相当于配置了 headers = "content-type/xml",
     * 前台传过来xml数据,后台才会消费(吃)这个数据,并运行之后的程序
     */
    @RequestMapping(value = "/consumes", consumes = "text/xml")
    public void consumes1() {
        System.out.println("consume方法...xml");
    }

    @RequestMapping(value = "/consumes", consumes = "text/html")
    public void consumes2() {
        System.out.println("consume方法...html");
    }

    /**
     * 生产
     * 前台希望后台生产出html页面
     * 相当于配置了headers="accept=text/html"
     * 还相当于响应的头信息中有 content-type=text/xml
     */
    @RequestMapping(value = "/produces", produces = "text/html")
    public void produce() {
        System.out.println("produce...html");
    }

    /**
     * @RequestBody 把请求体中的所有内容都封装到指定的对象中
     * @param employee
     */
    @RequestMapping("/employee")
    public void test1(@RequestBody Employee employee) {
        // (@RequestBody String str) , 先String的json字符串,然后自己再用JSON工具转换也可以
        System.out.println(employee);
    }
}

@PathVariable

  • 必须使用该注解获取路径上的参数
package com.ruki.restful.controller;

import com.ruki.restful.domain.Employee;
import com.ruki.restful.domain.Salary;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 员工资源控制器
 */
@Controller
public class EmployeeController {
    /**
     * 获取某个员工的信息(路径传参)
     * 1. 确定资源 /employees/{name} 使用路径占位符{}
     * 2. 确定请求方式 GET
     * 3. 确定返回结果(类型、头信息、状态码), 员工对象, content-type=application/json, 201
     *
     * 如果@PathVariable 注解没有设置value,默认就是去路径上找相同名称的参数
     * @return
     */
    @GetMapping("/employees/{id}")
    @ResponseBody
    public Employee getById(@PathVariable("id") Long id) {
        // 由于从路径中取参数,所以必须在参数前加上 @PathVariable
        // 如果有很多参数最好加上指定的名字
        // 不加value , 默认找名字相同的
        // 不然只能取 ?id=2 这种形式的参数
        return new Employee(id, "tempEmp");
    }

    /**
     * 删除某个员工(路径传参)
     * 1. 确定资源 /employees/{name} 使用路径占位符{}
     * 2. 确定请求方式 DELETE
     * 3. 确定返回结果(类型、头信息、状态码), 员工对象, 空文档, 204
     *
     * 如果@PathVariable 注解没有设置value,默认就是去路径上找相同名称的参数
     * @return
     */
    @DeleteMapping("/employees/{id}")
    @ResponseBody
    public void deleteEmp(Long id, HttpServletResponse response) {
        System.out.println("成功删除id为" + id + "的员工!");
        response.setStatus(HttpServletResponse.SC_NO_CONTENT);//204 状态码,没有内容的返回值
    }

    /**
     * 获取某个员工某个月的薪资记录(路径传参)
     * 1. 确定资源 /employees/{id}/salaries/{month}
     * 2. 确定请求方式 GET
     * 3. 确定返回结果(类型、头信息、状态码), 薪资对象, content-type=application/json, 200
     *
     * @DateTimeFormat 前台传日期参数到后台接收时使用的注解,就是只负责把这个格式的日期找到,领进来后就不管了
     * @JsonFormat 后台返回json数据给前台时使用的注解
     *
     * @return
     */
    @GetMapping("/employees/{id}/salaries/{month}")
    @ResponseBody
    public Salary getSalaryById(@PathVariable("id") Long empId,
                                @PathVariable("month") @DateTimeFormat(pattern = "yyyy-MM") Date month) {
        return new Salary(1L, empId, BigDecimal.valueOf(100), month);
    }

    /**
     * 给某个员工添加薪资记录(路径传参)
     * 1. 确定资源 /employees/{id}/salaries 其余month、money参数以表单形式传入 postman -> body -> x-www-form-urlencoded
     * 2. 确定请求方式 POST
     * 3. 确定返回结果(类型、头信息、状态码), 薪资对象,content-type=application/json, 201
     *
     * 在Salary实体类的date上,加上@DateTimeFormat  @JsonFormat 这两个注解
     * 保证按固定格式保存到数据库,按固定格式返回出去
     *
     * 路径占位符中的参数,可以自动封装到自定义对象中的同名属性上
     * @return
     */
    @PostMapping("/employees/{employeeId}/salaries")
    @ResponseBody
    public Salary save(Salary salary) { //路径参数名 和 表单参数名 必须和对象实体类的名字一样 才会自动封装绑定
        return salary;
    }

    @PutMapping("/employees/{name}")
    @ResponseBody
    public Employee update(Employee emp) { //路径参数名 和 表单参数名 必须和对象实体类的名字一样 才会自动封装绑定
        System.out.println("PUT方法:" + emp);
        return emp;
    }
}

@RestController

  • @RestController=@ResponseBody+@Controller

    • 前者将返回值转化为JSON类型
    • 后者为控制器的标识

@RequestBody

  • 将前台传入的JSON对象,绑定实体类对象

4.3 请求处理

4.3.1 ajax

  • 需要在web.xml文件中配置过滤器httpPutFormContentFilterController方法才能将put请求方式的参数成功接收。
    //ajax请求
    <script src="js/jquery-2.1.0.min.js"></script>
    <script>
        $(function() {
            $("#btn").click(function () {
                $.ajax({
                   url: "/employees/{name}",
                   type: "put", //PUT请求方式
                   data: {
                       name: "张三" //要传的ajax参数,必须配置过滤器,不然无法接收到参数
                   }
                });
            });
        });
    </script>
  • httpPutFormContentFilter过滤器
<!--  处理put或patch请求方式的过滤器-->
<filter>
    <filter-name>httpPutFormContentFilter</filter-name>
    <filter-class>org.springframework.web.filter.HttpPutFormContentFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>httpPutFormContentFilter</filter-name>
    <servlet-name>dispatcherServlet</servlet-name>
</filter-mapping>

4.3.2 form

  • form表单中只有GETPOST两种请求方法,必须设置HiddenHttpMethodFilter过滤器,以隐藏域的形式,将PUT请求方式传入到后台
  • <input type="hidden" name="_method" value="put">

    • name必须设置为_method

      • 源码判断的名称是_method
    <form action="/employees/{name}" method="post">
        <input type="hidden" name="_method" value="put">
        <input type="text" name="name">
        <input type="submit" value="提交">
    </form>
  • HiddenHttpMethodFilter过滤器
<!-- 浏览器不支持put,delete等method,由该filter将/blog?_method=delete转换为标准的http delete方法 -->
<filter>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <servlet-name>dispatcherServlet</servlet-name>
</filter-mapping>
Last modification:December 7th, 2019 at 08:52 pm