Spring Boot 从“会用”到“精通”:Rest风格原理

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 覆盖默认行为
相关推荐
Thneonl11 小时前
ClickHouse UPDATE:一秒返回,三小时才生效
数据库·后端
SL_staff11 小时前
破除执行黑箱:用目标地图、资源热力图与健康度看板构建可下钻的执行事实链
java·开源·全栈
骑着蜗牛撵大象32711 小时前
让 Codex 值夜班:把告警排查与巡检接成可回滚的自动化链路
android·java·运维·ai·自动化·codex·智能体
木头科技11 小时前
【AI 工程化第十八篇】Spring AI 语音 Agent 实战:实时转写、WebSocket、流式 TTS 与打断机制
java·人工智能·安全·spring·性能优化
步行cgn11 小时前
Spring MVC 核心配置文件详解:springmvc.xml 每一行配置的意义
xml·spring·mvc
cfm_291411 小时前
SpringPrototype作用域
java
铁皮饭盒11 小时前
这6个小模型各有绝活, 且都可cpu/浏览器端运行
前端·后端
大风起兮云飞扬丶12 小时前
Java——枚举
java·开发语言
qq_4069810312 小时前
大白话rust系列08 当变量遮蔽遇上引用,探究变量遮蔽的本质
开发语言·后端·rust
ReStock活库12 小时前
Zephyr异步SPI调用返回后,为什么不能立即复用发送缓冲区?
java·前端·网络