1. 程式人生 > >詳解SpringMVC中Controller的方法中引數的工作原理[附帶原始碼分析]

詳解SpringMVC中Controller的方法中引數的工作原理[附帶原始碼分析]

目錄

前言

SpringMVC是目前主流的Web MVC框架之一。 

SpringMVC中Controller的方法引數可以是Integer,Double,自定義物件,ServletRequest,ServletResponse,ModelAndView等等,非常靈活。本文將分析SpringMVC是如何對這些引數進行處理的,使讀者能夠處理自定義的一些引數。

現象

本文使用的demo基於maven。我們先來看一看對應的現象。 

@Controller
@RequestMapping(value = "/test")
public class TestController {
  
  @RequestMapping
("/testRb") @ResponseBody public Employee testRb(@RequestBody Employee e) { return e; } @RequestMapping("/testCustomObj") @ResponseBody public Employee testCustomObj(Employee e) { return e; } @RequestMapping("/testCustomObjWithRp") @ResponseBody public Employee testCustomObjWithRp
(@RequestParam Employee e) { return e; } @RequestMapping("/testDate") @ResponseBody public Date testDate(Date date) { return date; } }

首先這是一個Controller,有4個方法。他們對應的引數分別是帶有@RequestBody的自定義物件、自定義物件、帶有@RequestParam的自定義物件、日期物件。

接下來我們一個一個方法進行訪問看對應的現象是如何的。

首先第一個testRb:

 

第二個testCustomObj:

第三個testCustomObjWithRp:

第四個testDate:

為何返回的Employee物件會被自動解析為xml,請看樓主的另一篇部落格:戳我

為何Employee引數會被解析,帶有@RequestParam的Employee引數不會被解析,甚至報錯?

為何日期型別不能被解析?

SpringMVC到底是如何處理這些方法的引數的?

@RequestBody、@RequestParam這兩個註解有什麼區別?

帶著這幾個問題。我們開始進行分析。

原始碼分析

本文所分析的原始碼是Spring版本4.0.2

在分析原始碼之前,首先讓我們來看下SpringMVC中兩個重要的介面。

兩個介面分別對應請求方法引數的處理、響應返回值的處理,分別是HandlerMethodArgumentResolverHandlerMethodReturnValueHandler,這兩個介面都是Spring3.1版本之後加入的。

       

SpringMVC處理請求大致是這樣的:

首先被DispatcherServlet截獲,DispatcherServlet通過handlerMapping獲得HandlerExecutionChain,然後獲得HandlerAdapter。

HandlerAdapter在內部對於每個請求,都會例項化一個ServletInvocableHandlerMethod進行處理,ServletInvocableHandlerMethod在進行處理的時候,會分兩部分別對請求跟響應進行處理

之後HandlerAdapter得到ModelAndView,然後做相應的處理。

本文將重點介紹ServletInvocableHandlerMethod對請求以及響應的處理。

1. 處理請求的時候,會根據ServletInvocableHandlerMethod的屬性argumentResolvers(這個屬性是它的父類InvocableHandlerMethod中定義的)進行處理,其中argumentResolvers屬性是一個HandlerMethodArgumentResolverComposite類(這裡使用了組合模式的一種變形),這個類是實現了HandlerMethodArgumentResolver介面的類,裡面有各種實現了HandlerMethodArgumentResolver的List集合。

2. 處理響應的時候,會根據ServletInvocableHandlerMethod的屬性returnValueHandlers(自身屬性)進行處理,returnValueHandlers屬性是一個HandlerMethodReturnValueHandlerComposite類(這裡使用了組合模式的一種變形),這個類是實現了HandlerMethodReturnValueHandler介面的類,裡面有各種實現了HandlerMethodReturnValueHandler的List集合。

ServletInvocableHandlerMethod的returnValueHandlers和argumentResolvers這兩個屬性都是在ServletInvocableHandlerMethod進行例項化的時候被賦值的(使用RequestMappingHandlerAdapter的屬性進行賦值)。

RequestMappingHandlerAdapter的argumentResolvers和returnValueHandlers這兩個屬性是在RequestMappingHandlerAdapter進行例項化的時候被Spring容器注入的。

其中預設的ArgumentResolvers:

預設的returnValueHandlers:

我們在json、xml自動轉換那篇文章中已經瞭解,使用@ResponseBody註解的話最終返回值會被RequestResponseBodyMethodProcessor這個HandlerMethodReturnValueHandler實現類處理。

我們通過原始碼發現,RequestResponseBodyMethodProcessor這個類其實同時實現了HandlerMethodReturnValueHandler和HandlerMethodArgumentResolver這兩個介面。

RequestResponseBodyMethodProcessor支援的請求型別是Controller方法引數中帶有@RequestBody註解,支援的響應型別是Controller方法帶有@ResponseBody註解。 

RequestResponseBodyMethodProcessor響應的具體處理是使用訊息轉換器。

處理請求的時候使用內部的readWithMessageConverters方法。

然後會執行父類(AbstractMessageConverterMethodArgumentResolver)的readWithMessageConverters方法。

下面來我們來看看常用的HandlerMethodArgumentResolver實現類(本文粗略講下,有興趣的讀者可自行研究)。

1. RequestParamMethodArgumentResolver

 支援帶有@RequestParam註解的引數或帶有MultipartFile型別的引數

2. RequestParamMapMethodArgumentResolver

  支援帶有@RequestParam註解的引數 && @RequestParam註解的屬性value存在 && 引數型別是實現Map介面的屬性

3. PathVariableMethodArgumentResolver

支援帶有@PathVariable註解的引數 且如果引數實現了Map介面,@PathVariable註解需帶有value屬性

4. MatrixVariableMethodArgumentResolver

支援帶有@MatrixVariable註解的引數 且如果引數實現了Map介面,@MatrixVariable註解需帶有value屬性 

5. RequestResponseBodyMethodProcessor

 本文已分析過

6. ServletRequestMethodArgumentResolver

 引數型別是實現或繼承或是WebRequest、ServletRequest、MultipartRequest、HttpSession、Principal、Locale、TimeZone、InputStream、Reader、HttpMethod這些類。

(這就是為何我們在Controller中的方法裡新增一個HttpServletRequest引數,Spring會為我們自動獲得HttpServletRequest物件的原因)

7. ServletResponseMethodArgumentResolver

 引數型別是實現或繼承或是ServletResponse、OutputStream、Writer這些類

8. RedirectAttributesMethodArgumentResolver

 引數是實現了RedirectAttributes介面的類

9. HttpEntityMethodProcessor

 引數型別是HttpEntity

從名字我們也看的出來, 以Resolver結尾的是實現了HandlerMethodArgumentResolver介面的類,以Processor結尾的是實現了HandlerMethodArgumentResolver和HandlerMethodReturnValueHandler的類。

下面來我們來看看常用的HandlerMethodReturnValueHandler實現類。

1. ModelAndViewMethodReturnValueHandler

返回值型別是ModelAndView或其子類

2. ModelMethodProcessor

返回值型別是Model或其子類

3. ViewMethodReturnValueHandler

返回值型別是View或其子類 

4. HttpHeadersReturnValueHandler

返回值型別是HttpHeaders或其子類  

5. ModelAttributeMethodProcessor

返回值有@ModelAttribute註解

6. ViewNameMethodReturnValueHandler

返回值是void或String

其餘沒講過的讀者可自行檢視原始碼。

下面開始解釋為何本文開頭出現那些現象的原因:

1. 第一個方法testRb以及地址 http://localhost:8888/SpringMVCDemo/test/testRb?name=1&age=3

  這個方法的引數使用了@RequestBody,之前已經分析過,被RequestResponseBodyMethodProcessor進行處理。之後根據http請求頭部的contentType然後選擇合適的訊息轉換器進行讀取。

  很明顯,我們的訊息轉換器只有預設的那些跟部分json以及xml轉換器,且傳遞的引數name=1&age=3,傳遞的頭部中沒有content-type,預設使用了application/octet-stream,因此觸發了HttpMediaTypeNotSupportedException異常

  解放方案: 我們將傳遞資料改成json,同時http請求的Content-Type改成application/json即可。

      

