您的位置:首页 > 编程语言 > Java开发

详解SpringMVC中Controller的方法中参数的工作原理[附带源码分析]

2015-02-11 16:17 916 查看


目录

前言

现象

源码分析
HandlerMethodArgumentResolver与HandlerMethodReturnValueHandler接口介绍

HandlerMethodArgumentResolver与HandlerMethodReturnValueHandler接口的具体应用

常用HandlerMethodArgumentResolver介绍

常用HandlerMethodReturnValueHandler介绍

本文开头现象解释以及解决方案

编写自定义的HandlerMethodArgumentResolver

总结

参考资料


前言

SpringMVC是目前主流的Web MVC框架之一。 
如果有同学对它不熟悉,那么请参考它的入门blog:http://www.cnblogs.com/fangjian0423/p/springMVC-introduction.html

SpringMVC中Controller的方法参数可以是Integer,Double,自定义对象,ServletRequest,ServletResponse,ModelAndView等等,非常灵活。本文将分析SpringMVC是如何对这些参数进行处理的,使读者能够处理自定义的一些参数。


现象

本文使用的demo基于maven。我们先来看一看对应的现象。 
[b]@Controller[/b]
[b]@RequestMapping[/b](value = "/test")
[b]public[/b] [b]class[/b] [b]TestController[/b] {

[b]@RequestMapping[/b]("/testRb")
[b]@ResponseBody[/b]
[b]public[/b] Employee [b]testRb[/b]([b]@RequestBody[/b] Employee e) {
[b]return[/b] e;
}

[b]@RequestMapping[/b]("/testCustomObj")
[b]@ResponseBody[/b]
[b]public[/b] Employee [b]testCustomObj[/b](Employee e) {
[b]return[/b] e;
}

[b]@RequestMapping[/b]("/testCustomObjWithRp")
[b]@ResponseBody[/b]
[b]public[/b] Employee [b]testCustomObjWithRp[/b]([b]@RequestParam[/b] Employee e) {
[b]return[/b] e;
}

[b]@RequestMapping[/b]("/testDate")
[b]@ResponseBody[/b]
[b]public[/b] Date [b]testDate[/b](Date date) {
[b]return[/b] date;
}

}

首先这是一个Controller,有4个方法。他们对应的参数分别是带有@RequestBody的自定义对象、自定义对象、带有@RequestParam的自定义对象、日期对象。
接下来我们一个一个方法进行访问看对应的现象是如何的。
首先第一个testRb:
 


第二个testCustomObj:



第三个testCustomObjWithRp:



第四个testDate:



 
为何返回的Employee对象会被自动解析为xml,请看楼主的另一篇博客:戳我

为何Employee参数会被解析,带有@RequestParam的Employee参数不会被解析,甚至报错?
为何日期类型不能被解析?
SpringMVC到底是如何处理这些方法的参数的?
@RequestBody、@RequestParam这两个注解有什么区别?
带着这几个问题。我们开始进行分析。


源码分析

本文所分析的源码是Spring版本4.0.2
在分析源码之前,首先让我们来看下SpringMVC中两个重要的接口。
两个接口分别对应请求方法参数的处理、响应返回值的处理,分别是HandlerMethodArgumentResolverHandlerMethodReturnValueHandler,这两个接口都是Spring3.1版本之后加入的。


       


SpringMVC处理请求大致是这样的:
首先被Dispatche
12093
rServlet截获,DispatcherServlet通过handlerMapping获得HandlerExecutionChain,然后获得HandlerAdapter。
HandlerAdapter在内部对于每个请求,都会实例化一个ServletInvocableHandlerMethod进行处理,ServletInvocableHandlerMethod在进行处理的时候,会分两部分别对请求跟响应进行处理
之后HandlerAdapter得到ModelAndView,然后做相应的处理。
本文将重点介绍ServletInvocableHandlerMethod对请求以及响应的处理。



1. 处理请求的时候,会根据ServletInvocableHandlerMethod的属性argumentResolvers(这个属性是它的父类InvocableHandlerMethod中定义的)进行处理,其中argumentResolvers属性是一个HandlerMethodArgumentResolverComposite类(这里使用了组合模式的一种变形),这个类是实现了HandlerMethodArgumentResolver接口的类,里面有各种实现了HandlerMethodArgumentResolver的List集合。



2. 处理响应的时候,会根据ServletInvocableHandlerMethod的属性returnValueHandlers(自身属性)进行处理,returnValueHandlers属性是一个HandlerMethodReturnValueHandlerComposite类[b](这里使用了组合模式的一种变形),这个类是实现了HandlerMethodReturnValueHandler接口的类,里面有各种实现了HandlerMethodReturnValueHandler的List集合。[/b]



