REST:Representational State Transfer,表现层资源状态转移。
传统的软件系统仅在本地工作,但随着项目规模的扩大和复杂化,不但整个项目会拓展为分布式架构,很多功能也会通过网络访问第三方接口来实现。在通过网络访问一个功能的情况下,我们不能轻易假设网络状况稳定可靠。所以当一个请求发出后没有接收到对方的回应,那我们该如何判定本次操作成功与否?
下面以保存操作为例来说明一下针对功能和针对资源进行操作的区别:
针对功能设计系统
保存一个 Employee 对象,没有接收到返回结果,判定操作失败,再保存一次。但是其实在服务器端保存操作已经成功了,只是返回结果在网络传输过程中丢失了。而第二次的补救行为则保存了重复、冗余但 id 不同的数据,这对整个系统数据来说是一种破坏。
针对资源设计系统
针对 id 为 3278 的资源执行操作,服务器端会判断指定 id 的资源是否存在。如果不存在,则执行保存操作新建数据;如果存在,则执行更新操作。所以这个操作不论执行几次,对系统的影响都是一样的。在网络状态不可靠的情况下可以多次重试,不会破坏系统数据。
幂等性:如果一个操作执行一次和执行 N 次对系统的影响相同,那么我们就说这个操作满足幂等性。而幂等性正是 REST 规范所倡导的。
REST是针对资源设计系统,所以在REST中一个URL就对应一个资源, 为实现操作幂等性奠定基础。
在REST中,针对同一资源的增删改查操作的URL是完全相同的,它是通过Http协议的不同请求方式来区分不同操作的
REST 风格主张在项目设计、开发过程中,具体的操作符合 HTTP 协议定义的请求方式的语义。
| 操作 | 请求方式 |
|---|---|
| 查询操作 | GET |
| 保存操作 | POST |
| 删除操作 | DELETE |
| 更新操作 | PUT |
另有一种说法:
- POST 操作针对功能执行,没有锁定资源 id,是非幂等性操作。
- PUT 操作锁定资源 id,即使操作失败仍然可以针对原 id 重新执行,对整个系统来说满足幂等性。
- id 对应的资源不存在:执行保存操作
- id 对应的资源存在:执行更新操作
REST风格提倡 URL 地址使用统一的风格设计,从前到后各个单词使用斜杠分开,不使用问号键值对方式携带请求参数,而是将要发送给服务器的数据作为 URL 地址的一部分,以保证整体风格的一致性。还有一点是不要使用请求扩展名。
使用问号键值对的方式给服务器传递数据太明显,容易被人利用来对系统进行破坏。使用 REST 风格携带数据不再需要明显的暴露数据的名称。
| 操作 | 传统风格 | 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 |
在 HTML 中,GET 和 POST 请求可以天然实现,但是 DELETE 和 PUT 请求无法直接做到。SpringMVC 提供了 HiddenHttpMethodFilter 帮助我们将 POST 请求转换为 DELETE 或 PUT 请求。
<filter>
<filter-name>hiddenHttpMethodFilterfilter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilterfilter-class>
filter>
<filter-mapping>
<filter-name>hiddenHttpMethodFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
<form th:action="@{/rest/movie}" method="post">
<input type="hidden" name="_method" value="put"/>
<button>发送请求button>
form>
@PutMapping("/movie")
public String updateMovie(){
logger.debug("PUT请求....");
return "target";
}
通常删除超链接会出现在列表页面:
<h3>将XXX请求转换为DELETE请求h3>
<div id="app">
<table id="dataTable">
<tr>
<th>姓名th>
<th>年龄th>
<th>删除th>
tr>
<tr>
<td>张三td>
<td>40td>
<td>
<a th:href="@{/rest/movie}" @click="deleteMovie">删除a>
td>
tr>
<tr>
<td>李四td>
<td>30td>
<td>
<a th:href="@{/rest/movie}" @click="deleteMovie">删除a>
td>
tr>
table>
div>
创建负责转换的表单
<form id="myForm" method="post">
<input type="hidden" name="_method" value="delete"/>
form>
使用vue给删除超链接绑定单击响应函数:

