• SpringMVC中Controller


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

    目录

    前言

    SpringMVC是目前主流的Web MVC框架之一。 

    如果有同学对它不熟悉,那么请参考它的入门blog:http://www.cnblogs.com/fangjian0423/p/springMVC-introduction.html

    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中两个重要的接口。

    两个接口分别对应请求方法参数的处理、响应返回值的处理,分别是HandlerMethodArgumentResolverHandlerMethodReturnValueHandler,这两个接口都是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:

    我们在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中加入自定义属性编辑器。

    @InitBinder
    public void initBinder(WebDataBinder binder) {
      SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
      binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
    }
    

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

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

    编写自定义的HandlerMethodArgumentResolver

    通过前面的分析,我们明白了SpringMVC处理Controller中的方法的参数流程。

    现在,如果方法中有两个参数,且都是自定义类参数,那该如何处理呢?

    @RequestMapping("/save")
    public ModelAndView saveAll(@FormModel Employee employee, @FormModel Dept dept, ModelAndView view) {
    view.setViewName("test/success"); view.addObject("employee", employee);
    view.addObject("dept", dept); return view; }

    我们就来试试吧。

    很明显,要处理这个只能自己实现一个实现HandlerMethodArgumentResolver的类。

    楼主参考了该博客中的实现。 关于实现的细节,将在之后的博客中详细介绍。

    结果如下:

    总结

    写了这么多,主要还是巩固一下自己对SpringMVC对请求及响应的处理做一个细节的总结吧,不知道大家有没有清楚这个过程。

    想熟悉这部分内容最主要的还是要熟悉HandlerMethodArgumentResolver和HandlerMethodReturnValueHandler这两个接口以及属性编辑器、数据绑定机制。

    本文难免有错误,希望读者能指出来。

    参考资料

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

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

     
     
    分类: java
    标签: springspringmvc
  • 相关阅读:
    383. Ransom Note
    598. Range Addition II
    453. Minimum Moves to Equal Array Elements
    492. Construct the Rectangle
    171. Excel Sheet Column Number
    697. Degree of an Array
    665. Nondecreasing Array
    视频网站使用H265编码能提高视频清晰度吗?
    现阶段的语音视频通话SDK需要解决哪些问题?
    企业远程高清会议平台视频会议系统在手机端使用的必备要求有哪些?
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3735823.html
Copyright © 2020-2023  润新知