ServletInvocableHandlerMethod的returnValueHandlers和argumentResolvers这两个属性都是在ServletInvocableHandlerMethod进行实例化的时候被赋值的(使用RequestMappingHandlerAdapter的属性进行赋值)。



RequestMappingHandlerAdapter的argumentResolvers和returnValueHandlers这两个属性是在RequestMappingHandlerAdapter进行实例化的时候被Spring容器注入的。



其中默认的ArgumentResolvers:



默认的returnValueHandlers:



 
我们在json、xml自动转换那篇文章中已经了解,使用@ResponseBody注解的话最终返回值会被RequestResponseBodyMethodProcessor这个HandlerMethodReturnValueHandler实现类处理。
我们通过源码发现,RequestResponseBodyMethodProcessor这个类其实同时实现了HandlerMethodReturnValueHandler和HandlerMethodArgumentResolver这两个接口。



RequestResponseBodyMethodProcessor支持的请求类型是Controller方法参数中带有@RequestBody注解,支持的响应类型是Controller方法带有@ResponseBody注解。 



RequestResponseBodyMethodProcessor响应的具体处理是使用消息转换器。



处理请求的时候使用内部的readWithMessageConverters方法。



然后会执行父类(AbstractMessageConverterMethodArgumentResolver)的readWithMessageConverters方法。



 
下面来我们来看看常用的HandlerMethodArgumentResolver实现类(本文粗略讲下,有兴趣的读者可自行研究)。
1. RequestParamMethodArgumentResolver
 支持带有@RequestParam注解的参数或带有MultipartFile类型的参数
2. RequestParamMapMethodArgumentResolver
  支持带有@RequestParam注解的参数 && @RequestParam注解的属性value存在 && 参数类型是实现Map接口的属性
3. PathVariableMethodArgumentResolver
支持带有@PathVariable注解的参数 且如果参数实现了Map接口,@PathVariable注解需带有value属性
4. MatrixVariableMethodArgumentResolver
支持带有@MatrixVariable注解的参数 且如果参数实现了Map接口,@MatrixVariable注解需带有value属性 
5. RequestResponseBodyMethodProcessor
 本文已分析过
6. ServletRequestMethodArgumentResolver
 参数类型是实现或继承或是WebRequest、ServletRequest、MultipartRequest、HttpSession、Principal、Locale、TimeZone、InputStream、Reader、HttpMethod这些类。
(这就是为何我们在Controller中的方法里添加一个HttpServletRequest参数,Spring会为我们自动获得HttpServletRequest对象的原因)
7. ServletResponseMethodArgumentResolver
 参数类型是实现或继承或是ServletResponse、OutputStream、Writer这些类
8. RedirectAttributesMethodArgumentResolver
 参数是实现了RedirectAttributes接口的类
9. HttpEntityMethodProcessor
 参数类型是HttpEntity
从名字我们也看的出来, 以Resolver结尾的是实现了HandlerMethodArgumentResolver接口的类,以Processor结尾的是实现了HandlerMethodArgumentResolver和HandlerMethodReturnValueHandler的类。
 
下面来我们来看看常用的HandlerMethodReturnValueHandler实现类。
1. ModelAndViewMethodReturnValueHandler
返回值类型是ModelAndView或其子类
2. ModelMethodProcessor
返回值类型是Model或其子类
3. ViewMethodReturnValueHandler
返回值类型是View或其子类 
4. HttpHeadersReturnValueHandler
返回值类型是HttpHeaders或其子类  
5. ModelAttributeMethodProcessor
返回值有@ModelAttribute注解
6. ViewNameMethodReturnValueHandler
返回值是void或String
其余没讲过的读者可自行查看源码。
 
下面开始解释为何本文开头出现那些现象的原因:
1. 第一个方法testRb以及地址 http://localhost:8888/SpringMVCDemo/test/testRb?name=1&age=3
  这个方法的参数使用了@RequestBody,之前已经分析过,被RequestResponseBodyMethodProcessor进行处理。之后根据http请求头部的contentType然后选择合适的消息转换器进行读取。
  很明显,我们的消息转换器只有默认的那些跟部分json以及xml转换器,且传递的参数name=1&age=3,传递的头部中没有content-type,默认使用了application/octet-stream,因此触发了HttpMediaTypeNotSupportedException异常
  解放方案: 我们将传递数据改成json,同时http请求的Content-Type改成application/json即可。
      






完美解决。
2. testCustomObj方法以及地址 http://localhost:8888/SpringMVCDemo/test/testCustomObj?name=1&age=3
这个请求会找到ServletModelAttributeMethodProcessor这个resolver。默认的resolver中有两个ServletModelAttributeMethodProcessor,只不过实例化的时候属性annotationNotRequired一个为true,1个为false。这个ServletModelAttributeMethodProcessor处理参数支持@ModelAttribute注解,annotationNotRequired属性为true的话,参数不是简单类型就通过,因此选择了ServletModelAttributeMethodProcessor,最终通过DataBinder实例化Employee对象,并写入对应的属性。
3. testCustomObjWithRp方法以及地址 http://localhost:8888/SpringMVCDemo/test/testCustomObjWithRp?name=1&age=3
这个请求会找到RequestParamMethodArgumentResolver(使用了@RequestParam注解)。RequestParamMethodArgumentResolver在处理参数的时候使用request.getParameter(参数名)即request.getParameter("e")得到,很明显我们的参数传的是name=1&age=3。因此得到null,RequestParamMethodArgumentResolver处理missing
value会触发MissingServletRequestParameterException异常。 [粗略讲下,有兴趣的读者请自行查看源码]
    解决方案:去掉@RequestParam注解,让ServletModelAttributeMethodProcessor来处理。
4. testDate方法以及地址 http://localhost:8888/SpringMVCDemo/test/testDate?date=2014-05-15
这个请求会找到RequestParamMethodArgumentResolver。因为这个方法与第二个方法一样,有两个RequestParamMethodArgumentResolver,属性useDefaultResolution不同。RequestParamMethodArgumentResolver支持简单类型,ServletModelAttributeMethodProcessor是支持非简单类型。最终步骤跟第三个方法一样,我们的参数名是date,于是通过request.getParameter("date")找到date字符串(这里参数名如果不是date,那么最终页面是空白的,因为没有@RequestParam注解,参数不是必须的,RequestParamMethodArgumentResolver处理null值返回null)。最后通过DataBinder找到合适的属性编辑器进行类型转换。最终找到java.util.Date对象的构造函数
public Date(String s),由于我们传递的格式不是标准的UTC时间格式,因此最终触发了IllegalArgumentException异常。
    解决方案:
    1. 传递参数的格式修改成标准的UTC时间格式:http://localhost:8888/SpringMVCDemo/test/testDate?date=Sat, 17 May 2014 16:30:00 GMT



    2.在Controller中加入自定义属性编辑器。
[b]@InitBinder[/b]
[b]public[/b] [b]void[/b] [b]initBinder[/b](WebDataBinder binder) {
SimpleDateFormat dateFormat = [b]new[/b] SimpleDateFormat("yyyy-MM-dd");
binder.registerCustomEditor(Date.class, [b]new[/b] CustomDateEditor(dateFormat, [b]false[/b]));
}

 这个@InitBinder注解在实例化ServletInvocableHandlerMethod的时候被注入到WebDataBinderFactory中的,而WebDataBinderFactory是ServletInvocableHandlerMethod的一个属性。在RequestMappingHandlerAdapter源码的803行getDataBinderFactory就是得到的WebDataBinderFactory。



之后RequestParamMethodArgumentResolver通过WebDataBinderFactory创建的WebDataBinder里的自定义属性编辑器找到合适的属性编辑器(我们自定义的属性编辑器是用CustomDateEditor处理Date对象,而testDate的参数刚好是Date),最终CustomDateEditor把这个String对象转换成Date对象。


编写自定义的HandlerMethodArgumentResolver

通过前面的分析,我们明白了SpringMVC处理Controller中的方法的参数流程。
现在,如果方法中有两个参数,且都是自定义类参数,那该如何处理呢?
很明显,要处理这个只能自己实现一个实现HandlerMethodArgumentResolver的类。
先定义1个注解FormObj:
[b]@Target[/b]({ElementType.PARAMETER})
[b]@Retention[/b](RetentionPolicy.RUNTIME)
[b]public[/b] [b]@interface[/b] [b]FormObj[/b] {
//参数别名
String value() [b]default[/b] "";
//是否展示, 默认展示
[b]boolean[/b] show() [b]default[/b] [b]true[/b];
}

