首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java Web 文件上傳下載完整指南:基於 Commons FileUpload 與流式傳輸

     編輯:Java 相關知識

前言

文件上傳下載是 Web 應用的基礎功能。本文以 Apache Commons FileUpload 為核心,構建一個支援多文件、大文件、進度監控、安全限制的上傳組件,並實現斷點續傳友好、內存高效的下載方案。


一、依賴準備

<!-- Maven -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version> <!-- 或更新版 -->
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>

二、上傳核心 Servlet 實現

1. 配置參數類

public class UploadConfig {
    private String repositoryPath = System.getProperty("java.io.tmpdir"); // 暫存目錄
    private String uploadPath = "/data/uploads";                            // 最終存儲目錄
    private long maxFileSize = 100 * 1024 * 1024;                           // 100MB 單文件上限
    private long maxRequestSize = 500 * 1024 * 1024;                        // 500MB 總請求上限
    private int sizeThreshold = 1024 * 1024;                                // 1MB 閾值,超過寫暫存文件
    // getter/setter 省略
}

2. 通用上傳處理器

@WebServlet("/upload")
@MultipartConfig // Servlet 3.0+ 可選,配合 FileUpload 使用更靈活
public class FileUploadServlet extends HttpServlet {
    private final UploadConfig config = new UploadConfig();
    private final DiskFileItemFactory factory;
    private final ServletFileUpload upload;

    public FileUploadServlet() {
        factory = new DiskFileItemFactory();
        factory.setRepository(new File(config.getRepositoryPath()));
        factory.setSizeThreshold(config.getSizeThreshold());
        
        upload = new ServletFileUpload(factory);
        upload.setFileSizeMax(config.getMaxFileSize());
        upload.setSizeMax(config.getMaxRequestSize());
        upload.setHeaderEncoding("UTF-8"); // 解決文件名中文亂碼
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("application/json;charset=UTF-8");
        PrintWriter writer = resp.getWriter();
        Map result = new HashMap<>();

        try {
            List items = upload.parseRequest(req);
            List> uploaded = new ArrayList<>();

            for (FileItem item : items) {
                if (item.isFormField()) {
                    // 普通表單字段
                    req.setAttribute(item.getFieldName(), item.getString("UTF-8"));
                } else {
                    // 文件欄位
                    String originalName = FilenameUtils.getName(item.getName()); // 防止路徑遍歷
                    String savedName = UUID.randomUUID() + "_" + originalName;   // 避免重名
                    File savedFile = new File(config.getUploadPath(), savedName);
                    savedFile.getParentFile().mkdirs();
                    
                    item.write(savedFile); // 自動處理內存/暫存文件寫入
                    item.delete();         // 釋放暫存資源
                    
                    Map info = new HashMap<>();
                    info.put("originalName", originalName);
                    info.put("savedName", savedName);
                    info.put("size", String.valueOf(item.getSize()));
                    info.put("contentType", item.getContentType());
                    uploaded.add(info);
                }
            }
            result.put("success", true);
            result.put("files", uploaded);
        } catch (FileSizeLimitExceededException e) {
            result.put("success", false); result.put("msg", "單文件超過限制");
        } catch (SizeLimitExceededException e) {
            result.put("success", false); result.put("msg", "總請求體超過限制");
        } catch (Exception e) {
            log.error("上傳異常", e);
            result.put("success", false); result.put("msg", "服務器內部錯誤");
        }
        writer.write(new Gson().toJson(result));
    }
}

3. 前端直傳表單

<form id="uploadForm" enctype="multipart/form-data" method="post">
    <input type="file" name="files" multiple />
    <button type="submit">上傳</button>
    <progress id="progress" value="0" max="100"></progress>
</form>
<script>
// 使用 FormData + XMLHttpRequest 實現進度條
document.getElementById('uploadForm').addEventListener('submit', e => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const xhr = new XMLHttpRequest();
    xhr.upload.onprogress = e => {
        if (e.lengthComputable) document.getElementById('progress').value = (e.loaded / e.total) * 100;
    };
    xhr.onload = () => console.log(xhr.responseText);
    xhr.open('POST', '/upload');
    xhr.send(formData);
});</script>

三、下載實現:高效、安全、支援 Range

