SpringMVC源碼閱讀:Controller中參數解析

1.前言

SpringMVC是目前J2EE平臺的主流Web框架,不熟悉的園友能夠看SpringMVC源碼閱讀入門,它交代了SpringMVC的基礎知識和源碼閱讀的技巧html

本文將經過源碼(基於Spring4.3.7)分析,弄清楚Controller是如何匹配咱們傳入的參數,並定義簡單的參數解析器git

2.源碼分析

demo源碼在這裏,回到DispatcherServlet的doDispatch方法,DispatchServlet分析見SpringMVC源碼閱讀:核心分發器DispatcherServletgithub

doDispatch方法943行獲取了HandlerAdapter,ctrl+h打開類繼承圖,找到RequestMappingHandlerAdapter,RequestMappingHandlerAdapter支持HandlerMethod的方法參數和返回類型,HandlerMethod是3.1版本引入的,爲參數、返回值和註解提供便捷的封裝web

在RequestMappingHandlerAdapter的invokeHandlerMethod方法中,設置ArgumentResolver和ReturnValueHandlerspring

798行和799行給RequestMappingHandlerAdapter定義的ArgumentResolver和ReturnValueHandler賦值,4.2版本之前在createRequstMapping方法,此方法在4.2已被刪除json

827行ServletInvocableHandlerMethod調用invokeAndHandle方法,經過定義的HandlerMethodReturnValueHandler處理返回值,點開invokeAndHandle方法進入ServletInvocableHandlerMethod類瀏覽器

116行處理經過HandlerMethodArgumentResolver來解析參數,132~133行使用註冊過的HandlerMethodReturnValueHandlermvc

afterPropertiesSet方法實現了InitializingBean接口初始化了Handler和Resolver,簡單地說,啓動服務纔會運行afterPropertiesSetapp

517行設置ArgumentResolver,525行設置ReturnValueHandler框架

點看getDefaultArgumentResolvers方法,看看它到底作了什麼

getDefaultArgumentResolvers方法把各類HandlerMethodArgumentResolver放入List並返回

同理,getDefaultReturnValueHandlers方法把各類HandlerMethodReturnValueHandler放入List並返回

如今在回到ServletInvocableHandlerMethod類,咱們發現了returnValueHandlersHandlerMethodReturnValueHandlerComposite類型的,神祕的HandlerMethodReturnValueHandlerComposite是什麼?

查看類實現圖,咱們發現HandlerMethodReturnValueHandlerComposite繼承HandlerMethodReturnValueHandler

咱們能夠看到,HandlerMethodReturnValueHandlerComposite類裏有HandlerMethodReturnValueHandler類型的list,作過樹結構的園友們應該知道,這裏用到了組合模式,即類包含自身對象組。可是呢,它的類名後面加上了Composite,不能嚴格意義上說是組合模式,能夠說是組合模式的變種,由於HandlerMethodReturnValueHandler是個Interface,因此不是嚴格意義上的「組合模式」

咱們用一樣的思路尋找與HandlerMethodArgumentResolver對應的Composite類,我在ServletInvocableHandlerMethod沒有找到HandlerMethodArgumentResolverComposite,(在4.3版本以前能夠在ServletInvocableHandlerMethod找到),不用擔憂,使用絕招

快捷鍵ctrl+shift+r,用idea強大的全局搜索來找HandlerMethodArgumentResolverComposite的蹤影

這裏注意一下,全局搜索選擇Scope,才能夠在文件全部路徑下搜索(包括Maven源碼包)

最後咱們看到了HandlerMethodArgumentResolverComposite在InvocableHandlerMethod出現,這個類名以爲有些眼熟吧,它是ServletInvocableHandlerMethod的父類

ServletInvocableHandlerMethod調用InvocableHandlerMethod的invokeForRequest方法中使用了HandlerMethodArgumentResolverComposite

打開HandlerMethodArgumentResolverComposite,和HandlerMethodReturnValueHandlerComposite相似,使用組合模式的變種

好了,參數解析基本流程完畢,咱們如今來具體看看支持和參數相映射的註解的參數解析類,對着HandlerMethodArgumentResolver按ctrl+h

能夠看到龐大的類繼承圖,咱們看支持@RequestBody的RequestResponseBodyMethodProcessor類

可能會有園友好奇,爲何我知道RequestResponseBodyMethodProcessor類支持@RequestBody?

一個簡便的方法是直接看類名,開源項目Spring的代碼質量很是高,它們的類名言簡意賅,看類名大概就知道它是作什麼的;類名若是看不出來,就點進去看註釋,註釋很規範、詳細

打開RequestResponseBodyMethodProcessor類

支持帶有@RequestBody的參數,支持帶有@ResponseBody的返回值

寫一個方法進行測試

   @RequestMapping(value = "/testRb",produces={"application/json; charset=UTF-8"},method = RequestMethod.POST)
    @ResponseBody
    public Employee testRb(@RequestBody Employee e) {
        return e;
    }

 

http://localhost:8080/springmvcdemo/test/testRb,傳入參數爲{"age":1,"id":2},我用的Postman測試請求,直接瀏覽器地址欄輸入,默認Get請求會報錯,不嫌麻煩能夠本身手寫Ajax,參數類型設置成Json測試

