杰瑞科技汇

Java Web文件上传下载如何实现?

我会从最基础的传统 Servlet 方式讲起,然后介绍目前主流的 Spring Boot 方式,并附上完整的代码示例。

Java Web文件上传下载如何实现?-图1
(图片来源网络,侵删)

核心概念与原理

文件上传原理

文件上传的本质是 HTTP 协议中的 multipart/form-data 请求

  • 普通表单 (application/x-www-form-urlencoded): 数据以 key=value 的形式拼接在 URL 后面,只能上传文本,不能上传文件。
  • 文件上传表单 (multipart/form-data): 它会将表单的数据拆分成多个部分(Part),每个部分可以是文本字段,也可以是文件内容,浏览器会将这些部分打包成一个 HTTP 请求发送给服务器。

服务器端需要专门的解析器来解析这种复杂的 multipart 请求,从中提取出文件和其他字段。

文件下载原理

文件下载的本质是 服务器将文件内容通过 HTTP 响应发送给浏览器

  • Content-Type (响应头): 告诉浏览器响应体的数据类型,对于下载,我们通常不使用浏览器能直接解析的类型(如 text/html),而是使用 application/octet-stream(二进制流),这样浏览器就不会尝试渲染它,而是会触发下载。
  • Content-Disposition (响应头): 这是关键,它告诉浏览器如何处理响应体,我们使用 attachment,并配合 filename="..." 来指定下载时保存的文件名。
    • Content-Disposition: attachment; filename="myFile.txt"

传统 Servlet 方式实现

这种方式是 Java Web 的基础,理解它有助于你了解底层原理。

Java Web文件上传下载如何实现?-图2
(图片来源网络,侵删)

准备工作:引入依赖

如果你使用 Maven,需要在 pom.xml 中添加 commons-fileuploadcommons-io 依赖。commons-io 提供了很多实用的 I/O 工具类。

<dependencies>
    <!-- Servlet API -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>4.0.1</version>
        <scope>provided</scope>
    </dependency>
    <!-- Apache Commons FileUpload -->
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.4</version>
    </dependency>
    <!-- Apache Commons IO -->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.11.0</version>
    </dependency>
</dependencies>

文件上传

a) 前端页面 (upload.html)

关键点:<form>enctype 必须设为 multipart/form-data<input>type 必须是 file

<!DOCTYPE html>
<html>
<head>文件上传</title>
</head>
<body>
    <h1>文件上传</h1>
    <form action="upload" method="post" enctype="multipart/form-data">
        选择文件: <input type="file" name="file" />
        <input type="submit" value="上传" />
    </form>
</body>
</html>

b) 后端 Servlet (UploadServlet.java)

Java Web文件上传下载如何实现?-图3
(图片来源网络,侵删)
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
    // 上传文件存储目录
    private static final String UPLOAD_DIRECTORY = "upload";
    // 配置上传参数
    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
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 检查是否为多媒体上传
        if (!ServletFileUpload.isMultipartContent(request)) {
            // 如果不是则停止
            PrintWriter writer = response.getWriter();
            writer.println("Error: 表单必须包含 enctype=multipart/form-data");
            writer.flush();
            return;
        }
        // 配置上传参数
        DiskFileItemFactory factory = new DiskFileItemFactory();
        factory.setSizeThreshold(MEMORY_THRESHOLD);
        factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
        ServletFileUpload upload = new ServletFileUpload(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 {
            // 解析请求的内容提取文件数据
            @SuppressWarnings("unchecked")
            List<FileItem> formItems = upload.parseRequest(request);
            if (formItems != null && formItems.size() > 0) {
                for (FileItem 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);
                        request.setAttribute("message", "文件 " + fileName + " 上传成功!");
                    }
                }
            }
        } catch (Exception ex) {
            request.setAttribute("message", "错误信息: " + ex.getMessage());
        }
        // 跳转到 message.jsp 页面
        getServletContext().getRequestDispatcher("/message.jsp").forward(request, response);
    }
}

c) 提示页面 (message.jsp)

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>上传结果</title>
</head>
<body>
    <h2>${message}</h2>
    <a href="upload.html">返回上传页面</a>
</body>
</html>

文件下载

