Spring MVC <Form>表单中支持REST风格DELETE、PUT类型方法的方式和原理

本文介绍了在Spring MVC项目中,Form表单默认不支持RESTful风格的问题及其解决方案。通过在表单中添加隐藏字段`_method`,并配置`spring.mvc.hiddenmethod.filter.enabled`为true,可以使得表单提交时模拟PUT和DELETE请求。文章还解析了HiddenHttpMethodFilter的工作原理,解释了为何这种方法能够实现RESTful操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、背景

博主一直都是在做前后端分离的项目,最近在系统的学习Spring系列,发现集成在后端应用的页面中的Form表单默认并不支持REST风格;因此有了今天的故事

二、不支持REST的写法

Controller:

@RestController
public class RestDemoController {

    @GetMapping("/user")
    public String getUser() {
        return "getUser";
    }

    @PostMapping("/user")
    public String postUser() {
        return "postUser";
    }

    @PutMapping("/user")
    public String putUser() {
        return "putUser";
    }

    @DeleteMapping("/user")
    public String deleteUser() {
        return "deleteUser";
    }
}

在classpath:/static/目录下有一个user.html文件
在这里插入图片描述

内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
getUser:
<form action="/user" method="get">
    <input value="Submit" type="submit">
</form>

postUser:
<form action="/user" method="post">
    <input value="Submit" type="submit">
</form>

PutUser:
<form action="/user" method="put">
    <input value="Submit" type="submit">
</form>

DeleteUser:
<form action="/user" method="delete">
    <input value="Submit" type="submit">
</form>

</body>
</html>

走页面访问,我们发现:get和post类型的方法可以正常访问,而put和delete类型的方法都是访问到get方法;
在这里插入图片描述

三、支持REST的写法

针对如何Form表单实现PUT和DELETE方法,Spring MVC提供了如下方式:

  • 在form表单中添加name为_method,type为hidden,value为put/delete的输入框:

    <form action="/user" method="post">
        <input name="_method" type="hidden" value="put">
        <input value="Submit" type="submit">
    </form>
    
  • 在后台的application.yml文件中开启识别form中"_method"名称功能

    spring:
      # 开启识别form中"_method"功能
      mvc:
        hiddenmethod:
          filter:
            enabled: true
    

user.html页面如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
getUser:
<form action="/user" method="get">
    <input value="Submit" type="submit">
</form>

postUser:
<form action="/user" method="post">
    <input value="Submit" type="submit">
</form>

PutUser:
<form action="/user" method="post">
    <input name="_method" type="hidden" value="put">
    <input value="Submit" type="submit">
</form>

DeleteUser:
<form action="/user" method="post">
    <input name="_method" type="hidden" value="delete">
    <input value="Submit" type="submit">
</form>

</body>
</html>

application.yml如下:

spring:
  # 开启识别form中"_method"功能
  mvc:
    hiddenmethod:
      filter:
        enabled: true

验证:可以正常访问到put/delete方法
在这里插入图片描述

四、原理(HiddenHttpMethodFilter源码解析)

为什么通过上述的配置就可以让form支持REST风格呢?

我们通过yaml文件中的配置spring.mvc.hiddenmethod.filter.enable属性往里跟一下。
请添加图片描述
全局搜索spring.mvc.hiddenmethod.filter,进入到WebMvcAutoConfiguration类中:

@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
    return new OrderedHiddenHttpMethodFilter();
}

我们接着看new一个OrderedHiddenHttpMethodFilter其中都做了什么?

public class OrderedHiddenHttpMethodFilter extends HiddenHttpMethodFilter implements OrderedFilter {

    /**
	 * The default order is high to ensure the filter is applied before Spring Security.
	 */
    public static final int DEFAULT_ORDER = REQUEST_WRAPPER_FILTER_MAX_ORDER - 10000;

    private int order = DEFAULT_ORDER;

    @Override
    public int getOrder() {
        return this.order;
    }

    /**
	 * Set the order for this filter.
	 * @param order the order to set
	 */
    public void setOrder(int order) {
        this.order = order;
    }

}

从代码上俩看,这里并没有和REST相关的实际操作,我们接着去看看OrderedHiddenHttpMethodFilter的父类HiddenHttpMethodFilter

HiddenHttpMethodFilter#doFilterInternal()方法从命令来看应该就是过滤的核心逻辑了:

  • POST请求过来,会解析出请求中的_method属性,然后将其value值转换为大写,最后通过将请求的类型替换掉。
  • 请求的类型必须是POST,并且允许的REST方法只有PUT、DELETE、PATCH,别的方法都不会被转换,也就是说维持原样。
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
    throws ServletException, IOException {

    HttpServletRequest requestToUse = request;

    // 只有当请求的类型是POST,并且没有异常的情况才会进入
    if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
        // this.methodParam的值就是我们在Form表单中配置的`_method`
        String paramValue = request.getParameter(this.methodParam);
        if (StringUtils.hasLength(paramValue)) {
            String method = paramValue.toUpperCase(Locale.ENGLISH);
            // ALLOWED_METHODS表示所有允许的方法类型,目前只支持PUT、DELETE、PATCH
            if (ALLOWED_METHODS.contains(method)) {
                requestToUse = new HttpMethodRequestWrapper(request, method);
            }
        }
    }

    filterChain.doFilter(requestToUse, response);
}
public static final String DEFAULT_METHOD_PARAM = "_method";

private String methodParam = DEFAULT_METHOD_PARAM;

private static final List<String> ALLOWED_METHODS =
    Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),
 HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>员工信息</title> <script type="text/javascript" th:src="@{/js/vue.global.js}"></script> </head> <body> <table id="dataTable" border="1" cellspacing="0" cellpadding="0" style="text-align:center"> <tr> <th colspan="5">员工信息</th> </tr> <tr> <th>编号</th> <th>姓名</th> <th>邮箱</th> <th>性别</th> <th>操作</th> </tr> <tr th:each="employee : ${employeeList}"> <td th:text="${employee.id}"></td> <td th:text="${employee.lastName}"></td> <td th:text="${employee.email}"></td> <td th:text="${employee.gender}"></td> <td> <a @click="deleteEmployee" th:href="@{'/employee/' + ${employee.id}}">删除</a> <a href="">修改</a> </td> </tr> </table> <form id="deleteForm" method="post"> <input type="hidden" name="_method" value="delete"> </form> <script type="text/javascript"> var vue = new Vue({ el: "#dataTable", methods: { deleteEmployee: function (event) { var deleteForm = document.getElementById("deleteForm"); deleteForm.action = event.target.href; deleteForm.submit(); event.preventDefault(); } } }); </script> </body> </html> //删除员工信息 @RequestMapping(value = "/employee/{id}",method = RequestMethod.DELETE) public String deleteEmployee(@PathVariable("id") Integer id){ employeeDao.delete(id); return "redirect:/employee"; } <?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"> <!--配置编码过滤器--> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceResponseEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!--配置 处理请求方式putdelete的HiddenHttpmethodFilter--> <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> <!--配置SpringMVC的前端控制器DispatcherServlet--> <servlet> <servlet-name>DispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springMVC.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>DispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app> <?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 http://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.rest"/> <!--配置thymeleaf视图解析器--> <bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver"> <property name="order" value="1"/> <property name="characterEncoding" value="UTF-8"/> <property name="templateEngine"> <bean class="org.thymeleaf.spring5.SpringTemplateEngine"> <property name="templateResolver"> <bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver"> <!-- 视图前缀 --> <property name="prefix" value="/WEB-INF/templates/"/> <!-- 视图后缀 --> <property name="suffix" value=".html"/> <property name="templateMode" value="HTML5"/> <property name="characterEncoding" value="UTF-8"/> </bean> </property> </bean> </property> </bean> <!--配置视图控制器--> <mvc:view-controller path="/" view-name="index"></mvc:view-controller> <!--开放对静态资源的访问呢--> <mvc:default-servlet-handler></mvc:default-servlet-handler> <!--开启mvc注解驱动--> <mvc:annotation-driven></mvc:annotation-driven> </beans> 报错 405 找不到方法
最新发布
07-05
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秃秃爱健身

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值