header寫成application/json,請求類型寫POST

Body傳入Json格式參數

如今咱們進入resolveArgument方法

 

127行獲取參數信息,128行調用readWithMessageConverters方法獲取參數值

131行建立WebDataBinder,用於校驗數據格式是否正確

點開128行readWithMessageConverters方法,看看它作什麼

148行獲取請求信息,如頭信息

咱們看到Content-Type正是咱們在Postman中設置的"application/json"

150行獲取參數,調用父類AbstractMessageConverterMethodArgumentResolver的readWithMessageConverters方法,父類方法用於從請求信息中讀取方法參數值

152行查看參數註解是不是@RequestBody

繼續深刻,進入AbstractMessageConverterMethodArgumentResolver的readWithMessageConverters方法

167行從Headers取得Content-Type

172~175行若是Content-Type爲空,默認給咱們Content-Type設置"application/octet-stream"

185行獲取Http請求方法

191行用消息轉換器讀取請求體

 

接下來,咱們分析下經常使用的@RequestParam註解是如何處理參數的

用這個測試方法

    @RequestMapping("/auth")
    public String auth(@RequestParam String username, HttpServletRequest req) {
        req.getSession().setAttribute("loginUser", username);
        return "redirect:/";
    }

 

找到RequestParamMethodArgumentResolver類,該類的核心方法是resolveName方法

158行獲取請求信息

159行獲取MultipartHttpServletRequest請求信息,用於文件上傳

175行獲取參數值

打斷點我發現,在RequestParamMethodArgumentResolver的父類AbstractNamedValueMethodArgumentResolver中,resolveArgument方法會先執行。後續咱們自定義的參數解析器主要就是重寫resolveArgument方法

 

97行獲取參數名稱

103行調用resolveName方法獲取參數值,該方法被AbstractNamedValueMethodArgumentResolver子類RequestParamMethodArgumentResolver實現,剛纔咱們已經分析過

 

我再說下其餘經常使用的HandlerArgumentResolver實現類,就不源碼分析了,有時間我會補充上,園友能夠自行打斷掉調試查看之

1.PathVariableMethodArgumentResolver

支持帶有@PathVariable註解的參數,用來得到請求url中的動態參數

2.MatrixVariableMethodArgumentResolver

支持帶有@MatrixVariable註解的參數,顧名思義,矩陣變量,多個變量可使用「;」分隔

3.RequestParamMethodArgumentResolver

支持帶有@RequestParam註解的參數,也支持MultipartFile類型的參數,本文已分析

4.RequestResponseBodyMethodProcessor

支持帶有@RequestBody、@ResponseBody註解的參數,本文已分析

 

再看看經常使用的HandlerMethodReturnValueHandler

1.ModelAndViewMethodReturnValueHandler

返回ModelAndView,把view和model信息賦值給ModelAndViewContainer

2.ViewMethodReturnValueHandler

返回View

3.HttpHeadersReturnValueHandler

返回HttpHeaders

4.SteamingResponseBodyReturnValueHandler

返回ResponseEntity<StreamingResponseBody>

 

3.實例


    @RequestMapping(value = "/testRb",produces={"application/json; charset=UTF-8"},method = RequestMethod.POST)
    @ResponseBody
    public Employee testRb(@RequestBody Employee e) {
        return e;
    }

    @RequestMapping(value="/testCustomObj", produces={"application/xml; charset=UTF-8"},method = RequestMethod.GET)
    @ResponseBody
    public XmlActionResult<Employee> testCustomObj(@RequestParam(value = "id") int id,
                                                   @RequestParam(value = "name") String name) {
        XmlActionResult<Employee> actionResult = new XmlActionResult<Employee>();
        Employee e = new Employee();
        e.setId(id);
        e.setName(name);
        e.setAge(20);
        e.setDept(new Dept(2,"部門"));
        actionResult.setCode("200");
        actionResult.setMessage("Success with XML");
        actionResult.setData(e);
        return actionResult;
    }

    @RequestMapping(value = "/testCustomObjWithRp", produces={"application/json; charset=UTF-8"})
    @ResponseBody
    public Employee testCustomObjWithRp(Employee e) {
        return e;
    }

    @RequestMapping(value = "/testDate", produces={"application/json; charset=UTF-8"})
    @ResponseBody
    public Date testDate(Date date) {
        return date;
    }

3.1 測試@RequestBody

在Postman中輸入請求http://localhost:8080/springmvcdemo/test/testRb

發出請求,進入了RequestResponseBody的resolveArgument方法,參數咱們能夠看到

源碼分析參照   2.源碼分析

3.2 測試@RequestParam

在瀏覽器中輸入http://localhost:8080/springmvcdemo/test/testCustomObjWithRp?id=1&name=s

返回結果以下,返回的是XML格式,(下一部分我再敘述MessageConverter部分的知識,咱們這裏只關注@RequestParam)

輸入請求後,進入了RequestParamMethodArgumentResolver的父類AbstracNamedValueMethodArgumentResolver的reloveArgument方法,因咱們有兩個@RequestParam,會進入reloveArgument兩次

 

