tomcat 改为 jetty后 文件上传报错:Missing content for multipart request 的解决记录

1、前言

  • jetty版本:9.4.52.v20230823
  • springboot版本:2.6.14

以前的项目大多是用的 apache tomcat ,但是最近新开发的一个项目,由于这个项目功能不是那么重,所以使用了比tomcat更轻量级的jetty作为 servelt容器,关于这俩的区别,可以看看这篇文章:apache-tomcat-vs-eclipse-jetty ,或者网上其他的博文。

明明在tomcat中好使的代码,换了jetty后为什么就报错了呢,下边我们仔细分析下。

2、异常信息与debug分析

情况是这样:最近刚开发的几个文件上传的接口(使用了org.springframework.web.multipart方式上传的文件)比如其中之一: 

image.png

 ,在文件上传时统一都报错这个: Missing content for multipart request 这个异常我在stackoverflow上以及github上都搜到了,看来有很多人遇到了,这里给出链接:

但是这些帖子都没有 直接解决我的问题,所以我决定记录一下,也许就有人遇到,并且看到我这篇文章呢!

异常详情如下:

 

less

复制代码

org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is java.io.IOException: Missing content for multipart request at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.handleParseFailure(StandardMultipartHttpServletRequest.java:127) ~[spring-web-5.3.24.jar:5.3.24] at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:115) ~[spring-web-5.3.24.jar:5.3.24] at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.<init>(StandardMultipartHttpServletRequest.java:88) ~[spring-web-5.3.24.jar:5.3.24] at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:122) ~[spring-web-5.3.24.jar:5.3.24] at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1209) ~[spring-webmvc-5.3.24.jar:5.3.24] at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1043) ~[spring-webmvc-5.3.24.jar:5.3.24] at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:964) ~[spring-webmvc-5.3.24.jar:5.3.24] at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.24.jar:5.3.24] at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.3.24.jar:5.3.24] at javax.servlet.http.HttpServlet.service(HttpServlet.java:665) ~[javax.servlet-api-4.0.1.jar:4.0.1] at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.24.jar:5.3.24] at javax.servlet.http.HttpServlet.service(HttpServlet.java:750) ~[javax.servlet-api-4.0.1.jar:4.0.1] at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:799) ~[jetty-servlet-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.servlet.ServletHandler$ChainEnd.doFilter(ServletHandler.java:1656) ~[jetty-servlet-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter.doFilter(WebSocketUpgradeFilter.java:292) ~[websocket-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.servlet.FilterHolder.doFilter(FilterHolder.java:193) ~[jetty-servlet-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1626) ~[jetty-servlet-9.4.52.v20230823.jar:9.4.52.v20230823] at com.xxx.dashboard.config.web.filter.PermissionManagerForTornadoFilter.doFilter(PermissionManagerForTornadoFilter.java:97) ~[classes/:?] at org.eclipse.jetty.servlet.FilterHolder.doFilter(FilterHolder.java:193) ~[jetty-servlet-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1626) ~[jetty-servlet-9.4.52.v20230823.jar:9.4.52.v20230823] at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:96) ~[spring-boot-actuator-2.6.14.jar:2.6.14] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.24.jar:5.3.24] at org.eclipse.jetty.servlet.FilterHolder.doFilter(FilterHolder.java:193) ~[jetty-servlet-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1626) ~[jetty-servlet-9.4.52.v20230823.jar:9.4.52.v20230823] at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.24.jar:5.3.24] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.24.jar:5.3.24] at org.eclipse.jetty.servlet.FilterHolder.doFilter(FilterHolder.java:193) ~[jetty-servlet-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1626) ~[jetty-servlet-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:552) ~[jetty-servlet-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:600) ~[jetty-security-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:235) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1624) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1440) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:505) ~[jetty-servlet-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1594) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1355) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.Server.handle(Server.java:516) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:487) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:732) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:479) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:277) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311) ~[jetty-io-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:105) ~[jetty-io-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.io.ChannelEndPoint$1.run(ChannelEndPoint.java:104) ~[jetty-io-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:883) ~[jetty-util-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1034) ~[jetty-util-9.4.52.v20230823.jar:9.4.52.v20230823] at java.lang.Thread.run(Thread.java:829) ~[?:?] Caused by: java.io.IOException: Missing content for multipart request at org.eclipse.jetty.util.MultiPartInputStreamParser.parse(MultiPartInputStreamParser.java:602) ~[jetty-util-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.util.MultiPartInputStreamParser.getParts(MultiPartInputStreamParser.java:491) ~[jetty-util-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.MultiParts$MultiPartsUtilParser.getParts(MultiParts.java:144) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.Request.getParts(Request.java:2450) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.eclipse.jetty.server.Request.getParts(Request.java:2420) ~[jetty-server-9.4.52.v20230823.jar:9.4.52.v20230823] at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:95) ~[spring-web-5.3.24.jar:5.3.24]

可以看到异常是从 jetty的 MultiPartInputStreamParser.java 这个类报出来的,根据名字可以大概知道 jetty内部也会解析我们的MultiPart文件,并且在解析时,没有读到这个文件的内容 所以报错了。接下来定位异常的具体代码:

首先我们进入到 MultiPartInputStreamParser.java这个类,由于是jar包肯定是编译后的,如果直接从异常点击进去后602行并不是报错行,这时想找到真正的602行的内容得去没编译前的代码中找,所以我们需要点击 Download sources来下载MultiPartInputStreamParser类的源代码,如下: 

image.png

 下载源码后,再从异常中点击,这回就真正进入到报错的位置了,看下602行确实是报错的内容(因为line是null所以报错): 

image.png

接下来就需要找到原因了,我们启动项目并debug看看(因为上边的line是null所以我就进入readLine方法看看为什么返回了null): 

image.png

 看下org.eclipse.jetty.util.ReadLineInputStream类的readLine方法,开始读取输入流 

image.png

org.eclipse.jetty.util.ReadLineInputStream类的supper 其实就是 jdk的BufferedInputStream,BufferedInputStream类的read方法的注释写的比较有参考意义,他说 如果读到流的最末尾 则返回-1。而我们正好返回-1 ,到这里我想应该能大致猜测出,应该是这个输入流在jetty读取之前,已经被读了,所以jetty再去读的话,此流被标识为已读到末尾 即 :EOF ,从而返回了-1。如下: 

image.png

 而在readLine方法中是如下逻辑,即 如果读取此流返回-1的话 则返回一个null,从而造成了line为null,从而抛出了我们上边看到的:Missing content for multipart request异常! 

image.png

 

image.png

那么到这里基本可以猜测是jetty读取之前已经被其他地方读取了,那么找到这个读取的代码,将是解决问题的关键,但是jetty之前有很多逻辑,我们该如何找到 这个上传的输入流 ,到底是在哪个地方被提前读取了呢?

这时候我们就应该想到 ,不管是哪个地方读取这个 multipart inputStream 最终一定是要掉 java.io.inputStream的 read方法的,因为所有输入流的读取最终都是要走这个基类的read方法,所以我们现在把所有断点都清掉,只在java.io.InputStream 类的 read方法打上一个断点,如下: 

image.png

 重新请求,可以看到当前的断点正好是拦截到了MultiPartStream inputstream 这个输入流,而从(上图左下角)调用链路来看,此时还没有走到jetty的MultiPartInputStreamParser类中,所以可以猜测,当前这次对输入流的读取就是在jetty读取输入流之前读取的那次,上边我们分析了也正是因为这个,所以造成了jetty读取时read返回-1从而报错。而从调用链路中可以观察到,(置灰的都是框架的代码,白色的是我们自己写的代码,可以看到在比较靠前的位置走了我们自定义的一个关于权限以及参数解析的的filter中的 doFilter和argsHandler方法),顺点击调用链中的这个argHandler,发现直接跳到了这行代码: 

image.png

 从上边可以看到如果Content-Type是multipart/form-data;则会调用org.springframework.web.multipart.commons.CommonsMultipartResolver的resolveMultipart方法来解析这个Multipart 而解析就会读取这个Multipart inputstream所以造成了 此流在jetty读取时返回-1(代表此流已经被读取完)从而跑了异常。所以到这里我们的问题就找到了。

3、找到原因,尝试解决

那么如何解决呢? 我们点进这个 CommonsMultipartResolver类的 resolveMultipart方法看看: 

image.png

 哦呦? 有了resolveLazily的参数引起了我的注意,翻译过来就是懒加载解析或者说延迟解析,这个我有点惊喜,心想那我配置成懒加载是不是就能不读取multipart 这个输入流了呢?所以我决定试一把!(其实有时候解决问题就是分析之后一点一点试出来的!真的。)

在我的webMvc中配置CommonsMultipartResolver这个bean并且设置为延迟解析: 

image.png

注入上边定义的bean并重启项目并请求: 

image.png

 进入resolveMultipart方法,可以看到延迟解析为true 

image.png

继续往下走,可以看到已经进入到controller并且multipartFile是有值的: 

image.png

 解析到数据: 

image.png

实际上,这个延迟加载看起来有两方面作用,一是让权限过滤器延迟解析了multiPart文件即在使用时解析,另一个作用就是作用于了jetty,因为开启延迟加载后,下边这段代码始终没有走(具体深层次原因暂时就不深究了 ,总之现在通过延迟解析 解决了我当前的燃眉之急): 

image.png

从上边可以看出来,这个延迟解析参数为true,真正的效果是:

使得jetty根本就不解析multipart文件了(事实证明jetty不解析此文件也没有什么影响至少现在没有发现问题),所以也不会读取时读到文件-1(EOF)报错了,所以也就解决了这个异常。更深次的情况和源码我暂时先不去关心,总之现在属于完美的解决了问题并且没有引发其他的问题。

4、结语

在搜索这个bug时我还看到jetty对大文件的支持好像要报错,但是目前为止我没遇到,到时候遇到了再去解决吧。

叨叨几句:

jetty是比tomcat轻量级,但是这也就意味着有些东西需要自己去实现或者集成,而tomcat更多的是大而全的一个servlet服务器有很多东西都是现成的拿来就用的,开发成本更低,更适用于企业级的web应用,而jetty更多的是轻量级的 ,适用于长连接场景的(当然你可以用netty写)服务器,最后一句话送给看到这里的同学:技术没有最牛逼最高大上的,只有最合适的!


最后:

如果有人遇到同样的异常,欢迎在评论区分享你的解决办法。

  • 26
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Spring Boot 2,使用Jetty服务器上传文件可以通过以下步骤实现: 1. 添加以下依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId> </dependency> ``` 2. 在application.properties文件添加以下配置: ``` server.port=8080 spring.servlet.multipart.enabled=true spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=10MB spring.servlet.multipart.file-size-threshold=2KB ``` 其,`spring.servlet.multipart.enabled=true`表示启用文件上传功能,`spring.servlet.multipart.max-file-size`和`spring.servlet.multipart.max-request-size`分别表示上传文件的最大大小和请求的最大大小,`spring.servlet.multipart.file-size-threshold`表示当上传文件大小超过此阈值,将使用磁盘存储文件。 3. 在控制器添加文件上传接口: ```java @RestController public class FileUploadController { @PostMapping("/upload") public String uploadFile(@RequestParam("file") MultipartFile file) throws IOException { // 处理上传文件 if (!file.isEmpty()) { byte[] bytes = file.getBytes(); String fileName = file.getOriginalFilename(); // 存储文件 // ... return "上传成功"; } else { return "文件为空"; } } } ``` 在以上代码,`@PostMapping("/upload")`表示接受POST请求,并且请求路径为`/upload`,`@RequestParam("file")`表示接收名为`file`的文件参数。 关于Tomcat文件上传问题,可以参考类似的步骤,在application.properties文件添加以下配置: ``` spring.servlet.multipart.enabled=true spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=10MB spring.servlet.multipart.file-size-threshold=2KB ``` 然后在控制器添加文件上传接口即可。需要注意的是,Tomcat默认不支持文件上传,需要手动添加相应的依赖,比如在Maven项目,可以添加以下依赖: ```xml <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> <version>${tomcat.version}</version> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-el</artifactId> <version>${tomcat.version}</version> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-websocket</artifactId> <version>${tomcat.version}</version> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <version>${tomcat.version}</version> </dependency> ``` 其,`${tomcat.version}`为Tomcat版本号。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值