<script th:src="@{/static/javaScript/vue.js}">script>
var vue = new Vue({
"el":"#app",
"methods":{
deleteMovie(){
console.log("aaaaaaa")
//真正发送删除请求:
//1. 阻止标签的默认行为
event.preventDefault()
//2. 创建一个空表单:先动态设置表单的action
var myForm = document.getElementById("myForm");
myForm.action = event.target.href
//并且使用js代码提交表单
myForm.submit()
}
}
});
@DeleteMapping("/movie")
public String deleteMovieById(){
logger.debug("DELETE请求....");
return "target";
}
请看下面链接:
/emp/20
/shop/product/iphone
如果我们想要获取链接地址中的某个部分的值,就可以使用 @PathVariable 注解,例如上面地址中的20、iphone部分。
<a th:href="@{/rest/movie/2}">携带参数movieIda>
//注意:{movieId是一个占位符},表示获取该位置的值,@PathVariable("movieId")表示获取占位符为"movieId"的值
@GetMapping("/movie/{movieId}")
public String findMovieById(@PathVariable("movieId") Integer movieId){
logger.debug("GET请求...."+movieId);
return "target";
}
<a th:href="@{/rest/movie/2/22/123}">携带多个参数a>
@GetMapping("/movie/{categoryId}/{groupId}/{movieId}")
public String findMovieById(@PathVariable("categoryId") Integer categoryId,@PathVariable("groupId")Integer groupId,@PathVariable("movieId") Integer movieId){
logger.debug("GET请求...."+categoryId+":"+groupId+":"+movieId);
return "target";
}
将前面的传统CRUD案例复制并且重新导入
| 功能 | URL 地址 | 请求方式 |
|---|---|---|
| 访问首页 | / | GET |
| 查询全部数据 | /soldier | GET |
| 删除 | /soldier/2 | DELETE |
| 跳转到添加数据的表单 | /soldier/add.html | GET |
| 执行保存 | /soldier | POST |
| 跳转到更新数据的表单 | /soldier/2 | GET |
| 执行更新 | /soldier | PUT |
不需要做修改
@GetMapping
public String findAll(Model model){
List<Soldier> soldierList = soldierService.findAll();
model.addAttribute("soldierList", soldierList);
return PAGE_LIST;
}
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页title>
head>
<body>
<h1>欢迎来到首页h1>
<a th:href="@{/soldier}">展示士兵列表a>
body>
html>
重点在于将 GET 请求转换为 DELETE。基本思路是:通过一个通用表单,使用 Vue 代码先把 GET 请求转换为 POST,然后再借助 hiddenHttpMethodFilter 在服务器端把 POST 请求转为 DELETE。
<td>
<a th:href="@{/soldier/}+${soldier.soldierId}" onclick="deleteSoldier()">删除 a>
td>
<form id="deleteById" method="post">
<input type="hidden" name="_method" value="delete"/>
form>
<script>
function deleteSoldier() {
// 阻止a标签的默认发送请求
event.preventDefault();
// 获取form标签
var element = document.getElementById("deleteById");
// 设置form标签的action值为a标签的href
element.setAttribute("action", event.target.href)
// 进行发送请求
element.submit();
}
script>
public static final String LIST_ACTION = "redirect:/soldier";
public static final String PAGE_EDIT = "edit";
public static final String PAGE_LIST = "list";
@Autowired
private SoldierService soldierService;
@DeleteMapping("/{id}")
public String deleteById(@PathVariable("id") Integer soldierId){
soldierService.deleteById(soldierId);
// 重新查询所有,重定向查询所有方法
return LIST_ACTION;
}
<!--一定要配置在解决乱码的Filter之后-->
<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>/*
list.html修改跳转add.html的路径 处理器方法无需修改
<tr>
<td colspan="5">
<a th:href="@{/add.html}">添加士兵a>
td>
tr>
@PutMapping
public String addOrUpdate(Soldier soldier){
soldierService.addOrUpdate(soldier);
return LIST_ACTION;
}
/**
* 新增或修改
* @param soldier
*/
void addOrUpdate(Soldier soldier);
@Transactional
@Override
public void addOrUpdate(Soldier soldier) {
// 判断是否携带id 未携带id是新增士兵
if (null == soldier.getSoldierId()){
Soldier soldierDaoByName = null;
try {
soldierDaoByName = soldierDao.findByName(soldier.getSoldierName());
} catch (Exception e) {
e.printStackTrace();
}
// 判断是否存在 不存在则添加
if (null == soldierDaoByName){
soldierDao.add(soldier);
}else {
return;
}
}else{
// 更新士兵
soldierDao.update(soldier);
}
}
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>添加士兵页面title>
head>
<body>
<form th:action="@{/soldier}" method="post">
<input type="hidden" name="_method" value="put">
士兵名称: <input type="text" name="soldierName"> <br>
士兵武器:<input type="text" name="soldierWeapon"> <br>
<button>提交button>
form>
body>
html>
@GetMapping("/{id}")
public String getSoldierById(@PathVariable("id") Integer soldierId, Model model){
Soldier soldier = soldierService.getSoldierById(soldierId);
model.addAttribute("soldier", soldier);
return PAGE_EDIT;
}
<td>
<a th:href="@{/soldier/}+${soldier.soldierId}">修改a>
td>
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>编辑士兵页面title>
head>
<body>
<form th:action="@{/soldier}" method="post">
<input type="hidden" name="_method" value="put">
<input type="hidden" name="soldierId" th:value="${soldier.soldierId}">
士兵姓名:<input type="text" name="soldierName" th:value="${soldier.soldierName}"> <br>
士兵武器:<input type="text" name="soldierWeapon" th:value="${soldier.soldierWeapon}"> <br>
<button>提交button>
form>
body>
html>
处理器方法无需修改 和添加功能共用一个方法
@PutMapping
public String addOrUpdate(Soldier soldier){
soldierService.addOrUpdate(soldier);
return LIST_ACTION;
}

DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<script src="/static/js/vue.js">script>
<script src="/static/js/axios.js">script>
head>
<body>
<div id="app">
<a href="javascript:;" @click="sendCommonParams">发送异步请求携带普通类型的参数a> <br>
div>
<script>
new Vue({
el:"#app",
data:{
},
methods:{
// 使用axios发送异步请求,并且携带普通类型的请求参数
sendCommonParams(){
axios({
"url":"/user/commonParams",
"method":"POST",
"params":{
"age":20,
"name":"zs",
"address":"sz"
}
}).then(response=>{
console.log(response.data.data)
})
}
}
})
script>
body>
html>
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-webmvcartifactId>
<version>5.3.1version>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
<version>1.2.3version>
dependency>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>3.1.0version>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.24version>
<scope>providedscope>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
<version>2.12.1version>
dependency>
dependencies>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<context:component-scan base-package="com.atguigu">context:component-scan>
<mvc:annotation-driven>mvc:annotation-driven>
<mvc:default-servlet-handler>mvc:default-servlet-handler>
beans>
<configuration debug="true">
<appender name="STDOUT"
class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%line] [%msg]%npattern>
encoder>
appender>
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
root>
<logger name="org.springframework.web.servlet.DispatcherServlet" level="DEBUG"/>
configuration>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>dispatcherServletservlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
<init-param>
<param-name>contextConfigLocationparam-name>
<param-value>classpath:spring.xmlparam-value>
init-param>
<load-on-startup>1load-on-startup>
servlet>
<servlet-mapping>
<servlet-name>dispatcherServletservlet-name>
<url-pattern>/url-pattern>
servlet-mapping>
<filter>
<filter-name>CharacterEncodingFilterfilter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilterfilter-class>
<init-param>
<param-name>encodingparam-name>
<param-value>UTF-8param-value>
init-param>
<init-param>
<param-name>forceRequestEncodingparam-name>
<param-value>trueparam-value>
init-param>
<init-param>
<param-name>forceResponseEncodingparam-name>
<param-value>trueparam-value>
init-param>
filter>
<filter-mapping>
<filter-name>CharacterEncodingFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
web-app>
package com.atguigu.pojo;
import lombok.Data;
@Data
public class User {
private Integer age;
private String name;
private String address;
}
package com.atguigu.controller;
import com.atguigu.pojo.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping("/commonParams")
public String commonParameter(User user){
// 获取异步请求携带的普通类型参数,和以前获取同步请求携带的参数是一样的
System.out.println("user========>" + user);
return "success";
}
}

<a href="javascript:;" @click="sendJsonParams">发送异步请求携带JSON类型的参数a> <br>
// 使用axios发送异步请求,携带JSON类型的请求参数
sendJsonParams(){
axios({
"url":"/user/jsonParams",
"method":"POST",
"data":{
"age":30,
"name":"ls",
"address":"sz"
}
}).then(response=>{
console.log(response.data.data)
})
}
如果忘记导入jackson依赖,会看到下面的错误页面

