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.参考

文章难免有不足之处,欢迎指正

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

SpringMVC源码阅读:Controller中参数解析的更多相关文章

  1. SpringMVC源码阅读:定位Controller

    1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将通过源码分析,弄清楚Spr ...

  2. SpringMVC源码阅读:视图解析器

    1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将通过源码(基于Spring ...

  3. SpringMVC源码阅读:异常解析器

    1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将通过源码(基于Spring ...

  4. SpringMVC源码阅读:过滤器

    1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将通过源码(基于Spring ...

  5. SpringMVC源码阅读系列汇总

    1.前言 1.1 导入 SpringMVC是基于Servlet和Spring框架设计的Web框架,做JavaWeb的同学应该都知道 本文基于Spring4.3.7源码分析,(不要被图片欺骗了,手动滑稽 ...

  6. SpringMVC源码阅读:属性编辑器、数据绑定

    1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将通过源码(基于Spring ...

  7. SpringMVC源码阅读:核心分发器DispatcherServlet

    1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将介绍SpringMVC的核 ...

  8. SpringMVC源码阅读:Json,Xml自动转换

    1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将通过源码(基于Spring ...

  9. SpringMVC源码阅读:拦截器

    1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将通过源码(基于Spring ...

随机推荐

  1. Configure Pi as simulation hardware for Simulink

    1. Only version not older than R2013 supports Raspberry Pi. First, download support package in Matla ...

  2. mac下的抓包工具 -- Charles

    # 背景 换了mac电脑,

  3. JS——图片预览功能

    <script type="text/javascript">    function DisplayImage(fileTag) {        document. ...

  4. Windows10 家庭版添加【本地组策略编辑器】

    Windows10 家庭版默认没有[本地组策略编辑器],添加方法: 新建记事本复制以下内容 @echo off pushd "%~dp0" dir /b C:\Windows\se ...

  5. SQL处理数据并发,解决ID自增

    1 创建MaxIdProcess表,由于存储ID的最大值 CREATE TABLE [dbo].[MaxIdProcess]( ,) NOT NULL, --自增ID ) NOT NULL, --存储 ...

  6. TCP BBR - 如何安装、启动、停止BBR!

    TCP BBR从Linux 4.9 内核开始,就作为它内核的一部分存在了,如果想使用BBR,那么首先就是判断内核版本是否大于4.9,如果符合版本标准,那么直接启动BBR就可以了,如果低于4.9,升级内 ...

  7. poj1125

    题目大意:哎,意思看了半天,看了别人的解释才明白,就是说从一个经纪人出发传递消息,直到所有人都收到消息 也就是说只要找到该经纪人到其它所有点的最短距离中的最大一个时间,就说明最后一个也收到消息了. 而 ...

  8. FunDA(12)- 示范:强类型数据源 - strong typed data sources

    FunDA设计的主要目的是解决FRM(Functional Relation Mapping)如Slick这样的批次型操作工具库数据源行间游动操作的缺失问题.FRM产生的结果集就是一种静态集合,缺乏动 ...

  9. node 无脑生成小程序二维码图

    RT 新建createwxaqrcode.js: const request = require('request') const fs = require('fs') // eg:生成购物车列表圆形 ...

  10. js中call、apply、bind的使用

    写在前面的话 这三个方法都是来自Function.prototype上,所以所有的函数都可以使用. 他们有一个共同点,就是可以指定函数执行时的内部this指向. call和apply的区别在于参数的方 ...