文章目录
RESTful
1. REST概念
表述性状态转换(Representational State Transfer,REST),描述了一个架构样式的网络系统,比如web应用)。
它是一种软件架构风格、设计风格
,而不是标准,只是提供了一组设计原则和约束条件,它主要用于客户端和服务端交互类的软件。基于这个风格设计的软件可以更简洁、更有层次、更易于实现缓存等机制。
它本身并没有什么使用性,其核心价值在于如何设计出符合REST风格的网络接口。
2. RESTful概念
REST指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是RESTful。
2.1 RESTful的特性
-
资源(Resources)
互联网所有的事物都可以被抽象为资源
。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的存在。可以用一个URI(统一资源定位符)指向它,每种资源对应一个特性的URI。要获取这个资源,访问它的URI就可以,因此URI即为每一个资源的独一无二的识别符。 -
表现层(Representation)
把资源具体呈现出来的形式
,叫做它的表现层(Representation)。比如,文本可以用txt格式表现,也可以用HTML格式、XML格式、JSON格式表现,甚至可以采用二进制格式。 -
状态转换(State Transfer)
每发出一个请求,就代表了客户端和服务器的一次交互过程。HTTP协议,是一个无状态协议,即所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生“状态转换”(State Transfer)。而这种转换是建立在表现层之上的,所以就是“表现层状态转换”。
具体来说就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。他们分别对应四种基本操作:
GET用来获取资源,POST用来新建资源,PUT用来更新资源,DELETE用来删除资源。
2.2 RESTful和原有方式操作资源的对比
-
原来操作资源的方式
- http://localhost:8080/getExpress.do?id=1
- http://localhost:8080/insertExpress.do
- http://localhost:8080/updatexpress.do?id=1
- http://localhost:8080/deleteExpress.do?id=1
- ……
-
RESTful操作资源的方式
- GET /expresses #查询所有的快递信息列表
- GET /express/1006 #查询一个快递信息
- POST /express #新建一个快递信息
- PUT /express/1006 #更新一个快递信息(全部更新)
- PATCH /express/1006 #更新一个快递信息(部分更新)
- DELETE /express/1006 #删除一个快递信息
- ……
3. API设计/URL设计
3.1 RESTful核心思想
RESTful 的核心思想就是客户端的用户发出的数据操作指令都是”动词 + 宾语”的结构。
例如GET /expresses 这个命令,GET是动词,/expresses 是宾语。
3.2 RESTful中的五种动词
动词通常对应五种 HTTP 处理方法,对应 CRUD 操作,分别是:
- GET:读取(Read)
- POST:新建(Create)
- PUT:更新(Update)
- PATCH:更新(Update),通常是部分更新
- DELETE:删除(Delete)
注:
- 根据 HTTP 规范,动词一律大写。
- 一些代理只支持POST和GET方法, 为了使用这些有限方法支持RESTful API,需要一种办法覆盖
http原来的方法。使用订制的HTTP头 X-HTTP-Method-Override 来覆盖POST 方法.
3.3 RESTful中的宾语
宾语就是 API 的 URL,是 HTTP 动词作用的对象。它应该是名词,不能是动词。
- 比如,/expresses 这个 URL 就是正确的。而对于 /getAllExpresses 、/getExpress 、/createExpress 等,这些URL都是不推荐的,因为带上了动词,不是推荐写法。
- 另外,不要混淆名词单数和复数,为了保持简单,只在操作所有资源的时候使用复数,其他(增删改)的情况一般都是使用单数。
3.4 避免多级URL
如果资源中有多级分类,也不建议写出多级的URL,尽量使用单级URL。
- 示例1:要获取球队中某个队员,有人可能这么写:GET /team/1001/player/1005 。但是,这种写法的语义不够明确,所以推荐使用查询字符串做后缀,改写为 GET /team/1001?player=1005 。
- 示例2:例如查询所有的还未取出的快递,使用 GET /expresses/statu 这种URL是不推荐的,使用 GET /expresses?statu=false 是推荐的。
4. HTTP状态码
客户端的用户发起的每一次请求,服务器都必须给出响应。响应包括 HTTP 状态码和数据两部分。
HTTP 状态码就是一个三位数,分成五个类别。这五大类总包含了100多种状态码解释,客户端只需查看状态码,就可以判断出发生了什么情况,所以服务器应该返回尽可能精确的状态码。五类状态码分别为:1xx 相关信息、2xx 操作成功、3xx 重定向、4xx 客户端错误和 5xx 服务器错误。
注:API 不需要1xx状态码,所以这个类别直接忽略。
4.1 状态码2xx
200 状态码表示操作成功,但是不同的方法可以返回更精确的状态码。
GET: 200 OK |
表示一切正常 |
---|---|
POST: 201 Created |
表示新的资源已经成功创建 |
PUT: 200 OK |
表示资源已经成功更新 |
PATCH: 200 OK |
表示部分资源已经成功更新 |
DELETE: 204 No Content |
表示资源已经成功删除 |
4.2 状态码3xx
API 用不到 301 状态码(永久重定向)和 302 状态码(暂时重定向, 307 也是这个含义),因为它们可以由应用级别返回,浏览器会直接跳转,API 级别可以不考虑这两种情况。
API 用到的 3xx 状态码,主要是 303 See Other ,表示参考另一个 URL。它与 302 和 307 的含义一样,也是”暂时重定向”,区别在于 302 和 307 用于 GET 请求,而 303 用于 POST 、 PUT 和 DELETE 请求。收到 303 以后,浏览器不会自动跳转,而会让用户自己决定下一步怎么办。
304 Not Modified | 客户端使用缓存数据 |
---|
4.3 状态码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 格式,但是客户端要求返回 XML 格式。 |
422 Unprocessable Entity |
客户端上传的附件无法处理,导致请求失败。 |
429 Too Many Requests |
客户端的请求次数超过限额。 |
4.4 状态码5xx
5xx 状态码表示服务端错误。一般来说,API 不会向用户透露服务器的详细信息,所以只要两个状态码就够了。
500 Internal Server Error |
客户端请求有效,服务器处理时发生了意外。 |
---|---|
503 Service Unavailable |
服务器无法处理请求,一般用于网站维护状态。 |
5. 服务器响应
服务器返回的信息一般不推荐纯文本,而是建议大家选择JSON 对象,因为这样才能返回标准的结构化数据。所以,服务器回应的 HTTP 头的 Content-Type 属性要设为 application/json 。客户端请求时,也要明确告诉服务器,可以接受 JSON 格式,即请求的 HTTP 头的 ACCEPT 属性也要设成 application/json 。
当发生错误的时候,除了返回状态码之外,也要返回错误信息。所以我们可以自己封装要返回的信息。
6. 案例
先引入json的依赖、实体类的创建(这里就不赘述了),然后前端页面准备:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>restful</title>
<script src="/js/jquery-1.11.1.js"></script>
</head>
<body>
<form id="myForm" action="" method="post">
球队ID:<input type="text" name="teamId" id="teamId" /><br/>
球队名称:<input type="text" name="teamName" /><br/>
球队位置:<input type="text" name="location" /><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 () {
//给 查询所有GET 按钮绑定单击事件
//给 查询单个GET 按钮绑定单击事件
//给 查询添加POST 按钮绑定单击事件
//给 查询删除DELETE 按钮绑定单击事件
});
</script>
6.1 RESTful风格的查询
-
前端AJAX请求
//给 查询所有GET 按钮绑定单击事件 $("#btnGetAll").click(function () {//发起异步请求 $.ajax({ type: "GET", url: "teams", //RESTful风格的API定义 data: "", dataType:"json", success: function(list){ alert( "Data Saved: " + list ); var str=""; for(var i=0;i<list.length;i++){ var obj=list[i]; str+=obj.teamId+"----"+obj.teamName+"----"+obj.location+"<br/>"; } $ ("#showResult").html(str); } }); }); //给 查询单个GET 按钮绑定单击事件 $("#btnGetOne").click(function () {//发起异步请求 $.ajax({ type: "GET", url: "team/"+$("#teamId").val(), //RESTful风格的API定义 data: "", dataType:"json", success: function(obj){ alert( "Data Saved: " + obj ); if(obj==""){ $("#showResult").html("没有符合条件的数据!"); }else $("#showResult").html(obj.teamId+"----"+obj.teamName+"----"+obj.location+"<br/>"); } }); });
-
后端控制器处理部分
@RequestMapping(value = "teams", method = RequestMethod.GET) @ResponseBody public List<Team> getTeamList() { System.out.println("查询所有GET-----------"); System.out.println(teamList); return teamList; } @RequestMapping(value = "team/{id}", method = RequestMethod.GET) @ResponseBody public Team getTeam(@PathVariable("id") int id) { System.out.println("查询单个GET-----------"); for (Team team : teamList) { if (team.getTeamId() == id){ return team; } } return null; }
6.2 RESTful风格的添加
-
前端AJAX请求
//给 查询添加POST 按钮绑定单击事件 $("#btnPost").click(function () {//发起异步请求 alert($("#myForm").serialize()); $.ajax({ type: "POST", url: "team", //RESTful风格的API定义 data: $("#myForm").serialize(), //表单的所有数据以?&形式追加在URL后面 例如 /restful/team?teamId=1006&teamName=kuaichuan&location=las dataType:"json", success: function(msg){ //alert( "Data Saved: " + msg ); $("#showResult").html(msg ? "添加成功":"添加失败"); } }); });
-
后端控制器处理部分
@RequestMapping(value = "team", method = RequestMethod.POST) @ResponseBody public boolean insertTeam(Team team) { System.out.println("添加POST-----------"); boolean add = teamList.add(team); System.out.println(teamList); return add; }
6.3 RESTful风格的更新
-
前端AJAX请求
//给 查询更新PUT 按钮绑定单击事件 $("#btnPut").click(function(){ alert($("#myForm").serialize()); $.ajax({ type: "POST", //type: "PUT",//这种也可以,但是就不能在data传递数据了,原因看6.5小节 url: "team/"+$("#teamId").val(), //data: $("#myForm").serialize(), data: $("#myForm").serialize()+"&_method=PUT", dataType:"json", //headers:{"X-HTTP-Method-Override":"GET"}, success: function(msg){ $("#showResult").html(msg ? "更新成功" : "更新失败"); } }); });
-
后端控制器处理部分
@RequestMapping(value = "team/{id}", method = RequestMethod.PUT) @ResponseBody public boolean updateTeam(@PathVariable("id") int id, Team team) { boolean flag = false; System.out.println("更新PUT-----------"); for (Team team1 : teamList) { if (team1.getTeamId() == id){ team1.setLocation(team.getLocation()); flag = true; break; } } System.out.println(teamList); return flag; }
6.4 RESTful风格的删除
-
前端AJAX请求
//给 查询删除DELETE 按钮绑定单击事件 $("#btnDel").click(function(){ alert($("#myForm").serialize()); $.ajax({ type: "DELETE", url: "team/"+$("#teamId").val(), //data: $("#myForm").serialize(), //data: $("#myForm").serialize()+"&_method=DELETE", dataType:"json", //headers:{"X-HTTP-Method-Override":"GET"}, success: function(msg){ $("#showResult").html(msg ? "删除成功" : "删除失败"); } }); });
-
后端控制器处理部分
@RequestMapping(value = "team/{id}", method = RequestMethod.DELETE) @ResponseBody public boolean deleteTeam(@PathVariable("id") int id) { boolean flag = false; System.out.println("删除DELETE-----------"); for (Team team1 : teamList) { if (team1.getTeamId() == id){ teamList.remove(team1); flag = true; break; } } System.out.println(teamList); return flag; }
6.5 RESTful风格的更新和删除遇到的问题
在Ajax中,采用Restful风格PUT和DELETE请求传递参数(data)无效,传递到后台的参数值为null。
6.5.1 原因
Tomcat封装请求参数的过程:
-
将请求体中的数据,封装成一个map;
-
request.getParameter(key)会从这个map中取值
-
SpringMvc封装POJO对象的时候,会把POJO中每个属性的值进行request.getParamter();
AJAX发送PU或者DELETE请求时,请求体中的数据通过request.getParamter()拿不到。因为Tomcat一检测到是PUT或者DELETE就不会封装请求体中的数据为map,只有POST形式的请求才封装请求为map。
6.5.2 解决方法
-
前端页面中改用ajax发送POST请求,并在url中加 &_method=”PUT” 或者 &_method=”DELETE” 即可;
-
web.xml中进行配置HiddenHttpMethodFilter 。
注意以下过滤器的配置顺序。
<!-- 使用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>
7. 封装自定义响应数据(状态码+消息+数据)
可以对要响应回去的状态码、消息、数据进行一个封装,然后返回即可。同样的道理,还可以把响应码、响应的消息做成枚举类,让后端去处理的时候做到见名知意,这里直接简单地给出一个简易封装的实体类,不多赘述。
public class AjaxResultVO<T> {
private Integer code;//响应的状态码
private String msg;//响应的消息
private List<T> dataList;//响应的数据集合
private T data;//响应的数据
public AjaxResultVO() {
code = 200;
msg = "";
dataList = null;
data = null;
}
public AjaxResultVO(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public AjaxResultVO(Integer code, String msg, List<T> dataList) {
this.code = code;
this.msg = msg;
this.dataList = dataList;
}
public AjaxResultVO(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public AjaxResultVO(Integer code, String msg, List<T> dataList, T data) {
this.code = code;
this.msg = msg;
this.dataList = dataList;
this.data = data;
}
//getter && setter
}