关于 SpringMVC 和 Jackson jar包之间的关系,需要注意:当 SpringMVC 需要解析 JSON 数据时就需要使用 Jackson 的支持。但是 SpringMVC 的 jar 包并没有依赖 Jackson,所以需要我们自己导入。
我们自己导入时需要注意:SpringMVC 和 Jackson 配合使用有版本的要求。二者中任何一个版本太高或太低都不行。
SpringMVC 解析 JSON 数据包括两个方向:
另外,如果导入了 Jackson 依赖,但是没有开启 mvc:annotation-driven 功能,那么仍然会返回上面的错误页面。
也就是说,我们可以这么总结 SpringMVC 想要解析 JSON 数据需要两方面支持:
还有一点,如果运行环境是 Tomcat7,那么在 Web 应用启动时会抛出下面异常:
org.apache.tomcat.util.bcel.classfile.ClassFormatException: Invalid byte tag in constant pool: 19
解决办法是使用 Tomcat8 或更高版本。
@RequestMapping("/jsonParams")
public String jsonParams(@RequestBody User user){
// 1.获取Json请求体类型的参数必须封装到POJO对象或者是Map中
// 2.POJO参数或者Map参数的前面一定要加入@RequestBody注解,不然获取的值都为null
// 3.项目中一定要引入jackson的依赖
System.out.println("user========>" + user);
return "success";
}

DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<script src="/static/js/vue.js">script>
<script src="/static/js/axios.js">script>
head>
<body>
<div id="app">
<a href="javascript:;" @click="sendCommonParams">发送异步请求携带普通类型的参数a> <br>
<a href="javascript:;" @click="sendJsonParams">发送异步请求携带JSON类型的参数a> <br>
div>
<script>
new Vue({
el:"#app",
data:{
},
methods:{
// 使用axios发送异步请求,并且携带普通类型的请求参数
sendCommonParams(){
axios({
"url":"/user/commonParams",
"method":"POST",
"params":{
"age":20,
"name":"zs",
"address":"sz"
}
}).then(response=>{
console.log(response.data.data)
})
},
// 使用axios发送异步请求,携带JSON类型的请求参数
sendJsonParams(){
axios({
"url":"/user/jsonParams",
"method":"POST",
"data":{
"age":30,
"name":"ls",
"address":"sz"
}
}).then(response=>{
console.log(response.data.data)
})
}
}
})
script>
body>
html>
前提是项目中引入了jackson的依赖
创建result包及类
package com.atguigu.controller;
import com.atguigu.pojo.User;
import com.atguigu.result.Result;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/user")
public class UserController {
@ResponseBody
@RequestMapping("/commonParams")
public Result commonParameter(User user){
// 获取异步请求携带的普通类型参数,和以前获取同步请求携带的参数是一样的
System.out.println("user========>" + user);
return Result.ok(user);
}
@ResponseBody
@RequestMapping("/jsonParams")
public Result jsonParams(@RequestBody User user){
// 1.获取Json请求体类型的参数必须封装到POJO对象或者是Map中
// 2.POJO参数或者Map参数的前面一定要加入@RequestBody注解,不然获取的值都为null
// 3.项目中一定要引入jackson的依赖
System.out.println("user========>" + user);
// 将user封装到Result中 返回给客户端 必须添加ResponseBody注解
return Result.ok(user);
}
}
返回给客户端json格式的数据


出现上面的错误页面,表示SpringMVC 为了将 实体类对象转换为 JSON 数据, 需要转换器。但是现在找不到转换器。它想要成功完成转换需要两方面支持:
问题出现的原因:
上面二者不一致。SpringMVC 要坚守一个商人的良心,不能干『挂羊头,卖狗肉』的事儿。解决办法有三种思路:
<servlet-mapping>
<servlet-name>dispatcherServletservlet-name>
<url-pattern>*.htmlurl-pattern>
<url-pattern>*.jsonurl-pattern>
servlet-mapping>
如果类中每个方法上都标记了 @ResponseBody 注解,那么这些注解就可以提取到类上。
类上的ResponseBody 注解可以和Controller 注解合并为RestController 注解。所以使用了RestController 注解就相当于给类中的每个方法都加了ResponseBody 注解。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any (or empty String otherwise)
* @since 4.0.1
*/
@AliasFor(annotation = Controller.class)
String value() default "";
}