完美解決。

2. testCustomObj方法以及地址 http://localhost:8888/SpringMVCDemo/test/testCustomObj?name=1&age=3

這個請求會找到ServletModelAttributeMethodProcessor這個resolver。預設的resolver中有兩個ServletModelAttributeMethodProcessor,只不過例項化的時候屬性annotationNotRequired一個為true,1個為false。這個ServletModelAttributeMethodProcessor處理引數支援@ModelAttribute註解,annotationNotRequired屬性為true的話,引數不是簡單型別就通過,因此選擇了ServletModelAttributeMethodProcessor,最終通過DataBinder例項化Employee物件,並寫入對應的屬性。

3. testCustomObjWithRp方法以及地址 http://localhost:8888/SpringMVCDemo/test/testCustomObjWithRp?name=1&age=3

這個請求會找到RequestParamMethodArgumentResolver(使用了@RequestParam註解)。RequestParamMethodArgumentResolver在處理引數的時候使用request.getParameter(引數名)即request.getParameter("e")得到,很明顯我們的引數傳的是name=1&age=3。因此得到null,RequestParamMethodArgumentResolver處理missing value會觸發MissingServletRequestParameterException異常。 [粗略講下,有興趣的讀者請自行檢視原始碼]

    解決方案:去掉@RequestParam註解,讓ServletModelAttributeMethodProcessor來處理。

4. testDate方法以及地址 http://localhost:8888/SpringMVCDemo/test/testDate?date=2014-05-15

這個請求會找到RequestParamMethodArgumentResolver。因為這個方法與第二個方法一樣,有兩個RequestParamMethodArgumentResolver,屬性useDefaultResolution不同。RequestParamMethodArgumentResolver支援簡單型別,ServletModelAttributeMethodProcessor是支援非簡單型別。最終步驟跟第三個方法一樣,我們的引數名是date,於是通過request.getParameter("date")找到date字串(這裡引數名如果不是date,那麼最終頁面是空白的,因為沒有@RequestParam註解,引數不是必須的,RequestParamMethodArgumentResolver處理null值返回null)。最後通過DataBinder找到合適的屬性編輯器進行型別轉換。最終找到java.util.Date物件的建構函式 public Date(String s),由於我們傳遞的格式不是標準的UTC時間格式,因此最終觸發了IllegalArgumentException異常。

    解決方案:

    1. 傳遞引數的格式修改成標準的UTC時間格式:http://localhost:8888/SpringMVCDemo/test/testDate?date=Sat, 17 May 2014 16:30:00 GMT

    2.在Controller中加入自定義屬性編輯器。

@InitBinder
public void initBinder(WebDataBinder binder) {
  SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
  binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}

 這個@InitBinder註解在例項化ServletInvocableHandlerMethod的時候被注入到WebDataBinderFactory中的,而WebDataBinderFactory是ServletInvocableHandlerMethod的一個屬性。在RequestMappingHandlerAdapter原始碼的803行getDataBinderFactory就是得到的WebDataBinderFactory。

之後RequestParamMethodArgumentResolver通過WebDataBinderFactory建立的WebDataBinder裡的自定義屬性編輯器找到合適的屬性編輯器(我們自定義的屬性編輯器是用CustomDateEditor處理Date物件,而testDate的引數剛好是Date),最終CustomDateEditor把這個String物件轉換成Date物件。

編寫自定義的HandlerMethodArgumentResolver

通過前面的分析,我們明白了SpringMVC處理Controller中的方法的引數流程。

現在,如果方法中有兩個引數,且都是自定義類引數,那該如何處理呢?

很明顯,要處理這個只能自己實現一個實現HandlerMethodArgumentResolver的類。

