当前位置: 首页 > news >正文

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

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

文章目录

    • 写在前面的话
    • SpringMVC 出参处理
      • 学前准备与回顾
      • @ResponseBody 出参处理
      • 自定义出参用法
    • 总结陈词

CSDN.gif

写在前面的话

上一篇博文《学会 SpringMVC 系列 · 剖析入参处理》的学习,大致了解了SpringMVC请求流程中的入参处理环节,接下来介绍出参处理相关分析。后续的几篇博文,会将流程中涉及的若干关键环节单独拿出来讲解,并结合实战中的运用,帮助领略SpringMVC带来的定制和扩展能力。

相关博文
《学会 SpringMVC 系列 · 基础篇》
《学会 SpringMVC 系列 · 剖析篇(上)》
《学会 SpringMVC 系列 · 剖析入参处理》
《学会 SpringMVC 系列 · 剖析出参处理》
《学会 SpringMVC 系列 · 返回值处理器》
《学会 SpringMVC 系列 · 消息转换器 MessageConverters》
《学会 SpringMVC 系列 · 写入拦截器 ResponseBodyAdvice》
《学会 SpringMVC 系列 · 剖析初始化》
《程序猿入职必会(1) · 搭建拥有数据交互的 SpringBoot 》


SpringMVC 出参处理

学前准备与回顾

与入参处理一致,本篇 SpringMVC 源码分析系列文章,继续使用 《搭建拥有数据交互的 SpringBoot 》博文搭建的 SpringBoot3.x 项目为基础,以此学习相关源码,对应 SpringMVC 版本为 6.1.11。另外,为保证知识连贯性,继续总结和回顾一下主体流程。

【一次请求的主链路节点】
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)

Tips:基于上方内容,本篇出参处理将继续从HandlerMethodReturnValueHandlerComposite展开。


@ResponseBody 出参处理

由于目前大部分场景都是前后端分离开发模式,后端较少返回视图页面,基本都采用 @ResponseBody 返回数据。
这里也以最常见的@ResponseBody为示例,展开介绍。

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

【运行流程】
1、先跑一下接口,忽略之前的逻辑,断点在出参处理的入口处,如下所示。
这里 returnValue 是前面接口实际返回的数据,returnType 是返回值类型(MethodParameter)。
image.png
2、接着进入 selectHandler 方法,这个按字面意思,就是查找合适的的处理器,这里又可以看到熟悉的配方。
遍历 HandlerMethodReturnValueHandler 的列表,依次执行其 supportsReturnType 判断是否满足。
这里还有一个异步判定逻辑,暂不展开。
image.png
3、这里找到 RequestResponseBodyMethodProcessor,要求类或方法包含@ResponseBody即可。
image.png
4、找到合适的处理器后,返回执行 RequestResponseBodyMethodProcessor 的 handleReturnValue方法。
image.png
5、该方法往下走到 writeWithMessageConverters,看名字就是使用消息转换器进行write。
这里再次遍历消息转换器列表,依次执行canWrite方法判定,由于示例项目有引入了fastjson相关依赖,这边匹配到了
FastJsonHttpMessageConverter。由于这步代码比较多一些,下方贴一下源代码。
image.png

// 遍历 HttpMessageConverter 出参转换器列表
for (HttpMessageConverter<?> converter : this.messageConverters) {GenericHttpMessageConverter genericConverter =(converter instanceof GenericHttpMessageConverter ghmc ? ghmc : null);// 匹配符合要求的出参转换器,这里找到了FastJsonHttpMessageConverterif (genericConverter != null ?((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :converter.canWrite(valueType, selectedMediaType)) {// 再正式write之前,还会先调用ResponseBodyAdvice列表的beforeBodyWrite方法body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,(Class<? extends HttpMessageConverter<?>>) converter.getClass(),inputMessage, outputMessage);if (body != null) {Object theBody = body;LogFormatUtils.traceDebug(logger, traceOn ->"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");addContentDispositionHeader(inputMessage, outputMessage);// 正式转换数据if (genericConverter != null) {genericConverter.write(body, targetType, selectedMediaType, outputMessage);}else {((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);}}else {if (logger.isDebugEnabled()) {logger.debug("Nothing to write: null body");}}return;}
}

6、这里在执行 FastJsonHttpMessageConverter#write 之前,会先执行 RequestResponseBodyAdviceChain 的beforeBodyWrite方法,遍历 ResponseBodyAdvice 列表,依次执行 supports 方法进行匹配。
注意,如果有自定义的ResponseBodyAdvice此时就可以触发,这个也是一个扩展点。

private <T> Object processBody(@Nullable Object body, MethodParameter returnType, MediaType contentType,Class<? extends HttpMessageConverter<?>> converterType,ServerHttpRequest request, ServerHttpResponse response) {for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) {if (advice.supports(returnType, converterType)) {body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType,contentType, converterType, request, response);}}return body;
}

7、处理完上述步骤后,正式使用FastJsonHttpMessageConverter进行write转换数据,最后是走到下方,直接输出流返回。
image.png

【总结一下,链路流程】
HandlerMethodReturnValueHandlerComposite#handleReturnValue(出参处理的入口)
HandlerMethodReturnValueHandlerComposite#selectHandler(找出参处理器)
RequestResponseBodyMethodProcessor#supportsReturnType(判断出参匹配)
RequestResponseBodyMethodProcessor#handleReturnValue(出参处理逻辑)
AbstractMessageConverterMethodProcessor#writeWithMessageConverters(找出参转换器处理)
FastJsonHttpMessageConverter#canWrite(匹配出参转换器)
RequestResponseBodyAdviceChain#beforeBodyWrite(执行write前允许修改body)
FastJsonHttpMessageConverter#write(执行实际出参转换)


自定义出参用法

通过上述源码分析,大概看出来,如果需要自定义出参行为,可以从三个层面下手:返回值处理器、返回值消息转换器、响应前拦截
对应的关键词:HandlerMethodReturnValueHandler、MessageConverters、 ResponseBodyAdvice

Tips:由于篇幅所限,这几个扩展点后续章节单独介绍。


总结陈词

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

CSDN_END.gif

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • MacOS 中 Office 历史记录一键清理
  • 2024全新Thinkphp聊天室H5实时聊天室群聊聊天室自动分配账户完群组/私聊/禁言等功能/全开源运营版本
  • 源代码加密防泄漏如何做?
  • 如何实现element-ui 后台中点击按钮,将文本内容复制到剪贴板
  • 【RunnerGo】离线安装成功版本
  • Transwarp Data Studio 4.0 :适应AI新时代实现三大能力提升
  • java基础--字符串用法
  • zotero安装与使用
  • Spring统一功能处理:拦截器、响应与异常的统一管理
  • MM 特殊采购类型
  • 加密简史:从古代到现代的方法
  • Linux网络编程之dpdk的环境配置详解
  • 自动化测试与手动测试的区别!
  • 时光不等人:java每日一练
  • python入门基础篇(一)
  • 【翻译】babel对TC39装饰器草案的实现
  • Angular4 模板式表单用法以及验证
  • ES6 学习笔记(一)let,const和解构赋值
  • Eureka 2.0 开源流产,真的对你影响很大吗?
  • iBatis和MyBatis在使用ResultMap对应关系时的区别
  • JavaScript 一些 DOM 的知识点
  • JavaScript学习总结——原型
  • java正则表式的使用
  • LeetCode29.两数相除 JavaScript
  • MD5加密原理解析及OC版原理实现
  • nginx 负载服务器优化
  • nodejs调试方法
  • React as a UI Runtime(五、列表)
  • Swoft 源码剖析 - 代码自动更新机制
  • 案例分享〡三拾众筹持续交付开发流程支撑创新业务
  • 工程优化暨babel升级小记
  • 吐槽Javascript系列二:数组中的splice和slice方法
  • 用Node EJS写一个爬虫脚本每天定时给心爱的她发一封暖心邮件
  • 终端用户监控:真实用户监控还是模拟监控?
  • k8s使用glusterfs实现动态持久化存储
  • mysql面试题分组并合并列
  • ​flutter 代码混淆
  • ​LeetCode解法汇总518. 零钱兑换 II
  • # Apache SeaTunnel 究竟是什么?
  • ### RabbitMQ五种工作模式:
  • ###C语言程序设计-----C语言学习(6)#
  • #define,static,const,三种常量的区别
  • (¥1011)-(一千零一拾一元整)输出
  • (论文阅读笔记)Network planning with deep reinforcement learning
  • (十七)Flink 容错机制
  • (限时免费)震惊!流落人间的haproxy宝典被找到了!一切玄妙尽在此处!
  • (学习日记)2024.03.12:UCOSIII第十四节:时基列表
  • (转)Mysql的优化设置
  • *算法训练(leetcode)第四十五天 | 101. 孤岛的总面积、102. 沉没孤岛、103. 水流问题、104. 建造最大岛屿
  • .a文件和.so文件
  • .h头文件 .lib动态链接库文件 .dll 动态链接库
  • .net core 实现redis分片_基于 Redis 的分布式任务调度框架 earth-frost
  • .Net Core 中间件与过滤器
  • .NET/C# 使用 #if 和 Conditional 特性来按条件编译代码的不同原理和适用场景
  • .NET/C# 使用 SpanT 为字符串处理提升性能