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

JavaWeb 文件上傳入門教程:Commons FileUpload 核心用法與最佳實踐

     編輯:Java 相關知識

文件上傳三要素

  • form method="post":GET 請求長度受限,無法傳輸二進制文件
  • enctype="multipart/form-data":指定請求體為多部分編碼,邊界分隔各字段
  • input type="file":提供文件選擇控件

依賴引入

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    1.4
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    2.11.0
</dependency>

核心類職責

類別職責關鍵方法
DiskFileItemFactory磁盤存儲工廠,配置閾值與臨時目錄setSizeThreshold(), setRepository()
ServletFileUpload解析請求核心類isMultipartContent(), parseRequest(), setFileSizeMax()
FileItem單個表單項 (普通字段或文件)isFormField(), getFieldName(), getString(), getInputStream(), getName()

基礎上傳 Servlet 完整實現

@WebServlet("/upload")
@MultipartConfig // Servlet 3.0+ 可選,配合 Commons FileUpload 需手動解析
public class UploadServlet extends HttpServlet {
    
    private static final String UPLOAD_DIR = "WEB-INF/upload";
    private static final int MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
    private static final int MAX_REQUEST_SIZE = 50 * 1024 * 1024; // 50MB
    
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        req.setCharacterEncoding("UTF-8");
        resp.setContentType("text/html;charset=UTF-8");
        
        // 1. 獲取真實存儲路徑
        String storePath = getServletContext().getRealPath(UPLOAD_DIR);
        File storeDir = new File(storePath);
        if (!storeDir.exists()) storeDir.mkdirs();
        
        // 2. 配置工廠
        DiskFileItemFactory factory = new DiskFileItemFactory();
        factory.setSizeThreshold(1024 * 1024); // 1MB 閾值,超過寫臨時文件
        factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
        
        // 3. 創建解析器
        ServletFileUpload upload = new ServletFileUpload(factory);
        upload.setHeaderEncoding("UTF-8"); // 解決文件名中文亂碼
        upload.setFileSizeMax(MAX_FILE_SIZE);
        upload.setSizeMax(MAX_REQUEST_SIZE);
        
        // 4. 校驗請求類型
        if (!ServletFileUpload.isMultipartContent(req)) {
            resp.getWriter().println("請求類型錯誤,必須為 multipart/form-data");
            return;
        }
        
        // 5. 解析並處理
        try {
            List items = upload.parseRequest(req);
            for (FileItem item : items) {
                if (item.isFormField()) {
                    // 普通表單字段
                    String fieldName = item.getFieldName();
                    String fieldValue = item.getString("UTF-8");
                    System.out.println(fieldName + " = " + fieldValue);
                } else {
                    // 文件字段
                    processFileItem(item, storePath, resp);
                }
            }
            resp.getWriter().println("上傳成功!");
        } catch (FileUploadException e) {
            handleUploadException(e, resp);
        } catch (Exception e) {
            log.error("上傳處理異常", e);
            resp.getWriter().println("上傳失敗:" + e.getMessage());
        }
    }
    
    private void processFileItem(FileItem item, String storePath, HttpServletResponse resp) 
            throws Exception {
        String originalName = item.getName();
        if (originalName == null || originalName.trim().isEmpty()) return;
        
        // 只保留文件名 (去除路徑)
        String fileName = originalName.substring(originalName.lastIndexOf("\\") + 1);
        fileName = fileName.substring(fileName.lastIndexOf("/") + 1); // Unix 路徑兼容
        
        // 生成唯一文件名防衝突
        String savedName = UUID.randomUUID().toString() + "_" + fileName;
        File savedFile = new File(storePath, savedName);
        
        // 流式寫入
        try (InputStream in = item.getInputStream();
             FileOutputStream out = new FileOutputStream(savedFile)) {
            
            byte[] buffer = new byte[8192];
            int len;
            while ((len = in.read(buffer)) > 0) {
                out.write(buffer, 0, len);
            }
        }
        
        // 刪除臨時文件 (Commons FileUpload 大文件會產生臨時文件)
        item.delete();
        
        System.out.println("文件保存:" + savedFile.getAbsolutePath());
    }
    
    private void handleUploadException(FileUploadException e, HttpServletResponse resp) 
            throws IOException {
        if (e instanceof FileSizeLimitExceededException) {
            resp.getWriter().println("單文件超過大小限制 (10MB)");
        } else if (e instanceof SizeLimitExceededException) {
            resp.getWriter().println("總請求超過大小限制 (50MB)");
        } else {
            resp.getWriter().println("上傳解析失敗:" + e.getMessage());
        }
    }
}

