直接上结论:
因为 SpringBoot 版本原因,在我目前使用的 2.2.4 版本中,需要在springapplication.xml
文件中 添加配置:
spring.mvc.hiddenmethod.filter.enabled = true
什么是 REST
Restful 目的只是让 url 看起来更简洁实用,是资源状态的一种表达。
Restful 的使用
由于 H5 的 form 表单仅支持 POST 和 GET 两种请求,实现 restfulAPI 还需要 PUT 和 DELETE ,所以需要使用 HiddenHttpMethodFilter
组件在发送HTTP请求时对请求类型进行伪装。
<!--需要区分是员工修改还是添加;表单页面只支持get和post方式,所以不能直接把method="post"改成method="put"-->
<form th:action="@{/emp}" method="post">
<!--发送 put请求修改员工数据-->
<!--
1、SpringMVC中配置HiddenHttpMethodFilter;(SpringBoot自动配置好的,所用是将请求转成我们指定的方式)
2、页面创建一个post表单
3、创建一个input项,name="_method";值就是我们指定的请求方式
-->
<!--这是一个添加、修改二合一页面,判断如果是修改,才显示这个input标签 -->
<!--此隐藏域可以被HiddenHttpMethodFilter所处理,然后分发到不同的HttpMethod的处理器上-->
<input type="hidden" name="_method" value="put" th:if="${emp!=null}"/>
前端处理
表单页面只支持get
和post
方式,所以页面创建一个post
表单,表单里面创建一个input
项,name="_method"
,值就是我们指定的请求方式。
后台处理
后台需要用 HiddenHttpMethodFilter
,该组件 SpringBoot 已经自动配置好,无需再 @Bean
组件
错误分析
今天在学 SpringBoot 的时候遇到了由于 starter 版本 1 和 2 不同造成的小坑:
后台总是无法映射到 Controller
里对应的 PUT
请求,报错:
后台报错:Request method ‘POST’ not supported
EmployeeController.java 的部分代码如下:
/**
* 员工删除
* @param id
* @return
*/
@DeleteMapping("/emp/{id}") // 处理 delete 请求
public String deleteEmployee(@PathVariable("id") Integer id) {
System.out.println("删除员工id:" + id);
employeeDao.delete(id);
return "redirect:/emps";
}
一开始以为是前端页面的问题,F12 看了一下 HTTP 请求头和表单提交信息:
看上图中显示的请求信息,提交的是post请求,表单中的_method
属性值为delete
,没啥问题
然后去瞄了眼 webmvc 的自动配置类 WebFluxAutoConfiguration:
(路径是org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java
)
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
发现较先前版本多了@ConditionalOnProperty
的注解,也就是条件引入。查看括号内的内容可以知道,这个组件是否加入容器决定于这个属性,再看下SpringBoot配置的metadata元数据对这个property的说明:
(路径是C:\Users\Bug\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\2.2.4.RELEASE\spring-boot-autoconfigure-2.2.4.RELEASE.jar!\META-INF\spring-configuration-metadata.json
)
{
"name": "spring.mvc.hiddenmethod.filter.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable Spring's HiddenHttpMethodFilter.",
"defaultValue": false
},
可以知道 SpringBoot 仅在spring.mvc.hiddenmethod.filter.enabled
这个 property 的值为 true 时才会引入这个组件,但 SpringBoot 默认该属性值为 false ,所以该版本这个组件是默认没有加入容器的。
所以我们只需在配置文件里加上这一句:spring.mvc.hiddenmethod.filter.enabled = true
即可。
附:Spring 的 HiddenHttpMethodFilter 源码
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.filter;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.util.WebUtils;
/**
* {@link javax.servlet.Filter} that converts posted method parameters into HTTP methods,
* retrievable via {@link HttpServletRequest#getMethod()}. Since browsers currently only
* support GET and POST, a common technique - used by the Prototype library, for instance -
* is to use a normal POST with an additional hidden form field ({@code _method})
* to pass the "real" HTTP method along. This filter reads that parameter and changes
* the {@link HttpServletRequestWrapper#getMethod()} return value accordingly.
* Only {@code "PUT"}, {@code "DELETE"} and {@code "PATCH"} HTTP methods are allowed.
*
* <p>The name of the request parameter defaults to {@code _method}, but can be
* adapted via the {@link #setMethodParam(String) methodParam} property.
*
* <p><b>NOTE: This filter needs to run after multipart processing in case of a multipart
* POST request, due to its inherent need for checking a POST body parameter.</b>
* So typically, put a Spring {@link org.springframework.web.multipart.support.MultipartFilter}
* <i>before</i> this HiddenHttpMethodFilter in your {@code web.xml} filter chain.
*
* @author Arjen Poutsma
* @author Juergen Hoeller
* @since 3.0
*/
public class HiddenHttpMethodFilter extends OncePerRequestFilter {
private static final List<String> ALLOWED_METHODS =
Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),
HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
/** Default method parameter: {@code _method}. */
public static final String DEFAULT_METHOD_PARAM = "_method";
private String methodParam = DEFAULT_METHOD_PARAM;
/**
* Set the parameter name to look for HTTP methods.
* @see #DEFAULT_METHOD_PARAM
*/
public void setMethodParam(String methodParam) {
Assert.hasText(methodParam, "'methodParam' must not be empty");
this.methodParam = methodParam;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
HttpServletRequest requestToUse = request;
if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH);
if (ALLOWED_METHODS.contains(method)) {
requestToUse = new HttpMethodRequestWrapper(request, method);
}
}
}
filterChain.doFilter(requestToUse, response);
}
/**
* Simple {@link HttpServletRequest} wrapper that returns the supplied method for
* {@link HttpServletRequest#getMethod()}.
*/
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;
}
}
}