Spring Boot核心技術之Rest對映以及原始碼的分析

該部落格主要是Rest對映以及原始碼的分析,主要是思路的學習。SpringBoot版本:2.4.9

環境的搭建

主要分兩部分:

Index.html

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. </head>
  7. <body>
  8. <form action="/user" method="get">
  9. <input value="REST-GET提交" type="submit" />
  10. </form>
  11. <form action="/user" method="post">
  12. <input value="REST-POST提交" type="submit" />
  13. </form>
  14. <form action="/user" method="delete">
  15. <input value="REST-DELETE 提交" type="submit"/>
  16. </form>
  17. <form action="/user" method="put">
  18. <input value="REST-PUT提交"type="submit" />
  19. </form>
  20. </body>
  21. </html>

controller

  1. package com.xbhong.Controller;
  2. import org.springframework.stereotype.Controller;
  3. import org.springframework.web.bind.annotation.*;
  4. @RestController
  5. public class myContro {
  6. @RequestMapping(value = "/user",method = RequestMethod.GET)
  7. public String getUser(){
  8. return "GET-張三";
  9. }
  10. // @PostMapping("/user")
  11. @RequestMapping(value = "/user",method = RequestMethod.POST)
  12. public String saveUser(){
  13. return "POST-張三";
  14. }
  15. @RequestMapping(value = "/user",method = RequestMethod.PUT)
  16. public String putUser(){
  17. return "PUT-張三";
  18. }
  19. @RequestMapping(value = "/user",method = RequestMethod.DELETE)
  20. public String deleteUser(){
  21. return "DELETE-張三";
  22. }
  23. }

測試功能是否可用:

觀察效果:

可以看到最後兩個請求都變成Get的請求了。由此我們可以引出來如下問題:

  1. 為什麼不同的請求會出現相同的結果
  2. 怎麼實現的才能完成我們的功能

後面的文章就是圍繞上述的問題進行展開的。

解決問題:

像之前的SpringMVC中的表單請求通過HiddenHttpMethodFilter實現的,這樣我們查一下在SpringBoot中是怎麼樣的。

預設雙擊Shift鍵,輸入WebMvcAutoConfiguration這個類

找到預設配置的過濾器:

  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. }

通過OrderedHiddenHttpMethodFilter進入父類HiddenHttpMethodFilter:

在使用之前,我們需要將後兩個的請求方式改寫成post方式,並且需要在請求的時候傳入一個_method方法(設定隱藏域);

  1. <form action="/user" method="post">
  2. <input name="_method" type="hidden" value="DELETE"/>
  3. <input value="REST-DELETE 提交" type="submit"/>
  4. </form>
  5. <form action="/user" method="post">
  6. <input name="_method" type="hidden" value="PUT" />
  7. <input value="REST-PUT提交"type="submit" />
  8. </form>

為什麼要這樣設定呢?我們到HiddenHttpMethodFilter中看下。

流程:

  1. 第一步儲存了傳入的請求
  2. 當該請求時post,並且請求沒有異常,才能進入下面方法,不是Post請求將直接通過過濾器鏈放行。
  3. 獲取請求中的引數(this.methodParam)
  4. DEFAULT_METHOD_PARAM = _method獲得(作為真正的請求方式)

然後再測試,發現還是沒有實現。

我再回到WebMvcConfiguration中:

  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. }

首先該類存在於容器中。

  1. @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)

當容器中不存在HiddenHttpMethodFilter這個類的時候,下面內容開啟(條件裝配);

  1. @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)

表示:繫結的配置檔案中:spring.mvc.hiddenmethod.filter名字為enable是預設不開啟的(後續版本可能開啟)。這樣我們就找到了問題的所在。

所以我們需要在配置檔案中配置(兩種方法都可以)。

yaml:

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

properties:

  1. spring.mvc.hiddenmethod.filter.enabled=true

重啟專案:成功解決。

原始碼分析:

主要是分析doFilterInternal:

  1. @Override
  2. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
  3. throws ServletException, IOException {
  4. HttpServletRequest requestToUse = request;
  5. if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
  6. String paramValue = request.getParameter(this.methodParam);
  7. if (StringUtils.hasLength(paramValue)) {
  8. String method = paramValue.toUpperCase(Locale.ENGLISH);
  9. if (ALLOWED_METHODS.contains(method)) {
  10. requestToUse = new HttpMethodRequestWrapper(request, method);
  11. }
  12. }
  13. }
  14. filterChain.doFilter(requestToUse, response);
  15. }
  • 表單提交會帶上_method=PUT

  • 請求過來被HiddenHttpMethodFilter攔截

  • 請求是否正常,並且是POST

  • 獲取到_method的值。

  • 相容以下請求;PUT.DELETE.PATCH

    當方法走到上述程式碼11行時,進入ALLOWED_METHODS:

    1. private static final List<String> ALLOWED_METHODS =
    2. Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),
    3. HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));

    如果請求裡的引數在ALLOWED_METHODS存在,則執行下面程式碼。

  • 原生request(post),包裝模式requesWrapper重寫了getMethod方法,返回的是傳入的值。

    進入HttpMethodRequestWrapper物件中,向上找父類。本質還是HttpServletRequest

    由下面程式碼:

    1. private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
    2. private final String method;
    3. public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
    4. super(request);
    5. this.method = method;
    6. }
    7. @Override
    8. public String getMethod() {
    9. return this.method;
    10. }
    11. }

    可知,接收到前面的請求封裝為HttpMethodRequestWrapper返回。

  • 過濾器鏈放行的時候用wrapper。以後的方法呼叫getMethod是呼叫requesWrapper的。

部分補充:

由於原始碼中規則:將獲得請求中的引數無條件的以英文格式轉完成大寫,所以前端的value="PUT"value的值大小寫無影響。

  1. String paramValue = request.getParameter(this.methodParam);
  2. if (StringUtils.hasLength(paramValue)) {
  3. String method = paramValue.toUpperCase(Locale.ENGLISH);

Rest使用客戶端工具,

  • 如PostMan直接傳送Put、delete等方式請求,無需Filter。

參考文獻:

B站尚矽谷

結束:

如果你看到這裡或者正好對你有所幫助,希望能點個關注或者推薦,感謝;

有錯誤的地方,歡迎在評論指出,作者看到會進行修改。