文章目录
一.前言
1.Rest风格的请求
我们现在一般喜欢用Rest风格的请求,即使用HTTP请求方式动词来表示对资源的操作。
举个例子:
- 比如我们以前对学生信息进行相关的增删改查操作,定义的url路径可能是这样的:
/getStudent
获取学生信息/deleteStudent
删除学生信息/editStudent
修改学生信息/savaStudent
保存学生信息
如果这个项目比较大,我们的路径起名字都感觉很麻烦。
- 现在我们用Rest风格这样做:
- 所有对学生的操作都叫
/Student
,如何表示对学生的增删改查呢,使用HTTP请求方式动词来表示对资源的操作。 GET
方式请求表示获取学生信息DELETE
方式请求表示删除学生信息PUT
方式请求表示修改学生信息POST
方式请求表示保存学生信息
利用这些不同的请求方式动词来区分不同的请求。
- 所有对学生的操作都叫
2.表单如何发出delete和put请求
我们以前用SpringMVC来完成这些事情,我们需要配置一个叫HiddenHttpMethodFilter的Filter;
我们来到WebMvcAutoConfiguration中,可以看到它已经配置了一个HiddenHttpMethodFilter,如下:
也就是说默认我们的rest功能是可以用的,但是它通过@ConditionalOnProperty设置配置属性前缀spring.mvc.hiddenmethod.filter的配置属性名字enabled默认为false,来默认该功能是不开启的,我们在配置文件中手动开启它:
spring:
mvc:
hiddenmethod:
filter:
enabled: true #选择性开启
为什么要配置HiddenHttpMethodFilter呢,因为我们的表单提交的请求方式中只支持GET
和POST
方式的请求,它不支持DELETE
和 PUT
方式的请求,如下:
我们的表单的method只有两个选项,get和post:
那我们如何用表单发出delete和put请求呢? 我们可以这样做(为什么这样做我们之后在源码分析中会讲到),method还是等于post请求方式,但是添加了一个name="_method"
、value="DELETE"
或者value="put"
的input标签(value中的delete或者put大小写都可以,之后我们在讲解源码的过程中会看到SpringBoot会自动把其值全部变成大写),并且我们把这个input标签隐藏起来,如下:
这里先说一下为什么name="_method"
,这已经在HiddenHttpMethodFilter类中写好了,如下:
我们也可以调用setMethodParam设置它的值,如下:
自己新建一个配置类,用@Bean注入对象到容器中,在该方法返回对象前,调用setMethodParam来设置methodParam(也就是name)的值,如下:(这里只是做一个演示,下文的所有代码中没有该配置类)
3.完整代码示例:
Controller如下:
@RestController
public class HelloController {
// @RequestMapping(value = "/student",method = RequestMethod.GET)
@GetMapping("/student")
public String getUser(){
return "`GET` 方式请求表示获取学生信息";
}
// @RequestMapping(path = "/student",method = RequestMethod.POST)
@PostMapping("/student")
public String saveUser(){
return "`POST` 方式请求表示保存学生信息";
}
// @RequestMapping(value = "/student",method = RequestMethod.PUT)
@PutMapping("/student")
public String putUser(){
return "`PUT` 方式请求表示修改学生信息";
}
// @RequestMapping(value = "/student",method = RequestMethod.DELETE)
@DeleteMapping("/student")
public String deleteUser(){
return "`DELETE` 方式请求表示删除学生信息";
}
}
index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>SpringBoot Web ,欢迎您!</h1>
测试REST风格:
<!--method为空默认为get方式请求-->
<form action="/student" method="">
<input value="GET请求" type="submit">
</form>
<form action="/student" method="post">
<input value="POST请求" type="submit">
</form>
<form action="/student" method="post">
<input name="_method" value="DELETE" type="hidden">
<input value="DELETE请求" type="submit">
</form>
<form action="/student" method="post">
<input name="_method" value="put" type="hidden">
<input value="PUT请求" type="submit">
</form>
<hr>
</body>
</html>
欢迎页面:
点击测试get请求:
点击测试post请求:
点击测试delete请求:
点击测试put请求:
可以看到这四种方式的请求我们都可以获取并访问到,那么底层源码是怎么写的呢,接下来我将具体讲解源码所做的一些事情。
二.源码分析
1.HiddenHttpMethodFilter类中的doFilterInternal方法
上述提到的hiddenHttpMethodFilter()方法中返回了OrderedHiddenHttpMethodFilter类的对象,把OrderedHiddenHttpMethodFilter对象注册到容器中:
而OrderedHiddenHttpMethodFilter类有继承了HiddenHttpMethodFilter类:
现在我们只需要来看HiddenHttpMethodFilter类了,而HiddenHttpMethodFilter类中的doFilterInternal方法是处理上述过程的主要方法体,我们给doFilterInternal方法打上断点,如下:
2.一步步分析源码:
启动debug来调试程序,接下来我们来一步步分析:
首先我们访问我们的欢迎页面:
点击put请求,表单提交会带上name = value的信息, 在我们的put表单中,name="_method" value=“put”,如下:
所以表单提交时会带上_method=put的信息。
1.第一行代码
表单提交后请求会被HiddenHttpMethodFilter拦截,来到了doFilterInternal方法中,首先拿到我们的原生请求request,把它赋值给一个局部变量requestToUse,如下:
第一行代码:
2.第二行代码
然后进入if条件判断中,判断我们的原生request请求方式是不是POST方式(也就是我们form表单的method属性是不是post方式)和判断当前请求是否没有错误,如下:
刚才我们写的form表单:
第二行代码:
因为我们点击的是put,它的method属性是post,然后也没有什么错误,我们满足条件,进入if语句体中。
3.第三行代码
然后呢,接下来它调用request.getParameter方法获取请求参数并把它赋值给局部变量paramValue:
第三行代码:
其中request.getParameter实参的this.methodParam是什么呢?我们点进去看一下,如下:
它就是_method
字符,这也就解释了为什么之前我们要在form表单中再添加一个input标签,其name值为_method
,request.getParameter(_method)就或许到了其value值,因为我们刚才点击put请求,表单提交会带上name = value的信息, 在我们的put表单中,name="_method" value=“put”,所以局部变量paramValue得到的值为value的值,为put。
4.第四行代码
接着往下走,我们来到第四行代码,这里又是一个if语句判断,StringUtils.hasLength(paramValue)
方法它判断的是字符串是否为null或者为为空,显然我们的paramValue不为null也不为空,为put,满足if判断条件。
StringUtils.hasLength方法:
第四行代码:
5.第五行代码
接着往下走,我们的第五行代码是把我们的paramValue局部变量变成大写,也就是把put变成大写的PUT,然后赋值给新的局部变量method,如下:
第五行代码:
6.第六行代码
又到了if语句判断的地方,这里判断我们的局部变量method(也就是PUT)是否包含在ALLOWED_METHODS这个List集合中,为什么ALLOWED_METHODS是一个集合呢?我们点进去看一下就知道了:
它是这么一个集合,它里面有PUT、DELETE、和PATCH,它们都是枚举类型,我们分别点进去看一下:
它们的name方法是得到其名称,也就是同名的字符串,也就得到了PUT,DELETE和PATCH字符串。
我们的局部变量method(也就是PUT)在其中,if语句为true。
第六行代码:
7.第七行代码
继续往下走,接下来遇到了一个new HttpMethodRequestWrapper新建了一个对象来取代原来的原生request,我们点进这个HttpMethodRequestWrapper中看一下:
我们发现这个HttpMethodRequestWrapper继承了HttpServletRequestWrapper,我们点进HttpServletRequestWrapper看一下:
发现这个HttpServletRequestWrapper最终还是实现了HttpServletRequest,所以呢HttpServletRequestWrapper的子类HttpMethodRequestWrapper他还是一个HttpServletRequest请求。
现在我们回到HttpMethodRequestWrapper类中,具体看其类内部是怎么回事:
我们调用了HttpMethodRequestWrapper的构造器,传递了两个参数分别是原生的request请求和method(也就是PUT)请求方式,在构造器中我们把请求方式method赋值给类的属性method,如下:
然后重写了父类的getMethod方法,返回我们刚刚赋值的method(也就是PUT )。
调用了构造器之后把其创建的对象赋值给了局部变量requestToUse,当我们调用requestToUse的getMethod方法时,我们返回的就是PUT了,这个地方使用了一个包装设计模式,很巧妙。
第七行代码:
8.第八行代码
调用filter过滤器filterchain的dofilter方法,将把自身接收到的请求request对象和response对象和自身对象即filterchain作为下一个过滤器的dofilter的形参传递过去,这样才能使得过滤器传递下去。但是这里的request对象是requestToUse,也就是我们刚刚重写了getMethod方法的requestToUse,当调用getMethod的方法时返回的是PUT。
第八行代码:
dubug放行后运行结果:
上述我们都说的是表单提交的方式发送请求。
如果是客户端直接发送请求,我们的请求方式可以直接是PUT、DELETE等方式。如果不是POST请求它会只运行第一行代码和第八行代码。