a) 后端 Servlet (DownloadServlet.java)

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLEncoder;
@WebServlet("/download")
public class DownloadServlet extends HttpServlet {
    private static final String UPLOAD_DIRECTORY = "upload";
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 获取要下载的文件名
        String fileName = request.getParameter("fileName");
        if (fileName == null || fileName.isEmpty()) {
            request.setAttribute("message", "文件名不能为空!");
            request.getRequestDispatcher("/message.jsp").forward(request, response);
            return;
        }
        // 构建文件在服务器上的完整路径
        String appPath = getServletContext().getRealPath("/");
        String filePath = appPath + File.separator + UPLOAD_DIRECTORY + File.separator + fileName;
        File downloadFile = new File(filePath);
        if (!downloadFile.exists()) {
            request.setAttribute("message", "文件不存在: " + fileName);
            request.getRequestDispatcher("/message.jsp").forward(request, response);
            return;
        }
        // 设置响应头
        response.setContentType("application/octet-stream");
        // 处理中文文件名乱码问题
        String encodedFileName = URLEncoder.encode(fileName, "UTF-8");
        response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\"");
        // 将文件写入响应流
        try (FileInputStream inStream = new FileInputStream(downloadFile)) {
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = inStream.read(buffer)) != -1) {
                response.getOutputStream().write(buffer, 0, bytesRead);
            }
        }
    }
}

b) 下载链接 (message.jsp 或其他页面)

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>文件下载</title>
</head>
<body>
    <h2>文件列表</h2>
    <ul>
        <!-- 假设服务器上传目录下有 a.txt 和 b.jpg -->
        <li><a href="download?fileName=a.txt">下载 a.txt</a></li>
        <li><a href="download?fileName=b.jpg">下载 b.jpg</a></li>
    </ul>
</body>
</html>

Spring Boot 方式实现 (推荐)

Spring Boot 通过 MultipartFile 接口极大地简化了文件上传和下载的操作,我们不再需要手动解析 multipart 请求。

准备工作:引入依赖

Spring Boot Web starter 已经包含了处理文件上传所需的核心依赖。

<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Lombok (可选,简化代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

配置上传限制

application.propertiesapplication.yml 中配置文件上传的相关参数。

# 单个文件大小限制
spring.servlet.multipart.max-file-size=10MB
# 总请求大小限制
spring.servlet.multipart.max-request-size=10MB
# 临时文件存储位置 (可选)
# spring.servlet.multipart.location=/tmp

文件上传

a) 前端页面

与 Servlet 方式完全相同。

b) 后端 Controller (FileUploadController.java)

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Controller
public class FileUploadController {
    // 定义一个上传目录,放在项目根目录下的 upload 文件夹
    private static final String UPLOADED_FOLDER = System.getProperty("user.dir") + "/upload/";
    @GetMapping("/")
    public String index() {
        return "upload"; // 返回 upload.html 视图
    }
    @PostMapping("/upload")
    public String handleFileUpload(@RequestParam("file") MultipartFile file,
                                   RedirectAttributes redirectAttributes) {
        if (file.isEmpty()) {
            redirectAttributes.addFlashAttribute("message", "请选择一个文件上传");
            return "redirect:uploadStatus";
        }
        try {
            // 确保上传目录存在
            Path uploadPath = Paths.get(UPLOADED_FOLDER);
            if (!Files.exists(uploadPath)) {
                Files.createDirectories(uploadPath);
            }
            // 获取原始文件名
            String originalFilename = file.getOriginalFilename();
            // 构建目标文件路径
            Path destination = uploadPath.resolve(originalFilename);
            // 将文件保存到目标路径
            Files.copy(file.getInputStream(), destination);
            redirectAttributes.addFlashAttribute("message",
                    "You successfully uploaded '" + originalFilename + "' to '" + destination + "'");
        } catch (IOException e) {
            e.printStackTrace();
            redirectAttributes.addFlashAttribute("message", "上传失败: " + e.getMessage());
        }
        return "redirect:/uploadStatus";
    }
    @GetMapping("/uploadStatus")
    public String uploadStatus() {
        return "uploadStatus"; // 返回 uploadStatus.html 视图
    }
}

