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

Java 文件上傳技術深度解析:Commons FileUpload 核心原理、分片存儲與生產級避坑

     編輯:Java 相關知識

HTTP 文件上傳本質

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXYZ

------WebKitFormBoundaryXYZ
Content-Disposition: form-data; name="file"; filename="test.jpg"
Content-Type: image/jpeg

<二進制文件內容>
------WebKitFormBoundaryXYZ
Content-Disposition: form-data; name="desc"

用戶輸入的描述文本
------WebKitFormBoundaryXYZ--
  • 表單必須 enctype="multipart/form-data" 且 method="post"
  • 每個部分由 boundary 分隔,包含 Header + Body

Commons FileUpload 核心三大類

類職責關鍵方法
DiskFileItemFactory創建 FileItem 工廠,控制內存閾值、臨時目錄setSizeThreshold(1024*1024)、setRepository(File)
ServletFileUpload解析 Request,返回 ListparseRequest(req)、setFileSizeMax()、setSizeMax()、setProgressListener()
FileItem代表一個表單項 (文件或普通字段)isFormField()、getInputStream()、getString(encoding)、write(File)、delete()

標準上傳 Servlet 完整代碼

@WebServlet("/upload")
@MultipartConfig(
    fileSizeThreshold = 1024 * 1024,     // 1MB 寫臨時文件
    maxFileSize = 10 * 1024 * 1024,      // 10MB 單文件
    maxRequestSize = 50 * 1024 * 1024    // 50MB 總請求
)
public class UploadServlet extends HttpServlet {
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("UTF-8");
        resp.setContentType("application/json;charset=UTF-8");

        String uploadBase = getServletContext().getRealPath("/upload");
        File baseDir = new File(uploadBase);
        if (!baseDir.exists()) baseDir.mkdirs();

        try {
            // 1. 解析請求
            List items = new ServletFileUpload(new DiskFileItemFactory()).parseRequest(req);
            
            Map result = new HashMap<>();
            List savedFiles = new ArrayList<>();

            for (FileItem item : items) {
                if (item.isFormField()) {
                    // 普通字段
                    result.put(item.getFieldName(), item.getString("UTF-8"));
                } else {
                    // 文件字段
                    if (item.getSize() == 0) continue; // 空文件域
                    
                    String originalName = item.getName();
                    String ext = originalName.substring(originalName.lastIndexOf(".")).toLowerCase();
                    
                    // 安全校驗:白名單
                    if (!ALLOWED_EXTS.contains(ext)) {
                        throw new IllegalArgumentException("禁止的文件類型: " + ext);
                    }
                    
                    // 唯一文件名:UUID + 原始後綴
                    String saveName = UUID.randomUUID().toString().replace("-", "") + ext;
                    
                    // Hash 目錄分佈:避免單目錄文件過多
                    String dir1 = Integer.toHexString(saveName.hashCode() & 0xF);
                    String dir2 = Integer.toHexString((saveName.hashCode() & 0xF0) >> 4);
                    File saveDir = new File(baseDir, dir1 + "/" + dir2);
                    if (!saveDir.exists()) saveDir.mkdirs();
                    
                    File dest = new File(saveDir, saveName);
                    item.write(dest); // 內部自動處理內存/臨時文件寫入
                    
                    savedFiles.add(dir1 + "/" + dir2 + "/" + saveName);
                }
            }
            
            result.put("code", 0);
            result.put("files", savedFiles);
            result.put("msg", "上傳成功");
            resp.getWriter().write(new ObjectMapper().writeValueAsString(result));
            
        } catch (Exception e) {
            log.error("上傳失敗", e);
            resp.getWriter().write("{\"code\":-1,\"msg\":\"" + e.getMessage() + "\"}");
        }
    }
}

進階專題:大文件分片上傳與斷點續傳

前端分片邏輯

// 文件切片:5MB/片
const CHUNK_SIZE = 5 * 1024 * 1024;
const chunks = [];
for (let start = 0; start < file.size; start += CHUNK_SIZE) {
    chunks.push(file.slice(start, start + CHUNK_SIZE));
}

