一、問題簡介
如題,請求 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.Servlet 的 void 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
ModelAttributeMethodProcessor 的 resolveArgument 方法中幾處核心調用:
- attribute = createAttribute(name, parameter, binderFactory, webRequest); 構造一個空對象(比如本文示例中的 ParamMap 對象);
- WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); 創建一個 WebDataBinder 對象,該對象將用於把http請求中的數據綁定到 attribute 中;
- bindRequestParameters(binder, webRequest); 完成 http 請求數據綁定到 attribute 中的操作;
四、ServletRequestDataBinder
具體的綁定操作實現,可以參考 ServletRequestDataBinder 的 bind 方法:
- 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;
}