RESTful风格

1、REST概念

REST(英文: Representational State Transfer,简称REST, 意思:表述性状态转换,描述了一个架构样式的网络系统,比如web应用)。

它是一种软件架构风格、 设计风格,而不是标准,只是提供了一组设计原则和约柬条件,它主要用于客户端和服务端交互类的软件。基于这个风格设计的软件可以更简介,更有层次,更易于实现缓存等机制。

它本身并没有什么使用性,其核心价值在于如何设计出符合REST风格的网络接口。

2、RESTful概念

REST指的是一组架构约束条件和原则。 满足这些约束条件和原则的应用程序或设计就是RESTful,

  • RESTful的特性:
    • 资源(Resources):互联网所有的事物都可以被抽象为资源。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的存在。可以用一个URI (統一资源定位符)指向它,每种资源对应一个特性的URI。 要获取这个资源,访问它的URI就可以,因此URI即为每一个资源的独一 无二 的识别符。
    • 表现层(Representation):把资源具体星现出来的形式,叫做它的表现层(Representation)。 比如,文本可以用txt格式表现,也可以用HTML格式、XML格式、JSON格式表现,甚至可以采用二进制格式。
    • 状态转换(State Transfer):每发出一个请求,就代表了客户端和服务器的一次交互过程。HTTP协议,是一个无状态协议,即所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生“状态转换"(State Transfer)。而这种转换是建立在表现层之上的,所以就是”表现层状态转换"。

具体来说就是HTTP协议里面,四个表示操作方式的动词: GETPOSTPUTDELETE他们分别对应四种基本操作: GET用来获取资源,POST用来新建资源,PUT用来更新资源,DELETE用来删除资源。

原来的操作资源的方式:

localhost:8080/getExpress.do?id=1
localhost:8080/saveExpress.do?
localhost:8080/updateExpress.do?
localhost:8080/deleteExpress.do?id=1

原来用的方式当然没有问题,但是如果有更简洁的方式就更好了,此时就是RESTful风格。
使用RESTful操作资源:

GET /expresses#查询所有的快递信息列表
GET /express/1006#查询一个快递信息
POST /express#新建一个快递信息
PUT/express/1006#更新一个快递信息(全部更新)
PATCH /express/1006#更新一个快递信息(部分更新)
DELETE /express/1006#删除一个快递信息

3、API设计/URL设计

动词+宾语

RESTful的核心思想就是:客户端的用户发出的数据操作指令都是”动词+宾语“的结构。比如,GET/expresses这个命令,GET是动词,/expresses是宾语。

于是,在编写controller的方法的时候,就可以使用springmvc提供的@RequestMapping注解的method属性来指定它可以 用于处理什么类型的请求。

动词通常就是五种HTTP方法,对应CRUD 操作。
GET:读取(Read)
POST:新建(Create)
PUT:更新(Update) 
PATCH:更新(Update),通常是部分更新
DELETE:删除(Delete)

PS: 1、根据HTTP规范,动词一律大写。
	2、一些代理只支持POST和GET方法,为了使用这些有限方法支持RESTful API, 需要一种办法覆盖http原来的方法。 使用订制的HTTP头X-HTTP-Method-Override来覆盖POST方法。

宾语必须是名词

宾语就是API的URL,是HTTP动词作用的对象。它应该是名词,不能是动词。
比如,/expresses 这个URL就是正确的。

以下这些URL都是不推荐的,因为带上了动词,不是推荐写法。
/getAllExpresses
/getExpress
/createExpress
/deleteAllExpress

避免多级URL

如果资源中有多级分类,也不建议写出多级的URL。例如要获取球队中某个队员,有人可能这么写:

​ GET /team/1001/player/1005

这种写法的语义不够明确,所以推荐使用查询字符串做后缀,改写为

​ GET /team/1001? player=1005.

再例如查询所有的还未职出的快递,你该如何编写呢?
GET /expresses/statu 不推荐
GET /expresses?statumfalse 推荐

4、HTTP状态码

