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

Commons FileUpload 文件上傳入門實戰:從依賴配置到數據庫存儲 Blob 完整流程

     編輯:Java 相關知識

Apache Commons FileUpload 是 Java 文件上傳的經典組件,雖然 Spring MVC 提供了更高層的 MultipartFile 抽象,但理解其底層工作原理對排查問題、適配非 Spring 環境、實現自定義存儲邏輯至關重要。本文從零構建完整上傳流程:依賴引入、前端表單、Servlet 解析、文件/字段區分、輸入流直接寫入數據庫 BLOB 字段,並解決 MySQL 大數據包限制問題。

Maven 依賴配置

<dependencies>
    <!-- 核心上傳組件 -->
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.5</version>
    </dependency>
    <!-- IO 工具類依賴 -->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.11.0</version>
    </dependency>
    <!-- 數據庫驅動 (以 MySQL 為例) -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
</dependencies>

前端上傳頁面

<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<body>
<h2>文件上傳測試</h2>
<form action="UploadServlet" method="post" enctype="multipart/form-data">
    描述: <input type="text" name="description" /><br/>
    文件: <input type="file" name="uploadFile" /><br/>
    <input type="submit" value="上傳" />
</form>
</body>
</html>

核心 Servlet 實現

@WebServlet("/UploadServlet")
public class UploadServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        req.setCharacterEncoding("UTF-8");
        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();

        // 1. 檢查是否為 multipart 請求
        if (!ServletFileUpload.isMultipartContent(req)) {
            out.println("錯誤: 請求格式必須為 multipart/form-data");
            return;
        }

        // 2. 配置工廠與解析器
        DiskFileItemFactory factory = new DiskFileItemFactory();
        // 設置臨時文件目錄 (項目外部路徑)
        String tempPath = getServletContext().getRealPath("/WEB-INF/temp");
        File tempDir = new File(tempPath);
        if (!tempDir.exists()) tempDir.mkdirs();
        factory.setRepository(tempDir);
        factory.setSizeThreshold(1024 * 1024); // 1MB 內存閾值

        ServletFileUpload upload = new ServletFileUpload(factory);
        upload.setHeaderEncoding("UTF-8"); // 關鍵:解決中文文件名亂碼
        upload.setFileSizeMax(10 * 1024 * 1024); // 10MB
        upload.setSizeMax(50 * 1024 * 1024);     // 50MB

        // 3. 解析請求
        try {
            @SuppressWarnings("unchecked")
            List<FileItem> items = upload.parseRequest(req);
            
            String description = null;
            InputStream fileStream = null;
            String fileName = null;
            String fileType = null;

            for (FileItem item : items) {
                if (item.isFormField()) {
                    // 普通表單字段
                    String fieldName = item.getFieldName();
                    String value = item.getString("UTF-8");
                    if ("description".equals(fieldName)) {
                        description = value;
                    }
                } else {
                    // 文件字段
                    String originalName = item.getName();
                    if (originalName == null || originalName.isEmpty()) continue;
                    
                    // 處理瀏覽器傳遞的完整路徑 (IE 等老舊瀏覽器)
                    int idx = originalName.lastIndexOf("\\");
                    fileName = (idx >= 0) ? originalName.substring(idx + 1) : originalName;
                    
                    // 獲取擴展名作為類型
                    int dotIdx = fileName.lastIndexOf('.');
                    fileType = (dotIdx > 0) ? fileName.substring(dotIdx + 1).toLowerCase() : "";
                    
                    // 直接獲取輸入流,不落地磁盤
                    fileStream = item.getInputStream();
                }
            }

            // 4. 入庫處理
            if (fileStream != null && fileName != null) {
                UploadDAO dao = new UploadDAO();
                dao.saveFile(fileStream, fileName, fileType, description);
                out.println("✅ 文件 '" + fileName + "' 上傳並入庫成功!描述: " + description);
            } else {
                out.println("❌ 未接收到有效文件");
            }

        } catch (FileUploadException e) {
            out.println("❌ 解析失敗: " + e.getMessage());
            e.printStackTrace();
        } catch (Exception e) {
            out.println("❌ 服務器錯誤: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

數據庫存儲 DAO:BLOB 寫入

public class UploadDAO {
    // 表結構建議:
    // CREATE TABLE t_file (
    //   id BIGINT PRIMARY KEY AUTO_INCREMENT,
    //   file_name VARCHAR(255),
    //   file_type VARCHAR(50),
    //   file_data LONGBLOB,  -- 關鍵:使用 LONGBLOB 支援 4GB
    //   description TEXT,
    //   create_time DATETIME DEFAULT CURRENT_TIMESTAMP
    // );

    public void saveFile(InputStream in, String fileName, String fileType, String description) 
            throws SQLException {
        String sql = "INSERT INTO t_file (file_name, file_type, file_data, description) VALUES (?, ?, ?, ?)";
        
        try (Connection conn = JdbcUtils.getConnection();
             PreparedStatement ps = conn.prepareStatement(sql)) {
            
            ps.setString(1, fileName);
            ps.setString(2, fileType);
            // 關鍵:setBlob 直接從輸入流讀取,驅動層處理分塊傳輸
            ps.setBlob(3, in);
            ps.setString(4, description);
            
            ps.executeUpdate();
        } catch (SQLException e) {
            // 捕獲 "Packet too large" 等錯誤
            throw new RuntimeException("數據庫寫入失敗", e);
        }
    }
}

JDBC 工具類

public class JdbcUtils {
    private static final String URL = "jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true";
    private static final String USER = "root";
    private static final String PASS = "123456";

    static {
        try { Class.forName("com.mysql.cj.jdbc.Driver"); }
        catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); }
    }

    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(URL, USER, PASS);
    }

    public static void close(AutoCloseable... resources) {
        for (AutoCloseable r : resources) {
            if (r != null) try { r.close(); } catch (Exception ignored) {}
        }
    }
}