// 並發上傳 (限制 3 個並發)
async function uploadChunks(file, chunks) {
    const fileHash = await calculateHash(file); // SparkMD5 計算文件唯一標識
    for (let i = 0; i < chunks.length; i += 3) {
        await Promise.all(chunks.slice(i, i+3).map((chunk, idx) => 
            uploadChunk(fileHash, i+idx, chunks.length, chunk)
        ));
    }
    // 通知後端合併
    await fetch('/upload/merge', {method:'POST', body: JSON.stringify({hash: fileHash, name: file.name})});
}

後端合併邏輯


@PostMapping("/upload/chunk")
public void uploadChunk(@RequestParam String hash, @RequestParam int chunkIndex,
                        @RequestParam int totalChunks, @RequestParam MultipartFile chunk) {
    File chunkDir = new File(TEMP_DIR, hash);
    if (!chunkDir.exists()) chunkDir.mkdirs();
    chunk.transferTo(new File(chunkDir, String.valueOf(chunkIndex)));
}

@PostMapping("/upload/merge")
public void mergeChunks(@RequestParam String hash, @RequestParam String fileName) {
    File chunkDir = new File(TEMP_DIR, hash);
    File[] chunks = chunkDir.listFiles((d, n) -> n.matches("\\d+"));
    Arrays.sort(chunks, Comparator.comparingInt(f -> Integer.parseInt(f.getName())));
    
    String ext = fileName.substring(fileName.lastIndexOf("."));
    String saveName = hash + ext;
    File dest = new File(STORAGE_DIR, saveName);
    
    try (FileOutputStream fos = new FileOutputStream(dest)) {
        for (File chunk : chunks) {
            Files.copy(chunk.toPath(), fos);
            chunk.delete(); // 合併後刪除分片
        }
    }
    chunkDir.delete(); // 刪除臨時目錄
    // 入庫文件元數據...
}

生產級避坑指南 (必讀)

風險點後果防護措施
文件類型繞過上傳 .jsp/.php 獲取 Shell1) 白名單校驗後綴 2) Magic Number 校驗文件頭 3) 存儲目錄禁用腳本執行權限
文件名注入路徑遍歷 ../ 覆蓋關鍵文件不使用原始文件名,一律 UUID 重命名
大文件 OOM內存溢出、服務不可用DiskFileItemFactory 設置閾值、分片上傳、流式寫入
DoS 攻擊佔滿磁盤/帶寬限制單文件/總大小、單 IP 並發上傳數、啟用防毒掃描
併發衝突同名文件覆蓋UUID/雪花算法命名、分佈式鎖
臨時文件洩漏磁盤佔用持續增長定時清理任務、FileItem.delete()、JVM 關閉鉤子
中文文件名亂碼下載/預覽失敗原始名單獨存數據庫,物理文件用 UUID

現代替代方案對比

方案優點缺點適用場景
Commons FileUpload成熟穩定、Servlet 標準API 較老舊、無原生分片支持傳統 Servlet/JSP 項目
Servlet 3.0 @MultipartConfig零依賴、容器原生支持功能較簡單、配置靈活性低Spring Boot/Servlet 容器
Spring MultipartFile封裝優雅、集成驗證、自動綁定依賴 Spring MVCSpring 生態項目首選
對象存儲直傳 (OSS)後端無壓力、CDN 加速、天然分佈式需簽名授權流程、成本較高生產環境、大文件、高併發

總結

Java 文件上傳的核心在於 multipart 解析、臨時文件管理、安全校驗、存儲策略。從 Commons FileUpload 到 Servlet 3.0 再到 OSS 直傳,技術演進的本質是 將文件 IO 壓力從應用服務器剝離。無論用哪種框架,白名單校驗、UUID 命名、Hash 目錄、分片合併、臨時文件清理 這五大工程實踐永遠有效。

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