Current request is not a multipart request问题排查

news2024/11/25 18:25:13

概述

在应用工程里看到如下被标记为@deprecated的代码,这对有代码洁癖的我而言是无法忍受的:
在这里插入图片描述

row.getCell(10).setCellType(Cell.CELL_TYPE_STRING);
String hospital = row.getCell(0).getStringCellValue();

对应的poi版本号?是的,你没猜错,使用次数最多的版本3.17!!

<dependency>
  <groupId>org.apache.poi</groupId>
  <artifactId>poi</artifactId>
  <version>3.17</version>
</dependency>
<dependency>
  <groupId>org.apache.poi</groupId>
  <artifactId>poi-ooxml</artifactId>
  <version>3.17</version>
</dependency>

洁癖

Google一番,终于找到一个靠谱的解决方案-StackOverflow-setcelltypecelltype-string-is-deprecated:

You can just call row.setCellValue(String) you don’t have to set the cell type beforehand.

删除此行代码即可。

验证

这就完了吗?当然不!作为一个有追求的资深程序员,发现不clean的code,修改只是第一步,接下来还需要验证,此番修改是否会引发新的问题。

一步步找到Controller层接口,好在就一个接口,改动也只会影响这一个接口。具体来说,这是一个Excel文件上传接口,解析Excel内容,然后把Excel数据insert or update到MySQL数据库。本地启动应用,Postman模拟请求:
在这里插入图片描述
然后去数据库验证一下,一切完美?

问题爆出

等等,这个接口前端在哪里用到呢?通过IDEA强大的全局代码搜索能力(是的,作为一个小型公司的资深开发&技术经理,需要了解公司的全部业务,当然也需要知道前后端开发概况,如代码库),当然需要提前把前后端代码都导入到一个目录下,然后IDEA打开此目录,则会自动把此目录下的全部子目录下的全部工程导入到IDEA的工作空间:
在这里插入图片描述
总之,找到接口调用处,知道具体的业务场景,结果不管是测试环境还是生产环境都有问题,报错如下:
在这里插入图片描述

排查

借助于ELK,可以快速找到错误堆栈日志:

Current request is not a multipart request
org.springframework.web.multipart.MultipartException: Current request is not a multipart request
at org.springframework.web.servlet.mvc.method.annotation.RequestPartMethodArgumentResolver.resolveArgument(RequestPartMethodArgumentResolver.java:157)
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:126)
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)

对应的后端接口代码:

@PostMapping(value = "/uploadExcel")
    public Response<String> uploadExcel(@RequestPart("excelFile") MultipartFile file, @RequestParam("flag") String flag, @RequestParam("channel") String channel) {
}

上面这段代码将近3年没人改过。并且Postman模拟接口请求,文件上传功能是正常的!!!

看了下前端代码,发现有Axios组件库升级之类的修改,于是想着让前端去排查。扔过来一张截图,并坚持声称前端代码没有问题,把我噎得够呛:
在这里插入图片描述

初次尝试

Google搜了一圈没有找到靠谱的解决方法。其中包括stackoverflow这篇multipartexception-current-request-is-not-a-multipart-request,在controller层增加配置,改成下面这样:

@PostMapping(value = "/uploadExcel", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})

前文提到过,因为Postman模拟文件上传功能是好的,也就是说本地无法复现问题,当然也就不能验证修改,只能提交代码发布到测试环境:提交->打版本号->编译构建->滚动发布->测试环境验证。虽然有CI流水线这一套,但是也需要5分钟左右。

测试环境报了另一个错误:

Content type 'application/json;charset=UTF-8' not supported
org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/json;charset=UTF-8' not supported
at org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.handleNoMatch(RequestMappingInfoHandlerMapping.java:214)

无解。。搁置一个周末。。

本地调试环境

既然Postman模拟接口请求没有问题,那咱就启动前端工程(虽然已经将近一年不写前端代码),前端工程直接调用本地后端工程,尝试本地调试复现问题。

前端工程使用React,看到熟悉的package.json文件:
在这里插入图片描述
直接在IDEA的Terminal里执行命令:npm install,会在项目根目录下生成node_modules文件夹。
在这里插入图片描述
有4种启动方式,如上截图所示,有Run模式和Debug模式。是的,你没看错,前端代码使用IDEA工程也可以Debug。就在上一家公司的上一份工作里,一年里我还提交过四百多次前端代码。

现在这份工作9个月多,第一次启动前端工程。这里我也习惯性使用Debug模式,结果并不行。Anyway最终以Run模式执行dev启动成功。

再来看看上面截图dev命令提到的server.js文件:

const devProxy = {
  '/api': {
    target: 'http://10.18.65.51:8848',
    // pathRewrite: { '^/api': '' },
    // target: 'https://stg-open.aaaaa.com',
    pathRewrite: { '^/api': '/api' },
    changeOrigin: true,
  },
}

主要有两个配置:

  • target:指向的后端服务。如果指向的是域名,则需要在nginx里配置域名对应的后端服务,如果指向的是IP,则是联调时的后端服务所在的机器IP。此处前后端工程是同一个机器启动的。IP当然需要修改为本机IP,使用命令查看:ifconfig en0即可。
  • pathRewrite:发送请求时,请求路径重写。就是多少一个/api的差别。这也太眼熟了吧,和Spring Cloud Gateway网关路由配置差不多意思。

浏览器打开http://localhost:3001,看到熟悉的界面,后端服务接口设置断点,界面操作。ok,本地联调环境已具备。

复现成功

一开始后端只启动一个merchant工程,也就是上面Postman截图里的merchant/open/uploadExcel接口,修改server.js为:

target: 'http://10.18.65.51:8849', // merchant服务占用端口
pathRewrite: { '^/api': '' }, // 直接请求merchant服务

在这里插入图片描述
上传成功,本地没有复现。

那测试环境为啥有问题呢?

看到pathRewrite,前面也提到Spring Cloud Gateway网关路由配置。

对了,测试和生产环境里所有的服务请求都是走Gateway网关。

ok,再启动一个Gateway服务,同时需要修改server.js为:

target: 'http://10.18.65.51:8848', // gateway服务占用端口
pathRewrite: { '^/api': '/api' }, // 直接请求gateway服务,后端gateway服务再负责转发请求到merchant服务

问题复现!!所以,问题出现在Gateway网关服务。

看到曙光

我们再来看看Gateway服务的Apollo配置:
在这里插入图片描述
这里有个RequestLogFilter!!

打断点,再来一次。前端页面点击文件上传,请求进入到gateway服务,断点进入RequestLogFilter!!

来看看源码:

import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.List;

/**
 * 所有请求的日志过滤器
 **/
@Slf4j
@Component
public class RequestLogFilter extends AbstractGatewayFilterFactory<Config> {

    private final List<HttpMessageReader<?>> messageReaders;

    public RequestLogFilter() {
        super(Config.class);
        this.messageReaders = HandlerStrategies.withDefaults().messageReaders();
    }

    private RewriteFunction<String, String> rewriteFunction() {
        return (exchange, body) -> {
            String url = exchange.getRequest().getURI().getPath();
            log.info("*****请求信息日志拦截*****,请求的路径:{},请求的入参数据:{}", url, body);
            return Mono.just(body);
        };
    }