JSP 上傳表單

<%@ page pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>文件上傳</title>
</head>
<body>
    <form action="${pageContext.request.contextPath}/upload" method="post" 
          enctype="multipart/form-data">
        用戶名:<input type="text" name="username" required><br/><br/>
        文件 1:<input type="file" name="file1"><br/>
        文件 2:<input type="file" name="file2"><br/><br/>
        <input type="submit" value="上傳">
    </form>
</body>
</html>

進階功能擴展

1. 文件類型驗證 (白名單)

private static final Set ALLOWED_EXT = Set.of("jpg", "jpeg", "png", "gif", "pdf", "doc", "docx");

private boolean isAllowed(String fileName) {
    int dot = fileName.lastIndexOf('.');
    if (dot <= 0) return false;
    String ext = fileName.substring(dot + 1).toLowerCase();
    return ALLOWED_EXT.contains(ext);
}

2. 進度監聽

upload.setProgressListener(new ProgressListener() {
    @Override
    public void update(long bytesRead, long contentLength, int items) {
        int percent = (int) (bytesRead * 100.0 / contentLength);
        // 可存入 Session 或 Redis,前端 AJAX 輪詢顯示進度
        System.out.printf("上傳進度: %d%% (%d/%d bytes)%n", percent, bytesRead, contentLength);
    }
});

3. 迭代器模式 (大文件/高性能)

FileItemIterator iter = upload.getItemIterator(request);
while (iter.hasNext()) {
    FileItemStream item = iter.next();
    String fileName = item.getName();
    try (InputStream is = item.openStream()) {
        // 直接流式處理,不緩存 FileItem 對象
        Files.copy(is, Paths.get(storePath, makeFileName(fileName)));
    }
}

Spring MVC / Spring Boot 整合

配置 MultipartResolver (Spring MVC)

@Bean
public CommonsMultipartResolver multipartResolver() {
    CommonsMultipartResolver resolver = new CommonsMultipartResolver();
    resolver.setMaxUploadSize(50 * 1024 * 1024);
    resolver.setMaxUploadSizePerFile(10 * 1024 * 1024);
    resolver.setDefaultEncoding("UTF-8");
    return resolver;
}

Controller 直接接收 MultipartFile

@PostMapping("/upload")
public String upload(@RequestParam("username") String username,
                     @RequestParam("file1") MultipartFile file1,
                     @RequestParam("file2") MultipartFile file2) {
    
    if (file1.isEmpty() && file2.isEmpty()) return "error";
    
    saveFile(file1);
    saveFile(file2);
    return "success";
}

private void saveFile(MultipartFile file) throws IOException {
    if (file.isEmpty()) return;
    String savedName = UUID.randomUUID() + "_" + file.getOriginalFilename();
    Path path = Paths.get(uploadDir, savedName);
    Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING);
}

安全與最佳實踐清單

  • ✅ **存儲目錄置於 `WEB-INF` 或應用外部**,防止直接 URL 訪問
  • ✅ **文件名重命名**:UUID + 原名,防止覆蓋、路徑遍歷攻擊 (`../../etc/passwd`)
  • ✅ **限制大小**:單文件、總請求雙重限制
  • ✅ **類型驗證**:副檔名白名單 + 可選 Magic Number (文件頭) 驗證
  • ✅ **臨時文件清理**:`item.delete()` / `Files.delete()` 釋放磁盤空間
  • ✅ **編碼統一**:`setHeaderEncoding("UTF-8")` + `item.getString("UTF-8")`
  • ✅ **異步處理**:大文件上傳返回任務 ID,後台處理,前端輪詢狀態
  • ✅ **防病毒掃描**:生產環境集成 ClamAV 等掃描引擎

常見問題排查

現象原因解決
request.getParameter 為 null 未設置 enctype 或未解析 multipart 檢查 form enctype、確保 Filter/Servlet 正確解析
中文文件名亂碼 Header 編碼未設置 `upload.setHeaderEncoding("UTF-8")`
大文件上傳失敗 超過默認大小限制 設置 `setFileSizeMax` / `setSizeMax` / Spring `maxUploadSize`
臨時文件佔滿磁盤 未調用 item.delete() finally 塊中調用 delete(),或配置定時清理任務
Spring Boot 註解不生效 缺少 MultipartResolver Bean 添加 CommonsMultipartResolver 或使用 StandardServletMultipartResolver
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved