REST(Representational State Transfer,表述性状态转换) 描述了一个架构样式的网络系统,比如web应用。
它是一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件,主要用户客户端和服务端交互软件。基于这个风格设计的软件可以更简洁更有层次,更易于实现缓存机制。
其核心价值在于如何设计出符合REST风格的接口。
REST指的是一组架构设计原则和约束条件,满足这些约束条件和原则的应用程序或设计就是RESTful。
RESTful特点:
资源Resources:互联网所有事物都可以抽象为资源,可以是文本、图像、视频、服务等,总之就是一个具体存在。可以使用URI(统一资源定位符)指向它,每种资源对应一个特性的URI。要获取这个资源,访问URI就可以,因此URI是每个资源的唯一识别符。
表现层Representation:把资源具体呈现出来的形式,叫做它的表现层。比如文本可以用txt表示也可以用html、xml、json,甚至二进制表示。
状态转换State Transfer:每发出一个请求,就代表客户端与服务端进行了一次交互。Http是一个无状态协议,即所有状态都保存在服务端。因此,客户端想要操作服务器,必须通过某种方式,让服务端发生状态转换State Transfer。这种状态转换建立在表现层,所以叫做表现层状态转换。
具体来说,HTTP协议中有四种操作动词:GET、POST、PUT、DELETE,他们对应四种操作:GET获取资源,POST新建资源,PUT更新资源,DELETE删除资源。
以前操作资源方式:
- http://localhost:8080/getExpress.do?id=1
- http://localhost:8080/saveExpress.do
- http://localhost:8080/updateExpress.do
- http://localhost:8080/deleteExpress.do?id=1
使用RESTful操作资源:
- GET /expresses #查询所有快递列表信息
- GET /express/1001 #查询一个快递信息
- POST /express #新建一个快递信息
- PUT /express/1001 #更新一个快递信息
- DELETE /express/1001 #删除一个快递信息
二、API/URL设计
2.1、动词+宾语
RESTful的核心思想就是客户端向服务端发出的指令是“动词+宾语”的结构,例如:GET /expresses这个指令,GET是动词,/expresses是宾语。
动词就是常用的5中HTTP方法,对应CRUD操作。
- GET:Read(读取)
- POST:Create(新建)
- PUT:Update(更新)
- PATCH:Update(部分更新)
- DELETE:delete(删除)
PS:根据HTTP规范,动词一律大写。 一些代理只支持GET和POST方法,为了是这些有限方法支持RESTful API,需要一种方法覆盖原来的http方法。使用定制的HTTP头来X-HTTP-Method-Override来覆盖POST方法。
2.2、宾语必须是名词
宾语就是API的URL,是HTTP动词作用的对象,它应该是名词而不是动词。如:/expresses 这个 URL 就是正确的。
以下的URL都是不推荐的,因为带上了动词: /getAllExpresses /getExpress /createExpress /deleteAllExpress
2.3、避免多级URL
如果资源中有多级分类,也不建议写多级URL,例如:获取球队中某个队员, GET /team/1001/player/100
这种写法语义不明确,推荐使用查询字符串作为后缀:GET /team/1001?player=100
例如查询未取快递:
GET /expresses/statu 不推荐
GET /expresses?statu=false 推荐
三、HTTP状态码
客户端每次发出的请求,服务端都必须给出响应,响应包括HTTP状态码和数据两部分。
HTTP状态码是一个三位数,分成5个类别。5个大类包含100多种状态码,每一中状态码都有标准或者约定的解释,客户端只需要查看状态码,就可以知道发生了什么事情,所以服务器应该返回尽可能精准的状态码。
五类状态码如下:
- 1xx: 相关信息
- 2xx: 操作成功
- 3xx: 重定向
- 4xx: 客户端错误
- 5xx: 服务端错误
API不需要1XX状态码,所以这个类别可以忽略。
3.1、状态码2xx
200状态码表示操作成功,但是不同的方法可以返回更加精准的状态码。
- GET: 200 OK 表示一切正常
- POST: 201 Created 表示新的资源已经创建成功
- PUT: 200 OK
- PATCH: 200 OK
- DELETE:204 No Content 表示已经删除成功
3.2、状态码3XX
API用不到301状态码(永久重定向)和302状态码(暂时重定向),因为它们可以由应用级别返回,浏览器可以直接跳转,API不用考虑这两种情况。
API用到的3XX状态主要是303 See Other,表示参考另一个URL,它与“302”和“307”一样,表示“暂时重定向”,区别在于“302”和“307”用于GET请求,而303用于POST、PUT、DELETE。收到303后,浏览器不会自动跳转,而是让用户自己决定下一步怎么办。
- 304:Not Modified 客户端使用缓存数据
3.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:客户端请求次数太多
3.4、状态码5XX
5XX状态码表示服务端错误。一般来说,API不会向客户端透露服务端详细信息,所以状态码较少。
- 500 Internal Server Error:客户端请求有效,服务端内部处理出错了。
- 503 Service Unavailable:服务器无法处理请求,一般用于网站维护状态。
3.5、服务器响应
服务端响应一般不推荐使用文本,推荐使用json格式。所以服务端响应的HTTP头信息Content-Type属性设为application/json,客户端请求时,也要明确告诉服务端可以接受json格式,即请求的HTTP头ACCEPT属性也要设置为application/json。
四、案例
4.1、RESTful风格的查询
@Controller
public class RestfulController {
private static List<Job> jobList = new ArrayList<>();
static {
for (int i = 0; i < 10; i++) {
jobList.add(new Job(i, "任务" + i, "描述信息", new Date()));
}
}
/**
* 查询所有任务
*
* @return
*/
@RequestMapping(value = "jobs", method = RequestMethod.GET)
@ResponseBody
public List<Job> getAll() {
System.out.println("RestfulController-------------------------getAll");
return jobList;
}
/**
* 查询单个任务
* @param id
* @return
*/
@RequestMapping(value = "job/{id}", method = RequestMethod.GET)
@ResponseBody
public Job getOne(@PathVariable("id") int id) {
System.out.println("RestfulController-------------------------getOne");
for (Job job : jobList) {
if (job.getId() == id) {
return job;
}
}
return null;
}
/**
* 任务操作页面跳转
* @return
*/
@RequestMapping("hello")
public String hello() {
return "jsp/restful";
}
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>restful</title>
<script type="text/javascript" src="/js/jquery-1.11.1.js"></script>
<script type="text/javascript">
$(function () {
//查询所有GET
$("#btnGetAll").click(function () {
$.ajax({
type:"GET",
url:"jobs",
data:"",
dataType:"json",
success:function (data) {
alert(JSON.stringify(data));
let html="";
$.each(data,function (i,d) {
html+="id:"+d.id+",name:"+d.name+",description:"+d.description+"<br/>";
})
$("#showResult").html(html);
}
})
});
//查询单个GET
$("#btnGetOne").click(function () {
$.ajax({
type:"GET",
url:"job/"+$("input[name='id']").val(),
data:"",
dataType:"json",
success:function (data) {
alert(JSON.stringify(data));
if(data==undefined||data=="") $("#showResult").html("没有符合条件的数据");
$("#showResult").html("id:"+data.id+",name:"+data.name+",description:"+data.description);
}
})
});
});
</script>
</head>
<body>
<form id="myForm" action="" method="post">
任务ID:<input type="text" name="id" />
任务名称:<input type="text" name="name" />
任务描述:<input type="text" name="description" />
<br/><hr/>
<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>
4.2、RESTful风格添加
/**
* 任务添加
* @param job
* @return
*/
@RequestMapping(value = "job",method = RequestMethod.POST)
@ResponseBody
public String add(Job job){
System.out.println("RestfulController-------------------------add");
jobList.add(job);
return "201";
}
<script>
//添加
$("#btnPost").click(function () {
$.ajax({
type:"POST",
url:"job",
data:$("#myForm").serialize(),
dataType:"json",
success:function (data) {
$("#showResult").html(data);
}
})
});
</script>
4.3、RESTful风格修改
/**
* 任务更新
* @param id
* @param jobData
* @return
*/
@RequestMapping(value = "job/{id}",method = RequestMethod.PUT)
@ResponseBody
public String update(@PathVariable("id")int id,Job jobData){
System.out.println(id);
for(Job job:jobList){
if(job.getId()==id){
job.setName(jobData.getName());
job.setDescription(jobData.getDescription());
return "204";
}
}
return "404";
}
<script>
//更新
$("#btnPut").click(function () {
$.ajax({
type:"POST",
url:"job/"+$("input[name='id']").val(),
data:$("#myForm").serialize()+"&_method=PUT",
dataType:"json",
success:function (data) {
$("#showResult").html(data);
}
})
});
</script>
4.4、RESTful风格删除
/**
* 任务删除
* @param id
* @return
*/
@RequestMapping(value = "job/{id}",method = RequestMethod.DELETE)
@ResponseBody
public String Delete(@PathVariable("id")int id){
System.out.println(id);
for(Job job:jobList){
if(job.getId()==id){
jobList.remove(job);
return "204";
}
}
return "500";
}
<script>
//删除
$("#btnDel").click(function () {
$.ajax({
type:"POST",
url:"job/"+$("input[name='id']").val(),
data:"_method=DELETE",
dataType:"json",
success:function (data) {
$("#showResult").html(data);
}
})
});
</script>
4.5、RESTful风格更新删除遇到的问题
在Ajax中,采用RESTful风格PUT和DELETE传递参数无效,传递到后台的参数为null。
产生原因在于Tomcat封装请求参数过程:
- 将请求体中的数据封装成为一个map;
- request.getParameter(key)会从这个map中取值;
- SpringMVC封装POJO对象时,会把POJO对象中每个属性的值进行request.getParameter();Ajax发送PUT或者DELETE请求时,请求体中数据通过request.getParameter()拿不到。Tomcat检测到时PUT或者DELETE请求时不会封装请求体中数据为map,只有POST请求才会封装为map。
解决方案:
1、在前端进行ajax请求时候在url参数中添加 &_method=PUT 或者 &_method=DELETE
2、在web.xml配置文件中添加配置:
<!-- 使用RESTful风格的URI 将页面普通的POST请求转为指定的DELETE或者PUT请求
原理:在Ajax发送POST请求后,带_method参数,将其修改为DELETE或者PUT请求-->
<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>
五、自定义响应结果
5.1、封装响应实体类
/**
* @Author: JSONLiu
* @Description: 响应结果实体
* @Date Created in 2022-02-09 15:54
* @Modified By:
*/
public class Result<T> {
/**
* 响应码
*/
private Integer code;
/**
* 响应消息
*/
private String message;
/**
* 响应数据
*/
private T data;
public Result() {
}
public Result(Integer code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public static <T> Result success(T data){
return new Result(200,"ok",data);
}
public static <T> Result warn(String message){
return new Result(404,message,null);
}
public static <T> Result success(Integer code,T data){
return new Result(code,"ok",data);
}
public static <T> Result warn(Integer code,String message){
return new Result(code,message,null);
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
5.2、修改控制器返回值
@Controller
public class RestfulController {
private static List<Job> jobList = new ArrayList<>();
static {
for (int i = 0; i < 10; i++) {
jobList.add(new Job(i, "任务" + i, "描述信息", new Date()));
}
}
/**
* 查询所有任务
*
* @return
*/
@RequestMapping(value = "jobs", method = RequestMethod.GET)
@ResponseBody
public Result<List<Job>> getAll() {
System.out.println("RestfulController-------------------------getAll");
return Result.success(jobList);
}
/**
* 查询单个任务
*
* @param id
* @return
*/
@RequestMapping(value = "job/{id}", method = RequestMethod.GET)
@ResponseBody
public Result<Job> getOne(@PathVariable("id") int id) {
System.out.println("RestfulController-------------------------getOne");
for (Job job : jobList) {
if (job.getId() == id) {
return Result.success(job);
}
}
return Result.warn("无数据");
}
/**
* 任务操作页面跳转
*
* @return
*/
@RequestMapping("hello")
public String hello() {
return "jsp/restful";
}
/**
* 任务添加
*
* @param job
* @return
*/
@RequestMapping(value = "job", method = RequestMethod.POST)
@ResponseBody
public Result<Boolean> add(Job job) {
System.out.println("RestfulController-------------------------add");
jobList.add(job);
return Result.success(true);
}
/**
* 任务更新
*
* @param id
* @param jobData
* @return
*/
@RequestMapping(value = "job/{id}", method = RequestMethod.PUT)
@ResponseBody
public Result<Boolean> update(@PathVariable("id") int id, Job jobData) {
System.out.println(id);
for (Job job : jobList) {
if (job.getId() == id) {
job.setName(jobData.getName());
job.setDescription(jobData.getDescription());
return Result.success(true);
}
}
return Result.warn(404, "更新的ID不存在");
}
/**
* 任务删除
*
* @param id
* @return
*/
@RequestMapping(value = "job/{id}", method = RequestMethod.DELETE)
@ResponseBody
public Result<Boolean> Delete(@PathVariable("id") int id) {
System.out.println(id);
for (Job job : jobList) {
if (job.getId() == id) {
jobList.remove(job);
return Result.success(true);
}
}
return Result.warn(500, "要删除的ID不存在");
}
}
5.3、修改前端页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>restful</title>
<script type="text/javascript" src="/js/jquery-1.11.1.js"></script>
<script type="text/javascript">
$(function () {
//查询所有GET
$("#btnGetAll").click(function () {
$.ajax({
type: "GET",
url: "jobs",
data: "",
dataType: "json",
success: function (result) {
if(result.code!=200){
alert(result.message);
}
let data=result.data;
let html = "";
$.each(data, function (i, d) {
html += "id:" + d.id + ",name:" + d.name + ",description:" + d.description + "<br/>";
})
$("#showResult").html(html);
}
})
});
//查询单个GET
$("#btnGetOne").click(function () {
$.ajax({
type: "GET",
url: "job/" + $("input[name='id']").val(),
data: "",
dataType: "json",
success: function (result) {
if(result.code!=200){
alert(result.message);
}
let data=result.data;
$("#showResult").html("id:" + data.id + ",name:" + data.name + ",description:" + data.description);
}
})
});
//添加
$("#btnPost").click(function () {
$.ajax({
type: "POST",
url: "job",
data: $("#myForm").serialize(),
dataType: "json",
success: function (data) {
if(data.code!=200){
alert(data.message);
}
$("#showResult").html(data.data);
}
})
});
//更新
$("#btnPut").click(function () {
$.ajax({
type: "POST",
url: "job/" + $("input[name='id']").val(),
data: $("#myForm").serialize() + "&_method=PUT",
dataType: "json",
success: function (data) {
if(data.code!=200){
alert(data.message);
}
$("#showResult").html(data.data);
}
})
});
//删除
$("#btnDel").click(function () {
$.ajax({
type: "POST",
url: "job/" + $("input[name='id']").val(),
data: "_method=DELETE",
dataType: "json",
success: function (data) {
if(data.code!=200){
alert(data.message);
}
$("#showResult").html(data.data);
}
})
});
});
</script>
</head>
<body>
<form id="myForm" action="" method="post">
任务ID:<input type="text" name="id"/>
任务名称:<input type="text" name="name"/>
任务描述:<input type="text" name="description"/>
<br/>
<hr/>
<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>