REST 风格原理
一、是什么 ------ REST 风格到底是什么?
1. 一句话定义
REST(Representational State Transfer)风格就是使用 HTTP 请求方式动词(GET/POST/PUT/DELETE)来表示对资源的操作,而不是在 URL 中体现行为。
2. 对比:传统风格 vs REST 风格
| 操作 | 传统 URL | REST 风格 URL | HTTP 方法 |
|---|---|---|---|
| 获取用户 | /user/get?id=1 |
/user/1 |
GET |
| 新增用户 | /user/save |
/user |
POST |
| 修改用户 | /user/update |
/user |
PUT |
| 删除用户 | /user/delete?id=1 |
/user/1 |
DELETE |
REST 风格的核心理念:URL 定位资源,HTTP 方法描述操作。
3. 浏览器表单的限制
但这里有一个历史遗留问题:浏览器的原生 HTML <form> 表单只支持 GET 和 POST 两种请求方式,不能直接发送 PUT 和 DELETE 请求。
Spring Boot(底层是 Spring MVC)的做法是使用 HiddenHttpMethodFilter(隐藏方法过滤器) 来"偷梁换柱"。
4. 用法
html
<!-- 表单 method 必须是 POST -->
<form method="post" action="/user/1">
<!-- 隐藏域 _method 指定真正的请求方式 -->
<input type="hidden" name="_method" value="PUT">
<button type="submit">修改</button>
</form>
二、为什么 ------ 为什么要这个 Filter?
1. 现实矛盾
- HTTP 协议层面:PUT、DELETE 是标准方法,框架(Spring MVC)完全支持
- 浏览器层面 :HTML
<form>只能发 GET 和 POST,不支持 PUT/DELETE - AJAX 可以发 PUT/DELETE,但传统表单提交不行
2. Spring 的解决方案
在请求到达 Controller 之前,用一个 Filter(过滤器) 拦截 POST 请求,读取隐藏域 _method 的值,把 POST "伪装" 成 PUT 或 DELETE。
核心思想:装饰器模式(Decorator Pattern) ------ 不修改原始 Request 对象,而是创建一个包装类重写
getMethod()方法。
3. 版本坑:Spring Boot 2.2.x 之后默认关闭
这曾经是一个被广泛讨论的话题。在 Spring Boot 2.2.x 之前 ,HiddenHttpMethodFilter 是默认自动装配的。但2.2.x 之后,官方将它默认关闭了。
为什么关闭?
现在绝大多数项目都是前后端分离(Vue + Axios、React + Fetch),前端发起的 AJAX 请求可以直接发送真正的 PUT / DELETE HTTP 报文,不再需要借助 HTML 表单伪装。为了节省内存和性能开销,Spring Boot 默认关掉了它。
如何开启(如果需要服务端渲染 + 表单提交的场景):
yaml
# 源码位置:springboot2-master/boot-05-web-01/src/main/resources/application.yaml
spring:
mvc:
hiddenmethod:
filter:
enabled: true
三、怎么做 ------ HiddenHttpMethodFilter 的源码机制
1. 自动装配条件
在 WebMvcAutoConfiguration 类中:
java
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled")
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
OrderedHiddenHttpMethodFilter 继承自 HiddenHttpMethodFilter,我们直接看后者的核心逻辑。
2. 核心过滤逻辑 ------ doFilterInternal()
java
public class HiddenHttpMethodFilter extends OncePerRequestFilter {
// 允许"伪装"的方法集合
private static final List<HttpMethod> ALLOWED_METHODS;
static {
ALLOWED_METHODS = Collections.unmodifiableList(Arrays.asList(
HttpMethod.PUT.name(), // "PUT"
HttpMethod.DELETE.name(), // "DELETE"
HttpMethod.PATCH.name() // "PATCH"
));
}
public static final String DEFAULT_METHOD_PARAM = "_method"; // 默认隐藏域名称
private String methodParam = "_method";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest requestToUse = request;
// 条件一:必须是 POST 请求
// 条件二:请求没有异常标记
if ("POST".equals(request.getMethod())
&& request.getAttribute("javax.servlet.error.exception") == null) {
// 获取隐藏域 _method 的值
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH); // 统一转大写
// 判断是否在允许列表中
if (ALLOWED_METHODS.contains(method)) {
// ★ 核心:用包装类替换原始 request
requestToUse = new HttpMethodRequestWrapper(request, method);
}
}
}
// 放行包装后的 request
filterChain.doFilter(requestToUse, response);
}
}
3. 装饰器模式 ------ HttpMethodRequestWrapper
java
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
private final String method;
public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
super(request);
this.method = method;
}
@Override
public String getMethod() {
return this.method; // ★ 重写!返回 _method 的值(PUT/DELETE/PATCH)
}
}
这是经典的装饰器模式(Decorator Pattern):
HttpServletRequestWrapper是装饰器基类,实现了HttpServletRequest接口HttpMethodRequestWrapper只重写了getMethod()这一个方法- 其他所有方法(
getParameter()、getHeader()等)都原封不动地委托给原始 request
执行效果 :Filter 放行后的整个处理链中,任何组件调用 request.getMethod() 拿到的都是 "PUT" 或 "DELETE",而不再是 "POST"。
4. 完整流程图
浏览器表单提交(POST + _method=PUT)
↓
Servlet 容器(Tomcat)接收请求
↓
HiddenHttpMethodFilter.doFilterInternal()
├── 判断:是 POST 请求?且无异常?→ 是
├── 获取 _method 的值:"put" → 转大写 → "PUT"
├── 判断:"PUT" 在 ALLOWED_METHODS 中?→ 是
├── 创建 HttpMethodRequestWrapper(request, "PUT")
└── filterChain.doFilter(wrapper, response) // 放行包装后的 request
↓
DispatcherServlet
↓
HandlerMapping 匹配 @PutMapping("/user")
↓
Controller 方法执行
四、怎么自定义 ------ 修改默认的 _method 参数名
1. 原理
如果不想用 _method 这个名字,可以自定义。因为自动装配的条件是 @ConditionalOnMissingBean(HiddenHttpMethodFilter.class),所以只要我们自己注册一个,Spring Boot 就不会再自动装配了。
HiddenHttpMethodFilter 提供了 setMethodParam() 方法:
java
public void setMethodParam(String methodParam) {
Assert.hasText(methodParam, "'methodParam' must not be empty");
this.methodParam = methodParam;
}
2. 实现
java
// 源码位置:springboot2-master/boot-05-web-01/.../config/WebConfig.java
@Configuration(proxyBeanMethods = false)
public class WebConfig {
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m"); // 把约定参数从 _method 改成 _m
return methodFilter;
}
}
配置后,前端表单需要写成:
html
<input type="hidden" name="_m" value="PUT"> <!-- 不再是 _method -->
3. 前端传值大小写问题
源码中这行代码是关键:
java
String method = paramValue.toUpperCase(Locale.ENGLISH);
无论前端传的是 put、Put、PUT,都会被统一转成大写 PUT,所以前端不区分大小写。
五、总结
| 核心点 | 说明 |
|---|---|
| 核心组件 | HiddenHttpMethodFilter(OncePerRequestFilter 子类) |
| 约定参数 | _method(可通过 setMethodParam() 修改) |
| 设计模式 | 装饰器模式(HttpMethodRequestWrapper 重写 getMethod()) |
| 触发条件 | POST 请求 + 无异常 + _method 值在 ALLOWED_METHODS 中 |
| 支持方法 | PUT、DELETE、PATCH |
| 默认状态 | Spring Boot 2.2.x+ 默认关闭 ,需手动 enabled: true |
| 大小写 | 不区分,源码中 toUpperCase() 统一处理 |
| 自定义 | 注册自己的 HiddenHttpMethodFilter Bean 覆盖默认行为 |