SpringMVC原理学习(二)参数解析器

 如下是所有的参数解析器,下面会通过代码演示常见的参数解析器

1、初步了解 RequestMappingHandlerAdapter 的调用过程 

先了解一下 HandlerMethod,后面代码会用到。

HandlerMethod封装了很多属性,在访问请求方法的时候可以方便的访问到方法、方法参数、方法上的注解、所属类等并且对方法参数封装处理,也可以方便的访问到方法参数的注解等信息。

    public HandlerMethod(Object bean, Method method) {
        this((Object)bean, (Method)method, (MessageSource)null);
    }

    protected HandlerMethod(Object bean, Method method, @Nullable MessageSource messageSource) {
        Assert.notNull(bean, "Bean is required");
        Assert.notNull(method, "Method is required");
        this.bean = bean;
        this.beanFactory = null;
        this.messageSource = messageSource;
        this.beanType = ClassUtils.getUserClass(bean);
        this.method = method;
        this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
        ReflectionUtils.makeAccessible(this.bridgedMethod);
        this.parameters = this.initMethodParameters();
        this.evaluateResponseStatus();
        this.description = initDescription(this.beanType, this.method);
    }

    public MethodParameter[] getMethodParameters() {
        return this.parameters;
    }

其中的 initMethodParameters() 方法会初始化方法的参数信息:

User类:

public class User {
    private String name;
    private int age;
    
    // 省略了基本方法
}

Controller类

public class Controller {
    public void test(
            @RequestParam("name1") String name1, // name1=张三
            String name2,                        // name2=李四
            @RequestParam("age") int age,        // age=18 格式转换
            @RequestParam(name = "home", defaultValue = "${JAVA_HOME}") String home1, // spring 获取数据
            @RequestParam("file") MultipartFile file, // 上传文件
            @PathVariable("id") int id,               //  /test/124   /test/{id}
            @RequestHeader("Content-Type") String header,
            @CookieValue("token") String token,
            @Value("${JAVA_HOME}") String home2, // spring 获取数据  ${} #{}
            HttpServletRequest request,          // request, response, session ...
            @ModelAttribute("abc") User user1,          // name=zhang&age=18
            User user2,                          // name=zhang&age=18
            @RequestBody User user3              // json
    ) {
    }
}

配置类

@Configuration
public class WebConfig {
}

 主启动类

public class A21 {

    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
        DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();
        // 准备测试 Request
        HttpServletRequest request = mockRequest();

        // 要点1. 控制器方法被封装为 HandlerMethod
        HandlerMethod handlerMethod = new HandlerMethod(new Controller(), Controller.class.getMethod("test",
                String.class, String.class, int.class, String.class, MultipartFile.class, int.class, String.class,
                String.class, String.class, HttpServletRequest.class, User.class, User.class, User.class));

        //多个解析器的组合 组合器的设计模式
        HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();

        composite.addResolvers(
                //beanFactory:需要在Spring容器里获取值,如 ${JAVA_HOME}
                // false:表示必须有 @RequestParam    true:表示不带 @RequestParam 的参数都会交给该解析器解析
                new RequestParamMethodArgumentResolver(beanFactory, false)
        );

        // 要点4. 解析每个参数值
        for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
            //获取参数上的注解,并连接起来
            String annotions = Arrays.stream(parameter.getParameterAnnotations()).map(a -> a.annotationType().getSimpleName()).collect(Collectors.joining());
            String str = annotions.length() > 0 ? "@" + annotions : " ";

            //是否支持此参数
            if (composite.supportsParameter(parameter)) {
                //解析参数,获取参数的值
                //参数一:方法参数   参数二:ModelAndViewContainer,处理保存Model和View
                //参数三:请求   参数四:数据绑定工厂
                Object v = composite.resolveArgument(parameter, null, new ServletWebRequest(request), null);
                System.out.println("[" + parameter.getParameterIndex() + "] " + str + " " + parameter.getParameterType().getSimpleName()
                        + " " + parameter.getParameterName() + " -> " + v + " [" + v.getClass() + "]");

            } else {
                System.out.println("[" + parameter.getParameterIndex() + "] " + str + parameter.getParameterType().getSimpleName()
                        + " " + parameter.getParameterName());
            }
        }
    }

    // 模拟请求
    private static HttpServletRequest mockRequest() {
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setParameter("name1", "zhangsan");
        request.setParameter("name2", "lisi");
        // 参数一:参数名 参数二:文件名 参数三:内容
        request.addPart(new MockPart("file", "abc", "hello".getBytes(StandardCharsets.UTF_8)));
        // 找出/test/{id} 与 /test/123 的映射关系
        // map:id=123
        Map<String, String> map = new AntPathMatcher().extractUriTemplateVariables("/test/{id}", "/test/123");
        //放到 request 作用域
        request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, map);
        request.setContentType("application/json");
        request.setCookies(new Cookie("token", "123456"));
        request.setParameter("name", "张三");
        request.setParameter("age", "18");
        Gson gson = new Gson();
        String json = gson.toJson(new User("王五", 4));
        request.setContent(json.getBytes(StandardCharsets.UTF_8));

        return new StandardServletMultipartResolver().resolveMultipart(request);
    }

}

结果:所有带箭头的都是被解析的,发现有两个问题:

  • 参数类型都为 String,方法参数的类型没有被解析
  • 方法参数的名字为 null

添加数据绑定工厂:ServletRequestDataBinderFactory

  •  方法参数的类型被成功解析

设置参数名的解析器

  • parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
  • 得到方法参数的名字
public class A21 {

    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory(WebConfig.class);
        // 准备测试 Request
        HttpServletRequest request = mockRequest();

        // 要点1. 控制器方法被封装为 HandlerMethod
        HandlerMethod handlerMethod = new HandlerMethod(new Controller(), Controller.class.getMethod("test",
                String.class, String.class, int.class, String.class, MultipartFile.class, int.class, String.class,
                String.class, String.class, HttpServletRequest.class, User.class, User.class, User.class));

        // 要点2. 准备对象绑定与类型转换
        ServletRequestDataBinderFactory bindFactory = new ServletRequestDataBinderFactory(null, null);


        //多个解析器的组合 组合器的设计模式
        HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();

        composite.addResolvers(
                //beanFactory:需要在Spring容器里获取值,如 ${JAVA_HOME}
                // false:表示必须有 @RequestParam    true:表示不带 @RequestParam 的参数都会交给该解析器解析
                new RequestParamMethodArgumentResolver(beanFactory, false)
        );

        // 要点4. 解析每个参数值
        for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
            //获取参数上的注解,并连接起来
            String annotions = Arrays.stream(parameter.getParameterAnnotations()).map(a -> a.annotationType().getSimpleName()).collect(Collectors.joining());
            String str = annotions.length() > 0 ? "@" + annotions : " ";

            // 设置参数名的解析器
            parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());

            //是否支持此参数
            if (composite.supportsParameter(parameter)) {
                //解析参数,获取参数的值
                //参数一:方法参数   参数二:ModelAndViewContainer,处理保存Model和View
                //参数三:请求   参数四:数据绑定工厂
                Object v = composite.resolveArgument(parameter, null, new ServletWebRequest(request), bindFactory);
                System.out.println("[" + parameter.getParameterIndex() + "] " + str + " " + parameter.getParameterType().getSimpleName()
                        + " " + parameter.getParameterName() + " -> " + v + " [" + v.getClass() + "]");

            } else {
                System.out.println("[" + parameter.getParameterIndex() + "] " + str + parameter.getParameterType().getSimpleName()
                        + " " + parameter.getParameterName());
            }
        }
    }
}

添加 @RequestMapping 解析器时参数设置为 true,表示不带 @RequestParam 的参数都会交给该解析器解析

composite.addResolvers(
    //beanFactory:需要在Spring容器里获取值,如 ${JAVA_HOME}
    // false:表示必须有 @RequestParam    true:表示不带 @RequestParam 的参数都会交给该解析器解析
    new RequestParamMethodArgumentResolver(beanFactory, true)
  );

 结果:

 

原因:解析 @PathVariable 时报错,@PathVariable 并不属于该解析器解析,解决办法后面会说。

 2、常见参数的解析

准备 ModelAndViewContainer 用来存储中间 Model 结果,解析 @ModelAttribute 会把结果存储在里面。

Controller类有这样的写法,是因为存储的时候如果不指定名字,会按类型的小写存储,由于两个参数类型一样,第二个结果会覆盖掉第一个结果。

@ModelAttribute("abc") User user1,          // name=zhang&age=18
User user2,                          // name=zhang&age=18
public class A21 {

    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
        DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();
        // 准备测试 Request
        HttpServletRequest request = mockRequest();

        // 要点1. 控制器方法被封装为 HandlerMethod
        HandlerMethod handlerMethod = new HandlerMethod(new Controller(), Controller.class.getMethod("test",
                String.class, String.class, int.class, String.class, MultipartFile.class, int.class, String.class,
                String.class, String.class, HttpServletRequest.class, User.class, User.class, User.class));

        // 要点2. 准备对象绑定与类型转换
        ServletRequestDataBinderFactory bindFactory = new ServletRequestDataBinderFactory(null, null);

        // 要点3. 准备 ModelAndViewContainer 用来存储中间 Model 结果
        ModelAndViewContainer container = new ModelAndViewContainer();

        //多个解析器的组合 组合器的设计模式
        HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();

        composite.addResolvers(
                //beanFactory:需要在Spring容器里获取值,如 ${JAVA_HOME}
                // false:表示必须有 @RequestParam    true:表示不带 @RequestParam 的参数都会交给该解析器解析(报错)
                new RequestParamMethodArgumentResolver(beanFactory, false),
                new PathVariableMethodArgumentResolver(),
                new RequestHeaderMethodArgumentResolver(beanFactory),
                new ServletCookieValueMethodArgumentResolver(beanFactory),
                new ExpressionValueMethodArgumentResolver(beanFactory),
                new ServletRequestMethodArgumentResolver(),
                new ServletModelAttributeMethodProcessor(false), //必须有 @ModelAttribute
                //位置,放在下面会被 ServletModelAttributeMethodProcessor 解析
                new RequestResponseBodyMethodProcessor(Arrays.asList(new MappingJackson2HttpMessageConverter())),
                new ServletModelAttributeMethodProcessor(true), //省略了 @ModelAttribute
                new RequestParamMethodArgumentResolver(beanFactory, true)//省略了 @RequestParam

        );

        // 要点4. 解析每个参数值
        for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
            //获取参数上的注解,并连接起来
            String annotions = Arrays.stream(parameter.getParameterAnnotations()).map(a -> a.annotationType().getSimpleName()).collect(Collectors.joining());
            String str = annotions.length() > 0 ? "@" + annotions : " ";

            // 设置参数名的解析器
            parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());

            //是否支持此参数
            if (composite.supportsParameter(parameter)) {
                //解析参数,获取参数的值
                //参数一:方法参数   参数二:ModelAndViewContainer,处理保存Model和View
                //参数三:请求   参数四:数据绑定工厂
                Object v = composite.resolveArgument(parameter, container, new ServletWebRequest(request), bindFactory);
                System.out.println("[" + parameter.getParameterIndex() + "] " + str + " " + parameter.getParameterType().getSimpleName()
                        + " " + parameter.getParameterName() + " -> " + v + " [" + v.getClass() + "]");
                System.out.println("模型数据为:" + container.getModel());

            } else {
                System.out.println("[" + parameter.getParameterIndex() + "] " + str + parameter.getParameterType().getSimpleName()
                        + " " + parameter.getParameterName());
            }
        }
    }
}

结果:

这样添加的原因:防止 @RequestBody 被 ServletModelAttributeMethodProcessor解析,并且保证省略 @ModelAttribute 的参数也可以被 ServletModelAttributeMethodProcessor 解析。

new ServletModelAttributeMethodProcessor(false)
new RequestResponseBodyMethodProcessor(Arrays.asList(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true), //省略了 @ModelAttribute

如果我这样写 

new ServletModelAttributeMethodProcessor(true)
new RequestResponseBodyMethodProcessor(Arrays.asList(new MappingJackson2HttpMessageConverter())),

可以看到解析的结果发生变化,ServletModelAttributeMethodProcessor 解析了 @RequestBody 注解,发生了错误的解析。

3、总结

  1. 初步了解 RequestMappingHandlerAdapter 的调用过程

    1. 控制器方法被封装为 HandlerMethod

    2. 准备对象绑定与类型转换

    3. 准备 ModelAndViewContainer 用来存储中间 Model 结果

    4. 解析每个参数值

  2. 解析参数依赖的就是各种参数解析器,它们都有两个重要方法

    • supportsParameter 判断是否支持方法参数

    • resolveArgument 解析方法参数

  3. 常见参数的解析

    • @RequestParam

    • 省略 @RequestParam

    • @RequestParam(defaultValue)

    • MultipartFile

    • @PathVariable

    • @RequestHeader

    • @CookieValue

    • @Value

    • HttpServletRequest 等

    • @ModelAttribute

    • 省略 @ModelAttribute

    • @RequestBody

  4. 组合模式在 Spring 中的体现

  5. @RequestParam, @CookieValue 等注解中的参数名、默认值, 都可以写成活的, 即从 ${ } #{ }中获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小鲁蛋儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值