Apache Commons FileUpload 快速入门与集成示例

Apache Commons FileUpload 2.0.0-M5 快速入门:Maven 与离线 jar 依赖引入、核心 API、core 直接解析与服务端集成两个可独立运行的完整示例及实测回显。

Apache Commons FileUploadApache Commons FileUpload2026/10/14

Apache Commons FileUpload 快速入门与集成示例

目标:以最小代价跑通 Apache Commons FileUpload 2.0.0-M5 的完整链路——依赖引入、核心 API、无 Web 服务器的 multipart 解析示例与 Servlet 集成示例,全部代码可独立编译运行,已在 Ubuntu 22.04.5 服务器实测通过。

一、环境信息

本节列出文档实测的服务器基础信息。

  • 操作系统:Ubuntu 22.04.5 LTS(x86_64)
  • JDK:OpenJDK 17.0.20.1
  • 构建工具:Apache Maven 3.6.3
  • 组件版本:Apache Commons FileUpload 2.0.0-M5(core + jakarta-servlet5)
  • 依赖:commons-io 2.21.0、jakarta.servlet-api 5.0.0、Jetty 11.0.26
  • 实验目录:/opt/fu-lab(bin 包)、/opt/fu-lab/jetty-demo(Maven 工程)

二、前置条件

本节说明运行示例前需要准备的环境(完整安装步骤见《Ubuntu 22.04 安装 Apache Commons FileUpload 指南》篇)。

  • JDK 17 与 Maven 可用:java -version 输出 17.x
  • FileUpload 2.0.0-M5 bin 包已解压至 /opt/fu-lab,或 Maven 工程已建(二者其一即可)

验证:

java -version 2>&1 | head -1
ls /opt/fu-lab/commons-fileupload2-2.0.0-M5/commons-fileupload2-core-2.0.0-M5.jar

实测回显:

openjdk version "17.0.20.1" 2026-08-18
/opt/fu-lab/commons-fileupload2-2.0.0-M5/commons-fileupload2-core-2.0.0-M5.jar

三、依赖引入

本节说明两种引入依赖的方式(bin 包直引与 Maven 坐标),二选一即可。

3.1 方式一:bin 包直引 jar(离线)

将 core jar 与运行期依赖 commons-io 加入 classpath:

CP=/opt/fu-lab/commons-fileupload2-2.0.0-M5/commons-fileupload2-core-2.0.0-M5.jar:/opt/fu-lab/deps/commons-io-2.21.0.jar
echo $CP

实测回显:

/opt/fu-lab/commons-fileupload2-2.0.0-M5/commons-fileupload2-core-2.0.0-M5.jar:/opt/fu-lab/deps/commons-io-2.21.0.jar

3.2 方式二:Maven 坐标(在线)

在 pom.xml 中声明(Jakarta Servlet 5.0 场景,自动带入 core 与 commons-io):

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-fileupload2-jakarta-servlet5</artifactId>
  <version>2.0.0-M5</version>
</dependency>

⚠️ 注意:Servlet 版本不同选不同坐标——Servlet 5.0 用 commons-fileupload2-jakarta-servlet5,Servlet 6.0 用 -servlet6,旧 javax.servlet 用 commons-fileupload2-javax;勿用已停更的 commons-fileupload2-jakarta(仅到 M1)。

💡 技术拓展:FileUpload 1.x 与 2.x 的 API 差异是升级必读——

差异点1.x2.x
工厂创建new DiskFileItemFactory()DiskFileItemFactory.builder().get()
Servlet 上传类ServletFileUploadJakartaServletFileUpload / JakartaServletDiskFileUpload
FileItem 文件名getFileName()getName()
请求上限setSizeMax(long)setMaxSize(long)
单文件上限setFileSizeMax(long)setMaxFileSize(long)
头字符集setHeaderEncoding(String)setHeaderCharset(Charset)

本文示例全部按 2.x 的 builder + setName 风格编写。

四、核心 API 速览

本节快速梳理 2.x 的核心类与用法要点,随后两节给可运行示例。

4.1 核心类

类职责
DiskFileItemFactoryFileItem 工厂实现(阈值/临时目录/缓冲/字符集)
FileUpload(AbstractFileUpload 子类)multipart 请求解析入口(parseRequest/parseParameterMap)
DiskFileItem解析产出的单个 part(字段或文件)
RequestContext请求抽象(ContentType/ContentLength/InputStream)
FileUploadException 系列解析异常(含 FileUploadSizeException 等)

