掘金 后端 ( ) • 2024-05-15 19:46

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上都搜到了,看来有很多人遇到了,这里给出链接:

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

异常详情如下:

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]

可以看到异常是从 jettyMultiPartInputStreamParser.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.inputStreamread方法的,因为所有输入流的读取最终都是要走这个基类的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写)服务器,最后一句话送给看到这里的同学:技术没有最牛逼最高大上的,只有最合适的!


最后:

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