然后是HandlerMethodArgumentResolver:
[b]public[/b] [b]class[/b] [b]FormObjArgumentResolver[/b] [b]implements[/b] HandlerMethodArgumentResolver {

[b]@Override[/b]
[b]public[/b] [b]boolean[/b] [b]supportsParameter[/b](MethodParameter parameter) {
[b]return[/b] parameter.hasParameterAnnotation(FormObj.[b]class[/b]);
}

[b]@Override[/b]
[b]public[/b] Object [b]resolveArgument[/b](MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) [b]throws[/b] Exception {
FormObj formObj = parameter.getParameterAnnotation(FormObj.[b]class[/b]);

String alias = getAlias(formObj, parameter);

//拿到obj, 先从ModelAndViewContainer中拿,若没有则new1个参数类型的实例
Object obj = (mavContainer.containsAttribute(alias)) ?
mavContainer.getModel().get(alias) : createAttribute(alias, parameter, binderFactory, webRequest);

//获得WebDataBinder,这里的具体WebDataBinder是ExtendedServletRequestDataBinder
WebDataBinder binder = binderFactory.createBinder(webRequest, obj, alias);

Object target = binder.getTarget();

[b]if[/b](target != [b]null[/b]) {
//绑定参数
bindParameters(webRequest, binder, alias);
//JSR303 验证
validateIfApplicable(binder, parameter);
[b]if[/b] (binder.getBindingResult().hasErrors()) {
[b]if[/b] (isBindExceptionRequired(binder, parameter)) {
[b]throw[/b] [b]new[/b] BindException(binder.getBindingResult());
}
}
}

[b]if[/b](formObj.show()) {
mavContainer.addAttribute(alias, target);
}

[b]return[/b] target;
}

[b]private[/b] Object [b]createAttribute[/b](String alias, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest webRequest) {
[b]return[/b] BeanUtils.instantiateClass(parameter.getParameterType());
}

[b]private[/b] void [b]bindParameters[/b](NativeWebRequest request, WebDataBinder binder, String alias) {
ServletRequest servletRequest = request.getNativeRequest(ServletRequest.[b]class[/b]);

MockHttpServletRequest newRequest = [b]new[/b] MockHttpServletRequest();

Enumeration<String> enu = servletRequest.getParameterNames();
[b]while[/b](enu.hasMoreElements()) {
String paramName = enu.nextElement();
[b]if[/b](paramName.startsWith(alias)) {
newRequest.setParameter(paramName.substring(alias.length()+1), request.getParameter(paramName));
}
}
((ExtendedServletRequestDataBinder)binder).bind(newRequest);
}

[b]protected[/b] void [b]validateIfApplicable[/b](WebDataBinder binder, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
[b]for[/b] (Annotation annot : annotations) {
[b]if[/b] (annot.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = AnnotationUtils.getValue(annot);
binder.validate(hints [b]instanceof[/b] Object[] ? (Object[]) hints : [b]new[/b] Object[] {hints});
[b]break[/b];
}
}
}

[b]protected[/b] [b]boolean[/b] [b]isBindExceptionRequired[/b](WebDataBinder binder, MethodParameter parameter) {
[b]int[/b] i = parameter.getParameterIndex();
Class<?>[] paramTypes = parameter.getMethod().getParameterTypes();
[b]boolean[/b] hasBindingResult = (paramTypes.length > (i + 1) && Errors.[b]class[/b].isAssignableFrom(paramTypes[i + 1]));

[b]return[/b] !hasBindingResult;
}

[b]private[/b] String [b]getAlias[/b](FormObj formObj, MethodParameter parameter) {
//得到FormObj的属性value,也就是对象参数的简称
String alias = formObj.value();
[b]if[/b](alias == [b]null[/b] || StringUtils.isBlank(alias)) {
//如果简称为空,取对象简称的首字母小写开头
String simpleName = parameter.getParameterType().getSimpleName();
alias = simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1);
}
[b]return[/b] alias;
}

}

对应Controller:
[b]@Controller[/b]
[b]@RequestMapping[/b](value = "/foc")
[b]public[/b] [b]class[/b] [b]FormObjController[/b] {

[b]@RequestMapping[/b]("/test1")
[b]public[/b] String [b]test1[/b](@FormObj Dept dept, @FormObj Employee emp) {
[b]return[/b] "index";
}

[b]@RequestMapping[/b]("/test2")
[b]public[/b] String [b]test2[/b](@FormObj("d") Dept dept, @FormObj("e") Employee emp) {
[b]return[/b] "index";
}

[b]@RequestMapping[/b]("/test3")
[b]public[/b] String [b]test3[/b](@FormObj(value = "d", show = false) Dept dept, @FormObj("e") Employee emp) {
[b]return[/b] "index";
}

}

 
结果如下:








总结

写了这么多,主要还是巩固一下自己对SpringMVC对请求及响应的处理做一个细节的总结吧,不知道大家有没有清楚这个过程。
想熟悉这部分内容最主要的还是要熟悉HandlerMethodArgumentResolver和HandlerMethodReturnValueHandler这两个接口以及属性编辑器、数据绑定机制。
本文难免有错误,希望读者能指出来。


参考资料

http://www.iteye.com/topic/1127676

http://jinnianshilongnian.iteye.com/blog/1717180

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  spring 源码 mvc 数据 web
相关文章推荐