4.2 典型调用链

  1. DiskFileItemFactory.builder() 链式配置(setThreshold/setRepository 等)后调用 get() 构建工厂;
  2. 创建 AbstractFileUpload 子类并 setFileItemFactory(factory)(Servlet 场景直接用 JakartaServletDiskFileUpload);
  3. upload.parseRequest(requestContext) 解析出 List<DiskFileItem>;
  4. 对每个 item:isFormField() 区分字段与文件,getFieldName() 取名,getString(Charset) 取内容。

五、示例一:无 Web 服务器解析 multipart(core 直用)

本节给出不依赖任何 Web 服务器的完整示例:手工构造 multipart 请求体,用 core 直接解析并回显。适合在任意 Java 进程(批处理、网关)中复用解析能力。

5.1 完整代码

// FileUpload2CoreDemo.java(置于 /opt/fu-lab/src/)
import org.apache.commons.fileupload2.core.AbstractFileUpload;
import org.apache.commons.fileupload2.core.DiskFileItem;
import org.apache.commons.fileupload2.core.DiskFileItemFactory;
import org.apache.commons.fileupload2.core.FileItemInputIterator;
import org.apache.commons.fileupload2.core.RequestContext;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;

public class FileUpload2CoreDemo {
    public static void main(String[] args) throws Exception {
        // 1. 构造一个 multipart/form-data 请求体(一个普通字段 + 一个文件字段)
        String boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW";
        String body = "--" + boundary + "\r\n"
            + "Content-Disposition: form-data; name=\"author\"\r\n\r\n"
            + "Apache Commons FileUpload 2.0.0-M5\r\n"
            + "--" + boundary + "\r\n"
            + "Content-Disposition: form-data; name=\"report\"; filename=\"demo.txt\"\r\n"
            + "Content-Type: text/plain\r\n\r\n"
            + "hello from commons-fileupload2-core\r\n"
            + "--" + boundary + "--\r\n";
        byte[] bytes = body.getBytes(StandardCharsets.UTF_8);

        // 2. 实现 RequestContext 抽象请求
        RequestContext ctx = new RequestContext() {
            public String getCharacterEncoding() { return "UTF-8"; }
            public long getContentLength() { return bytes.length; }
            public String getContentType() { return "multipart/form-data; boundary=" + boundary; }
            public InputStream getInputStream() { return new ByteArrayInputStream(bytes); }
            public boolean isMultipartRelated() { return false; }
        };

        // 3. builder 构建工厂 + 解析
        DiskFileItemFactory factory = DiskFileItemFactory.builder()
                .setThreshold(10240)
                .setCharset(StandardCharsets.UTF_8)
                .get();
        AbstractFileUpload<Object, DiskFileItem, DiskFileItemFactory> upload = new AbstractFileUpload<>() {
            public FileItemInputIterator getItemIterator(Object request) { throw new UnsupportedOperationException(); }
            public Map<String, List<DiskFileItem>> parseParameterMap(Object request) { throw new UnsupportedOperationException(); }
            public List<DiskFileItem> parseRequest(Object request) { throw new UnsupportedOperationException(); }
        };
        upload.setFileItemFactory(factory);
        upload.setMaxSize(1024L * 1024 * 10);   // 整请求 10 MB

        // 4. 回显解析结果
        List<DiskFileItem> items = upload.parseRequest(ctx);
        System.out.println("Parsed FileItem count: " + items.size());
        for (DiskFileItem item : items) {
            System.out.println("  fieldName=" + item.getFieldName()
                + " | isFormField=" + item.isFormField()
                + " | fileName=" + item.getName()
                + " | contentType=" + item.getContentType()
                + " | value=" + item.getString(StandardCharsets.UTF_8));
        }
    }
}

代码要点:RequestContext 抽象了任意来源的请求(此处为内存字节流);parseRequest(RequestContext) 是解析核心;DiskFileItem 的 getName() 在文件字段返回文件名、普通字段返回 null。

5.2 编译与运行

mkdir -p /opt/fu-lab/out
javac -encoding UTF-8 -cp "/opt/fu-lab/commons-fileupload2-2.0.0-M5/commons-fileupload2-core-2.0.0-M5.jar:/opt/fu-lab/deps/commons-io-2.21.0.jar" -d /opt/fu-lab/out /opt/fu-lab/src/FileUpload2CoreDemo.java
java -Xms256m -Xmx256m -cp "/opt/fu-lab/out:/opt/fu-lab/commons-fileupload2-2.0.0-M5/commons-fileupload2-core-2.0.0-M5.jar:/opt/fu-lab/deps/commons-io-2.21.0.jar" FileUpload2CoreDemo

