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类,我们发现了returnValueHandlersHandlerMethodReturnValueHandlerComposite类型的,神秘的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