《学会 SpringMVC 系列 · 剖析入参处理》

📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍

CSDN.gif

写在前面的话

通过上一篇博文《学会 SpringMVC 系列 · 剖析篇(上)》的学习,大致了解了SpringMVC请求流程的代码走向。由于篇幅所限,没有介绍的十分详尽,接下来几篇博文,将流程中涉及的若干关键环节单独拿出来讲解,并结合实战中的运用,帮助领略SpringMVC带来的定制和扩展能力。

相关博文
《学会 SpringMVC 系列 · 基础篇》
《学会 SpringMVC 系列 · 剖析篇(上)》
《程序猿入职必会(1) · 搭建拥有数据交互的 SpringBoot 》


入参处理

学前准备与回顾

本篇 SpringMVC 源码分析系列文章,继续使用 《搭建拥有数据交互的 SpringBoot 》博文搭建的 SpringBoot3.x 项目为基础,以此学习相关源码,对应 SpringMVC 版本为 6.1.11。
通过《学会 SpringMVC 系列 · 剖析篇(上)》的分析,我们先总结回顾一下。

【一次请求的主链路节点】
DispatcherServlet#doDispatch(入口方法)
DispatcherServlet#getHandler(根据path找到对应的HandlerExecutionChain
DispatcherServlet#getHandlerAdapter(根据handle找到对应的HandlerAdapter
HandlerExecutionChain#applyPreHandle(触发拦截器的前置逻辑)
AbstractHandlerMethodAdapter#handle(核心逻辑)
HandlerExecutionChain#applyPostHandle(触发拦截器的后置逻辑)

【核心handle方法的主链路节点】
RequestMappingHandlerAdapter#handleInternal(入口方法)
RequestMappingHandlerAdapter#invokeHandlerMethod(入口方法2)
ServletInvocableHandlerMethod#invokeAndHandle(入口方法3)
InvocableHandlerMethod#invokeForRequest(参数和实际执行的所在,3.1)
InvocableHandlerMethod#getMethodArgumentValues(参数处理,3.1.1)
InvocableHandlerMethod#doInvoke(实际执行,3.1.2)
HandlerMethodReturnValueHandlerComposite#handleReturnValue(返回处理,3.2)


@RequestBody 入参处理

先以最常见的@RequestBody为示例,展开介绍。

@ResponseBody
@RequestMapping("/studyJson")
public ZyTeacherInfo studyJson(@RequestBody ZyTeacherInfo teacherInfo) {
    teacherInfo.setTeaName("战神");
    return teacherInfo;
}

【运行流程】
1、先拿到方法的形参列表,根据形参长度创建一个空数组,用来存储后续处理完的最终参数。

MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
	return EMPTY_ARGS;
}
Object[] args = new Object[parameters.length];

image.png
2、进入HandlerMethodArgumentResolverComposite#getArgumentResolver,先判断缓存是否存在,不存在就遍历参数解析器列表,依次判断其supportsParameter方法是否匹配,匹配则返回该解析器。
image.png
image.png
3、由于是@RequestBody入参,会找到 RequestResponseBodyMethodProcessor,它的判定比较简单,就是判断是否包含RequestBody注解。匹配成功则加入缓存,然后返回该处理器。
image.png
image.png
4、接着进入HandlerMethodArgumentResolverComposite#resolveArgument,会再调用getArgumentResolver取一次,这时候肯定是从缓存拿到的了(不清楚为什么取两次)。
总之拿到了 RequestResponseBodyMethodProcessor,紧接着执行它的resolveArgument方法。
image.png
image.png
5、再调用HandlerMethodArgumentResolverComposite#readWithMessageConverters,看方法名字就知道这是利用参数转换器进行实际的消息处理。
image.png
6、这边获取转换器列表,遍历调用canRead方法,看是否满足,这边找到了FastJsonHttpMessageConverter,然后调用其read方法利用JSON.parseObject方法转换为对象。
image.png
image.png
image.png
7、数据拿到了,流程开始返回,返回到getMethodArgumentValues这边,可以看到拿到的已经是处理后的对象了。
再就是返回到InvocableHandlerMethod#invokeForRequest,准备执行 doInvoke(args)
到此,入参流程基本告一段落。
image.png
image.png

【总结一下,链路流程】
InvocableHandlerMethod#getMethodArgumentValues(入参处理方法的入口)
HandlerMethodArgumentResolverComposite#getArgumentResolver(找合适的参数解析器)
RequestResponseBodyMethodProcessor#supportsParameter(匹配入参解析器)
RequestResponseBodyMethodProcessor#resolveArgument(执行入参处理器)
RequestResponseBodyMethodProcessor#readWithMessageConverters(找入参转换器)
AbstractHttpMessageConverter#canRead(匹配入参转换器)
FastJsonHttpMessageConverter#read(执行入参转换器实际逻辑)


@RequestParam 入参处理

先以最常见的@RequestParam为示例,展开介绍。

@ResponseBody
    @RequestMapping("/study")
    public String study(@RequestParam("msg") String name) {
        String msg = "Hello, Spring Boot 3!" + name;
        log.warn("study方法内的实际逻辑:" + msg);
        return msg;
    }

【运行流程】
1、先拿到方法的形参列表,根据形参长度创建一个空数组,用来存储后续处理完的最终参数。

MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
	return EMPTY_ARGS;
}
Object[] args = new Object[parameters.length];

image.png
2、进入HandlerMethodArgumentResolverComposite#getArgumentResolver,先判断缓存是否存在,不存在就遍历参数解析器列表,依次判断其supportsParameter方法是否匹配,匹配则返回该解析器。

public HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
	HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
	if (result == null) {
		for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
			if (resolver.supportsParameter(parameter)) {
				result = resolver;
				this.argumentResolverCache.put(parameter, result);
				break;
			}
		}
	}
	return result;
}

image.png
3、这里由于入参是@RequestParam,所以匹配上RequestParamArgumentResolver,它的supportsParameter方法很简单,就不贴了,看名字也像。
image.png
4、接下来再进入HandlerMethodArgumentResolverComposite#resolveArgument,这里再进依次第二步的getArgumentResolver方法,很明显,这次从缓存获取(不明白为什么取两次)。
获取到入参处理器不为空的时候,就执行它的resolveArgument方法。

public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
		NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
	HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
	if (resolver == null) {
		throw new IllegalArgumentException("Unsupported parameter type [" +
				parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
	}
	return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}

image.png
image.png
5、开始执行RequestParamArgumentResolver#resolveArgument,这里执行的是它父类AbstractNamedValueMethodArgumentResolver的resolveArgument方法,再进入它自身的resolveName方法。
这里可以看到核心获取逻辑其实就是 request.getParameterValues(name),很简单。
image.png
6、取到之后直接返回了,返回到InvocableHandlerMethod#invokeForRequest,准备执行 doInvoke(args)
可以看到,和@RequestBody不同,没有再去找什么入参转换器。
image.png

【总结一下,运行流程】
InvocableHandlerMethod#getMethodArgumentValues(入参处理方法的入口)
HandlerMethodArgumentResolverComposite#resolveArgument(找入参解析器)
RequestParamArgumentResolver#supportsParameter(匹配到入参解析器)
AbstractNamedValueMethodArgumentResolver#resolveArgument(调父类解析方法)
RequestParamArgumentResolver#resolveName(调自身解析方法,完成解析动作)


自定义用法

前面介绍了两种入参解析流程,在开展自定义逻辑之前,容我们先整理一下。
以 @RequestBody 和 @ResponseBody 的方法为例,可以挖掘出一些关键点:

  • RequestResponseBodyMethodProcessor#readWithMessageConverters(入参解析)
  • FastJsonHttpMessageConverter#read(入参转换)
  • RequestResponseBodyMethodProcessor#handleReturnValue(出参解析)
  • AbstractMessageConverterMethodProcessor#writeWithMessageConverters(出参解析)
  • FastJsonHttpMessageConverter#write(出参转换)

这几个步骤就是我们后续可以针对入参和出参处理部分,添加自定义逻辑的地方。
本篇主要分析入参,接下来介绍一下入参解析器和入参转换器的自定义。


自定义入参解析器

关键词:ArgumentResolvers、RequestResponseBodyMethodProcessor

【技术说明】
1、ArgumentResolvers 主要负责将 HTTP 请求中的参数解析为 Controller 方法的参数。
2、当客户端发送请求时,Spring MVC 将请求中的参数解析为方法的参数。ArgumentResolvers 允许你自定义解析规则,以支持各种类型的参数,包括基本类型、复杂对象、路径变量等。例如,@RequestParam、@PathVariable 注解都是通过参数解析器来解析请求中的参数的。
3、要实现自定义参数解析,只要实现 HandlerMethodArgumentResolver 接口,并且实现 supportsParameter 和resolveArgument方法,然后配置类中添加一下即可。

【实现示例】
Step1、自定义一个 MyHandlerMethodArgumentResolver,实现 HandlerMethodArgumentResolver 接口。

@Slf4j
public class MyHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {

    /**
     * 用于判定是否需要处理该参数分解,返回true为需要,并会去调用下面的方法resolveArgument
     */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return Student.class.isAssignableFrom(parameter.getParameterType());
    }

    /**
     * 真正用于处理参数分解的方法,返回的Object就是controller方法上的形参对象
     * 用途:仅仅用于测试,解析请求体内容,比如name#张三,age#20,将内容解析组装成Student对象再返回
     */
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        String str = getRequestBody(webRequest);
        String[] split = str.split(",");
        String name = split[0].split("#")[1];
        String age = split[1].split("#")[1];
        return Student.builder()
                .name(name)
                .age(Integer.parseInt(age))
                .id(1)
                .build();
    }

    /**
     * 从请求体获取内容
     * 也可以参考RequestResponseBodyMethodProcessor的读取方式
     */
    private String getRequestBody(NativeWebRequest webRequest) throws IOException {
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        assert request != null;
        BufferedReader reader = request.getReader();
        StringBuilder sb = new StringBuilder();
        char[] buf = new char[1024];
        int rd;
        while ((rd = reader.read(buf)) != -1) {
            sb.append(buf, 0, rd);
        }
        return sb.toString();
    }
}

Step2、再SpringMVC的配置类中,添加该入参解析器:

@Slf4j
public class CustomConfig implements WebMvcConfigurer {

    /**
     * 添加入参处理器
     */
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new MyHandlerMethodArgumentResolver());
    }
}

Step3、编写测试类

/**
 * 测试自定义入参解析器
 */
@ResponseBody
@RequestMapping("/studyJsonCustom")
public Student studyJsonCustom(Student student) {
    student.setEmail("战神");
    return student;
}

Step4、启动项目,验证一下效果
image.png

【示例的源码分析】
基本流程和前面一致,这边不展开赘述。
可以看到寻找匹配的参数解析器的时候,可选项多了一个。
image.png
image.png
另外,注意的是,如果都没找到符合的,比如用基础类型,会使用RequestParamMethodArgumentResolver,具体不展开。
image.png

【实战补充】
上述示例只是为了帮助理解,真实开发中,更多自定义入参解析器的情况是:
添加自定义注解,并为其指定特定功能,例如添加可以同时兼容form 和 json 的场景、或支持复杂数据自动解析等等。
值得一提的是,解析器可以注册这个,但最终只会生效一个,如果都找不到符合的,都会有兜底的方案,要适当注意一下添加的顺序。


自定义入参转换器

关键词:AbstractHttpMessageConverter

【技术说明】
作用:Message Converters 主要负责将 Controller 方法的返回值转换为 HTTP 响应的内容。
工作原理:当 Controller 方法返回一个对象时,Spring MVC 使用消息转换器将该对象转换为 HTTP 响应体的内容。消息转换器负责将 Java 对象转换为特定的媒体类型,例如 JSON、XML、HTML 等。Spring 提供了各种内置的消息转换器来支持不同的数据格式。
示例:如果你的 Controller 方法返回一个对象,Spring MVC 将根据请求的 Accept 头部信息和返回值类型选择适当的消息转换器,将对象转换为对应的媒体类型。

【示例说明】
由于篇幅受限,这部分内容和其他入参相关实战部分,一起放在下一篇继续介绍,


总结陈词

此篇文章介绍了SpringMVC 入参处理相关的分析,仅供学习参考。
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。

CSDN_END.gif

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

战神刘玉栋

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

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

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

打赏作者

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

抵扣说明:

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

余额充值