    @Override
    public GatewayFilter apply(Config config) {
        config.setRewriteFunction(String.class, String.class, rewriteFunction());
        return (exchange, chain) -> {
            Class inClass = config.getInClass();
            ServerRequest serverRequest = ServerRequest.create(exchange, this.messageReaders);
            Mono<?> responseBody = serverRequest.bodyToMono(inClass).flatMap((o) -> config.getRewriteFunction().apply(exchange, o));
            BodyInserter bodyInserter = BodyInserters.fromPublisher(responseBody, config.getOutClass());
            HttpHeaders headers = new HttpHeaders();
            headers.putAll(exchange.getRequest().getHeaders());
            headers.remove("Content-Length");
            if (config.getContentType() != null) {
                headers.set("Content-Type", config.getContentType());
            }
            CachedBody outputMessage = new CachedBody(exchange, headers);
            return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
                ServerHttpRequest decorator = this.decorate(exchange, headers, outputMessage);
                return chain.filter(exchange.mutate().request(decorator).build());
            }));
        };
    }

    ServerHttpRequestDecorator decorate(ServerWebExchange exchange, HttpHeaders headers, CachedBody outputMessage) {
        return new ServerHttpRequestDecorator(exchange.getRequest()) {
            @NotNull
            @Override
            public HttpHeaders getHeaders() {
                long contentLength = headers.getContentLength();
                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.putAll(super.getHeaders());
                httpHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8);
                if (contentLength > 0L) {
                    httpHeaders.setContentLength(contentLength);
                } else {
                    httpHeaders.set(OpenConstants.TRANSFER_ENCODING, OpenConstants.CHUNKED);
                }
                return httpHeaders;
            }

            @NotNull
            @Override
            public Flux<DataBuffer> getBody() {
                return outputMessage.getBody();
            }
        };
    }
}

大致瞟一眼就看到这里有对request进行解析的代码,也有设置HTTP headers的代码。问题大概率就出现在这里。

好在测试环境也有问题,那就删除spring.cloud.gateway.routes[23].filters[0] = RequestLogFilter这条配置项。再来一次。

!!!问题消失!!!

分析原因

RequestLogFilter是一个Filter(废话),用于在请求转发到对应的后端其他服务前,解析requestBody,并打印出来,类似于AOP日志记录。

去掉httpHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8);这行代码,莫名其妙报了另一个错误:

ERROR | com.aba.open.merchant.service.impl.MerchantOpenServiceImpl | uploadExcel | 93 | - 
java.io.IOException: ZIP entry size is too large or invalid
    at org.apache.poi.openxml4j.util.ZipArchiveFakeEntry.<init>(ZipArchiveFakeEntry.java:43)
    at org.apache.poi.openxml4j.util.ZipInputStreamZipEntrySource.<init>(ZipInputStreamZipEntrySource.java:53)
    at org.apache.poi.openxml4j.opc.ZipPackage.<init>(ZipPackage.java:106)
    at org.apache.poi.openxml4j.opc.OPCPackage.open(OPCPackage.java:307)
    at org.apache.poi.ooxml.util.PackageHelper.open(PackageHelper.java:47)
    at org.apache.poi.xssf.usermodel.XSSFWorkbook.<init>(XSSFWorkbook.java:309)
    at com.aba.open.merchant.service.impl.MerchantOpenServiceImpl.uploadExcel(MerchantOpenServiceImpl.java:68)

难搞。

附录

AOP

@Aspect
@Component
@Slf4j
public class ControllerLogAop {
    @Pointcut("execution(public * com.aaaaa.dialog.controller..*.*(..))")
    public void webLog() {
    }

    /**
     * 在切点之前织入
     */
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) {
        try {
            log.info("======= Start ===========");
            // 打印请求入参
            Object[] args = joinPoint.getArgs();
            Object[] arguments = new Object[args.length];
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof ServletRequest || args[i] instanceof ServletResponse || args[i] instanceof MultipartFile) {
                    continue;
                }
                arguments[i] = args[i];
            }
            log.info("类{}方法{},请求参数= {}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(), JSON.toJSONString(arguments));
        } catch (Exception e) {
            log.error("日志切面异常", e);
        }
    }

    /**
     * 在切点之后织入
     */
    @After("webLog()")
    public void doAfter() {
        log.info("=========== End =========");
    }

    /**
     * 环绕
     */
    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object result = null;
        try {
            long startTime = System.currentTimeMillis();
            result = proceedingJoinPoint.proceed();
            // 打印出参
            log.info("返回参数= {}", JSON.toJSONString(result));
            // 执行耗时
            log.info("耗时{} ms", System.currentTimeMillis() - startTime);
            return result;
        } catch (Exception e) {
            log.error("日志切面异常", e);
        }
        return result;
    }
}

