Current request is not a multipart request问题排查

概述

在应用工程里看到如下被标记为@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有交集甚至冗余嫌疑)。

参考

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

适用于iOS 的顶级苹果数据恢复软件

数据丢失可能随时发生在任何人身上&#xff0c;这可能是一种令人沮丧的经历。丢失 iOS 设备上的重要数据可能会造成特别严重的损失&#xff0c;因为其中可能包括有价值的照片、联系人、消息和其他重要文件。幸运的是&#xff0c;有多种数据恢复工具可以帮助用户恢复丢失的数据。…

filebeat日志收集工具

elk:filebeat日志收集工具和logstash相同 filebeat是一个轻量级的日志收集工具&#xff0c;所使用的系统资源比logstash部署和启动时使用的资源要小得多 filebeat可以运行在非Java环境&#xff0c;它可以代理logstash在非Java环境上收集日志 filebeat无法实现数据的过滤&…

定制开发办公软件在企业发展中的优势|app小程序网站搭建

定制开发办公软件在企业发展中的优势|app小程序网站搭建 如今&#xff0c;办公软件已经成为企业日常工作的必需品。很多企业为了提高工作效率和满足自身业务需要&#xff0c;选择定制开发办公软件。下面将介绍定制开发办公软件在企业发展中的优势。 首先&#xff0c;定制开发办…

DjiTello + YoloV5的无人机的抽烟检测

一、效果展示 注&#xff1a;此项目纯作者自己原创&#xff0c;创作不易&#xff0c;不经同意不给予搬运权限&#xff0c;转发前请联系我&#xff0c;源码较大需要者评论获取&#xff0c;谢谢配合&#xff01; 1、未启动飞行模型无人机的目标检测。 DjiTello YOLOV5抽烟检测 …

EDA实验-----正弦信号发生器的设计(Quartus II )

目录 一、实验目的 二、实验仪器 三、实验原理 四、实验内容 五、实验步骤 六、注意事项 七、实验过程&#xff08;操作过程&#xff09; 1.定制LPM_ROM模块 2.定制LPM_ROM元件 3.计数器定制 4.创建锁相环 5.作出电路图 6.顶层设计仿真 一、实验目的 学习使用Ver…

Echarts地图registerMap使用的GeoJson数据获取

https://datav.aliyun.com/portal/school/atlas/area_selector 可以选择省&#xff0c;市&#xff0c;区。 也可以直接在地图上点击对应区域。 我的应用场景 我这里用到这个还是一个特别老的大屏项目&#xff0c;用的jq写的。显示中国地图边界区域 我们在上面的这个地区选择…

【开源】基于Vue+SpringBoot的独居老人物资配送系统

项目编号&#xff1a; S 045 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S045&#xff0c;文末获取源码。} 项目编号&#xff1a;S045&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块三、系统展示四、核心代码4.1 查询社区4…

ELK---filebeat日志收集工具

filebeat日志收集工具 filebeat日志收集工具和logstash相同 filebeat的优点&#xff1a; filebeat是一个轻量级的日志收集工具&#xff0c;所使用的系统资源比logstash部署和启动时使用的资源要小的多 filebeat可以运行在非Java环境。它可以代替logstash在非Java环境上收集…

Java学习路线第一篇:Java基础(2)

这篇则分享Java学习路线第一part&#xff1a;Java基础&#xff08;2&#xff09; 从看到这篇内容开始&#xff0c;你就是被选定的天命骚年&#xff0c;将承担起学完Java基础的使命&#xff0c;本使命为单向契约&#xff0c;你可选择YES或者选择YES。 具体路线安排&#xff1a…

深度学习第1天:深度学习入门-Keras与典型神经网络结构

☁️主页 Nowl &#x1f525;专栏《机器学习实战》 《机器学习》 &#x1f4d1;君子坐而论道&#xff0c;少年起而行之 文章目录 神经网络 介绍 结构 基本要素 Keras 介绍 导入 定义网络 模型训练 前馈神经网络 特点 常见类型 代码示例 反馈神经网络 特点 …

AlphaFold的原理及解读

1、背景 蛋白质是生物体内一类重要的生物大分子&#xff0c;其结构复杂多样&#xff0c;蛋白质的结构对于理解其功能和参与的生物学过程具有重要意义。从生物学角度上看&#xff0c;蛋白质的结构可以分为四个层次&#xff1a;初级结构、二级结构、三级结构和四级结构。 初级结…

企业如何保障跨境金融业务中的数据安全传输?

随着全球化的不断深入&#xff0c;跨境金融业务日益频繁&#xff0c;然而在这些业务中&#xff0c;数据的安全传输一直是企业面临的重大挑战。跨境业务数据传输可能会遇到多种困难&#xff0c;如网络攻击、数据泄露、通信故障等。因此&#xff0c;企业需要采取有效的措施来确保…

【Mybatis系列】Mybatis之TypeHandler入门

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

【C语言】深入理解数据类型转换与运算

文章目录 1.数据类型转换在分析源程序之前&#xff0c;我们需要了解几个基本概念&#xff1a;现在来分析源程序中的变量及其对应的十进制真值以及扩展操作方式&#xff1a; 1.1. short si -32768;1.2. unsigned short usi si;1.3. int i si;1.4. unsigned ui usi; 2&#x…

U-Net及其变体在医学图像分割中的应用研究综述

U-Net及其变体在医学图像分割中的应用研究综述 论文来自&#xff1a;中国生物医学工程学报 2022 摘 要&#xff1a; 医学图像分割可以为临床诊疗和病理学研究提供可靠的依据&#xff0c;并能辅助医生对病人的病情做出准确的判断。 基于深度学习的分割网络的出现解决了传统自动分…

Git——Git应用入门

将会介绍以下知识&#xff1a; 搭建Git环境和创建Git版本库&#xff08;init、clone&#xff09;。文件添加、状态检查、创建注释和查看历史记录。与其他Git版本库交互&#xff08;pull、push&#xff09;。解决合并冲突。创建分支列表、列表切换和合并。创建标签。 1、版本控…

sqli-labs靶场详解(less17-less22)

目录 less-17 less-18 less-19 less-20 less-21 less-22 less-17 修改密码关卡 服务器后端 账号密码都存在数据库中 使用UPDATE进行修改密码 尝试username处 尝试好久尝试不出来应该是对用户名进行了过滤 于是对password进行注入 判断注入点 passwdadmin 报错&#xff1a…

vue3中的customRef创建一个自定义的 ref对象

customRef 创建一个自定义的 ref&#xff0c;并对其依赖项跟踪和更新触发进行显式控制 小案例: 自定义 ref 实现 debounce <template><div style"font-size: 14px;"><input v-model"text" placeholder"搜索关键字"/><…

智能优化算法应用:基于郊狼算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于郊狼算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于郊狼算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.郊狼算法4.实验参数设定5.算法结果6.参考文献7.MATLAB…

深思:C与C++相互调用问题

背景 上周&#xff0c;偶然看到同事愁眉苦脸的样子&#xff0c;便善意咨询了下发生了什么。简单沟通下&#xff0c;才知道他遇到了一个工程编译的问题&#xff0c;一直无法编译通过&#xff0c;困扰了他快一天时间。出于个人的求知欲和知识的渴望&#xff0c;我便主动与他一同分…