详解SpringMVC中Controller的⽅法中参数的⼯作原理
前⾔
SpringMVC是⽬前主流的Web MVC框架之⼀。
SpringMVC中Controller的⽅法参数可以是Integer,Double,⾃定义对象,ServletRequest,ServletResponse,ModelAndView等等,⾮常灵活。本⽂将分析SpringMVC是如何对这些参数进⾏处理的,使读者能够处理⾃定义的⼀些参数。
现象
本⽂使⽤的demo基于maven。我们先来看⼀看对应的现象。
@Controller
@RequestMapping(value = "/test")
public class TestController {
@RequestMapping("/testRb")
@ResponseBody
public Employee testRb(@RequestBody Employee e) {
return e;
}
@RequestMapping("/testCustomObj")
@ResponseBody
public Employee testCustomObj(Employee e) {
return e;
}
@RequestMapping("/testCustomObjWithRp")
@ResponseBody
public Employee testCustomObjWithRp(@RequestParam Employee e) {
return e;
}
@RequestMapping("/testDate")
@ResponseBody
public Date testDate(Date date) {
return date;
}
}
⾸先这是⼀个Controller,有4个⽅法。他们对应的参数分别是带有@RequestBody的⾃定义对象、⾃定义对象、带有@RequestParam的⾃定义对象、⽇期对象。接下来我们⼀个⼀个⽅法进⾏访问看对应的现象是如何的。
⾸先第⼀个testRb:
第⼆个testCustomObj:
第三个testCustomObjWithRp:
第四个testDate:
为何返回的Employee对象会被⾃动解析为xml,请看楼主的另⼀篇博客:
为何Employee参数会被解析,带有@RequestParam的Employee参数不会被解析,甚⾄报错?
为何⽇期类型不能被解析?
SpringMVC到底是如何处理这些⽅法的参数的?
@RequestBody、@RequestParam这两个注解有什么区别?
带着这⼏个问题。我们开始进⾏分析。
源码分析
本⽂所分析的源码是Spring版本4.0.2
在分析源码之前,⾸先让我们来看下SpringMVC中两个重要的接⼝。
两个接⼝分别对应请求⽅法参数的处理、响应返回值的处理,分别是HandlerMethodArgumentResolver和HandlerMethodReturnValueHandler,这两个接⼝都是Spring3.1版本之后加⼊的。
SpringMVC处理请求⼤致是这样的:
⾸先被DispatcherServlet截获,DispatcherServlet通过handlerMapping获得HandlerExecutionChain,然后获得HandlerAdapter。
HandlerAdapter在内部对于每个请求,都会实例化⼀个ServletInvocableHandlerMethod进⾏处理,ServletInvocableHandlerMethod在进⾏处理的时候,会分两部分别对请求跟响应进⾏处理。
之后HandlerAdapter得到ModelAndView,然后做相应的处理。
本⽂将重点介绍ServletInvocableHandlerMethod对请求以及响应的处理。
1. 处理请求的时候,会根据ServletInvocableHandlerMethod的属性argumentResolvers(这个属性是它的⽗类InvocableHandlerMethod中定义的)进⾏处理,其中argumentResolvers属性是⼀个HandlerMethodArgumentResolverComposite类(这⾥使⽤了组合模式的⼀种变形),这个类是实现了HandlerMethodArgumentResolver接⼝的类,⾥⾯有各种实现了HandlerMethodArgumentResolver的List集合。
2. 处理响应的时候,会根据ServletInvocableHandlerMethod的属性returnValueHandlers(⾃⾝属性)进⾏处理,returnValueHandlers属性是⼀个HandlerMethodReturnValueHandlerComposite类(这⾥使⽤了组合模式的⼀种变形),这个类是实现了HandlerMethodReturnValueHandler接⼝的类,⾥⾯有各种实现了HandlerMethodReturnValueHandler的List集合。
ServletInvocableHandlerMethod的returnValueHandlers和argumentResolvers这两个属性都是在ServletInvocableHandlerMethod进⾏实例化的时候被赋值的(使⽤RequestMappingHandlerAdapter的属性进⾏赋值)。
RequestMappingHandlerAdapter的argumentResolvers和returnValueHandlers这两个属性是在RequestMappingHandlerAdapter进⾏实例化的时候被Spring容器注⼊的。
其中默认的ArgumentResolvers:
默认的returnValueHandlers:
我们在已经了解,使⽤@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
其余没讲过的读者可⾃⾏查看源码。
下⾯开始解释为何本⽂开头出现那些现象的原因:
这个⽅法的参数使⽤了@RequestBody,,被RequestResponseBodyMethodProcessor进⾏处理。之后根据http请求头部的contentType然后选择合适的消息转换器进⾏读取。
很明显,我们的消息转换器只有默认的那些跟部分json以及xml转换器,且传递的参数name=1&age=3,传递的头部中没有content-type,默认使⽤了application/octet-stream,因此触发了HttpMediaTypeNotSupportedException异常
解放⽅案:我们将传递数据改成json,同时http请求的Content-Type改成application/json即可。
完美解决。
这个请求会到ServletModelAttributeMethodProcessor这个resolver。默认的resolver中有两个ServletModelAttributeMethodProcessor,只不过实例化的时候属性annotationNotRequired⼀个为true,1个为false。这个ServletModelAttributeMethodProcessor处理参数⽀持@ModelAttribute注解,annotationNotRequired属性为true的话,参数不是简单类型就通过,因此选择了ServletModelAttributeMethodProcessor,最终通过DataBinder实例化Employee对象,并写⼊对应的属性。
这个请求会到RequestParamMethodArgumentResolver(使⽤了@RequestParam注解)。RequestParamMethodArgumentResolver在处理参数的时候使⽤
null,RequestParamMethodArgumentResolver处理missing value会触发MissingServletRequestParameterException异常。[粗略讲下,有兴趣的读者请⾃⾏查看源码]
解决⽅案:去掉@RequestParam注解,让ServletModelAttributeMethodProcessor来处理。
这个请求会到RequestParamMethodArgumentResolver。因为这个⽅法与第⼆个⽅法⼀样,有两个RequestParamMethodArgumentResolver,属性useDefaultResolution不同。RequestParamMethodArgumentResolver⽀持简单类型,ServletModelAttributeMethodProcessor是⽀持⾮简单类型。最终步骤跟第三个⽅法⼀样,我们的参数名是date,于是通过Parameter("date")到date字符串(这⾥参数名如果不是date,那么最终页⾯是空⽩的,因为没有
@RequestParam注解,参数不是必须的,RequestParamMethodArgumentResolver处理null值返回null)。最后通过DataBinder到合适的属性编辑器进⾏类型转换。最终到java.util.Date对象的构造函数 public Date(String s),由于我们传递的格式不是标准的UTC时间格式,因此最终触发了IllegalArgumentException异常。
解决⽅案:
2.在Controller中加⼊⾃定义属性编辑器。
@InitBinder
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
}
这个@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:
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface FormObj {
//参数别名
String value() default "";
//是否展⽰, 默认展⽰
boolean show() default true;
}
然后是HandlerMethodArgumentResolver:
public class FormObjArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(FormObj.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { FormObj formObj = ParameterAnnotation(FormObj.class);
String alias = getAlias(formObj, parameter);
//拿到obj, 先从ModelAndViewContainer中拿,若没有则new1个参数类型的实例
Object obj = (ainsAttribute(alias)) ?
//获得WebDataBinder,这⾥的具体WebDataBinder是ExtendedServletRequestDataBinder
WebDataBinder binder = ateBinder(webRequest, obj, alias);
Object target = Target();
if(target != null) {
//绑定参数
bindParameters(webRequest, binder, alias);
//JSR303 验证
validateIfApplicable(binder, parameter);
if (BindingResult().hasErrors()) {
if (isBindExceptionRequired(binder, parameter)) {
throw new BindingResult());
}
}
}
if(formObj.show()) {
mavContainer.addAttribute(alias, target);
}
return target;
}
private Object createAttribute(String alias, MethodParameter parameter, WebDataBinderFactory bind
erFactory, NativeWebRequest webRequest) { return BeanUtils.ParameterType());
}
private void bindParameters(NativeWebRequest request, WebDataBinder binder, String alias) {
ServletRequest servletRequest = NativeRequest(ServletRequest.class);
MockHttpServletRequest newRequest = new MockHttpServletRequest();
Enumeration<String> enu = ParameterNames();
while(enu.hasMoreElements()) {
String paramName = Element();
if(paramName.startsWith(alias)) {
newRequest.setParameter(paramName.substring(alias.length()+1), Parameter(paramName));
}
}
((ExtendedServletRequestDataBinder)binder).bind(newRequest);
}
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
Annotation[] annotations = ParameterAnnotations();
for (Annotation annot : annotations) {
if (annot.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = Value(annot);
binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
break;
}
}
}
protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
int i = ParameterIndex();
Class<?>[] paramTypes = Method().getParameterTypes();
springmvc的注解有哪些boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
return !hasBindingResult;
}
private String getAlias(FormObj formObj, MethodParameter parameter) {
//得到FormObj的属性value,也就是对象参数的简称
String alias = formObj.value();
if(alias == null || StringUtils.isBlank(alias)) {
//如果简称为空,取对象简称的⾸字母⼩写开头
String simpleName = ParameterType().getSimpleName();
alias = simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1);
}
return alias;
}
}
对应Controller:
@Controller
@RequestMapping(value = "/foc")
public class FormObjController {
@RequestMapping("/test1")
public String test1(@FormObj Dept dept, @FormObj Employee emp) {
return "index";
}
@RequestMapping("/test2")
public String test2(@FormObj("d") Dept dept, @FormObj("e") Employee emp) {
return "index";
}
@RequestMapping("/test3")
public String test3(@FormObj(value = "d", show = false) Dept dept, @FormObj("e") Employee emp) {
return "index";
}
}
结果如下:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论