目录
4.REST的核心过滤器:HiddenHttpMethodFilter
一、什么是REST?
1.REST简介:
REST 全称是 Representational State Transfer(表现层状态转移),最早是由 HTTP 协议的作者之一 Roy Fielding 在他 2000 年的博士论文中提出。
在 REST 的世界里,网络上的所有事物(如一个用户、一张图片、一条订单)都可以被抽象为"资源 "。每个资源都有一个唯一的标识符(URI)。资源可以有多种展现形式,比如 JSON、XML 或 HTML,这就是所谓的"表现层"(Representation)。
当我们在浏览器中点击一个链接 或提交一个表单 时,就是在驱动服务器端资源的状态发生改变。
2.为什么SpringMVC要支持REST?
1) REST 风格的接口通常返回 JSON 格式,使得同一套后端接口 可以同时服务于 Web 端、iOS 和 Android 移动端,实现了"++前后端解耦++"。
2) 通过标准的路径(URL)和动词(HTTP Method),开发者一眼就能看出这个接口是查数据还是删数据,增强了代码的可读性和泛用性。
3.REST和HTTP常见请求方式之间的关系:
在 RESTful 架构中,我们通过不同的 HTTP 请求方式来表达对资源的操作语义:GET :获取资源(查);POST :新建资源(增);PUT :更新资源(改);DELETE:删除资源(删)。
但是,HTML的 form表单,其中的 method 属性原生仅支持 GET 方式 和 POST 方式**。这就意味着:**如果我们想在纯粹的 JSP 或 Thymeleaf 页面中通过点击一个按钮来发送 DELETE 或 PUT 请求,浏览器是做不到的,这显然阻碍了我们践行标准的 REST 规范。
既然浏览器只能发 POST,那我们就可以三十六计------"瞒天过海":在发送 POST 请求时,额外带上一个隐藏参数,告诉服务器:"虽然我发的是 POST,但请你把我当成 DELETE 来看待"。正好,SpringMVC 为我们提供了这样一个核心过滤器------HiddenHttpMethodFilter。
4.REST的核心过滤器:HiddenHttpMethodFilter
HiddenHttpMethodFilter的逻辑其实非常简单:它会拦截请求,检查是否为 POST ,并寻找请求中是否包含一个名为 _method的特殊参数。如果存在,它就会通过包装(Wrapper)将原有的 POST 请求模拟成参数中指定的请求方式(如DELETE、PUT)。HiddenHttpMethodFilter的核心源码逻辑如下:
java
// 截取自 Spring 源码:org.springframework.web.filter.HiddenHttpMethodFilter
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 1. 获取原始请求,判断是否为 POST
String method = request.getMethod();
if ("POST".equalsIgnoreCase(method) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
// 2. 获取请求中定义的特殊参数,默认参数名是 "_method"
String paramValue = request.getParameter(this.methodParam);
// 3. 如果参数不为空(比如是 PUT、DELETE、PATCH)
if (StringUtils.hasLength(paramValue)) {
String methodToUse = paramValue.toUpperCase(Locale.ENGLISH);
// 4. 校验是否为合法的 HTTP 方法名
if (ALLOWED_METHODS.contains(methodToUse)) {
// 5. 核心:通过 HttpMethodRequestWrapper 包装类,将原始请求"伪装"成新的方法
request = new HttpMethodRequestWrapper(request, methodToUse);
}
}
}
// 6. 将请求传递给后续的过滤器或控制层
filterChain.doFilter(request, response);
}
好嘞!理论铺垫完毕。既然我们知道了浏览器无法直接发送 DELETE 请求,也知道了 SpringMVC 是通过 HiddenHttpMethodFilter 来帮我们"移花接木"的,那么下面我们就通过一个具体的 Book 资源管理案例,来看看如何在代码中配置并使用它。
二、REST入门案例
0.案例需求:
我们将会编写一个BookHandler 类,用于实现对 书籍 的CRUD。
然后编写一个 rest.jsp 页面,通过简单的crud 来测试REST。
1.配置过滤器:
首先我们要在 web.xml 文件中配置 HiddenHttpMethodFilter ,这里up将过滤器配置在了最前面,web.xml 代码如下:
XML
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!-- 配置HiddenHttpMethodFilter -->
<!-- HiddenHttpMethodFilter 可以把 以post方式提交的delete和put请求进行转换. -->
<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>
<!-- ======================================================= -->
<!-- 配置中央控制器 -->
<servlet>
<servlet-name>mvcDispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<!-- 通过 contextConfigLocation 指定 DispatcherServlet -->
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>mvcDispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
<!--
url-pattern == /, 表示所有的用户请求都会经过 DispatcherServlet, 且支持Rest风格。
-->
</servlet-mapping>
</web-app>
2.配置注解驱动和静态资源放行:
接着,我们要在Spring的配置文件中,为 SpringMVC 配置 ++<mvc:annotation-driven/>++ 来开启全套注解支持,配置注解驱动后,Spring 会自动帮我们注册一些核心组件,以支持 @RequestMapping,JSR-303 数据校验等处理现代 Web 请求的能力。
我们还要配置 ++<mvc:default-servlet-handler/>++ 实现静态资源放行,这个配置我们可以理解为是 "DispatcherServlet" 的辅助,专门处理它搞不定的 CSS、JS、图片等文件。配置这一行后,Spring 会注册一个 DefaultServletHttpRequestHandler,这样的话,当一个请求进来,Spring 发现没有对应的 Controller 能处理时,它会把这个请求转交给服务器(如 Tomcat)自带的默认 Servlet。
需要注意的是,这两个配置往往是前后相随的,他们满足了用户既要用注解,又要加载静态资源 的需求**。**
applicationContext-mvc.xml 代码如下:
XML
<?xml version="1.0" encoding="UTF-8"?>
<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.cyan.web"/>
<!-- 激活注解(开启 Spring MVC 注解驱动) --> <!-- IMPORTANT -->
<mvc:annotation-driven/>
<!-- 配置静态资源放行 -->
<mvc:default-servlet-handler/>
<!-- 配置视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!--
视图解析器需要配置两个属性------prefix(前缀) 和 suffix(后缀),
视图解析器收到Servlet的 "return "login_OK";" 时,会用prefix和suffix与返回的资源名拼接。
-->
<property name="prefix" value="/WEB-INF/pages/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
3.前端页面:
我们先来写一个简单的 rest.jsp 页面,这个页面的主体部分共分为四部分,分别用于测试REST风格的"查询"、"添加"、"删除"和"修改"。rest.jsp 代码如下:(注意看注释!)
html
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>rest</title>
<style>
table, td {
border: cornflowerblue 2px solid;
border-collapse: collapse;
padding: 2px;
}
</style>
<script type="text/javascript" src="./js/jquery-3.7.0.min.js"></script>
<script type="text/javascript">
$(function () {
$("#deleteBook").click(function () { //id选择器
var href = this.href;
//将超链接的href属性的值 赋值给隐藏表单的action属性
$("#hiddenForm").attr("action", href);
$(":hidden").val("DELETE");
//这里就是提交删除请求了
$("#hiddenForm").submit();
//告诉那个超链接, 不要提交了.
return false;
});
})
</script>
</head>
<body>
<!--
1) GET请求的查询 和 POST请求的增加, 都是没有用到过滤器
2) 删除和修改书籍, 就要用到过滤器了.
2) 因为 默认情况下, 超链接发出的请求一定是 GET 请求, 假如我们想通过超链接发出一个DELETE请求,
此时就要用到过滤器的 public static final String DEFAULT_METHOD_PARAM = "_method";字段
-->
<h2 style="color: cornflowerblue">【Rest 测试】</h2> <br/>
<hr>
<h3 style="color: darkcyan">Rest 风格的url 查询书籍[get]</h3>
<a href="book/search/141">点击查询书籍</a> <br/>
<hr>
<h3 style="color: darkcyan">Rest 风格的url 添加书籍[post]</h3>
<form action="book/add" method="post">
<table>
<tr>
<td>name:</td>
<td><input name="bookName" type="text"></td>
</tr>
<tr>
<td><input type="submit" value="添加书籍"></td>
<td><input type="reset" value="重置输入"></td>
</tr>
</table>
</form> <br/>
<hr>
<h3 style="color: darkcyan">Rest 风格的url 删除书籍[delete]</h3>
<!--
1) 这里指定的 _method 就是用到了 HiddenHttpMethodFilter 的
public static final String DEFAULT_METHOD_PARAM = "_method";字段。
过滤器后台就是按这个名字来获取hidden域的值, 从而进行请求转换的.
2) 但是要用HiddenHttpMethodFilter的_method字段, 就必须以POST来发出请求. (源码要求)
只有以POST发出请求, 过滤器才能捕获并转换为我们需要的DELETE请求方式.
3) 具体来说, HiddenHttpMethodFilter支持转换三种不同的请求,分别是PUT,DELETE,和PATCH.
源码如下:
private static final List<String> ALLOWED_METHODS =
Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),
HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
4) 整体思路是:为了实现REST风格的请求,
借助JQuery, 将原本是超链接发出的请求 替换为 隐藏表单的提交.
-->
<a href="book/delete/233" id="deleteBook">删除指定 id 的书籍</a>
<%-- 隐藏的表单 --%>
<form action ="" method="post" id="hiddenForm">
<input type ="hidden" name="_method"/>
</form> <br/>
<hr>
<h3 style="color: darkcyan">Rest 风格的url 修改书籍[put]</h3>
<form action="book/update/{77}" method="post">
<input type="hidden" name="_method" value="PUT">
<input type="submit" value="修改书籍">
</form>
</body>
</html>
rest.jsp页面的效果如下:(四个绿色的小标题表示四个不同的测试部分)