3.3 測試無註解參數爲自定義對象

瀏覽器輸入請求http://localhost:8080/springmvcdemo/test/testCustomObjWithRp?id=1&name=s,返回結果以下

無註解咱們怎麼找究竟是哪一個HandlerMethodArgumentResolver實現類在處理呢?只要你認真看了第二部分源碼分析,相信你能夠輕鬆找到

在HandlerMethodArgumentResolverComposite(HandlerMethodArgumentResolver的實現類)第117行getArgumentResolver方法打上斷點,看看廬山真面目

原來,是ServletModelAttributeMethodProcessor爲咱們處理了自定義對象

3.4 測試參數爲簡單對象

在瀏覽器輸入請求http://localhost:8080/springmvcdemo/test/testDate?date=2018-01-30

在當前Controller加入InitBinder,使參數規範化傳遞

    //自定義屬性編輯器——日期
    @InitBinder
    public void initBinderDate(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
    }

返回了一個Unix時間戳

在HandlerMethodArgumentResolverComposite的resolveArugument方法打斷點,發現被RequestParamMethodArgumentResovler所解析

 在AbstracNamedValueMethodArgumentResolver的reloveArgument方法找到了咱們的參數,方法同測試3.2

4.自定義參數解析器

自定義參數註解TestObj

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface TestObj {
    //參數別名
    String value() default "";
}

自定義參數解析器TestObjArgumentResolver實現HandlerMethodArgumentResolver,解決兩個自定義類參數傳參的問題

public class TestObjArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(TestObj.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        TestObj testObj = parameter.getParameterAnnotation(TestObj.class);

        String alias = getAlias(testObj, parameter);

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


        //得到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());
                }
            }
        }
        return target;
    }


    private Object createAttribute(MethodParameter parameter) {
        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(TestObj testObj, MethodParameter parameter) {
        //獲得TestObj的屬性value,也就是對象參數的簡稱
        String alias = testObj.value();
        if(alias == null || StringUtils.isBlank(alias)) {
            //若是簡稱爲空,取對象簡稱的首字母小寫開頭
            String simpleName = parameter.getParameterType().getSimpleName();
            alias = simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1);
        }
        return alias;
    }


}

dispatcher-servlet.xml加入咱們自定義的參數解析器

<property name="customArgumentResolvers">
    <list>
       <bean class="org.format.demo.custom.TestObjArgumentResolver" />
    </list>
</property>

測試Controller

@Controller
@RequestMapping(value = "/foc")
public class TestObjController {
    @RequestMapping("/test1")
    @ResponseBody
    public Map test1(@TestObj Dept dept, @TestObj Employee emp) {
        Map resultMap = new HashMap();
        resultMap.put("Dept",dept);
        resultMap.put("Emp",emp);
        return resultMap;
    }

    @RequestMapping("/test2")
    @ResponseBody
    public Map test2(@TestObj("d") Dept dept, @TestObj("e") Employee emp) {
        Map resultMap = new HashMap();
        resultMap.put("d",dept);
        resultMap.put("e",emp);
        return resultMap;
    }

}

瀏覽器輸入http://localhost:8080/springmvcdemo/foc/test1?dept.id=1&dept.name=sss&employee.id=3&employee.name=ddf&employee.age=12

TestObjArgumentResolver中getAlias方法獲取別名

返回結果以下

瀏覽器輸入http://localhost:8080/springmvcdemo/foc/test2?d.id=1&d.name=sss&e.id=3&e.name=ddf&e.age=12

參數別名用咱們自定義的d和e

 

5.總結:

兩大接口:HandlerMethodArgumentResolver,HandlerMethodReturnValueHandler

ServletInvocableHandlerMethod調用invokeAndHandle方法,使用HandlerMethodReturnValueComposite,使用組合模式,放入HandlerMethodReturnValueHandler的list

同理HandlerMethodArgumentResolverComposite使用組合模式,放入HandlerMethodArgumentResolver的list

在RequestMappingHandlerAdapter中invokeHandlerMethod給ArgumentResolvers和ReturnValueHandlers賦值(4.2之前在createRequstMapping方法,此方法已刪除)

afterPropertiesSet方法注入ArgumentResolvers和ReturnValueHandlers到Spring容器

getDefaultArgumentResolvers設置默認的ArgumentResolvers

getDefaultReturnValueHandlers設置默認的ReturnValueHandlers

RequestResponseBodyMethodProcessor負責解析Controller裏@RequestBody,支持響應類型是@ResponseBody

RequestParamMethodArgumentResolver負責解析Controller裏@RequestParam

無註解狀況若是是簡單對象(如Date,Integer,Doubule等),由RequestParamMethodArgumentResovler處理,複雜對象(如自定義類)由ServletModelAttributeMethodProcessor處理

resolveArgument解析參數類型和值

6.參考

文章不免有不足之處,歡迎指正

https://docs.spring.io/spring/docs/4.3.7.RELEASE/spring-framework-reference/htmlsingle/#mvc-ann-responsebody

相關文章
相關標籤/搜索