⭐表示重要。
第一章:RESTful 概述
1.1 REST 概念
- REST:
Re
presentationalS
tateT
ransfer,表现层资源状态转移。- 定位:互联网软件架构风格。
- 倡导者:Roy Thomas Fielding。
- 文献:Roy Thomas Fielding的博士论文。
1.2 挑战和应付
1.2.1 一项技术标准规范统一天下😔?
- 端和端之间的交互协议、技术实现方案有多少种?
- SOAP
- WSDL
- XML - RPC
- Socket
- SMTP
- POP3
- ……
- 面对各种不同领域的各种不同协议、技术标准以及解决方案,程序员真的是无能为力😂!因为这些技术增加了学习成本,还非常难以调试,再加上参考资料的稀缺以及晦涩难懂,于是 HTTP 协议来了,那么它有什么优势?
- 在最广大的范围内,HTTP 协议拥有最广泛的共识。
- 报文结构清晰,简单明了。
- 无状态,消除了请求和请求之间的耦合关系。
- 无类型,却能够展示丰富的数据类型。
- 解耦合,双方一旦确定使用 HTTP 协议交互,那么就不需要关系双方采用什么技术,基于什么平台。
1.2.2 功能还是资源😔?
- 传统的软件系统仅在本地工作,但是随着项目规模的扩大和复杂化,不但整个项目会拓展为分布式架构,很多功能也会通过网络访问第三方接口来实现,在通过网络访问一个功能的情况下,我们不能轻易的假设网络状况非常可靠,所以当一个请求发出后却没有接收到对方的响应的时候,我们应该如何判断本次操作是否成功?
- 下面以保存为例来说明针对功能和针对资源进行操作的区别?
- 针对功能设计系统:调用服务器的接口,保存一个 Employee 对象,没有收到返回结果,判断为操作失败,再次保存一次。但是,其实服务器端的保存操作已经成功了,只是在网络传输的时候将返回结果丢失了,那么第二次补救行为则保存了重复的、冗余的除了 id 不同的数据,这对于整个系统数据来说是一种破坏,长此以往,会造成脏数据。
- 针对资源设计系统:针对 id 为 9527 的资源执行操作,服务器会判断指定 id 的资源是否存在?如果不存在,则执行保存操作;如果已经存在,则执行更新操作,所以这个操作不论执行多少次,对系统的影响都是一样的。在网络状态不可靠的情况下可以多次重试,不会破坏系统数据。
幂等性:如果一个操作执行一次或 n 次对系统的影响相同,那么我们就说这个操作满足幂等性。 幂等性 是 REST 规范所倡导的。
1.2.3 无论内外一视同仁😔?
- 确认 HTTP 协议作为项目架构设计规范和统一标准后,无论系统内部的远程方法还是系统外部的第三方接口,对我们当前所在模块来说都是网络上的资源,完全可以使用相同(或类似)的方式来访问和调用,这非常有利于我们构建大型、超大型互联网项目体系。
1.3 REST 规范的内涵
1.3.1 资源
- URL(Uniform Resource Locator,统一资源定位符):网络上的任何资源都可以通过 URL来定位。实际开发中,我们往往使用 URL 来对应一个具体的功能,而不是资源本身。REST 规范倡导使用 URL 对应网络上的各种资源,任何一个资源都可以通过 URL 访问到,为实现操作
幂等性
奠定基础。
1.3.2 状态转移
- REST 倡导针对资源本身的操作,所以对资源的操作如果满足幂等性,那么操作只会导致资源本身的状态发生变化而不会破坏整个系统的数据。
1.4 REST 规范的具体要求
1.4.1 四种请求方式对应你四种常见操作
- REST
主张
在项目设计、开发过程中,具体的操作符合 HTTP 协议定义的请求方式的语义
。 | 操作 | 请求方式 | | —- | —- | | 查询操作 | GET | | 保存操作 | POST | | 删除操作 | DELETE | | 更新操作 | PUT |
注意:另有一种说法
- POST 操作针对功能执行,没有锁定资源的 id ,是非幂等性操作。
- PUT 操作锁定资源的 id ,即使操作失败依然可以针对原 id 重新执行,对整个系统来说满足幂等性。
- id 对应的资源不存在,执行保存操作。
- id 对应的资源存在,执行更新操作。
1.4.2 URL 地址风格
- REST 提倡 URL 地址使用统一的风格设计,从前到后各个单词使用斜杠(/)分开,
不使用问号键值对方式携带请求参数
,而是将要发送给服务器的数据作为 URL 地址的一部分,以保证整体风格的一致性,还有不要使用请求扩展名
。 | 传统 URL 地址 | REST 风格地址 | | —- | —- | | /remove/emp?id=5 | /emp/5 |
1.5 REST 风格的好处
1.5.1 含蓄,安全
- 使用问号键值对方式携带请求参数的方式给服务器传递数据太明显,容易被人利用来对系统进行破坏,使用 REST 风格携带数据不再需要明显的暴露数据的名称。
1.5.2 风格统一
- URL 地址整体格式统一,从前到后始终都是使用斜杠(/)划分各个内容部分,用简单一致的格式表达语义。
1.5.3 严谨,规范
- 严格按照 HTTP 1.1 规范中定义的请求方式本身的语义进行操作。
1.5.4 简洁,优雅
- 过去做 CRUD 操作需要设计 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 |
1.5.5 丰富的语义
- 通过 URL 地址就可以知道资源之间的关系,它能够把一句话中的很多单词用斜杠(/)连起来,换言之,就是可以在 URL 地址中用一句话来充分的表达语义。
- 例如:
http://localhost:8080/shop
。http://localhost:8080/shop/product
。http://localhost:8080/shop/product/cellPhone
。http://localhost:8080/shop/product/cellPhone/iPhone
。
第二章:四种请求方式映射
2.1 HiddenHttpMethodFilter 和装饰模式
2.1.1 简介
- 在 HTML 中,GET 和 POST 请求可以天然实现,但是 PUT 和 DELETE 请求无法直接做到。SpringMVC 提供了 HiddenHttpMethodFilter 帮助我们将 POST 请求转换为 DELETE 或 PUT 请求。
2.1.2 HiddenHttpMethodFilter 源码要点
- ① 默认请求参数名常量:在 HiddenHttpMethodFilter 中,声明了一个常量 DEFAULT_METHOD_PARAM ,其值为 _method
public static final String DEFAULT_METHOD_PARAM = "_method";
- ② 和常量配套的成员变量:之所以会提供这个成员变量和配置的 setXxx() 方法,是允许我们在配置 Filter 的时候,通过初始化参数来修改这个变量;如果不修改,默认就是前面常量定义的值
private String methodParam = DEFAULT_METHOD_PARAM;
- ③ 以常量值为名称获取请求参数:
2.1.3 原始请求对象的包装
- ① 困难:
- 包装对象必须和原始对象是同一个类型。
- 保证同一个类型不能通过子类继承父类来实现:
- 子类对象:希望改变行为、属性的对象。
- 父类对象:随着 Servlet 容器的不同,各个容器对 HttpServletRequest 接口给出的实现也不同。如果继承了 A 容器给出的实现类,那么将来就不能再迁移到 B 容器了。
- 只能让包装对象和被包装的对象实现相同的接口:虽然使用动态代理技术大致上应该可以实现,但是一旦应用代理就必须为被包装的对象的每一个方法都进行代理,操作过于繁琐。
- 如果我们自己创建一个类实现 HttpServletRequest 接口?
- 困难1:我们其实并不知道具体该怎么做?
- 困难2:抽象方法实在太多了。
- ② HttpServletRequestWrapper 类:HttpServletRequestWrapper 类能够非常好的帮助我们对原始的 request 对象进行包装,它为什么能帮助我们解决上面的困难?
- HttpServletRequestWrapper 类替我们实现了 HttpServletRequest 接口。
- 为了让包装得到的新对象在任何 Servlet 容器平台上都能够正常工作,HttpServletRequestWrapper 类此处的设计非常巧妙,它借助原始的 request 对象本身来实现所有的具体功能。
- 在我们想通过包装的方式修改原始对象的行为或属性的时候,只需要在 HttpServletRequestWrapper 类的子类中重写对应的方法即可。
- ③ HttpMethodRequestWrapper 类:HttpMethodRequestWrapper 类就是 HiddenHttpMethodFilter 的一个内部类,在 HiddenHttpMethodFilter 类有如下行为实现了对原始对象的包装:
- 继承了官方包装类: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;
}
}
2.1.4 装饰者模式
- 装饰者模式也是二十三种设计模式之一,属于结构型模式,主要特点就是借助原始对象实现和原始对象一样的接口,同时通过重写父类方法修改被包装对象的行为。
2.2 PUT 请求(⭐)
2.2.1 前提说明
- 在已有的 SpringMVC 环境基础上执行。
2.2.2 web.xml
- 配置 HiddenHttpMethodFilter:
<!-- 配置HiddenHttpMethodFilter -->
<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>
- 完整的 web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!-- 配置过滤器解决 POST 请求的字符乱码问题 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<!-- encoding参数指定要使用的字符集名称 -->
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<!-- 请求强制编码 -->
<init-param>
<param-name>forceRequestEncoding</param-name>
<param-value>true</param-value>
</init-param>
<!-- 响应强制编码 -->
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 配置HiddenHttpMethodFilter -->
<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>
<!-- 配置SpringMVC中负责处理请求的核心Servlet,也被称为SpringMVC的前端控制器 -->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<!-- DispatcherServlet的全类名 -->
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 通过初始化参数指定SpringMVC配置文件位置 -->
<init-param>
<!-- 如果不记得contextConfigLocation配置项的名称,可以到DispatcherServlet的父类FrameworkServlet中查找 -->
<param-name>contextConfigLocation</param-name>
<!-- 使用classpath:说明这个路径从类路径的根目录开始才查找 -->
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<!-- 作为框架的核心组件,在启动过程中有大量的初始化操作要做,这些操作放在第一次请求时才执行非常不恰当 -->
<!-- 我们应该将DispatcherServlet设置为随Web应用一起启动 -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<!-- 对DispatcherServlet来说,url-pattern有两种方式配置 -->
<!-- 方式一:配置“/”,表示匹配整个Web应用范围内所有请求。这里有一个硬性规定:不能写成“/*”。只有这一个地方有这个特殊要求,以后我们再配置Filter还是可以正常写“/*”。 -->
<!-- 方式二:配置“*.扩展名”,表示匹配整个Web应用范围内部分请求 -->
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
2.2.3 表单
- 要点1:请求方式必须是 POST 。
- 要点2:新的请求方式名称通过请求参数发送。
- 要点3:请求参数的名称必须是 _method。
要点4:请求参数的值就是要修改的请求方式。
示例:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<!-- 原始的请求方式必须是POST -->
<form method="post" th:action="@{/emp}">
<!-- 通过表单隐藏域携带一个请求参数 -->
<!-- 请求参数名:_method -->
<!-- 请求参数值:put -->
<input name="_method" type="hidden" value="PUT">
<input type="submit" value="更新">
</form>
</body>
</html>
2.2.4 handler
- 示例:
package com.github.fairy.era.mvc.handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PutMapping;
/**
* 测试
*
* @author 许大仙
* @version 1.0
* @since 2021-11-11 14:58
*/
@Controller
public class DemoHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@PutMapping("/emp")
public String emp() {
logger.info("DemoHandler的{}方法", "emp");
return "target";
}
}
2.2.5 请求方式 Filter 对字符集 Filter 的影响
- 结论:当 web.xml 中两个 Filter 共存的时候,一定要让
CharacterEncodingFilter
先执行。 - 原因:
- 在
CharacterEncodingFilter
中通过request.setCharacterEncoding(encoding);
方法设置字符集。 request.setCharacterEncoding(encoding);
要求前面不能有任何获取请求参数的操作。- 但是
HiddenHttpMethodFilter
却有一个获取请求方式的操作:String paramValue = request.getParameter(this.methodParam);
- 在
2.3 DELETE 请求(⭐)
2.3.1 表单
- 示例:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<!-- 原始的请求方式必须是POST -->
<form method="post" th:action="@{/emp}">
<!-- 通过表单隐藏域携带一个请求参数 -->
<!-- 请求参数名:_method -->
<!-- 请求参数值:DELETE -->
<input name="_method" type="hidden" value="DELETE">
<input type="submit" value="删除">
</form>
</body>
</html>
2.3.2 handler
- 示例:
package com.github.fairy.era.mvc.handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
/**
* 测试
*
* @author 许大仙
* @version 1.0
* @since 2021-11-11 14:58
*/
@Controller
public class DemoHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@DeleteMapping("/emp")
public String emp() {
logger.info("DemoHandler的{}方法", "emp");
return "target";
}
}
第三章:@PathVarible 注解(⭐)
3.1 REST 风格的 URL 参数
- 请看下面的超链接:
/emp/20
/shop/product/iphone
- 如果我们想要获取超链接地址中的某个部分的值,就可以使用
@PathVarible
注解,如上面地址中的 20 、iphone 等。
3.2 操作
3.2.1 传递一个值
- 表单:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<a th:href="@{/emp/20}">传一个值</a><br/>
</body>
</html>
- handler:
package com.github.fairy.era.mvc.handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* 测试
*
* @author 许大仙
* @version 1.0
* @since 2021-11-11 14:58
*/
@Controller
public class DemoHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@GetMapping("/emp/{id}")
public String emp(@PathVariable("id") Integer id) {
logger.info("DemoHandler的emp方法的参数是{}", id);
return "target";
}
}
3.2.2 传递多个值
- 表单:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<a th:href="@{/emp/tom/18/50}">传多个值</a><br/>
</body>
</html>
- handler:
package com.github.fairy.era.mvc.handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* 测试
*
* @author 许大仙
* @version 1.0
* @since 2021-11-11 14:58
*/
@Controller
public class DemoHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@GetMapping("/emp/{empName}/{empAge}/{empSalary}")
public String emp(
@PathVariable("empName") String empName,
@PathVariable("empAge") Integer empAge,
@PathVariable("empSalary") Double empSalary) {
logger.info("DemoHandler的emp方法的参数是{},{},{}", empName, empAge, empSalary);
return "target";
}
}