杰瑞科技汇

Java Servlet上传文件如何实现?

文件上传的本质是客户端(通常是浏览器)通过 HTTP POST 请求,将文件数据作为请求体的一部分发送给服务器,Servlet 提供了 HttpServletRequest 来处理请求,但直接解析原始的请求流来获取文件数据非常繁琐且容易出错。

我们通常会使用成熟的第三方库来简化这个过程,最常用和推荐的库是 Apache Commons FileUpload

下面我将分为几个部分来详细说明:

  1. 核心原理
  2. 准备工作:添加依赖
  3. 前端 HTML 表单
  4. 后端 Servlet 实现
  5. 高级配置与最佳实践
  6. 常见问题与解决方案

核心原理

  • multipart/form-data 编码:标准的表单提交使用 application/x-www-form-urlencoded 编码,这种编码方式不适合传输二进制文件,文件上传必须使用 multipart/form-data 编码,它会将表单中的每个字段(包括文本和文件)分割成独立的部分(part),并用一个特殊的边界字符串隔开。
  • Servlet API 的限制:标准的 HttpServletRequest API 在 getInputStream()getReader() 时,如果请求体是 multipart/form-data 格式,它会尝试一次性读取整个请求体到内存中,这对于大文件来说是灾难性的,并且解析逻辑非常复杂。
  • 第三方库的作用:像 Apache Commons FileUpload 这样的库,专门用于解析 multipart/form-data 请求,它能够:
    • 高效地读取请求流,避免内存溢出。
    • 将请求体分解成一个个 FileItem 对象,每个对象代表一个表单字段(文本或文件)。
    • 提供简单易用的 API 来处理这些 FileItem

准备工作:添加依赖

你需要在你的项目中添加 commons-fileupload 和它的依赖 commons-io

如果你使用 Maven,在 pom.xml 中添加以下依赖:

<dependencies>
    <!-- Servlet API -->
    <dependency>
        <groupId>jakarta.servlet</groupId>
        <artifactId>jakarta.servlet-api</artifactId>
        <version>6.0.0</version>
        <scope>provided</scope>
    </dependency>
    <!-- Apache Commons FileUpload -->
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload2-jakarta</artifactId>
        <version>2.0.0-M1</version>
    </dependency>
    <!-- Apache Commons IO (FileUpload 的依赖) -->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.13.0</version>
    </dependency>
</dependencies>

注意:这里使用的是 commons-fileupload2-jakarta 版本,它兼容 Jakarta EE 9+(Tomcat 10+),如果你使用的是旧版的 Java EE(Tomcat 9 及以下),请使用 commons-fileupload 的 1.x 版本。


前端 HTML 表单

前端表单是文件上传的入口,有几个关键点:

  • method="post":必须使用 POST 方法。
  • enctype="multipart/form-data":这是最关键的一步,告诉浏览器要以多部分形式编码数据。
  • <input type="file">:创建文件选择控件。

示例 upload.html:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">文件上传示例</title>
</head>
<body>
    <h1>上传文件</h1>
    <form action="upload" method="post" enctype="multipart/form-data">
        <!-- name 属性用于后端识别这个字段 -->
        <label for="file">选择文件:</label>
        <input type="file" id="file" name="myFile" required>
        <br><br>
        <label for="description">文件描述:</label>
        <input type="text" id="description" name="description">
        <br><br>
        <button type="submit">上传</button>
    </form>
</body>
</html>
  • name="myFile":这个 name 值在后端 Servlet 中用来识别文件字段。
  • name="description":这是一个普通的文本字段,也会被上传。

后端 Servlet 实现

这是整个流程的核心,我们将创建一个 UploadServlet 来处理上传请求。

UploadServlet.java:

import org.apache.commons.fileupload2.core.DiskFileItem;
import org.apache.commons.fileupload2.core.DiskFileItemFactory;
import org.apache.commons.fileupload2.jakarta.JakartaServletFileUpload;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
@WebServlet("/upload") // 将 Servlet 映射到 /upload 路径
public class UploadServlet extends HttpServlet {
    // 上传文件存储目录
    private static final String UPLOAD_DIRECTORY = "uploads";
    // 配置上传参数
    private static final int MEMORY_THRESHOLD = 1024 * 1024 * 3;  // 3MB
    private static final int MAX_FILE_SIZE = 1024 * 1024 * 40;    // 40MB
    private static final int MAX_REQUEST_SIZE = 1024 * 1024 * 50; // 50MB
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        // 检查是否为 multipart 内容
        if (!JakartaServletFileUpload.isMultipartContent(request)) {
            // 如果不是,则停止
            PrintWriter writer = response.getWriter();
            writer.println("错误: 表单必须包含 enctype=multipart/form-data");
            writer.flush();
            return;
        }
        // 配置上传参数
        DiskFileItemFactory factory = new DiskFileItemFactory();
        // 设置内存临界值 - 超过后将产生临时文件存储于临时目录
        factory.setSizeThreshold(MEMORY_THRESHOLD);
        // 设置临时存储目录
        factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
        JakartaServletFileUpload upload = new JakartaServletFileUpload(factory);
        // 设置最大文件上传值
        upload.setFileSizeMax(MAX_FILE_SIZE);
        // 设置最大请求值 (包含文件和表单数据)
        upload.setSizeMax(MAX_REQUEST_SIZE);
        // 构建临时上传目录
        String uploadPath = getServletContext().getRealPath("") + File.separator + UPLOAD_DIRECTORY;
        File uploadDir = new File(uploadPath);
        if (!uploadDir.exists()) {
            uploadDir.mkdir();
        }
        try {
            // 解析请求内容, 提取文件数据
            List<DiskFileItem> formItems = upload.parseRequest(request);
            if (formItems != null && formItems.size() > 0) {
                // 迭代表单数据
                for (DiskFileItem item : formItems) {
                    // 处理不在表单中的字段
                    if (!item.isFormField()) {
                        String fileName = new File(item.getName()).getName();
                        String filePath = uploadPath + File.separator + fileName;
                        File storeFile = new File(filePath);
                        // 在控制台输出上传文件的信息
                        System.out.println("文件上传路径: " + filePath);
                        // 保存文件到硬盘
                        item.write(storeFile);
                    } else {
                        // 处理普通表单字段
                        String fieldName = item.getFieldName();
                        String fieldValue = item.getString("UTF-8"); // 指定编码,防止中文乱码
                        System.out.println("字段名: " + fieldName + ", 值: " + fieldValue);
                    }
                }
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        // 上传成功后,重定向到成功页面
        request.setAttribute("message", "文件上传成功!");
        getServletContext().getRequestDispatcher("/message.jsp").forward(request, response);
    }
}

代码解析:

  1. 检查 Content-TypeJakartaServletFileUpload.isMultipartContent(request) 用于确保请求确实是 multipart/form-data 类型。
  2. DiskFileItemFactory:这是一个工厂类,负责配置如何处理上传的文件。
    • setSizeThreshold:设置内存阈值,如果文件大小小于这个值,它会直接保存在内存中,如果大于,则会写入到临时目录(由 setRepository 指定)。
    • setRepository:设置临时文件存储的目录,通常是系统默认的临时目录。
  3. JakartaServletFileUpload:这是核心的解析器。
    • setFileSizeMax:限制单个文件的最大大小。
    • setSizeMax:限制整个请求(包括所有文件和字段)的最大大小。
  4. upload.parseRequest(request):这是最关键的一步,它解析 HttpServletRequest 并返回一个 List<FileItem> 列表,每个 FileItem 对应表单中的一个字段。
  5. 遍历 FileItem
    • item.isFormField():判断这个 FileItem 是不是一个普通的表单字段(文本)。
    • 如果是文件
      • item.getName():获取原始文件名。
      • item.write(storeFile):将文件内容写入到指定的 File 对象中,这是保存文件最简单的方式。
    • 如果是普通字段
      • item.getFieldName():获取字段名(如 description)。
      • item.getString("UTF-8"):获取字段值,并指定编码,防止中文乱码。
  6. 创建上传目录:在 getServletContext().getRealPath("") 获取的 Web 应用根目录下创建一个 uploads 文件夹来存放上传的文件。
  7. 响应:上传成功后,通常使用 forwardredirect 跳转到另一个页面,而不是直接在 Servlet 中输出 HTML。

高级配置与最佳实践

1 处理中文乱码