实测回显:

Parsed FileItem count: 2
  fieldName=author | isFormField=true | fileName=null | contentType=null | value=Apache Commons FileUpload 2.0.0-M5
  fieldName=report | isFormField=false | fileName=demo.txt | contentType=text/plain | value=hello from commons-fileupload2-core

解析结果:author 为普通字段(isFormField=true),report 为文件(isFormField=false,文件名 demo.txt,内容 hello from commons-fileupload2-core),示例运行成功。

六、示例二:Servlet 集成(embedded Jetty + curl -F)

本节给出标准 Web 场景的集成示例:嵌入式 Jetty 起最小 Servlet,接收真实 HTTP multipart 请求并回显,用 curl -F 验证。

6.1 Maven 工程与 pom.xml

mkdir -p /opt/fu-lab/jetty-demo/src/main/java/upload && cd /opt/fu-lab/jetty-demo

pom.xml(坐标说明见三、3.2):

<dependencies>
  <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-fileupload2-jakarta-servlet5</artifactId>
    <version>2.0.0-M5</version>
  </dependency>
  <dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-server</artifactId>
    <version>11.0.26</version>
  </dependency>
  <dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-servlet</artifactId>
    <version>11.0.26</version>
  </dependency>
  <dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <version>5.0.0</version>
    <scope>provided</scope>
  </dependency>
</dependencies>

6.2 上传 Servlet

// /opt/fu-lab/jetty-demo/src/main/java/upload/UploadServlet.java
package upload;

import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload2.core.DiskFileItem;
import org.apache.commons.fileupload2.core.DiskFileItemFactory;
import org.apache.commons.fileupload2.jakarta.servlet5.JakartaServletDiskFileUpload;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.List;

@WebServlet("/upload")
public class UploadServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/plain; charset=UTF-8");
        PrintWriter out = response.getWriter();
        try {
            DiskFileItemFactory factory = DiskFileItemFactory.builder()
                    .setThreshold(64 * 1024)
                    .setCharset(StandardCharsets.UTF_8)
                    .get();
            JakartaServletDiskFileUpload upload = new JakartaServletDiskFileUpload(factory);
            upload.setMaxSize(1024L * 1024 * 10);      // 整请求 10 MB
            upload.setMaxFileSize(1024L * 1024 * 5);   // 单文件 5 MB
            upload.setHeaderCharset(StandardCharsets.UTF_8);

            List<DiskFileItem> items = upload.parseRequest(request);
            out.println("Parsed FileItem count: " + items.size());
            for (DiskFileItem item : items) {
                out.println("  fieldName=" + item.getFieldName()
                    + " | isFormField=" + item.isFormField()
                    + " | fileName=" + item.getName()
                    + " | contentType=" + item.getContentType()
                    + " | size=" + item.getSize()
                    + " | value=" + item.getString(StandardCharsets.UTF_8));
            }
        } catch (Exception e) {
            out.println("ERROR: " + e.getClass().getName() + ": " + e.getMessage());
        }
    }
}

要点:JakartaServletDiskFileUpload 是 2.x Jakarta 集成层的开箱即用类(内部已装配 DiskFileItemFactory 泛型);parseRequest(HttpServletRequest) 直接吃 Servlet 请求;异常统一捕获回显便于排障。

6.3 嵌入式 Jetty 启动类

// /opt/fu-lab/jetty-demo/src/main/java/upload/UploadServer.java
package upload;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;

public class UploadServer {
    public static void main(String[] args) throws Exception {
        Server server = new Server(18080);
        ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
        context.setContextPath("/");
        context.addServlet(new ServletHolder(new UploadServlet()), "/upload");
        server.setHandler(context);
        server.start();
        System.out.println("Jetty started on http://0.0.0.0:18080/upload");
        server.join();
    }
}

6.4 构建、启动与 curl -F 实测