c) 提示页面 (uploadStatus.html)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>Upload Status</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .message { padding: 10px; background-color: #f0f0f0; border-radius: 5px; }
    </style>
</head>
<body>
    <h1>Upload Status</h1>
    <div th:if="${message}" class="message" th:text="${message}"></div>
    <p><a href="/">Go to upload page</a></p>
</body>
</html>

文件下载

a) 后端 Controller (FileDownloadController.java)

import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
@Controller
public class FileDownloadController {
    private static final String UPLOADED_FOLDER = System.getProperty("user.dir") + "/upload/";
    @GetMapping("/download/{fileName:.+}")
    public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
        try {
            // 构建文件路径
            Path filePath = Paths.get(UPLOADED_FOLDER).resolve(fileName).normalize();
            Resource resource = new UrlResource(filePath.toUri());
            // 检查文件是否存在且可读
            if (!resource.exists() || !resource.isReadable()) {
                return ResponseEntity.notFound().build();
            }
            // 设置响应头
            String contentType = "application/octet-stream";
            String headerValue = "attachment; filename=\"" + resource.getFilename() + "\"";
            return ResponseEntity.ok()
                    .contentType(MediaType.parseMediaType(contentType))
                    .header(HttpHeaders.CONTENT_DISPOSITION, headerValue)
                    .body(resource);
        } catch (Exception e) {
            return ResponseEntity.internalServerError().build();
        }
    }
}

b) 下载链接

可以在 uploadStatus.html 中添加一个文件列表,并提供下载链接。

<!-- 在 uploadStatus.html 中添加 -->
<h2>已上传文件</h2>
<ul>
    <!-- 这里需要后端提供文件列表,或者前端扫描 upload 目录 -->
    <!-- 为了演示,我们硬编码几个链接 -->
    <li><a th:href="@{/download/a.txt}">下载 a.txt</a></li>
    <li><a th:href="@{/download/b.jpg}">下载 b.jpg</a></li>
</ul>

最佳实践与注意事项

  1. 安全性

    • 文件名安全: 不要直接使用用户提供的文件名,它可能包含路径(如 ../../etc/passwd)或恶意字符,应该提取纯文件名,并进行过滤或重命名。
    • 文件类型校验: 不要仅凭文件后缀判断类型,可以通过检查文件内容("Magic Number")来验证,防止上传伪装成图片的可执行文件。
    • 病毒扫描: 对上传的文件进行病毒扫描。
    • 存储位置: 不要将上传的文件放在 Web 根目录下,防止被直接访问,应该放在一个非公开的目录,并通过程序提供下载。
  2. 大文件处理

    • 临时文件: 对于大文件,commons-fileupload 和 Spring Boot 都会使用临时文件存储,直到请求处理完毕,确保服务器有足够的磁盘空间。
    • 进度显示: 高级的上传功能需要显示上传进度,这通常需要前端(如 XMLHttpRequestfetch API)和后端(如 Servlet 3.1+ 的异步特性或 WebSocket)的配合。
  3. 用户体验

    • 异步上传: 使用 AJAX 技术实现无刷新上传,避免页面卡顿。
    • 错误处理: 提供清晰的错误提示,告诉用户上传失败的原因(如文件过大、类型不符等)。
  4. 性能

    • 存储策略: 对于生产环境,文件不应存储在应用服务器的本地磁盘上,应该使用专门的对象存储服务,如 Amazon S3, 阿里云 OSS, MinIO 等,这提高了可靠性、可扩展性,并降低了应用服务器的负载。

总结对比

特性 传统 Servlet 方式 Spring Boot 方式
依赖 需要手动添加 commons-fileupload 内置在 spring-boot-starter-web
上传 手动解析 multipart 请求,获取 FileItem 通过 MultipartFile 接口,操作非常简单
下载 手动设置响应头,读取文件流写入 response 通过 Resource 对象,结合 ResponseEntity 轻松实现
配置 在代码中硬编码或通过 web.xml 配置 application.properties 中简单配置
代码量 较多,代码冗长 极少,非常简洁
推荐度 适合学习原理,或在不使用框架的旧项目中 强烈推荐,是现代 Java Web 开发的标准

对于新的 Java Web 项目,毫无疑问应该选择 Spring Boot,它能让你更专注于业务逻辑,而不是繁琐的底层实现。

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