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類,我們發現了 returnValueHandlers 是 HandlerMethodReturnValueHandlerComposite 型別的,神祕的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.參考
文章難免有不足之處,歡迎指正