Spring MVC 删除功能:用隐藏表单模拟 DELETE 请求的完整解析
原始代码
html
<script>
function del(event){
event.preventDefault();
if(confirm("确定要删除吗?")){
let userform = document.querySelector("#userForm");
userform.action = event.target.href;
userform.submit();
}
}
</script>
<form id="userForm" method="post">
<input type="hidden" name="_method" value="delete"/>
</form>
一、这段代码解决什么问题
浏览器中的超链接 <a> 只能发起 GET 请求,HTML 表单原生只支持 GET 和 POST 。而 RESTful 风格的删除操作要求使用 DELETE 方法。
这段代码的做法是:
- 点击"删除"链接时,先阻止链接默认的 GET 跳转。
- 弹出确认框,用户确认后才继续。
- 找到一个隐藏的 POST 表单,把表单的提交地址改成删除链接的地址。
- 提交这个 POST 表单,同时携带
_method=delete。 - 服务端的
HiddenHttpMethodFilter读取_method,把 POST 改写成 DELETE。 - Spring MVC 用
@DeleteMapping匹配并执行删除。
一句话概括:用超链接提供删除地址,用隐藏表单以 POST 方式提交,再用 _method 把 POST 伪装成 DELETE。
二、逐行代码解释
1. 定义删除函数
javascript
function del(event){
del 是自定义函数名,绑定在删除链接的 onclick 事件上。参数 event 是浏览器传入的事件对象,包含触发事件的元素、默认行为等信息。
2. 阻止链接默认跳转
javascript
event.preventDefault();
超链接 <a href="..."> 被点击时,浏览器默认会发起 GET 请求并跳转页面。preventDefault() 取消这个默认行为,否则页面会先跳走,后面的表单提交逻辑无法执行。
这一行必须放在函数最前面。
3. 弹出确认框
javascript
if(confirm("确定要删除吗?")){
confirm() 是浏览器内置的阻塞式确认框,返回布尔值:
- 点击"确定"返回
true。 - 点击"取消"返回
false。
只有返回 true 时才进入 if 内部执行删除提交。用户取消时,函数结束,不会发生任何请求。前端确认只是防误触,不能替代后端权限和参数校验。
4. 找到隐藏表单
javascript
let userform = document.querySelector("#userForm");
document.querySelector("#userForm") 根据 id 查找页面中的表单元素。
对应的表单是:
html
<form id="userForm" method="post">
querySelector 返回第一个匹配的元素。这里要求页面中 id="userForm" 唯一,否则可能选错表单。
5. 修改表单提交地址
javascript
userform.action = event.target.href;
event.target 是触发事件的元素,也就是被点击的删除链接。href 是这个链接的目标地址,例如:
http://localhost:8080/usermgt/user/1
这一行把隐藏表单的 action 设置为该地址。表单原本没有写 action,提交时会默认提交到当前页面;动态赋值后,提交目标就变成了删除链接指向的 URL。
更稳妥的写法是 event.currentTarget.href:
javascript
userform.action = event.currentTarget.href;
区别在于:如果 <a> 内部还有 <span>、<i> 等子元素,用户点到的可能是子元素,event.target 会指向子元素而不是 <a>,子元素没有 href。event.currentTarget 始终指向绑定事件的 <a> 元素。
6. 提交表单
javascript
userform.submit();
调用表单的原生 submit() 方法,立即以表单的 method 提交。这里的 method 是 post,所以浏览器发出的是 POST 请求。
注意:form.submit() 不会触发 onsubmit 事件。如果表单上有 onsubmit 校验,这里不会执行。当前表单只有隐藏字段,不需要额外校验。
7. 隐藏的 _method 字段
html
<input type="hidden" name="_method" value="delete"/>
这是整个方案的关键。浏览器提交的是 POST,但请求参数里带了一个 _method=delete。
服务端配置 HiddenHttpMethodFilter 后,过滤器会读取这个参数,把请求方法从 POST 改写成 DELETE。Spring MVC 随后按照 DELETE 方法去匹配 @DeleteMapping。
传统 web.xml 中需要配置:
xml
<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>
Spring Boot 中默认关闭,需要显式开启:
yaml
spring:
mvc:
hiddenmethod:
filter:
enabled: true
三、完整使用示例
删除链接通常写在表格的每一行中:
html
<tr th:each="user : ${users}">
<td th:text="${user.id}"></td>
<td th:text="${user.username}"></td>
<td>
<a th:href="@{/user/{id}(id=${user.id})}"
onclick="del(event)">删除</a>
</td>
</tr>
<form id="userForm" method="post">
<input type="hidden" name="_method" value="delete"/>
</form>
假设用户 id 是 1,th:href 渲染为:
/usermgt/user/1
点击删除后:
preventDefault()阻止链接跳转。- 确认框弹出。
- 表单
action被设置为http://localhost:8080/usermgt/user/1。 - 表单以 POST 提交,参数包含
_method=delete。 - 过滤器把 POST 改写成 DELETE。
- Spring MVC 匹配
@DeleteMapping("/user/{id}")。
后端方法示例:
java
@DeleteMapping("/user/{id}")
public String deleteUser(@PathVariable Long id) {
userService.deleteById(id);
return "redirect:/user";
}
四、执行流程图
点击删除链接
│
▼
preventDefault() 阻止 GET 跳转
│
▼
confirm() 弹出确认框
│
├── 取消 → 函数结束,无请求
│
└── 确定
│
▼
querySelector("#userForm") 找到隐藏表单
│
▼
form.action = 链接的 href
│
▼
form.submit() 以 POST 提交
│
▼
请求携带 _method=delete
│
▼
HiddenHttpMethodFilter 把 POST 改写为 DELETE
│
▼
DispatcherServlet 匹配 @DeleteMapping("/user/{id}")
│
▼
执行删除,重定向到列表
五、常见问题与注意事项
1. 忘记配置 HiddenHttpMethodFilter
如果过滤器没生效,服务端收到的仍然是 POST,@DeleteMapping 匹配不到,返回 405。日志里会看到:
POST "/usermgt/user/1"
HttpRequestMethodNotSupportedException
2. 表单 method 写成 GET
html
<form id="userForm" method="get">
这样提交的是 GET,_method 不会按预期生效。必须使用 method="post"。
3. event.target.href 取不到地址
如果删除链接内部有子元素,event.target 可能是子元素。改为:
javascript
userform.action = event.currentTarget.href;
4. 表单 id 重复
页面中如果有多个 id="userForm",querySelector 只会返回第一个,可能导致提交到错误的地址。确保 id 唯一。
5. 路径与后端映射不一致
链接 href 生成的路径必须和 @DeleteMapping 完全匹配。例如后端是:
java
@DeleteMapping("/user/{id}")
链接就应该是 /user/1。如果链接是 /user,后端却要求 /user/{id},会 405 或 404。
6. 前端确认不是安全机制
confirm() 只是防误触。真正的权限校验、存在性校验、关联数据检查必须在后端完成。
7. Spring Security 下的 CSRF
如果项目启用了 Spring Security,POST 表单需要携带 CSRF token,否则会被拒绝。隐藏表单中需要额外加:
html
<input type="hidden" th:name="${_csrf.parameterName}"
th:value="${_csrf.token}"/>
六、为什么不用 AJAX 或直接 GET 删除
| 方案 | 是否推荐 | 原因 |
|---|---|---|
<a href="/delete?id=1"> GET 删除 |
不推荐 | GET 应该是安全的、幂等的,爬虫和预加载可能误触发删除 |
隐藏表单 + _method |
推荐 | 不依赖 JavaScript 框架,传统 MVC 项目常用 |
fetch / axios 发 DELETE |
推荐 | 前后端分离项目更常见,控制更灵活 |
这段代码属于传统服务端渲染项目的经典做法,适合 Thymeleaf + Spring MVC 的表单场景。
七、记忆方法
1. 四步口诀
拦默认,问确认,改地址,交表单。
对应:
preventDefault → confirm → action = href → submit
2. 表单三要素
id 要唯一,
method 用 post,
_method 写 delete。
3. 链路五步
点击链接 → 阻止跳转 → 确认 → 表单改地址 → POST 带 _method
→ 过滤器改写 DELETE → @DeleteMapping 匹配
4. 两个容易记错的地方
event.target 可能不是链接,改用 currentTarget 更稳。
form.submit() 不会触发 onsubmit。
5. 一句话总结
链接只负责提供地址,表单负责发 POST,_method 负责把 POST 变成 DELETE,过滤器负责改写,后端 @DeleteMapping 负责接收。
八、总结
| 代码片段 | 作用 | 注意事项 |
|---|---|---|
event.preventDefault() |
阻止链接默认 GET 跳转 | 必须放在函数最前 |
confirm(...) |
删除前确认 | 只是防误触,不是安全机制 |
querySelector("#userForm") |
找到隐藏表单 | id 必须唯一 |
userform.action = href |
设置提交地址 | 用 currentTarget 更稳 |
userform.submit() |
以 POST 提交 | 不触发 onsubmit |
_method=delete |
让过滤器把 POST 改写为 DELETE | 需要 HiddenHttpMethodFilter |
method="post" |
表单提交方式 | 不能写成 GET |
这段代码的核心是 "用 POST 模拟 DELETE" 。前端用隐藏表单绕开浏览器对 DELETE 的限制,后端用过滤器还原真实方法,Spring MVC 再用 @DeleteMapping 匹配。记住"拦、问、改、交"四步,以及"id 唯一、method 是 post、_method 是 delete"三要素,这类删除功能就能独立写出来。最容易出错的地方是过滤器没开、表单 method 写成 GET、路径和后端映射不一致。