一、背景
博主一直都是在做前后端分离的项目,最近在系统的学习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()));