注意,由于 rest.jsp 中用到了JQuery,所以 up 这里在IDEA中引入的JQuery,如下图所示:

再来写一个简单的提示操作成功的页面,success.jsp 代码如下:
html
<%--
User : Cyan_RA9
Version : 24.0
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>原神牛逼</title>
</head>
<body>
<h2>Operation Successful!</h2>
</body>
</html>
4.后端Handler:
对应的 BookHandler类 代码如下:(注意看注释!)
java
package com.cyan.web.rest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
/**
* @author : Cyan_RA9
* @version : 23.0
*/
@RequestMapping(value = "/book")
@Controller
public class BookHandler {
// 实现 查询 书籍的方法
@GetMapping(value = "/search/{id}")
public String searchBook(@PathVariable("id") String id) {
System.out.println("查询id = " + id + "的书籍");
return "success";
}
// 实现 添加 书籍的方法
@PostMapping(value = "/add")
public String addBook(String bookName) {
//因为 BookName 是从表单直接提交过来的, 所以这里不需要配置@PathVariable.
System.out.println("添加name = " + bookName + "的书籍");
return "success";
}
// 实现 删除 书籍的方法
@RequestMapping(value = "/delete/{id}", method = RequestMethod.DELETE)
public String deleteBook(@PathVariable("id") String bookId) {
System.out.println("删除bookId = " + bookId + "的书籍");
//return "success"; //直接这么写会报错:JSP 只允许 GET、POST 或 HEAD
return "redirect:/book/success"; //重定向到下面那个方法
/*
这里 redirect 的第一个斜杠/ 会在服务器端被解析成 /Web工程名/, 然后拼接后面的
然后浏览器访问重定向的路径时, 拼接后的第一个斜杠/ 再被浏览器解析为
http://IP[域名]:port/, 然后拼接后面的, 就是完整的正确路径.
*/
}
@RequestMapping(value = "/success")
public String successMsg() {
return "success";
}
// 实现 修改 书籍的方法
@RequestMapping(value = "/update/{id}", method = RequestMethod.PUT)
public String updateBook(@PathVariable("id") String bookId) {
System.out.println("修改bookId = " + bookId + "的书籍~~~");
return "redirect:/book/success"; //重定向到successMsg方法
}
}
5.运行测试:
运行效果如下 GIF图所示:

Δ总结
- 🆗,以上就是本篇博文的全部内容了,感谢阅读!
- 在SpringMVC中实现 RESTful 风格的URL,核心就是 Spring 的HiddenHttpMethodFilter 过滤器。推荐大家把我上面这个案例自己写一下,自己动手,收获多多。
System.out.println("END-----------------------------------------------------------------");