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

Java Web 檔案上傳下載完整實戰:從 Commons FileUpload 到 NIO 高效能實作

     編輯:Java 綜合教學

檔案上傳核心流程

  1. 瀏覽器以 multipart/form-data 編碼傳送檔案流
  2. 伺服端解析請求,分離表單欄位與檔案項目
  3. 輸入流寫入伺服器儲存目錄(建議 WEB-INF 下,防直接存取)
  4. 處理中文檔名亂碼、檔案重名、目錄分散、大小/類型限制

核心依賴

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version> <!-- 建議升級至 1.5+ -->
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>

進階上傳 Servlet 完整實作

@WebServlet("/upload")
@MultipartConfig(
    fileSizeThreshold = 1024 * 100,      // 100KB 閾值,超過寫入暫存檔
    maxFileSize = 1024 * 1024,           // 單檔 1MB
    maxRequestSize = 1024 * 1024 * 10    // 總請求 10MB
)
public class UploadServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        String saveRoot = getServletContext().getRealPath("/WEB-INF/upload");
        String tempDir = getServletContext().getRealPath("/WEB-INF/temp");
        new File(tempDir).mkdirs();
        
        String message = "上傳成功";
        try {
            // 解析所有 Part (Servlet 3.0+ 原生支援,無需 Commons FileUpload)
            for (Part part : req.getParts()) {
                if (part.getName().equals("file")) { // 檔案欄位
                    String originalName = Paths.get(part.getSubmittedFileName()).getFileName().toString();
                    if (originalName.isEmpty()) continue;
                    
                    // 檔案類型驗證
                    String ext = originalName.substring(originalName.lastIndexOf('.') + 1).toLowerCase();
                    if (Set.of("zip", "rar", "exe", "sh").contains(ext)) {
                        throw new IllegalArgumentException("禁止上傳執行檔/壓縮檔");
                    }
                    
                    // 唯一檔名:UUID + 原始檔名
                    String storedName = UUID.randomUUID() + "_" + originalName;
                    // 雜湊分散儲存:兩層子目錄
                    int hash = storedName.hashCode();
                    String subDir = saveRoot + File.separator + (hash & 0xF) + File.separator + ((hash & 0xF0) >> 4);
                    new File(subDir).mkdirs();
                    
                    // NIO 高效複製 (Files.copy 內部使用零拷貝/直接緩衝區)
                    try (InputStream in = part.getInputStream()) {
                        Files.copy(in, Paths.get(subDir, storedName), StandardCopyOption.REPLACE_EXISTING);
                    }
                    part.delete(); // 刪除暫存檔
                } else { // 一般表單欄位
                    String value = req.getParameter(part.getName());
                    log.info("表單欄位: {} = {}", part.getName(), value);
                }
            }
        } catch (IllegalArgumentException e) {
            message = e.getMessage();
        } catch (Exception e) {
            log.error("上傳失敗", e);
            message = "系統錯誤:" + e.getMessage();
        }
        
        req.setAttribute("message", message);
        req.getRequestDispatcher("/message.jsp").forward(req, resp);
    }
}

關鍵細節最佳實踐

問題解決方案
安全性儲存於 WEB-INF 非公開目錄;驗證副檔名與 MIME 類型;掃描病毒
檔名衝突UUID + 原始檔名 或 時間戳 + 隨機數
單目錄檔案過多雜湊分散:hash & 0xF 兩層子目錄,單目錄檔案數可控
大檔案記憶體溢位fileSizeThreshold 閾值觸發暫存檔;NIO Files.copy 避免堆外記憶體
中文檔名亂碼Servlet 3.0+ 原生支援;舊版 fileUpload.setHeaderEncoding("UTF-8")
進度監聽fileUpload.setProgressListener 或前端 JS 監聽 progress 事件

檔案下載實作

@WebServlet("/download")
public class DownloadServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        String storedName = req.getParameter("filename"); // UUID_xxx.pdf
        if (storedName == null || storedName.isEmpty()) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }
        
        // 還原儲存路徑 (同上傳時的雜湊算法)
        int hash = storedName.hashCode();
        String subDir = getServletContext().getRealPath("/WEB-INF/upload") 
                        + File.separator + (hash & 0xF) 
                        + File.separator + ((hash & 0xF0) >> 4);
        
        Path filePath = Paths.get(subDir, storedName);
        if (!Files.exists(filePath)) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "檔案不存在");
            return;
        }
        
        // 解析原始檔名 (去除 UUID_ 前綴)
        String originalName = storedName.substring(storedName.indexOf('_') + 1);
        
        // 設定下載標頭 (RFC 5987 編碼支援中文檔名)
        resp.setContentType(Files.probeContentType(filePath));
        resp.setHeader("Content-Disposition", 
            "attachment; filename*=UTF-8''" + URLEncoder.encode(originalName, StandardCharsets.UTF_8));
        resp.setContentLengthLong(Files.size(filePath));
        
        // NIO 零拷貝高效輸出
        try (FileChannel in = FileChannel.open(filePath, StandardOpenOption.READ);
             WritableByteChannel out = Channels.newChannel(resp.getOutputStream())) {
            in.transferTo(0, Files.size(filePath), out);
        }
    }
}

前端列表頁範例 (JSTL)

<c:forEach var="file" items="${fileList}">
    <a href="download?filename=${file.storedName}">${file.originalName}</a>
    <span>${file.size} bytes</span>
    <span>${file.uploadTime}</span>
</c:forEach>

遷移建議:Spring Boot / Servlet 3.0+ 原生支援

現代專案不再需要 Commons FileUpload。@MultipartConfig + request.getParts() 即可完整替代,減少依賴、效能更優、API 更簡潔。

參考資源:xdp-gacl 博客

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