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

Commons FileUpload 文件上傳完整實戰:從 Servlet 到 Spring Boot 整合

     編輯:Java 相關知識

文件上傳是 Web 開發的高頻需求。Apache Commons FileUpload 作為經典組件,雖然 Spring MVC 已內建 MultipartFile 支援,但理解其底層原理對排查疑難雜症、實現自定義解析器、適配非 Spring 環境仍有重要價值。本文從核心原理到生產級最佳實踐,全鏈路解析文件上傳技術棧。

核心依賴與版本選擇

<!-- Maven 坐標(建議使用 1.4+ 版本,修復多個安全漏洞) -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.5</version><!-- 最新穩定版 -->
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>

前端表單關鍵點

<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="text" name="username" />
    <input type="file" name="avatar" />
    <input type="file" name="photos" multiple />
    <button type="submit">上傳</button>
</form>
  • enctype="multipart/form-data":必須設置,否則瀏覽器不會以分片形式發送文件數據
  • method="POST":GET 請求無請求體,無法承載文件二進制數據
  • name 屬性:每個 <input> 必須有 name,否則瀏覽器不發送該字段

Servlet 原生整合:手寫解析器

@WebServlet("/upload")
@MultipartConfig(
    location = "/tmp/upload",           // 臨時文件存儲目錄
    maxFileSize = 10 * 1024 * 1024,     // 10MB 單文件限制
    maxRequestSize = 50 * 1024 * 1024,  // 50MB 總請求限制
    fileSizeThreshold = 1024 * 1024     // 1MB 內存閾值,超過寫磁盤
)
public class FileUploadServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        req.setCharacterEncoding("UTF-8");
        resp.setContentType("application/json;charset=UTF-8");

        try {
            // 1. 獲取所有 Part(文件與普通字段統一抽象)
            Collection<Part> parts = req.getParts();
            
            Map<String, Object> result = new HashMap<>();
            List<Map<String, Object>> files = new ArrayList<>();
            
            for (Part part : parts) {
                String name = part.getName();
                String submittedFileName = part.getSubmittedFileName();
                
                if (submittedFileName != null && !submittedFileName.isEmpty()) {
                    // 文件字段
                    String savedPath = saveFile(part, submittedFileName);
                    files.add(Map.of(
                        "field", name,
                        "originalName", submittedFileName,
                        "savedPath", savedPath,
                        "size", part.getSize()
                    ));
                } else {
                    // 普通表單字段
                    String value = new String(part.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
                    result.put(name, value);
                }
            }
            result.put("files", files);
            writeJson(resp, result);
            
        } catch (Exception e) {
            writeError(resp, "UPLOAD_FAILED", e.getMessage());
        }
    }

    private String saveFile(Part part, String originalName) throws IOException {
        String ext = originalName.substring(originalName.lastIndexOf('.'));
        String newName = UUID.randomUUID() + ext;
        Path saveDir = Paths.get("/data/uploads", LocalDate.now().toString());
        Files.createDirectories(saveDir);
        Path target = saveDir.resolve(newName);
        part.write(target.toString()); // 自動處理臨時文件移動/刪除
        return target.toString();
    }
    // JSON 輔助方法省略...
}

Spring Boot 整合最佳實踐

1. 配置屬性

# application.yml
spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 50MB
      max-request-size: 100MB
      file-size-threshold: 2MB
      location: ${java.io.tmpdir}/multipart  # 臨時文件目錄
      resolve-lazily: true  # 延遲解析,按需讀取

2. 控制器實現

@RestController
@RequestMapping("/api/upload")
@Validated
public class UploadController {

    @PostMapping("/avatar")
    public R<String> uploadAvatar(
            @RequestParam("file") MultipartFile file,
            @RequestParam(value = "userId", required = false) Long userId) {
        
        // 1. 基礎校驗
        if (file.isEmpty()) return R.fail("文件不能為空");
        if (file.getSize() > 10 * 1024 * 1024) return R.fail("文件超過 10MB 限制");
        
        String contentType = file.getContentType();
        if (!List.of("image/jpeg", "image/png", "image/webp").contains(contentType)) {
            return R.fail("僅支持 JPG/PNG/WEBP 格式");
        }

        // 2. 業務處理(實際項目中委託 Service 層)
        String url = fileStorageService.storeAvatar(file, userId);
        return R.ok(url);
    }

    @PostMapping("/batch")
    public R<List<String>> batchUpload(
            @RequestParam("files") List<MultipartFile> files) {
        // 批量上傳邏輯...
    }
}

3. 存儲服務抽象(支援本地/雲存儲切換)

public interface FileStorageService {
    String store(MultipartFile file, String bizType);
    void delete(String url);
}

@Service
@ConditionalOnProperty(name = "storage.type", havingValue = "local", matchIfMissing = true)
public class LocalFileStorageService implements FileStorageService {
    
    @Value("${storage.local.base-path:/data/uploads}")
    private String basePath;
    
    @Value("${storage.local.domain:https://localhost:8080}")
    private String domain;

    @Override
    public String store(MultipartFile file, String bizType) {
        String ext = FilenameUtils.getExtension(file.getOriginalFilename());
        String fileName = UUID.randomUUID() + "." + ext;
        String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
        String relativePath = bizType + "/" + datePath + "/" + fileName;
        
        Path target = Paths.get(basePath, relativePath);
        try {
            Files.createDirectories(target.getParent());
            file.transferTo(target); // 高效零拷貝移動
        } catch (IOException e) {
            throw new StorageException("文件保存失敗", e);
        }
        return domain + "/files/" + relativePath;
    }
    // delete 實現省略...
}

生產環境關鍵安全與性能清單

維度風險防護措施
文件類型驗證 上傳惡意腳本、木馬、WebShell 雙重驗證:MIME Type + 魔數檢測 + 後綴白名單
文件名處理 路徑遍歷攻擊、覆蓋系統文件 UUID 重命名、路徑規範化、禁止用戶控制路徑
大小限制 DoS 攻擊佔滿磁盤/內存 多層限制:Nginx client_max_body_size + Spring maxRequestSize + 業務層校驗
存儲隔離 上傳目錄被直接訪問執行 存儲目錄置於 WebRoot 外、Nginx 禁用腳本執行、對象存儲簽名 URL
病毒掃描 惡意文件傳播 集成 ClamAV/ICAP 掃描服務、異步掃描流程
斷點續傳 大文件上傳失敗重傳浪費帶寬 分片上傳、MD5 秒傳、Resumable.js / Uppy 前端庫

常見問題排查

  1. MultipartFile 為 null:檢查表單 enctype、@RequestParam name 一致性、Spring MultipartResolver 配置
  2. 臨時文件未清理:實現 MultipartCleaner 或定時任務清理 spring.servlet.multipart.location 目錄
  3. 中文文件名亂碼:確保 Tomcat URIEncoding=UTF-8、Spring spring.mvc.servlet.multipart.standard-servlet-multipart-resolver 正確解析
  4. 大文件上傳超時:調整 Nginx proxy_read_timeout、Tomcat connectionTimeout、負載均衡器閒置超時

總結

從 Servlet 3.0 @MultipartConfig 到 Spring Boot 自動配置,文件上傳技術棧已高度成熟。核心原理始終是:解析 multipart/request → 內存/磁盤緩衝 → 業務校驗 → 安全存儲 → 返回訪問路徑。生產系統中,建議封裝統一的存儲抽象層,接入對象存儲(S3/OSS/MinIO)、CDN 加速、病毒掃描、審計日誌等企業級能力,構建安全、可擴展的文件服務平台。

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