一、什么是 Rest 风格的 URL 请求?

Rest 风格的一大特征就是:使用 HTTP 请求方式动词来表示对资源的操作。

以前只使用 Get / Post 请求,不同业务有不同的接口地址。
例如:

  • /getUser - 获取用户
  • /editUser - 修改用户
  • /addUser - 新增用户
  • /deleteUser - 删除用户

Rest 风格使用不同的请求类型来区分不同的业务接口,但接口地址相同。
例如:

  • /user - GET 请求 - 获取用户
  • /user - DELETE 请求 - 删除用户
  • /user - PUT 请求 - 修改用户
  • /user - POST 请求 - 保存用户

二、Rest风格使用与原理

2.1、几个常用的注解

标注在方法上

@RequestMapping

value 是 path 的别名,因此用哪个都一样。path/value 和 method 都支持数组。

样例:

  1. @RequestMapping(path = "/user", method = RequestMethod.GET)
  2. @RequestMapping(value = "/user", method = {RequestMethod.GET, RequestMethod.POST})
  3. @RequestMapping(value = {"/user", "/user1", "/user2"}, method = RequestMethod.GET)

@GetMapping

GetMapping 注解已经默认封装了@RequestMapping(method = RequestMethod.GET),所以,比前文使用 @RequestMapping(path = “/user”,method = RequestMethod.GET) 更方便。

  1. @GetMapping(path = '/user')
  2. //等价与
  3. @RequestMapping(path = "/user", method = RequestMethod.GET)

@PostMapping

PostMapping 注解已经默认封装了 @RequestMapping(method = RequestMethod.POST)

  1. @PostMapping(path = '/user')
  2. //等价与
  3. @RequestMapping(path = "/user", method = RequestMethod.POST)

@PutMapping

PutMapping 注解已经默认封装了 @RequestMapping(method = RequestMethod.PUT)

  1. @PutMapping(path = '/user')
  2. //等价与
  3. @RequestMapping(path = "/user", method = RequestMethod.PUT)

@DeleteMapping

DeleteMapping 注解已经默认封装了@RequestMapping(method = RequestMethod.Delete)

  1. @DeleteMapping(path = '/user')
  2. //等价与
  3. @RequestMapping(path = "/user", method = RequestMethod.DELETE)

使用方法概览

  1. @RequestMapping(path = "/user", method = RequestMethod.GET)
  2. public String getUser() {
  3. return "GET-张三";
  4. }
  5. @RequestMapping(path = "/user", method = RequestMethod.POST)
  6. public String saveUser() {
  7. return "POST-张三";
  8. }
  9. @RequestMapping(path = "/user", method = RequestMethod.PUT)
  10. public String putUser() {
  11. return "PUT-张三";
  12. }
  13. @RequestMapping(path = "/user", method = RequestMethod.DELETE)
  14. public String deleteUser() {
  15. return "DELETE-张三";
  16. }

分别使用 Get/Post/Put/Delete 类型请求 /user ,即可得到不同的结果。

前后端耦合适配

由于早期项目并没有采用前后端分离的架构,请求由 html 的

表单发起, method 只能选择 Get / Post 两种。因此在这种情况下 Spring Boot 需要做配置 spring.mvc.hiddenmethod.filter = true

  1. spring:
  2. mvc:
  3. hiddenmethod:
  4. filter:
  5. enabled: true

此时请求为 POST ,带上默认参数 _method = putdelete 来显示指定 ,就可以弥补 标签带来的局限性了。
image.png
image.png

如果需要修改 _method这个参数名,你可以新建一个配置类,并向容器中注册一个 HiddenHttpMethodFilter 组件,通过 setMethodParam 方法修改默认的 _method_m

  1. @Configuration(proxyBeanMethods = false)
  2. public class WebConfig {
  3. @Bean
  4. public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
  5. HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
  6. hiddenHttpMethodFilter.setMethodParam("_m");
  7. return hiddenHttpMethodFilter;
  8. }
  9. }

最后重启一下项目即可。

2.2、原理

表单提交要使用 REST 风格时

  • 表单提交会带上_method=PUT
  • 请求过来被HiddenHttpMethodFilter拦截
  • 请求是否正常,并且是POST
  • 获取到_method的值。
  • 兼容以下请求;PUT.DELETE.PATCH
  • 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。
  • 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。

    • 核心Filter;HiddenHttpMethodFilter
      • 用法: 表单method=post,隐藏域 _method=put
    • SpringBoot中手动开启
    • 扩展:如何把_method 这个名字换成我们自己喜欢的。

使用客户端工具

  • 如PostMan直接发送Put、delete等方式请求,无需Filter。

把默认的 _method 参数换成别的名字

原理

回顾到自动配置原理,

WebMvcAutoConfiguration.java

  1. @Bean
  2. @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
  3. @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
  4. public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
  5. return new OrderedHiddenHttpMethodFilter();
  6. }

SpringBoot 在容器中放 OrderedHiddenHttpMethodFilter 这个 filter 的时候,会先判断容器中是否已有这个类型的组件,如果没有,才默认给你放一个。这个组件默认用的是 _method 。

所以,我们只要自己给容器中放一个 HiddenHttpMethodFilter ,然后修改属性就可以了。

继续往下看,HiddenHttpMethodFilter 的源码里面有一个常量:

  1. public static final String DEFAULT_METHOD_PARAM = "_method";

虽然它是不可修改的,但是可以通过修改 methodParam 的值,即可达到我们想要的效果:

  1. private String methodParam = DEFAULT_METHOD_PARAM;

同时,它也提供了一个 set 方法给我们:

  1. public void setMethodParam(String methodParam) {
  2. Assert.hasText(methodParam, "'methodParam' must not be empty");
  3. this.methodParam = methodParam;
  4. }

实现

新建一个配置类,并向容器中注册一个 HiddenHttpMethodFilter 组件,通过 setMethodParam 方法修改默认的 _method 为 _m 。

  1. @Configuration(proxyBeanMethods = false)
  2. public class WebConfig {
  3. @Bean
  4. public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
  5. HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
  6. hiddenHttpMethodFilter.setMethodParam("_m");
  7. return hiddenHttpMethodFilter;
  8. }
  9. }

重启一下,使用 Postman 发送一个 POST 请求模拟 PUT 请求。

可以看到,我们已经把 _method 修改为 _m 了
image.png

三、请求映射原理

上面讲了 Rest 的使用原理,现在再来看一下请求映射的原理。

我们每次发请求,它们到底是怎么找到哪个对应的方法来处理这个请求的。

我们知道, Spring Boot 所有的请求过来,都会来到 DispatcherServlet ,因为 Spring Boot 底层还是使用的 Spring MVC 。Spring MVC 的 DispatcherServlet 是处理所有请求的开始。
image.png

由上图的继承关系图可以看到,DispatcherServlet 也是一个 HttpServlet。

请求映射与 Rest 风格 - 图5

SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet -> doDispatch()

  1. protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
  2. HttpServletRequest processedRequest = request;
  3. HandlerExecutionChain mappedHandler = null;
  4. boolean multipartRequestParsed = false;
  5. WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
  6. try {
  7. ModelAndView mv = null;
  8. Exception dispatchException = null;
  9. try {
  10. processedRequest = checkMultipart(request);
  11. multipartRequestParsed = (processedRequest != request);
  12. // 找到当前请求使用哪个Handler(Controller的方法)处理
  13. mappedHandler = getHandler(processedRequest);
  14. //HandlerMapping:处理器映射。/xxx->>xxxx

请求映射与 Rest 风格 - 图6

RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则。

请求映射与 Rest 风格 - 图7

所有的请求映射都在HandlerMapping中。

  • SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到index.html;
  • SpringBoot自动配置了默认 的 RequestMappingHandlerMapping
  • 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
  • 如果有就找到这个请求对应的handler
  • 如果没有就是下一个 HandlerMapping
  • 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping。自定义 HandlerMapping
    1. protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    2. if (this.handlerMappings != null) {
    3. for (HandlerMapping mapping : this.handlerMappings) {
    4. HandlerExecutionChain handler = mapping.getHandler(request);
    5. if (handler != null) {
    6. return handler;
    7. }
    8. }
    9. }
    10. return null;
    11. }