Rest的使用与原理

我们以前写一个请求的时候总是会在@RequestMapping里面写上/getUser 获取用户 、 /deleteUser 删除用户 、 /updateUser 修改用户 、 /saveUser 保存用户这类的请求,但是现在SpringBoot为我们提供了另外的一种方式来进行这些操作。

springboot 使用HTTP请求方式动词来表示对资源的操作 现在我们只需要写一个 /userGET来获取用户 、 DELETE来删除用户 、 PUT来修改用户 、 POST来保存用户

springboot 文件设置映射关系_springboot 文件设置映射关系

使用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>

springboot 文件设置映射关系_REST_02


这个时候如果我们点击DELETE提交,发现出来的结果居然是 GET-张三 !!!

springboot 文件设置映射关系_REST_03

这和我们预想的结果不一致,是什么原因导致了这个结果呢 ? 原因其实很简单,因为 HTML 表单还不支持 PUT 和 DELETE,它识别不了putdelete所以它就采用默认的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

springboot 文件设置映射关系_springmvc_04


这是什么原因造成的呢?让我们打开源码来一探究竟

表单提交要使用REST的原理

找到WebMvcAutoConfiguration这个类,找到下面这段代码

springboot 文件设置映射关系_spring boot_05


我们发现,请求过来之后会被HiddenHttpMethodFilter拦截,并且@ConditionalOnProperty这个注解中matchIfMissing这一个属性的默认值为false,这也就解释了为什么我们无法执行DELETE请求了。在yaml配置文件中将其改为true后再次访问DELETE请求

springboot 文件设置映射关系_springboot 文件设置映射关系_06


springboot 文件设置映射关系_springmvc_07


现在就可以正常访问了

那为什么一定要在 form 表单的 method 中写 POST 而不是 GET 呢?

继续往下看源码,在 HiddenHttpMethodFilter 类的 doFilterInternal 中给了我们解释

springboot 文件设置映射关系_spring boot_08


这个方法用来判断请求是否正常,并且是否为POST,然后它才会去获取到_method的值,那它又是如何获取到method的值的呢?

springboot 文件设置映射关系_springboot 文件设置映射关系_09


在官方源码的注释中我们就可以知道,它用一个包装模式requesWrapper重写了getMethod方法,返回的是传入的值。因此它才能执行PUTDELETEPATCH这些请求


以上说法仅用于 form表单 提交的时候,其他情境比如直接发送Put、delete等方式请求,无需Filter,也就不需要页面表单的Rest功能了。

这也正是源码中将matchIfMissing设为false的原因,因为这项功能仅在某些场景使用,当你需要用到时将其设置为true即可。

springboot 文件设置映射关系_spring boot_10


此外,我们在使用rest的时候其实也并不需要写那么长的注解,例如

可以将
@RequestMapping(value = "/user",method = RequestMethod.GET)
简写为
@GetMapping( "/user")

事实上,当我们去查看@GetMapping这个注解的源码的时候,会发现它和前一种写法本质上是一样的,两者并无区别,只是这样写帮助我们简化了代码

springboot 文件设置映射关系_REST_11

请求映射原理

只要我们使用了请求,那必然就会要涉及到HttpServlet,我们现在就来看看这个实现的过程

springboot 文件设置映射关系_springmvc_12


我们每一层的简单介绍一下,HttpServlet这个类呢主要处理的就是doGet这些请求,到了FrameworkServlet这里呢就变成了doService,但是这里的doService方法是个抽象方法,还没有具体实现。也就是说 SpringMVC 把对doService的实现放在了下一层:DispatcherServletFrameworkServlet 部分源码:

springboot 文件设置映射关系_spring boot_13


DispatcherServlet 部分源码:(感兴趣的可以自己去看源码)

springboot 文件设置映射关系_springboot 文件设置映射关系_14


而在这一大段代码中,最有用的信息是doDispatch,让我们来看一下这个方法里面又有些什么呢?

springboot 文件设置映射关系_请求映射原理_15


方法里面很多行代码官方都写了相应的注释,事实上所有的请求都会经过doDispatch()这个方法

springboot 文件设置映射关系_REST_16


我们对这里打个断点,来看一下他具体的流程是怎么样的

springboot 文件设置映射关系_springmvc_17

启动调试后,我们再次去访问 index 页面的时候,我们发现它产生了 5 个 handlerMapping

springboot 文件设置映射关系_spring boot_18


其中第二个就是欢迎页的支持,我们发现它将页面给转发到了index.html

springboot 文件设置映射关系_请求映射原理_19


回到正题,我们来看一下RequestMapping,我们可以看到,它往容器中放入了GET,POST,DELETE,PUT,还帮我们写好了 error 错误处理

springboot 文件设置映射关系_REST_20


并且,对于每个具体的请求,在 value 中都保存了相应的 Controller 位置,这也就是执行请求为什么会执行处理器中方法的原因

springboot 文件设置映射关系_请求映射原理_21


在往下,我们随便进入一个请求,比如进入 GET ,我们可以看到他在获取到了请求的路径之后还获取了一把锁,这个是为了防止多线程情况下出现数据安全问题

springboot 文件设置映射关系_springboot 文件设置映射关系_22


再往下看源码,我们发现对于获取到多个相同请求的情况下,系统会抛出个异常(这里的多个请求并不是指多线程情况下,而是你写代码的时候出现了同名请求的情况)

springboot 文件设置映射关系_请求映射原理_23

总结

所有的请求映射都在HandlerMapping中

  • SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到index.html;
  • SpringBoot自动配置了默认的 RequestMappingHandlerMapping
  • 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
  • 如果有就找到这个请求对应的handler
  • 如果没有就是下一个 HandlerMapping
  • 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping,自定义 HandlerMapping