ControllerLogAop这种配置类由于涉及到controller包路径的切入,即@Pointcut,可能需要在每个服务里都写一份(事实上我们目前也是这样做的,功能定位和RequestLogFilter有交集甚至冗余嫌疑)。

参考

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1260291.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基本数据结构二叉树(2)

目录 3.二叉树的顺序结构及实现 3.1 二叉树的顺序结构 3.2 堆的概念及结构 3.3 堆的实现 3.2.1 堆向下调整算法 3.2.2堆的创建 3.2.3 建堆时间复杂度 3.2.4 堆的插入 3.2.5 堆的删除 3.2.6 堆的代码实现 3.4 堆的应用 3.4.1 堆排序 3.4.2 TOP-K问题 3.二叉树的顺序…

陆正耀卖奶茶,是库迪不行了吗?

文 | 智能相对论 作者 | 胡静婕 咖啡还没“玩”明白&#xff0c;陆正耀又要开始卖奶茶了。 11月23日&#xff0c;陆正耀创立的库迪咖啡确认&#xff0c;其奶茶品牌“茶猫”系库迪咖啡旗下第二品牌&#xff0c;将于2024年1月正式上市&#xff0c;公司仍将采取风险共担的联营模…

软著项目推荐 深度学习中文汉字识别

文章目录 0 前言1 数据集合2 网络构建3 模型训练4 模型性能评估5 文字预测6 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 深度学习中文汉字识别 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xf…

浅谈UML的概念和模型之UML九种图

1、用例图&#xff08;use case diagrams&#xff09; 【概念】描述用户需求&#xff0c;从用户的角度描述系统的功能 【描述方式】椭圆表示某个用例&#xff1b;人形符号表示角色 【目的】帮组开发团队以一种可视化的方式理解系统的功能需求 【用例图】 2、静态图 类图&…

第二十六章 解读IoU、GIoU、DIoU、CIoU、EIoU 5大评价指标

目录 一、简介 二、IoU&#xff08;Intersection over Union&#xff09; 三、GIoU&#xff08;Generalized IoU&#xff09; 四、DIoU&#xff08;Distance-IoU&#xff09; 五、CIoU(Complete-IoU) 六、EIoU(Efficient-IoU) 七、pytorch代码实现 八、总结 一、简介 …

使用DeepBlueCLI对Windows日志进行取证(小记)

什么是Windows日志取证 Windows日志取证是指通过分析和收集Windows操作系统生成的日志信息&#xff0c;以获取关于系统活动、用户行为、安全事件等方面的数据 工具使用 工具介绍 DeepBlueCLI 是一个用于检测 Windows 系统中的安全事件和威胁的 PowerShell 脚本工具 工具下…

探究Kafka原理-4.API使用

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring源码、JUC源码、Kafka原理&#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44…

Kotlin学习之集合

原文链接 Kotlin Collections 现代的软件一般比较复杂&#xff0c;程序语言中的基本数据类型往往不能满足需要&#xff0c;除了基本的数据类型以外&#xff0c;还有对象的容器也非常的重要&#xff0c;比如线性容器&#xff08;数组&#xff0c;列表和Set&#xff09;和二维容…

Adversarial Attack and Defense on Graph Data: A Survey(2022 IEEE Trans)

Adversarial Attack and Defense on Graph Data: A Survey----《图数据的对抗性攻击和防御&#xff1a;综述》 图对抗攻击论文数据库&#xff1a; https://github.com/safe-graph/graph-adversarial-learning-literature 摘要 深度神经网络&#xff08;DNN&#xff09;已广泛应…

360压缩安装一半不动了?一分钟解决!

360压缩软件是我们常用的压缩软件&#xff0c;但是常常会遇到压缩安装到一半停止的情况&#xff0c;下面提供了一些可能的原因和解决办法&#xff0c;大家可以进行尝试~ 方法一&#xff1a;关闭防火墙和杀毒软件 有时候&#xff0c;防火墙和杀毒软件可能会阻止360压缩的安装过…

计算机组成原理——存储器(主存容量扩展)