先定義1個註解FormObj:

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface FormObj {
    //引數別名
    String value() default "";
    //是否展示, 預設展示
    boolean show() default true;
}

然後是HandlerMethodArgumentResolver:

public class FormObjArgumentResolver implements HandlerMethodArgumentResolver {
  
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(FormObj.class);
    }
    
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        FormObj formObj = parameter.getParameterAnnotation(FormObj.class);

        String alias = getAlias(formObj, parameter);

        //拿到obj, 先從ModelAndViewContainer中拿,若沒有則new1個引數型別的例項
        Object obj = (mavContainer.containsAttribute(alias)) ?
                mavContainer.getModel().get(alias) : createAttribute(alias, parameter, binderFactory, webRequest);


        //獲得WebDataBinder,這裡的具體WebDataBinder是ExtendedServletRequestDataBinder
        WebDataBinder binder = binderFactory.createBinder(webRequest, obj, alias);

        Object target = binder.getTarget();

        if(target != null) {
            //繫結引數
            bindParameters(webRequest, binder, alias);
            //JSR303 驗證
            validateIfApplicable(binder, parameter);
            if (binder.getBindingResult().hasErrors()) {
                if (isBindExceptionRequired(binder, parameter)) {
                    throw new BindException(binder.getBindingResult());
                }
            }
        }

        if(formObj.show()) {
            mavContainer.addAttribute(alias, target);
        }

        return target;
    }
    
    
    private Object createAttribute(String alias, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest webRequest) {
        return BeanUtils.instantiateClass(parameter.getParameterType());
    }
    
    private void bindParameters(NativeWebRequest request, WebDataBinder binder, String alias) {
        ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);

        MockHttpServletRequest newRequest = new MockHttpServletRequest();

        Enumeration<String> enu = servletRequest.getParameterNames();
        while(enu.hasMoreElements()) {
            String paramName = enu.nextElement();
            if(paramName.startsWith(alias)) {
                newRequest.setParameter(paramName.substring(alias.length()+1), request.getParameter(paramName));
            }
        }
        ((ExtendedServletRequestDataBinder)binder).bind(newRequest);
    }
    
    protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
        Annotation[] annotations = parameter.getParameterAnnotations();
        for (Annotation annot : annotations) {
            if (annot.annotationType().getSimpleName().startsWith("Valid")) {
                Object hints = AnnotationUtils.getValue(annot);
                binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
                break;
            }
        }
    }
    
    protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
        int i = parameter.getParameterIndex();
        Class<?>[] paramTypes = parameter.getMethod().getParameterTypes();
        boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));

        return !hasBindingResult;
    }
    
    private String getAlias(FormObj formObj, MethodParameter parameter) {
        //得到FormObj的屬性value,也就是物件引數的簡稱
        String alias = formObj.value();
        if(alias == null || StringUtils.isBlank(alias)) {
            //如果簡稱為空,取物件簡稱的首字母小寫開頭
            String simpleName = parameter.getParameterType().getSimpleName();
            alias = simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1);
        }
        return alias;
    }

  
}

對應Controller:

@Controller
@RequestMapping(value = "/foc")
public class FormObjController {
  
    @RequestMapping("/test1")
    public String test1(@FormObj Dept dept, @FormObj Employee emp) {
        return "index";
    }
    
    @RequestMapping("/test2")
    public String test2(@FormObj("d") Dept dept, @FormObj("e") Employee emp) {
        return "index";
    }
    
    @RequestMapping("/test3")
    public String test3(@FormObj(value = "d", show = false) Dept dept, @FormObj("e") Employee emp) {
        return "index";
    }
  
}

結果如下:

總結

寫了這麼多,主要還是鞏固一下自己對SpringMVC對請求及響應的處理做一個細節的總結吧,不知道大家有沒有清楚這個過程。

想熟悉這部分內容最主要的還是要熟悉HandlerMethodArgumentResolver和HandlerMethodReturnValueHandler這兩個介面以及屬性編輯器、資料繫結機制。

本文難免有錯誤,希望讀者能指出來。

參考資料