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


1.前言

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

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

2.源碼分析

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

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

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

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

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

116行處理通過HandlerMethodArgumentResolver來解析參數,132~133行使用注冊過的HandlerMethodReturnValueHandler

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM