You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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列表,要实现进度监听需要在解析前包装请求输入流统计字节读取量,步骤如下:

  1. 自定义带进度统计的输入流包装类
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);
    }
}
  1. 实现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);
    }
}
  1. 在Embedded Jetty中注册Filter,优先级高于业务Servlet
context.addFilter(UploadProgressFilter.class, "/files/upload", EnumSet.of(DispatcherType.REQUEST));
客户端实现上传进度的合理性说明

该方案是合理的,普通业务场景优先推荐客户端实现:

  • 服务端实现需要额外的流包装、进度统计开销,大流量场景下会显著提升服务端负载
  • 客户端通过XMLHttpRequest.upload或fetch的进度事件可以直接拿到上传进度,精度和服务端统计完全一致,不需要额外和服务端做交互,延迟更低
  • 不需要额外开发服务端进度推送(轮询、WebSocket)逻辑,开发成本更低
    仅在有防上传作弊、服务端校验字节完整性等特殊需求时,才需要服务端实现进度统计。

内容的提问来源于stack exchange,提问作者Alessandro Valentino

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 21:36:03