对于字扩展与位扩展的解释&#xff1a; 计算机原理中的字&#xff0c;位扩展&#xff0c;都给老子进来学&#xff0c;看不懂算我输&#xff01; 如果主存的容量无法满足 CPU 的需求&#xff0c;可以通过存储器扩展来解决&#xff0c;扩展的方式有两种&#xff1a; 主存的位数…

java学习part17

110-面向对象(高级)-关键字final的使用及真题_哔哩哔哩_bilibili 1.概念 tips&#xff1a;java里有const关键字&#xff0c;但是用于保留字&#xff0c;不会使用&#xff0c;目前没有意义。 final变量没有默认赋值&#xff0c;只能在以下三个地方赋值&#xff0c;且只能赋值一…

『VUE3后台—大事件管理系统』

项目地址:https://gitee.com/csheng-gitee/vue3-big-event-admin 技术栈:VUE3 + Pinia + Pnpm(本项目暂不用 typescript) 一、前期准备工作 1、创建项目 npm install -g pnpm pnpm create vue2、ESLint 配置 (1) 禁用 prettier 插件,下载 ESLint 插件;(2) 在 vscode 的…

好题分享(2023.11.19——2023.11.25)

目录 ​编辑 前情回顾&#xff1a; 前言&#xff1a; 认识循环队列&#xff1a; 实现循环队列的思路&#xff1a; 题目&#xff1a;《设计循环队列》 1.判满和判空&#xff1a; 2.添加数据和删除 3.计算循环队列的数据个数 4.返回对队尾元素 总结&#xff1a; 前情回…

内部类, Comparable接口, Comparator接口, Cloneable接口 ---java

目录 一. 内部类 1.1 静态内部类 1.2 实例内部类 1.3匿名内部类 二. 接口的使用实例 2.1 Comparable接口 2.2 Comparator接口 ---比较器 2.3 Cloneable接口 深拷贝浅拷贝 一. 内部类 当一个事物的内部&#xff0c;还有一个部分需要一个完整的结构进行描述&#xff0…

从零开始学Go web——第一天

文章目录 从零开始学Go web——第一天一、Go与web应用简介1.1 Go的可扩展性1.2 Go的模块化1.3 Go的可维护1.4 Go的高性能 二、web应用2.1 工作原理2.2 各个组成部分2.2.1 处理器2.2.2 模板引擎 三、HTTP简介四、HTTP请求4.1 请求的文本数据4.2 请求方法4.2.1 请求方法类型4.2.2…

C语言:输出所有“水仙花数”。“水仙花数”是指一个3位数,其各位数字的立方和等于该数本身,如153=1^3 +5^3+3^3

分析&#xff1a; 在主函数 main 中&#xff0c;程序首先定义四个整型变量 m、a、b 和 c&#xff0c;并用于计算和判断水仙花数。然后使用 printf 函数输出提示信息。 接下来&#xff0c;程序使用 for 循环结构&#xff0c;从 100 到 999 遍历所有三位数。对于每个遍历到的数 m…

C#常见的设计模式-创建型模式

引言 在软件开发过程中&#xff0c;设计模式是一种被广泛采用的思想和实践&#xff0c;可以提供一种标准化的解决方案&#xff0c;以解决特定问题。设计模式分为三种类型&#xff1a;创建型模式、结构型模式和行为型模式。本篇文章将重点介绍C#中常见的创建型模式。 目录 引言…

python实现自动刷平台学时

背景 前一阵子有个朋友让我帮给小忙&#xff0c;因为他每学期都要看视频刷学时&#xff0c;一门平均需要刷500分钟&#xff0c;一学期有3-4门需要刷的。 如果是手动刷的话&#xff0c;比较麻烦&#xff0c;能否帮他做成自动化的。搞成功的话请我吃饭。为了这顿饭&#xff0c;咱…

Elasticsearch:什么是非结构化数据?

非结构化数据定义 非结构化数据是指未按照设计的模型或结构组织的数据。 非结构化数据通常被归类为定性数据&#xff0c;可以是人类或机器生成的。 非结构化数据是最丰富的可用数据类型&#xff0c;经过分析后&#xff0c;可用于指导业务决策并在许多其他用例中实现业务目标。…