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

📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗

🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍

文章目录

写在前面的话

通过上一篇博文《学会 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为示例,展开介绍。

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

【运行流程】

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

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

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

3、由于是@RequestBody入参,会找到 RequestResponseBodyMethodProcessor,它的判定比较简单,就是判断是否包含RequestBody注解。匹配成功则加入缓存,然后返回该处理器。

4、接着进入HandlerMethodArgumentResolverComposite#resolveArgument,会再调用getArgumentResolver取一次,这时候肯定是从缓存拿到的了(不清楚为什么取两次)。

总之拿到了 RequestResponseBodyMethodProcessor,紧接着执行它的resolveArgument方法。

5、再调用HandlerMethodArgumentResolverComposite#readWithMessageConverters,看方法名字就知道这是利用参数转换器进行实际的消息处理。

6、这边获取转换器列表,遍历调用canRead方法,看是否满足,这边找到了FastJsonHttpMessageConverter,然后调用其read方法利用JSON.parseObject方法转换为对象。


7、数据拿到了,流程开始返回,返回到getMethodArgumentValues这边,可以看到拿到的已经是处理后的对象了。

再就是返回到InvocableHandlerMethod#invokeForRequest,准备执行 doInvoke(args)

到此,入参流程基本告一段落。

【总结一下,链路流程】

InvocableHandlerMethod#getMethodArgumentValues(入参处理方法的入口)

HandlerMethodArgumentResolverComposite#getArgumentResolver(找合适的参数解析器)

RequestResponseBodyMethodProcessor#supportsParameter(匹配入参解析器)

RequestResponseBodyMethodProcessor#resolveArgument(执行入参处理器)

RequestResponseBodyMethodProcessor#readWithMessageConverters(找入参转换器)

AbstractHttpMessageConverter#canRead(匹配入参转换器)

FastJsonHttpMessageConverter#read(执行入参转换器实际逻辑)


@RequestParam 入参处理

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

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

【运行流程】

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

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

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

java 复制代码
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;
}

3、这里由于入参是@RequestParam,所以匹配上RequestParamArgumentResolver,它的supportsParameter方法很简单,就不贴了,看名字也像。

4、接下来再进入HandlerMethodArgumentResolverComposite#resolveArgument,这里再进依次第二步的getArgumentResolver方法,很明显,这次从缓存获取(不明白为什么取两次)。

获取到入参处理器不为空的时候,就执行它的resolveArgument方法。

java 复制代码
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);
}


5、开始执行RequestParamArgumentResolver#resolveArgument,这里执行的是它父类AbstractNamedValueMethodArgumentResolver的resolveArgument方法,再进入它自身的resolveName方法。

这里可以看到核心获取逻辑其实就是 request.getParameterValues(name),很简单。

6、取到之后直接返回了,返回到InvocableHandlerMethod#invokeForRequest,准备执行 doInvoke(args)

可以看到,和@RequestBody不同,没有再去找什么入参转换器。

【总结一下,运行流程】

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 接口。

java 复制代码
@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的配置类中,添加该入参解析器:

java 复制代码
@Slf4j
public class CustomConfig implements WebMvcConfigurer {

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

Step3、编写测试类

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

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

【示例的源码分析】

基本流程和前面一致,这边不展开赘述。

可以看到寻找匹配的参数解析器的时候,可选项多了一个。

另外,注意的是,如果都没找到符合的,比如用基础类型,会使用RequestParamMethodArgumentResolver,具体不展开。

【实战补充】

上述示例只是为了帮助理解,真实开发中,更多自定义入参解析器的情况是:

添加自定义注解,并为其指定特定功能,例如添加可以同时兼容form 和 json 的场景、或支持复杂数据自动解析等等。

值得一提的是,解析器可以注册这个,但最终只会生效一个,如果都找不到符合的,都会有兜底的方案,要适当注意一下添加的顺序。


自定义入参转换器

关键词:AbstractHttpMessageConverter

【技术说明】

作用:Message Converters 主要负责将 Controller 方法的返回值转换为 HTTP 响应的内容。

工作原理:当 Controller 方法返回一个对象时,Spring MVC 使用消息转换器将该对象转换为 HTTP 响应体的内容。消息转换器负责将 Java 对象转换为特定的媒体类型,例如 JSON、XML、HTML 等。Spring 提供了各种内置的消息转换器来支持不同的数据格式。

示例:如果你的 Controller 方法返回一个对象,Spring MVC 将根据请求的 Accept 头部信息和返回值类型选择适当的消息转换器,将对象转换为对应的媒体类型。

【示例说明】

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


总结陈词

此篇文章介绍了SpringMVC 入参处理相关的分析,仅供学习参考。

💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。

相关推荐
老马啸西风7 分钟前
NLP 中文拼写检测纠正论文 C-LLM Learn to CSC Errors Character by Character
java
Cosmoshhhyyy29 分钟前
LeetCode:3083. 字符串及其反转中是否存在同一子字符串(哈希 Java)
java·leetcode·哈希算法
AI人H哥会Java42 分钟前
【Spring】基于XML的Spring容器配置——<bean>标签与属性解析
java·开发语言·spring boot·后端·架构
计算机学长felix1 小时前
基于SpringBoot的“大学生社团活动平台”的设计与实现(源码+数据库+文档+PPT)
数据库·spring boot·后端
sin22011 小时前
springboot数据校验报错
spring boot·后端·python
开心工作室_kaic1 小时前
springboot493基于java的美食信息推荐系统的设计与实现(论文+源码)_kaic
java·开发语言·美食
缺少动力的火车1 小时前
Java前端基础—HTML
java·前端·html
loop lee1 小时前
Redis - Token & JWT 概念解析及双token实现分布式session存储实战
java·redis
ThetaarSofVenice1 小时前
能省一点是一点 - 享元模式(Flyweight Pattern)
java·设计模式·享元模式