【SpringBoot的坑】Restful请求报错Request method 'POST' not supported,HiddenHttpMethodFilter无法将POST转换为PUT原因分析

直接上结论:

因为 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}"/> 

前端处理
表单页面只支持getpost方式,所以页面创建一个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;
		}
	}

}

发布了574 篇原创文章 · 获赞 203 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/sinat_42483341/article/details/104098417