主动写入流对@ResponseBody注解的影响 | 京东云技术团队

问题回溯

2023年Q2某日运营反馈一个问题,商品系统商家中心某批量工具模板无法下载,导致功能无法使用(因为模板是动态变化的)

商家中心报错(JSON串):

复制代码
{"code":-1,"msg":"失败"}

负责的同事看到失败后立即与我展开讨论(因为不是关键业务,所以不需要回滚,修复即可),我们发现新功能模板下载的代码与之前的代码有所不同,恰好之前的功能又可以正常运行,所以同事对现有代码进行改造然后预发布测试完成后再次上线。

其他业务代码:

复制代码
/**
 * 模板下载
 */
@RequestMapping("/doBatchWareSetAd")
public void doBatchWareSetAd(@RequestParam MultipartFile file, HttpServletResponse response) {
	wareBatchBusiness.doBatchWareSetAd(file, response, getLongOrgCode(), getCurrentUserPin(), getCurrentUserId());
}

问题业务代码:

复制代码
/**
 * 模板下载
 */
@RequestMapping("/doBatchWareSetAdDemo")
@ResponseBody
public Map<String, Object> doBatchWareSetAd(@RequestParam MultipartFile file, HttpServletResponse response) {
	return wareBatchBusiness.doBatchWareSetAd(file, response, getLongOrgCode(), getCurrentUserPin(), getCurrentUserId());
}

上线的结果是;仍然无法使用。

其实也正常:因为两种代码在预发布都可以正常运行,在线上出错只可能是因为其他原因,只不过我们不了解底层原理,害怕它 "可能" 有问题罢了,最终查询得到的结论是权限系统管理员在线上环境没有给我们配置相应的文件,导致请求为空,导致请求失败。

探索 @ResponseBody 与主动写入流的关系

我们都知道 @ResponseBody 注解可以帮助我们把返回对象转化为JSON,方便展示和交互。

那它到底是如何工作的呢,请看下面的讲解:

代码案例1:

复制代码
@RequestMapping("/test1")
@ResponseBody
public Map<String, String> test1(HttpServletResponse response) {
    Map<String, String> map = new HashMap<>();
    map.put("1", "1");
    return map;
}

// 响应
JSON报文

跟代码发现其核心处理类为:RequestResponseBodyMethodProcessor.java

方法:org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor#handleReturnValue 会处理其相关返回值。

真正的核心处理方法:org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters

关键DEBUG记录如图所示:

后续内容可以想象,肯定还有地方去把流按照指定的HEADER写入,因为和本文无关所以不深究。

再来看代码案例2:

复制代码
@RequestMapping("/test2")
@ResponseBody
public Map<String, String> test2(HttpServletResponse response) throws IOException {
    Map<String, String> map = new HashMap<>();
    map.put("1", "1");

    response.setContentType("application/vnd.ms-excel");
    response.setHeader("Content-Disposition", String.format(
        "attachment; filename=%s_%s.xls", "Demo", System.currentTimeMillis()));

    OutputStream out = response.getOutputStream();
    out.flush();
    out.close();
    return map;
}

// 响应
提示下载文件

关键DEBUG源码截图

可以发现Spring对这种方式操作文件流视作异常情况,然后抛出,在后续逻辑中完成整个请求,简单来说就是 @ResponseBody 注解没起到任何作用。

因此答案呼之欲出:当时功能不可用的罪魁祸首就是相关人员没有配置参数导致,与写法没有任何关系。

结论与启发

结论:

  1. 我们要相信自己的代码,至少是要相信已经经过测试的代码。
  2. 在委托他人或者自己配置环境参数,如权限、ZK等每次都保证预发布和线上同时配置,避免遗漏的情况。

启发:

聊了这么多,那我们这种类似场景的代码应该怎么写?

既然主动写入流会解除@ResponseBody的作用,反之又能发挥它的作用,那我们最佳方案是不是如下所示?

复制代码
@RequestMapping("/test1")
@ResponseBody
public Map<String, String> test1(HttpServletResponse response) {
    Map<String, String> map = new HashMap();
    if (获取不到文件配置 == true) {
        return map.put("msg", "获取不到文件配置");
    }
    
    response.setContentType("application/vnd.ms-excel");
    response.setHeader("Content-Disposition", String.format(
        "attachment; filename=%s_%s.xls", "Demo", System.currentTimeMillis()));

    OutputStream out = response.getOutputStream();
    out.flush();
    out.close();
    return map;
}

如此一来,当发生预期之外的情况,我们有非常明显的报错提示,当正常时又可以完美实现功能,妙哉(我觉得)~

作者:京东零售 柯贤铭

来源:京东云开发者社区 转载请注明来源

相关推荐
杨杨杨大侠17 小时前
RAG 到底是怎么工作的?从用户提问到模型回答的完整链路
spring·openai·ai编程
Joy T18 小时前
Spring AI 2.0 Agent 进阶:Memory、State 与 Context Engineering 常见技术全景
java·人工智能·后端·spring·agent入门·agent state
linmengmeng_131420 小时前
【总结】MyBatis-Plus批量插入与清表的正确姿势
java·spring boot·mysql·mybatis
高级程序源20 小时前
django大学生创新创业项目管理系统94923-计算机课程设计、毕业设计
javascript·vue.js·spring boot·后端·python·django·课程设计
wno70420 小时前
Spring Security短信验证码登录
java·python·spring
步行cgn20 小时前
Spring util 命名空间详解
java·后端·spring
暮雨哀尘21 小时前
JAVA基础练习(四):SpringBoot 实现简易登录功能
java·spring boot·eclipse·maven
VXbishe1 天前
【课程设计】基于SpringBoot的乡村政务服务系统的设计与实现-计算机毕设77011
javascript·vue.js·spring boot·python·php·课程设计·政务
MayBaymax1 天前
Spring AI Alibaba Graph 快速上手:黑板、节点、边
java·spring·ai·ai编程