📢 大家好,我是 【战神刘玉栋】,有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
入参处理相关的分析,仅供学习参考。
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。