  • 文件名乱码:在某些操作系统(如 Windows)和浏览器中,文件名可能包含中文。item.getName() 返回的文件名可能是乱码,可以使用以下方法处理:
    // 处理文件名乱码
    String fileName = new File(new String(item.getName().getBytes("ISO-8859-1"), "UTF-8")).getName();
  • 普通字段乱码:如代码所示,使用 item.getString("UTF-8") 即可。

2 使用 Servlet 3.0+ 的内置 API

从 Servlet 3.0 开始,规范引入了内置的文件上传 API,不再需要第三方库,使用方式如下:

// 在 Servlet 的 doPost 方法中
Part filePart = request.getPart("myFile"); // "myFile" 是 input 的 name 属性
String fileName = Paths.get(filePart.getSubmittedFileName()).getFileName().toString(); // 安全地获取文件名
InputStream fileContent = filePart.getInputStream();
// 保存文件
String uploadPath = getServletContext().getRealPath("") + File.separator + "uploads";
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) uploadDir.mkdir();
File file = new File(uploadPath + File.separator + fileName);
try (InputStream input = fileContent;
     OutputStream output = new FileOutputStream(file)) {
    input.transferTo(output);
}

优缺点

  • 优点:无需第三方库,API 更现代化。
  • 缺点
    • 不同 Servlet 容器(Tomcat, Jetty 等)的实现细节可能略有不同。
    • 功能相对简单,不如 Commons FileUpload 灵活(内存和临时文件的控制不如前者精细)。

对于新项目,如果不需要非常精细的控制,可以使用内置 API,但 Commons FileUpload 依然是业界标准,功能更强大、更稳定。

3 安全性考虑

  1. 文件名安全:绝对不要直接使用用户提供的文件名,恶意用户可能上传如 ../../../etc/passwd 这样的文件名,试图进行路径穿越攻击,应使用上面提到的 Paths.get(...).getFileName() 方法或类似逻辑来提取一个安全的文件名。
  2. 文件类型验证:不要仅依赖文件扩展名,文件扩展名可以被轻易伪造,应该检查文件的“魔法数字”(Magic Number),即文件头部的几个字节,来判断真实的文件类型。
  3. 病毒扫描:对于生产环境,上传的文件应经过杀毒软件的扫描。
  4. 存储位置:不要将上传的文件放在 Web 服务器的根目录下,否则可能被直接访问,应该放在 Web 根目录之外,或者通过一个专门的 Servlet 来提供文件下载服务,并在此过程中进行权限验证。

常见问题与解决方案

  • The temporary upload location is not valid

    • 原因DiskFileItemFactory 使用的临时目录无效或没有写入权限。
    • 解决:确保应用程序有权限在系统临时目录(如 Windows 的 C:\Users\XXX\AppData\Local\Temp)中创建和写入文件,可以在代码中打印出 System.getProperty("java.io.tmpdir") 来确认路径。
  • Uploaded file size exceeds 错误

    • 原因:上传的文件大小超过了在 JakartaServletFileUpload 中设置的 MAX_FILE_SIZEMAX_REQUEST_SIZE
    • 解决:调整这些阈值,或者在前端使用 JavaScript 库(如 jQuery File Upload)进行预检查。
  • FileNotFoundExceptionPermission Denied

    • 原因:应用程序没有权限在指定的上传目录(uploads)中创建文件。
    • 解决:确保 uploads 目录存在,并且运行 Tomcat 的用户(如 tomcat)对该目录有读写权限。

希望这份详细的指南能帮助你成功实现 Java Servlet 的文件上传功能!

分享:
扫描分享到社交APP
上一篇
下一篇