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.参考
文章难免有不足之处,欢迎指正
SpringMVC源码阅读:Controller中参数解析的更多相关文章
- SpringMVC源码阅读:定位Controller
1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将通过源码分析,弄清楚Spr ...
- SpringMVC源码阅读:视图解析器
1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将通过源码(基于Spring ...
- SpringMVC源码阅读:异常解析器
1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将通过源码(基于Spring ...
- SpringMVC源码阅读:过滤器
1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将通过源码(基于Spring ...
- SpringMVC源码阅读系列汇总
1.前言 1.1 导入 SpringMVC是基于Servlet和Spring框架设计的Web框架,做JavaWeb的同学应该都知道 本文基于Spring4.3.7源码分析,(不要被图片欺骗了,手动滑稽 ...
- SpringMVC源码阅读:属性编辑器、数据绑定
1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将通过源码(基于Spring ...
- SpringMVC源码阅读:核心分发器DispatcherServlet
1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将介绍SpringMVC的核 ...
- SpringMVC源码阅读:Json,Xml自动转换
1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将通过源码(基于Spring ...
- SpringMVC源码阅读:拦截器
1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将通过源码(基于Spring ...
随机推荐
- Windows.Web.Http.HttpClient.GetStringAsync 总是返回相同的结果
今天在测试博客园新闻WP8.1客户端的时候,发现电脑上浏览的新闻已经更新了.但手机上的还没更新,于是想到肯定是有bug了.于是建了一个Web测试项目,发现只有第一次发出了请求.一开始以为是MVVM的问 ...
- 使用jetty-maven-plugin运行maven多项目
1.准备工作 org.eclipse.jetty jetty-maven-plugin 9.2.11.v20150529 jdk 1.7 maven 3.1 2.采用maven管理多项目 ...
- Spring Boot 应用系列 1 -- Spring Boot 2 整合Spring Data JPA和Druid,双数据源
最近Team开始尝试使用Spring Boot + Spring Data JPA作为数据层的解决方案,在网上逛了几圈之后发现大家并不待见JPA,理由是(1)MyBatis简单直观够用,(2)以Hib ...
- c# 输入多个数字,当输入不是数字时显示出刚输入的所有数并按降序
输入多个数字,当输入不是数字时显示出刚输入的所有数并按降序 class Program { static void Main(string[] args) { //定于一个集合 List<int ...
- 关于ubuntu下看视频中文字幕乱码的问题
试了几个播放器都不行.....然后把字幕文件打开后重新保存成utf-8的.... 可以了!!!! 天呐改编码真是一个万能的办法~ 随手记
- 没有过的题QAQ
持续更新...纪念一下我的高分暴力...(好丢人啊qwq) NOI2014 动物园 80pts 用倍增暴力跳nxt数组 #include<iostream> #include<cst ...
- Mac OS 10.12 - 安装Homebrew,像Ubuntu里面的apt一样简单地安装和删除软件!
Homebrew — macOS 不可或缺的套件管理器,Homebrew官方网站如此介绍自己!!! 中文官网:https://brew.sh/index_zh-cn.html 一,安装 打开shell ...
- jzoj5832. 【省选模拟8.20】Emotional Flutter
tj:我們發現,每一次走過的步長都是k,設當前走的步數是x,走到了一個白條 那麼,每一次走就是把所有黑條都向前移k位,我們可以考慮把所有黑條的左邊界不斷的向前移動k,直到下一次移動時,其左邊界小於0, ...
- 简述在MySQL数据库中MyISAM和InnoDB的区别
区别主要有以下几点: (1)构成上,MyISAM的表在磁盘中有三个文件组成,分别是表定义文件(.frm).数据文件(.MYD).索引文件(.MYI),而InnoDB的表由表定义文件(.frm).表空间 ...
- 二:maven构建module
通常情况下,我们一个项目是需要分多个模块的,这是我们用maven管理项目就需要构建一个多模块的项目: 通常的结构是一个模块中有一个主项目,下面包含多个子项目,如果是web项目则子项目中有一个是java ...