1. 程式人生 > 其它 >SpringMVC是怎麼把get請求引數封裝到物件中的?

SpringMVC是怎麼把get請求引數封裝到物件中的?

一、問題簡介

如題,請求 http://localhost:8080/api/test?redirectUrl=https%3A%2F%2Fwww.baidu.com%2F&data=123 這樣的 URL,Web應用伺服器用以下控制器來接收:

import com.example.demo.dto.ParamMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class ClientController {

  @RequestMapping("/test")
  public String receive(ParamMap map) {
    System.out.println(map.getRedirectUrl());
    System.out.println(map.getData());
    return "ok";
  }

}

其中,ParamMap 是一個簡單的JavaBean物件:

public class ParamMap {

  private String redirectUrl;

  private String data;

  public String getRedirectUrl() {
    return redirectUrl;
  }

  public void setRedirectUrl(String redirectUrl) {
    this.redirectUrl = redirectUrl;
  }

  public String getData() {
    return data;
  }

  public void setData(String data) {
    this.data = data;
  }
}

URL 中的引數,是怎樣賦值給 ParamMap 的呢?

二、DispatcherServlet

根據 Servlet 規範,當 Servlet 容器允許某個 servlet 物件響應請求時,就會呼叫 javax.servlet.Servletvoid service(ServletRequest request, ServletResponse response) 方法。

SpringMVC 的核心 DispatcherServlet 正是繼承了 javax.servlet.http.HttpServlet ,實現“分發請求給對應處理器”的功能。

2.1 doDispatch

DispatcherServlet

分發請求的核心方法正是 doDispatch,其中

  • getHandler 負責尋找與URL路徑(如本文中的路徑 /api/test)相匹配的處理器
  • getHandlerAdapter 則是尋找處理器對應的介面卡
  • mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    • HTTP請求引數轉化為Controller方法引數
    • 觸發Controller中方法

簡要的呼叫順序如下所示(和 Debug 的呼叫棧的顯示順序相反):

doDispatch       -- DispatcherServlet (org.springframework.web.servlet)
handle           -- AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method)
handleInternal   -- RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
...
invokeAndHandle  -- ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)
...
resolveArgument  -- HandlerMethodArgumentResolverComposite (org.springframework.web.method.support)
resolveArgument  -- ModelAttributeMethodProcessor (org.springframework.web.method.annotation)

三、ModelAttributeMethodProcessor

ModelAttributeMethodProcessorresolveArgument 方法中幾處核心呼叫:

  • attribute = createAttribute(name, parameter, binderFactory, webRequest); 構造一個空物件(比如本文示例中的 ParamMap 物件);
  • WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); 建立一個 WebDataBinder 物件,該物件將用於把http請求中的資料繫結到 attribute 中;
  • bindRequestParameters(binder, webRequest); 完成 http 請求資料繫結到 attribute 中的操作;

四、ServletRequestDataBinder

具體的繫結操作實現,可以參考 ServletRequestDataBinderbind 方法:

  • MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request); 從 ServletRequest 引數建立一個 PropertyValues 例項;
  • doBind 方法把資料儲存到物件中

其中,MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request); 在構造時,有一個父類的建構函式如下:

public ServletRequestParameterPropertyValues(ServletRequest request, @Nullable String prefix, @Nullable String prefixSeparator) {
  super(WebUtils.getParametersStartingWith(request, (prefix != null ? prefix + prefixSeparator : null)));
}

其中,這裡就用到了 getParameterValues 抽取http請求引數並存儲到一個 Map 中:

public static Map<String, Object> getParametersStartingWith(ServletRequest request, @Nullable String prefix) {
  Assert.notNull(request, "Request must not be null");
  Enumeration<String> paramNames = request.getParameterNames();
  Map<String, Object> params = new TreeMap<>();
  if (prefix == null) {
    prefix = "";
  }
  while (paramNames != null && paramNames.hasMoreElements()) {
    String paramName = paramNames.nextElement();
    if (prefix.isEmpty() || paramName.startsWith(prefix)) {
      String unprefixed = paramName.substring(prefix.length());
      String[] values = request.getParameterValues(paramName);
      if (values == null || values.length == 0) {
        // Do nothing, no values found at all.
      }
      else if (values.length > 1) {
        params.put(unprefixed, values);
      }
      else {
        params.put(unprefixed, values[0]);
      }
    }
  }
  return params;
}