cd /opt/fu-lab/jetty-demo
mvn -q -DskipTests package
mvn -q dependency:build-classpath -Dmdep.outputFile=cp.txt
printf "hello from jetty curl -F upload test\nline2: multipart parsing ok\n" > /opt/fu-lab/REPORT.txt
nohup java -Xms256m -Xmx256m -cp "target/classes:$(cat cp.txt)" upload.UploadServer > /opt/fu-lab/jetty.log 2>&1 &
sleep 6
tail -2 /opt/fu-lab/jetty.log
curl -s http://127.0.0.1:18080/upload -F "author=Apache Commons FileUpload 2.0.0-M5" -F "report=@/opt/fu-lab/REPORT.txt;type=text/plain"

实测回显:

Jetty started on http://0.0.0.0:18080/upload
Parsed FileItem count: 2
  fieldName=author | isFormField=true | fileName=null | contentType=null | size=34 | value=Apache Commons FileUpload 2.0.0-M5
  fieldName=report | isFormField=false | fileName=REPORT.txt | contentType=text/plain | size=65 | value=hello from jetty curl -F upload test
line2: multipart parsing ok

Servlet 完整解析 curl -F 发送的真实 multipart 请求:普通字段 author 的值(34 字节)、文件字段 REPORT.txt 的文件名/类型/字节数(65)与内容全部正确回显,集成示例验证通过。

💡 技术拓展:文件上传方案不止 Commons FileUpload——

方案特点适用场景
原生 Servlet Partrequest.getPart(),Servlet 引擎内置,零依赖简单单文件上传
Commons FileUpload精细控制大小/存储/进度/文件数复杂业务上传
Spring Multipart封装成 MultipartFile,注解式开发Spring Boot 应用
Netty 解析器非 Servlet 栈、事件驱动自研协议/网关

本文采用 Commons FileUpload 2.x,因其与运行环境无关、限制项最全、支持离线 jar 直引。

七、扩展:保存上传文件到磁盘

本节说明文件字段落盘的标准写法(承接 6.2 的 items 循环)。

对非表单字段,用 getInputStream() 拷贝到目标目录(此处用 java.nio 实现,避免占用大堆):

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

// 在 items 循环内:文件字段保存
if (!item.isFormField() && item.getName() != null) {
    Path target = Path.of("/data/uploads", item.getName());
    Files.copy(item.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);
    out.println("  saved to " + target);
}

说明:/data/uploads 需先创建;文件名建议再做白名单/路径穿越校验(如过滤 ../ 与反斜杠),避免覆盖任意路径。

八、常见问题

Q1:DiskFileItemFactory.builder() 与 1.x 的 new DiskFileItemFactory() 有何不同?

定位:2.x 全面改为构建器模式,返回 Builder 对象,get() 产出工厂实例;1.x 直接构造。 解决:2.x 一律 DiskFileItemFactory.builder() 链式配置各 set 方法后调用 get()。

Q2:item.getFileName() 编译报错?

定位:2.x FileItem 接口只有 getName(),1.x 的 getFileName() 已移除。 解决:改用 getName()(文件字段返回文件名,普通字段返回 null)。

Q3:Servlet 5 与 Servlet 6 运行时怎么选坐标?

定位:坐标与 Web 服务器的 Servlet 版本强相关:Tomcat 10.0/Jetty 11 为 Servlet 5 → -servlet5;Tomcat 10.1/Jetty 12 为 Servlet 6 → -servlet6;Tomcat 9 及以下 → -javax。 解决:按 Web 服务器版本引入对应坐标,混用会抛 NoClassDefFoundError。

Q4:上传中文内容乱码?

定位:字符集链路任一环不一致(表单编码、工厂 charset、getString 实参)。 解决:三处统一 UTF-8(见 6.2 示例的 setCharset + setHeaderCharset + getString(StandardCharsets.UTF_8))。

Q5:解析异常如何分类处理?

定位:2.x 异常族为 FileUploadException 子类:FileUploadSizeException(大小超限,含 getPermitted/getActualSize)、FileUploadFileCountLimitException(个数超限)、FileUploadByteCountLimitException、FileUploadContentTypeException 等。 解决:捕获后按类型返回 413/400 等 HTTP 状态,勿笼统 500。

九、参考链接

  1. Apache Commons FileUpload 官网:https://commons.apache.org/proper/commons-fileupload/
  2. 官方 bin 发布目录:https://downloads.apache.org/commons/fileupload/binaries/
  3. Maven Central 构件目录:https://repo1.maven.org/maven2/org/apache/commons/commons-fileupload2-core/
  4. Jetty 官方文档:https://jetty.org/docs/
  5. Jakarta Servlet 规范:https://jakarta.ee/specifications/servlet/