Current request is not a multipart request问题排查

概述

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

java 复制代码
row.getCell(10).setCellType(Cell.CELL_TYPE_STRING);
String hospital = row.getCell(0).getStringCellValue();

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

xml 复制代码
<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)

对应的后端接口代码:

java 复制代码
@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层增加配置,改成下面这样:

java 复制代码
@PostMapping(value = "/uploadExcel", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})

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

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

java 复制代码
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文件:

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为:

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

上传成功,本地没有复现。

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

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

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

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

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

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

看到曙光

我们再来看看Gateway服务的Apollo配置:

这里有个RequestLogFilter!!

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

来看看源码:

java 复制代码
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

java 复制代码
@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有交集甚至冗余嫌疑)。

参考

相关推荐
脸大是真的好~9 小时前
尚硅谷 SpringCloud05 Gateway-断言-过滤器-跨域CORS
gateway
袁洛施9 小时前
Claude Code API Gateway 配置指南
gateway
百***24139 小时前
Nginx反向代理出现502 Bad Gateway问题的解决方案
运维·nginx·gateway
小坏讲微服务9 小时前
整合Spring Cloud Alibaba与Gateway实现跨域的解决方案
java·开发语言·后端·spring cloud·云原生·gateway
angushine19 小时前
SpringCloud Gateway缓存body参数引发的问题
spring cloud·缓存·gateway
没有bug.的程序员2 天前
Spring Cloud Gateway 性能优化与限流设计
java·spring boot·spring·nacos·性能优化·gateway·springcloud
小坏讲微服务2 天前
Spring Cloud Alibaba Gateway 集成 Redis 限流的完整配置
数据库·redis·分布式·后端·spring cloud·架构·gateway
半旧夜夏5 天前
【Gateway】服务调用和网关配置攻略
java·spring boot·spring cloud·gateway
小坏讲微服务5 天前
Nginx集群与SpringCloud Gateway集成Nacos的配置指南
spring boot·nginx·spring cloud·gateway
小坏讲微服务5 天前
使用 Spring Cloud Gateway 实现集群
java·spring boot·分布式·后端·spring cloud·中间件·gateway