MySQL 大文件寫入關鍵配置

默認 max_allowed_packet = 4MB,上傳大文件會報 Packet too large 錯誤,需調整:

方法一:臨時生效 (重啟失效)

mysql> SET GLOBAL max_allowed_packet = 67108864; -- 64MB
mysql> SHOW VARIABLES LIKE 'max_allowed_packet';

方法二:永久生效 (修改配置文件)

[mysqld]
max_allowed_packet=64M
# 重啟 MySQL 服務生效

驅動端建議配置

jdbc:mysql://localhost:3306/mydb?
    useSSL=false&
    serverTimezone=UTC&
    allowPublicKeyRetrieval=true&
    maxAllowedPacket=67108864  <!-- 驅動層也建議設置 -->

常見問題排查

現象原因解決
request.getParameter 為 null multipart 請求無法用 getParameter 獲取 必須通過 FileItem.getString() 獲取普通字段
中文文件名亂碼 未設置 headerEncoding upload.setHeaderEncoding("UTF-8")
文件名包含完整路徑 (C:\path\file.jpg) IE/舊版瀏覽器行為 lastIndexOf("\\") 截取文件名部分
上傳大文件報錯 Packet too large MySQL max_allowed_packet 過小 調大 MySQL 配置與驅動參數
臨時文件未清理佔滿磁盤 異常中斷未調用 delete() 使用 try-with-resources、定時清理任務
數據庫保留字衝突 (如 description) 列名與關鍵字衝突 反引號 \`description\` 或改名 descr

進階優化方向

  1. 流式直傳數據庫:本例已實現 InputStream → setBlob → DB 零臨時文件,內存佔用極低
  2. 異步處理:大文件入庫耗時長,可引入消息佇列異步寫入,前端輪詢進度
  3. 分片上傳/斷點續傳:前端分片,後端合併,需引入 Redis 記錄分片狀態
  4. 對象存儲替代:文件存 MinIO/S3/OSS,數據庫僅存元數據 (URL、MD5、大小)
  5. 事務一致性:文件入庫與業務表關聯在同一事務,或採用最終一致性補償機制

總結

Commons FileUpload 核心流程:DiskFileItemFactory 創建臨時存儲 → ServletFileUpload 解析 Multipart 請求 → 遍歷 FileItem 區分字段/文件 → 獲取 InputStream 直接業務處理。本文展示了「流式直寫數據庫 BLOB」的輕量級方案,適合中小文件、低併發場景。生產系統建議遷移至 Spring MVC MultipartFile + 對象存儲架構,獲得更好的擴展性、可維護性與雲原生適配能力。

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