1. 核心下載工具類

public class FileDownloadUtil {
    public static void download(HttpServletRequest req, HttpServletResponse resp, File file, String downloadName) 
            throws IOException {
        if (!file.exists() || !file.isFile()) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // 安全檢查:防止目錄遍歷
        String canonicalBase = new File("/data/uploads").getCanonicalPath();
        String canonicalFile = file.getCanonicalPath();
        if (!canonicalFile.startsWith(canonicalBase)) {
            resp.sendError(HttpServletResponse.SC_FORBIDDEN);
            return;
        }

        // Content-Type 推斷
        String mimeType = URLConnection.guessContentTypeFromName(file.getName());
        if (mimeType == null) mimeType = "application/octet-stream";
        resp.setContentType(mimeType);

        // Content-Disposition: 文件名編碼 (RFC 5987)
        String encodedName = StandardCharsets.UTF_8.encode(downloadName).toString(); // 簡化,實際用 UrlUtils
        resp.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedName);

        // 支援 Range 斷點續傳
        long fileLength = file.length();
        String rangeHeader = req.getHeader("Range");
        long start = 0, end = fileLength - 1;
        if (rangeHeader != null && rangeHeader.startsWith("bytes=")) {
            String[] ranges = rangeHeader.substring(6).split("-");
            start = Long.parseLong(ranges[0]);
            if (ranges.length > 1 && !ranges[1].isEmpty()) end = Long.parseLong(ranges[1]);
            resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
            resp.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileLength);
        }
        resp.setHeader("Accept-Ranges", "bytes");
        resp.setHeader("Content-Length", String.valueOf(end - start + 1));
        resp.setHeader("Last-Modified", new Date(file.lastModified()).toString());

        // 核心傳輸:使用 NIO 零拷貝 (transferTo) 或 BufferedStream
        try (RandomAccessFile raf = new RandomAccessFile(file, "r");
             OutputStream out = resp.getOutputStream()) {
            raf.seek(start);
            long remaining = end - start + 1;
            byte[] buffer = new byte[8192];
            int len;
            while (remaining > 0 && (len = raf.read(buffer, 0, (int)Math.min(buffer.length, remaining))) != -1) {
                out.write(buffer, 0, len);
                remaining -= len;
            }
            out.flush();
        }
    }
}

2. 下載 Servlet

@WebServlet("/download/*")
public class FileDownloadServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String pathInfo = req.getPathInfo(); // /download/uuid_filename.ext
        if (pathInfo == null || pathInfo.length() <= 1) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST); return;
        }
        String savedName = pathInfo.substring(1);
        File file = new File("/data/uploads", savedName);
        // 從資料庫查詢原始文件名,此處簡化
        String originalName = savedName.substring(savedName.indexOf('_') + 1);
        FileDownloadUtil.download(req, resp, file, originalName);
    }
}

四、安全與性能檢查清單

項目建議
文件名過濾僅保留基礎名稱,UUID 重命名存儲,資料庫映射原名
類型驗證檢查 Magic Number(文件頭),而非只信 Content-Type
大小限制Servlet 層 + FileUpload 雙重限制
暫存目錄獨立分區,定期清理,禁止腳本執行權限
下載授權結合業務權限校驗,不可直接暴露存儲路徑
大文件傳輸啟用 transferTo 零拷貝或 NIO DirectBuffer
並發控制下載限流、上傳併發數限制

五、進階擴展方向

  • 分片上傳/斷點續傳上傳:前端切片(如 WebUploader、Plupload),後端合併分片,記錄上傳狀態。
  • 對象存儲對接:上傳直傳 OSS(簽名直傳),下載簽名 URL 重定向,減輕應用服務器帶寬。
  • 病毒掃描:上傳完成後異步調用 ClamAV 等掃描引擎。
  • 壓縮/轉碼:圖片自動生成縮略圖、視頻轉碼,使用異步任務隊列。

總結

基於 Commons FileUpload 的流式解析配合 DiskFileItemFactory,能以常數內存處理任意大小文件上傳;下載端利用 RandomAccessFile 支援 Range 請求,實現高效斷點續傳。核心原則:**不信任客戶端輸入、不阻塞業務線程、不佔用過量堆內存**。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved