Rest的使用与原理
我们以前写一个请求的时候总是会在@RequestMapping
里面写上/getUser
获取用户 、 /deleteUser
删除用户 、 /updateUser
修改用户 、 /saveUser
保存用户这类的请求,但是现在SpringBoot为我们提供了另外的一种方式来进行这些操作。
springboot 使用HTTP请求方式动词来表示对资源的操作 现在我们只需要写一个 /user
用GET
来获取用户 、 DELETE
来删除用户 、 PUT
来修改用户 、 POST
来保存用户
使用HTML表单出现的问题
然后我们写一个表单来测试一下到底能不能行
<h1>欢迎您</h1>
测试REST风格;
<form action="/user" method="get">
<input value="REST-GET 提交" type="submit"/>
</form>
<form action="/user" method="post">
<input value="REST-POST 提交" type="submit"/>
</form>
<form action="/user" method="delete">
<input value="REST-DELETE 提交" type="submit"/>
</form>
<form action="/user" method="put">
<input value="REST-PUT 提交" type="submit"/>
</form>
这个时候如果我们点击DELETE提交
,发现出来的结果居然是 GET-张三 !!!
这和我们预想的结果不一致,是什么原因导致了这个结果呢 ? 原因其实很简单,因为 HTML 表单还不支持 PUT 和 DELETE,它识别不了put
和delete
所以它就采用默认的get
方式,因此需要对html页面代码进行修改,把它们改为post
方式
<h1>欢迎您</h1>
测试REST风格;
<form action="/user" method="get">
<input value="REST-GET 提交" type="submit"/>
</form>
<form action="/user" method="post">
<input value="REST-POST 提交" type="submit"/>
</form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="delete"/>
<input name="_m" type="hidden" value="delete"/>
<input value="REST-DELETE 提交" type="submit"/>
</form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="PUT"/>
<input value="REST-PUT 提交" type="submit"/>
</form>
这个时候你再去点击DELETE提交
,会发现这次出现的是POST
了,但依然不是我们想要的DELETE
这是什么原因造成的呢?让我们打开源码来一探究竟
表单提交要使用REST的原理
找到WebMvcAutoConfiguration这个类,找到下面这段代码
我们发现,请求过来之后会被HiddenHttpMethodFilter
拦截,并且@ConditionalOnProperty
这个注解中matchIfMissing这一个属性的默认值为false
,这也就解释了为什么我们无法执行DELETE
请求了。在yaml配置文件中将其改为true
后再次访问DELETE请求
现在就可以正常访问了
那为什么一定要在 form 表单的 method 中写 POST 而不是 GET 呢?
继续往下看源码,在 HiddenHttpMethodFilter
类的 doFilterInternal
中给了我们解释
这个方法用来判断请求是否正常,并且是否为POST,然后它才会去获取到_method
的值,那它又是如何获取到method
的值的呢?
在官方源码的注释中我们就可以知道,它用一个包装模式requesWrapper
重写了getMethod
方法,返回的是传入的值。因此它才能执行PUT
、DELETE
、PATCH
这些请求
以上说法仅用于 form表单 提交的时候,其他情境比如直接发送Put、delete等方式请求,无需Filter,也就不需要页面表单的Rest功能了。
这也正是源码中将matchIfMissing
设为false
的原因,因为这项功能仅在某些场景使用,当你需要用到时将其设置为true
即可。
此外,我们在使用rest
的时候其实也并不需要写那么长的注解,例如
可以将
@RequestMapping(value = "/user",method = RequestMethod.GET)
简写为
@GetMapping( "/user")
事实上,当我们去查看@GetMapping
这个注解的源码的时候,会发现它和前一种写法本质上是一样的,两者并无区别,只是这样写帮助我们简化了代码
请求映射原理
只要我们使用了请求,那必然就会要涉及到HttpServlet
,我们现在就来看看这个实现的过程
我们每一层的简单介绍一下,HttpServlet
这个类呢主要处理的就是doGet
这些请求,到了FrameworkServlet
这里呢就变成了doService
,但是这里的doService
方法是个抽象方法,还没有具体实现。也就是说 SpringMVC 把对doService
的实现放在了下一层:DispatcherServletFrameworkServlet 部分源码:
DispatcherServlet 部分源码:(感兴趣的可以自己去看源码)
而在这一大段代码中,最有用的信息是doDispatch
,让我们来看一下这个方法里面又有些什么呢?
方法里面很多行代码官方都写了相应的注释,事实上所有的请求都会经过doDispatch()
这个方法
我们对这里打个断点,来看一下他具体的流程是怎么样的
启动调试后,我们再次去访问 index 页面的时候,我们发现它产生了 5 个 handlerMapping
其中第二个就是欢迎页的支持,我们发现它将页面给转发到了index.html。
回到正题,我们来看一下RequestMapping
,我们可以看到,它往容器中放入了GET,POST,DELETE,PUT,还帮我们写好了 error 错误处理
并且,对于每个具体的请求,在 value 中都保存了相应的 Controller 位置,这也就是执行请求为什么会执行处理器中方法的原因
在往下,我们随便进入一个请求,比如进入 GET ,我们可以看到他在获取到了请求的路径之后还获取了一把锁,这个是为了防止多线程情况下出现数据安全问题
再往下看源码,我们发现对于获取到多个相同请求的情况下,系统会抛出个异常(这里的多个请求并不是指多线程情况下,而是你写代码的时候出现了同名请求的情况)
总结
所有的请求映射都在HandlerMapping中
- SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到index.html;
- SpringBoot自动配置了默认的 RequestMappingHandlerMapping
- 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
- 如果有就找到这个请求对应的handler
- 如果没有就是下一个 HandlerMapping
- 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping,自定义 HandlerMapping