客户端的用户发起的每一次请求, 服务器都必须给出响应。响应包括HTTP状态码数据两部分。

HTTP状态码就是一个三位数,分成五个类别。这五大类总包含了100多种状态码(不需要全都记住,不用紧张哈,覆盖了绝大部分可能遇到的情况。每一种状态码都有标准的(或者约定的)解释,客户端只需查看状态码,就可以判断出发生了什么情况,所以服务器应该返回尽可能精确的状态码。

五类状态码分别如下:
	1xx:相关信息
	2xx:操作成功
	3xx:重定向
	4xx:客户端错误
	5xx:服务器错误
	
	PS:API 不需要1xx状态码,所以这里可以忽略这个类别

状态码2xx

200状态码表示操作成功,但是不同的方法可以返回更精确的状态码

GRT:200 OK 表示一切正常
POST:201 Created 表示新的资源已经成功创建
PUT:200 OK
PATCH:200 OK
DELETE:204 No Content 表示资源已经成功删除

状态码3xx

API用不到301状态码(永久重定向)和302状态码(暂时重定向,307 也是这个含义),因为它们可以由应用级别返回,浏览器会直接跳转,API级别可以不考虑这两种情况。

API用到的3xx状态码,主要是303 See other,表示参考另一个URL。它与302和307的含义一样,也是暂时重定向",区别在于302和307用于GET请求,
而303用于POST、PUT 和DELETE请求。收到303以后,浏览器不会自动跳转,而会让用户自己决定下一步怎么办。

我们只需要关注一下304状态码就可以了
304 : Not Modified 客户端使用缓存数据

状态码4xx

4xx状态码表示客户端错误。

400 Bad Request:服务器不理解客户端的请求,未做任何处理。
401 Unauthorized: 用户未提供身份验证凭据,或者没有通过身份验证。
403 Forbidden:用户通过了身份验证,但是不具有访问资源所需的权限。
404 Not Found: 所请求的资源不存在,或不可用。
405 Method Not Allowed: 用户已经通过身份验证,但是所用的HTtp 方法不在他的权限之内。
410 Gone:所请求的资源已从这个地址转移,不再可用。
415 Unsupported Media Type: 客户端要求的返回格式不支持。比如,API只能返回JSON 格式,但是客户端要求返回XL格式。
422 Unprocessable Entity : 客户端上传的附件无法处理,导致请求失败。
429 Too Many Requests:客户端的请求次数超过限额。

状态码5xx

5xx状态码表示服务端错误。一般来说,API不会向用户透露服务器的详细信息,所以只要两个状态码就够了。

500 Internal Server Error: 客户端请求有效,服务器处理时发生了意外。
503 Service Unavailable:服务器无法处理请求,一般用于网站护状态。

5、服务器响应

服务器返回的信息一般不推荐纯文本,而是建议大家选择JSON对象,因为这样才能返回标准的结构化数据。

所以,服务器回应的HTTP头的Content-Type属性要设为application/json.客户端请求时,也要明确告诉服务器,可以接受JSON格式,即请求的HTTP头的ACCEPT属性也要设成applicat ion/json.

当发生错误的时候,除了返回状态码之外,也要返回错误信息。所以我们可以自己封装要返回的信息。

6、案例

RESTful风格的查询

前端页面

<html>
<head>
    <title>RESTful</title>
    <script src="/js/jquery2.1.4.js"></script>
</head>
<body>
    <form id="myForm" action="" method="post">
        球队ID:<input type="text" name="teamID" id="teamID"><br/>
        球队名称:<input type="text" name="tName" id="teamName"><br/>
        球队位置:<input type="text" name="tLocation" id="teamLocation"><br/>
        <button type="button" id="btnGetAll">查询所有GET</button>
        <button type="button" id="btnGetOne">查询单个GET</button>
        <button type="button" id="btnPost">添加POST</button>
        <button type="button" id="btnPut">更新PUT</button>
        <button type="button" id="btnDel">删除DELETE</button>
    </form>
<p id="showResult"></p>
</body>
</html>
<script>
    //页面加载完毕之后给按钮绑定事件
    $(function (){
        //1、给查询所有GET按钮绑定事件
        $("#btnGetAll").click(function (){
            $.ajax({
                type: "GET",
                url: "/restful/teams",
                data: "",
                success: function(list){
                    alert( "Data Saved: " + list );
                    var str = "";
                    for(var i=0;i<list.length;i++){
                        var obj=list[i];
                        str+="球队ID:"+obj.teamID+",球队名称:"+obj.tName+",球队位置:"+obj.tLocation+"<br/>";
                    }
                    $("#showResult").html(str);
                }
            });
        });

        //2、给查询单个GET绑定事件
        $("#btnGetOne").click(function (){
            $.ajax({
                type: "GET",
                url: "/restful/team/"+$("#teamID").val(),//RESTful风格的API定义(传参格式)
                data: "",
                success: function(obj){
                    if(obj == ""){
                        $("#showResult").html("此数据不存在");
                        return;
                    }
                    alert( "Data Saved: " + obj );
                    var str ="球队ID:"+obj.teamID+",球队名称:"+obj.tName+",球队位置:"+obj.tLocation+"<br/>";
                    $("#showResult").html(str);
                }
            });
        });

        //3、给添加POST绑定事件
        $("#btnPost").click(function (){
            alert($("#myForm").serialize());//输出测试
            $.ajax({
                type: "POST",
                url: "/restful/team/",
                data: $("#myForm").serialize(),//表单的所有数据以?&形式追加在URL后:team?teamID=1001&teamName=队伍&teamLocation=队伍地址
                success: function(msg){
                    if(msg == "201"){
                        $("#showResult").html("添加成功");
                        return;
                    }
                }
            });
        });

        //4、给更新PUt绑定事件
        $("#btnPut").click(function (){
            $.ajax({
                type: "POST",
                url: "/restful/updateTeam/",
                data: $("#myForm").serialize()+"&_method=PUT",//表单的所有数据以?&形式追加在URL后:team?teamID=1001&teamName=队伍&teamLocation=队伍地址
                success: function(msg){
                    if(msg == "200"){
                        $("#showResult").html("更新成功");
                        return;
                    }else if(msg == ""){
                        $("#showResult").html("没找到对应的数据");
                    }
                }
            });
        });

        //5、给删除DELETE绑定事件
        $("#btnDel").click(function (){
            $.ajax({
                type: "POST",
                url: "/restful/deleteTeam/",
                data: $("#myForm").serialize()+"&_method=DELETE",//表单的所有数据以?&形式追加在URL后:team?teamID=1001&teamName=队伍&teamLocation=队伍地址
                success: function(msg){
                    if(msg == "204"){
                        $("#showResult").html("删除成功");
                        return;
                    }else if(msg == ""){
                        $("#showResult").html("没找到对应的数据");
                    }
                }
            });
        });
    });
</script>
/**
 * RESTful风格控制器Controller
 */
@Controller
@RequestMapping("/restful")
public class RestfulController {
    private static List<Team> teamList;//代替数据库存储数据
    static {    //静态代码块,类加载时存入数据用于查询测试
        teamList = new ArrayList<>(3);
        for(int i=1;i<=3;i++){
            Team team = new Team();
            team.setTeamID(1000+i);
            team.settName("队名"+i);
            team.settLocation("队伍地址"+i);
            teamList.add(team);
        }
    }

    /**
     * 查询所有的球队
     * @return
     */
    @RequestMapping(value = "/teams",method = RequestMethod.GET)
    @ResponseBody
    public List<Team> getAll(){
        System.out.println("查询所有数据------GETAll发起请求");
        return teamList;
    }
    /**
     * 查询单个球队
     * @return
     */
    @RequestMapping(value = "/team/{teamID}",method = RequestMethod.GET)
    @ResponseBody
    public Team getOne(@PathVariable("teamID") String teamID){
        System.out.println("查询单个数据------GETOne发起请求");
        for(Team team:teamList){
            if(Integer.valueOf(teamID) == team.getTeamID()){
                return team;
            }
        }
        return null;
    }
    /**
     * 添加球队信息
     * @return
     */
    @RequestMapping(value = "/team",method = RequestMethod.POST)
    @ResponseBody
    public String addPost(Team team){
        System.out.println(team);
        System.out.println("添加数据------addPost发起请求");
        teamList.add(team);
        return "201";
    }
    @RequestMapping(value = "/updateTeam",method = RequestMethod.PUT)
    @ResponseBody
    public String updatePut(Team team){
        System.out.println("更新信息-----updatePUT发起请求");
        for(Team t:teamList){
            if(t.getTeamID()==team.getTeamID()){
                t.settName(team.gettName());
                t.settLocation(team.gettLocation());
                return "200";
            }
        }
        return null;
    }
    @RequestMapping(value = "/deleteTeam",method = RequestMethod.DELETE)
    @ResponseBody
    public String deleteDelete(Team team){
        System.out.println(team);
        System.out.println("删除信息-----DELETE发起请求");
        for(Team t:teamList){
            if(t.getTeamID()==team.getTeamID()){
                teamList.remove(t);
                return "204";
            }
        }
        return null;
    }
    @RequestMapping("/hello")
    public String hello(){
        return "restful";
    }
}

RESTful风格的更新和删除遇到的问题

原因

实际上,ajax中如果不用data传递参数,只使用url传递参数,是没有问题的。
在Ajax中,采用RESTful风格的PUT和DELETE请求传递参数无效,传递到后台的参数值为null的问题

原因:
	Tomcat封装请求参数的过程:
		1.将请求体中的数据,封装成一个map
		2.request.getParameter("id")会从这个map中取值
		3.SpringMvc封装POJO对象的时候,会把POJO中每个属性的值进行request.getParamter();
	AJAX发送PUT或者DELETE请求时,请求体中的数据通过request.getParamter()拿不到。
	Tomcat一检测到是PUT或者DELETE就不会封装请求体中的数据为map, 只有POST形式的请求才封装请求为map.

解决方法

前端页面中发送ajax请求的时候在url中加"&_method=PUT"或者"&_method=DELETE"

如图:

RESTful风格-LMLPHP

然后web.xml中配置过滤器
配置的时候多个过滤器需要注意顺序

<!--使用Rest风格的URI将页面普通的post请求转为指定的delete或者put请求
	原理:在Aajx中发送post请求后,带method参数,将其修改为PUT,或者DELETE请求-->
	<filter>
        <filter-name>httpMethodFilter</filter-name>
        <filter-class>
            org.springframework.web.filter.HiddenHttpMethodFilter
        </filter-class>
    </filter>
    <filter-mapping>
        <filter-name>httpMethodFilter </filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

自行封装响应结果

实际开发时,通常自行约定封装响应结果

如:自定义一个类,专门用于封装响应结果数据,controller中的返回类型永远是这个类的实体,前端要获取数据也只需要从这个实体内部取数据就可以了。

如下:

public class ResultVO<T> {
    private int status;//状态码
    private String msg;//响应信息
    private List<T> list;//返回的数据可能是集合
    private T obj;//返回的结果也有可能是对象。

    public ResultVO(int status, String msg, T obj) {
        this.status = status;
        this.msg = msg;
        this.obj = obj;
    }

    public ResultVO(int status, String msg, List<T> list) {
        this.status = status;
        this.msg = msg;
        this.list = list;
    }

    public ResultVO(int status, String msg) {
        this.status = status;
        this.msg = msg;
    }

    public ResultVO() {//默认构造方法,根据实际情况自行决定
        status = 200;
        msg = "";
        list = null;
        obj = null;
    }
}

如上,之前代码中的所有的返回值都可以封装为ResultVO类型,前端根据具体情况从返回的对象中获取数据即可。

04-27 02:31