Embedded Jetty 11 文件上传配置与上传进度监听实现问题
临时目录无缓存文件问题解决方案
你遇到的临时目录无文件的问题,通常由三个原因导致,对应解决方案如下:
- 配置时机与作用域错误:你在业务方法中动态给request设置multipart属性的方式生效优先级低,且Jetty的multipart解析逻辑可能在进入JAX-RS方法前就已经触发,正确的配置方式是在Embedded Jetty注册Servlet的阶段全局配置,无需使用
@MultipartConfig注解,示例代码如下:
import jakarta.servlet.MultipartConfigElement; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.glassfish.jersey.servlet.ServletContainer; // 初始化Servlet上下文时配置 ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); context.setContextPath("/"); ServletHolder jerseyServlet = context.addServlet(ServletContainer.class, "/*"); jerseyServlet.setInitParameter("jersey.config.server.provider.packages", "你的业务代码包路径"); // 完整配置Multipart参数,四个入参分别为:临时目录、单文件最大字节、请求总最大字节、内存阈值(超过该大小的文件才会写入临时磁盘) jerseyServlet.getRegistration().setMultipartConfig( new MultipartConfigElement( "/storage/tmp", 100 * 1024 * 1024, // 单文件最大100MB,按需调整 200 * 1024 * 1024, // 请求总大小最大200MB,按需调整 1 * 1024 // 超过1KB就写入临时磁盘,设为0可强制所有文件都落盘 ) );
- 阈值设置问题:默认的Multipart配置内存阈值较高,小于阈值的文件会直接存在内存中不会落盘,将上述配置的第四个参数设为较小的值即可看到临时文件。
- Jetty自动清理逻辑:Jetty默认会在请求处理完成后自动删除multipart临时文件,调试时需要在
request.getParts()执行后、请求返回前打断点,此时查看临时目录才能看到文件,请求结束后文件会被自动清理。
同时需要提前确认/storage/tmp目录存在,且Jetty进程对该目录有读写权限。
服务端上传进度监听实现方案
request.getParts()阻塞是因为Jetty默认的multipart解析逻辑会等所有请求数据接收完成后才返回parts列表,要实现进度监听需要在解析前包装请求输入流统计字节读取量,步骤如下:
- 自定义带进度统计的输入流包装类
import jakarta.servlet.ServletInputStream; import jakarta.servlet.ReadListener; import java.io.IOException; public class ProgressInputStream extends ServletInputStream { private final ServletInputStream originStream; private final long totalLength; private long readBytes = 0; private final ProgressListener listener; public ProgressInputStream(ServletInputStream originStream, long totalLength, ProgressListener listener) { this.originStream = originStream; this.totalLength = totalLength; this.listener = listener; } @Override public int read() throws IOException { int b = originStream.read(); if (b != -1) { readBytes++; listener.update(readBytes, totalLength); } return b; } @Override public int read(byte[] b, int off, int len) throws IOException { int cnt = originStream.read(b, off, len); if (cnt != -1) { readBytes += cnt; listener.update(readBytes, totalLength); } return cnt; } // 剩余方法直接委托给原始流实现即可 @Override public boolean isFinished() { return originStream.isFinished(); } @Override public boolean isReady() { return originStream.isReady(); } @Override public void setReadListener(ReadListener readListener) { originStream.setReadListener(readListener); } // 进度回调接口 public interface ProgressListener { void update(long readBytes, long totalBytes); } }
- 实现Filter拦截上传请求,包装输入流
import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequestWrapper; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.EnumSet; public class UploadProgressFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpReq = (HttpServletRequest) request; // 只拦截文件上传请求 if ("POST".equals(httpReq.getMethod()) && httpReq.getContentType() != null && httpReq.getContentType().startsWith("multipart/")) { long contentLength = httpReq.getContentLengthLong(); // 自定义进度回调逻辑,可根据业务存储到Session、推送前端等 ProgressInputStream.ProgressListener listener = (read, total) -> { System.out.printf("上传进度:%.2f%%\n", (read * 100.0) / total); }; ProgressInputStream wrappedStream = new ProgressInputStream(httpReq.getInputStream(), contentLength, listener); // 包装原始请求替换输入流 HttpServletRequest wrappedRequest = new HttpServletRequestWrapper(httpReq) { @Override public ServletInputStream getInputStream() { return wrappedStream; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(wrappedStream, getCharacterEncoding())); } }; chain.doFilter(wrappedRequest, response); return; } chain.doFilter(request, response); } }
- 在Embedded Jetty中注册Filter,优先级高于业务Servlet
context.addFilter(UploadProgressFilter.class, "/files/upload", EnumSet.of(DispatcherType.REQUEST));
客户端实现上传进度的合理性说明
该方案是合理的,普通业务场景优先推荐客户端实现:
- 服务端实现需要额外的流包装、进度统计开销,大流量场景下会显著提升服务端负载
- 客户端通过
XMLHttpRequest.upload或fetch的进度事件可以直接拿到上传进度,精度和服务端统计完全一致,不需要额外和服务端做交互,延迟更低 - 不需要额外开发服务端进度推送(轮询、WebSocket)逻辑,开发成本更低
仅在有防上传作弊、服务端校验字节完整性等特殊需求时,才需要服务端实现进度统计。
内容的提问来源于stack exchange,提问作者Alessandro Valentino
相关产品推荐
相关产品推荐

