⭐⭐⭐⭐⭐⭐
Github主页👉https://github.com/A-BigTree
笔记链接👉https://github.com/A-BigTree/Code_Learning
⭐⭐⭐⭐⭐⭐
如果可以,麻烦各位看官顺手点个star~😊
如果文章对你有所帮助,可以点赞👍收藏⭐支持一下博主~😆
3 RESTFul风格
3.1 RESTFul概述
3.1.1 REST概念
REST:Representational State Transfer,表现层资源状态转移。
- 定位:互联网软件架构风格;
- 倡导者:Roy Thomas Fielding;
- 文献:Roy Thomas Fielding的博士论文;
3.1.2 REST规范的内涵
资源
URL:Uniform Resource Locator
统一资源定位器。意思是网络上的任何资源都可以通过 URL 来定位。但是在实际开发中,我们往往是使用URL来对应一个具体的功能,而不是资源本身。REST规范则倡导使用URL对应网络上的各种资源,任何一个资源都可以通过一个URL访问到,为实现操作幂等性奠定基础。而这个资源可以是网络上的一个文本、音频、视频、图片等等……
幂等性:如果一个操作执行一次和执行N次对系统的影响相同,那么我们就说这个操作满足幂等性。而幂等性正是REST规范所倡导的。
状态转移
REST倡导针对资源本身操作,所以对资源的操作如果满足幂等性,那么操作只会导致资源本身的状态发生变化而不会破坏整个系统数据。
3.1.3 REST规范具体要求
四种请求方式
REST风格主张在项目设计、开发过程中,具体的操作符合HTTP协议定义的请求方式的语义。
操作 | 请求方式 |
---|---|
查询操作 | GET |
保存操作 | POST |
删除操作 | DELETE |
更新操作 | PUT |
另有一种说法:
- POST 操作针对功能执行,没有锁定资源 id,是非幂等性操作;
- PUT 操作锁定资源 id,即使操作失败仍然可以针对原 id 重新执行,对整个系统来说满足幂等性:
- id对应的资源不存在:执行保存操作
- id对应的资源存在:执行更新操作
URL地址风格
REST风格提倡URL地址使用统一的风格设计,从前到后各个单词使用斜杠分开,不使用问号键值对方式携带请求参数,而是将要发送给服务器的数据作为URL地址的一部分,以保证整体风格的一致性。还有一点是不要使用请求扩展名。
传统 URL 地址 | REST 风格地址 |
---|---|
/remove/emp?id=5 | /emp/5 |
- 用一句话描述当前资源
- 一句话中各个单词用斜杠分开,从前到后保持完全一致的书写风格
- 不要使用问号键值对的方式传递数据
- 需要传递数据时,把数据嵌入到URL地址中,作为地址的一部分
- 不要使用请求扩展名
3.1.4 REST风格的好处
含蓄安全
使用问号键值对的方式给服务器传递数据太明显,容易被人利用来对系统进行破坏。使用 REST 风格携带数据不再需要明显的暴露数据的名称。
风格统一
URL 地址整体格式统一,从前到后始终都使用斜杠划分各个单词,用简单一致的格式表达语义。
无状态
在调用一个接口(访问、操作资源)的时候,可以不用考虑上下文,不用考虑当前状态,极大的降低了系统设计的复杂度。
严谨优雅
严格按照HTTP1.1协议中定义的请求方式本身的语义进行操作。
简洁优雅
过去做增删改查操作需要设计4个不同的URL,现在一个就够了。
操作 | 传统风格 | REST 风格 |
---|---|---|
保存 | /CRUD/saveEmp | URL 地址:/CRUD/emp 请求方式:POST |
删除 | /CRUD/removeEmp?empId=2 | URL 地址:/CRUD/emp/2 请求方式:DELETE |
更新 | /CRUD/updateEmp | URL 地址:/CRUD/emp 请求方式:PUT |
查询(表单回显) | /CRUD/editEmp?empId=2 | URL 地址:/CRUD/emp/2 请求方式:GET |
丰富的语义
通过 URL 地址就可以知道资源之间的关系。它能够把一句话中的很多单词用斜杠连起来,反过来说就是可以在 URL 地址中用一句话来充分表达语义。
3.2 四种请求方式映射
3.2.1 HiddenHttpMethodFilter
与装饰模式
简介
在HTML中,GET和POST请求可以天然实现,但是DELETE和PUT请求无法直接做到。SpringMVC提供了HiddenHttpMethodFilter
帮助我们将POST请求转换为DELETE或PUT请求。
HiddenHttpMethodFilter
要点
默认请求参数名常量:
public static final String DEFAULT_METHOD_PARAM = "_method";
在HiddenHttpMethodFilter
中,声明了一个常量:DEFAULT_METHOD_PARAM
,常量值是"_method"
。
配套的成员变量:
private String methodParam = DEFAULT_METHOD_PARAM;
之所以会提供这个成员变量和配套的setXxx()
方法,是允许我们在配置Filter
时,通过初始化参数来修改这个变量。如果不修改,默认就是前面常量定义的值。
原始请求对象的包装
困难:
- 包装对象必须和原始对象是同一个类型
- 保证同一个类型不能通过子类继承父类实现
- 子类对象:希望改变行为、属性的对象
- 父类对象:随着Servlet容器的不同,各个容器对
HttpServletRequest
接口给出的实现不同。如果继承了 A 容器给出的实现类,那么将来就不能再迁移到 B 容器。
- 只能让包装对象和被包装对象实现相同接口
- 虽然使用动态代理技术大致上应该能实现,但是一旦应用代理就必须为被包装的对象的每一个方法都进行代理,操作过于繁琐。
- 如果我们自己创建一个类实现
HttpServletRequest
接口- 困难1:在不确定具体哪一个 Servlet 容器的情况下完全没办法实现
- 困难2:抽象方法实在太多
HttpServletRequestWrapper
:
HttpServletRequestWrapper
类能够非常好的帮助我们对原始request
对象进行包装。它为什么能帮我们解决上面的困难呢?
HttpServletRequestWrapper
类替我们实现了HttpServletRequest
接口;- 为了让包装得到的新对象在任何Servlet容器平台上都能够正常工作,
HttpServletRequestWrapper
类此处的设计非常巧妙:它借助原始的request对象本身来实现所有的具体功能; - 在我们想通过包装的方式来修改原始对象的行为或属性时,只需要在
HttpServletRequestWrapper
类的子类中重写对应的方法即可;
HttpMethodRequestWrapper
类:
HttpMethodRequestWrapper
类就是HiddenHttpMethodFilter
的一个内部类,在HttpMethodRequestWrapper
类中有如下行为实现了对原始对象的包装:
- 继承了官方包装类:
HttpServletRequestWrapper
; - 在构造器中将原始
request
对象传给了父类构造器; - 将我们指定的新请求方式传给了成员变量;
- 重写了父类(官方包装类)的
getMethod()
方法; - 外界想知道新包装对象的请求方式时,会来调用被重写的
getMethod()
方法,从而得到我们指定的请求方式;
/**
* Simple {@link HttpServletRequest} wrapper that returns the supplied method for
* {@link HttpServletRequest#getMethod()}.
*/
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
private final String method;
public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
// 在构造器中将原始 request 对象传给了父类构造器
super(request);
// 将我们指定的新请求方式传给了成员变量
this.method = method;
}
@Override
public String getMethod() {
return this.method;
}
}
3.2.2 PUT请求
web.xml
<filter>
<filter-name>hiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>hiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
表单
- 要点1:原请求方式必须是 post
- 要点2:新的请求方式名称通过请求参数发送
- 要点3:请求参数名称必须是_method
- 要点4:请求参数的值就是要改成的请求方式
<!-- 原请求方式必须是 post -->
<form th:action="@{/emp}" method="post">
<!-- 通过表单隐藏域携带一个请求参数 -->
<!-- 请求参数名:_method -->
<!-- 请求参数值:put -->
<input type="hidden" name="_method" value="put" />
<button type="submit">更新</button>
</form>
处理方法
// 映射请求地址:URL + 请求方式
@RequestMapping(value = "/emp", method = RequestMethod.PUT)
public String updateEmp() {
logger.debug("现在执行的是 updateEmp() 方法");
return "target";
}
3.2.3 DELETE请求
场景
<h3>将XXX请求转换为DELETE请求</h3>
<table id="dataTable">
<tr>
<th>姓名</th>
<th>年龄</th>
<th>删除</th>
</tr>
<tr>
<td>张三</td>
<td>40</td>
<td>
<a th:href="@{/emp}" @click="doConvert">删除</a>
</td>
</tr>
<tr>
<td>李四</td>
<td>30</td>
<td>
<a th:href="@{/emp}" @click="doConvert">删除</a>
</td>
</tr>
</table>
负责转换的表单
<!-- 创建一个通用表单,在删除超链接的单击响应函数中通过这个表单把GET请求转换为POST,进而再转DELETE -->
<form method="post" id="convertForm">
<input type="hidden" name="_method" value="delete" />
</form>
删除超链接绑定单击响应函数
<td>
<!-- /emp/{empId}/{pageNo} -->
<!-- οnclick="convertMethod(this)" 表示点击这个超链接时,调用 convertMethod() 函数 -->
<!-- this 代表当前超链接对象 -->
<!-- event 是代表当前事件的事件对象 -->
<a onclick="convertMethod(this, event)" th:href="@{/emp}">删除</a>
</td>
编写单击响应函数
<script type="text/javascript">
function convertMethod(anchorElement, event) {
// 获取超链接原本要访问的目标地址
var targetURL = anchorElement.href;
// 获取表单对象
var formEle = document.getElementById("convertForm");
// 把超链接原本要访问的地址设置给表单的 action 属性
formEle.action = targetURL;
// 提交表单
formEle.submit();
// 取消控件的默认行为:让超链接不会跳转
event.preventDefault();
}
</script>
处理方法
@RequestMapping(value = "/emp", method = RequestMethod.DELETE)
public String removeEmp() {
logger.debug("现在执行的是 removeEmp() 方法");
return "target";
}
3.3 @PathVariable
注解
3.3.1 操作
传一个值
<a th:href="@{/emp/20}">传一个值</a><br/>
// 实际访问地址:/emp/20
// 映射地址:/emp/{empId}是把变量部分用大括号标记出来,写入变量名
@RequestMapping("/emp/{empId}")
public String getEmpById(@PathVariable("empId") Integer empId) {
logger.debug("empId = " + empId);
return "target";
}
传多个值
<a th:href="@{/emp/tom/18/50}">传多个值</a><br/>
处理方法
// 实际地址:/emp/tom/18/50
@RequestMapping("/emp/{empName}/{empAge}/{empSalary}")
public String queryEmp(
@PathVariable("empName") String empName,
@PathVariable("empAge") Integer empAge,
@PathVariable("empSalary") Double empSalary
) {
logger.debug("empName = " + empName);
logger.debug("empAge = " + empAge);
logger.debug("empSalary = " + empSalary);
return "target";
}