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

文件上傳虛擬路徑架構:實現應用與用戶資源徹底分離

     編輯:Java 綜合教學

VFS 虛擬文件系統設計:解決文件存儲與應用部署強耦合難題

傳統 Web 專案將上傳文件直接存放在 webapps/ROOT/upload 目錄下,導致三大痛點:

  1. 部署覆蓋風險:重新發布 WAR/JAR 時,未外掛卷的文件會被清空。
  2. 多實例共享難:集群環境下各節點本地文件不同步,需引入 NFS/對象存儲。
  3. 靜態資源暴露:用戶可直接通過 URL 訪問上傳目錄,存在遍歷洩露與腳本執行風險。

本文介紹基於 VFS (Virtual File System) 虛擬路徑 的工程化方案,實現「應用無狀態、文件持久化、訪問可控」三大目標。

一、 核心架構設計

分層架構圖
┌─────────────────────────────────────┐
│         Controller 層               │  接收 MultipartFile / 下載請求
├─────────────────────────────────────┤
│         Service 層                  │  業務邏輯、權限校驗、元數據入庫
├─────────────────────────────────────┤
│         VFSUtil (核心工具類)         │  路徑解析、流操作、異常統一
├─────────────────────────────────────┤
│   物理存儲: 本地磁盤 / NFS / S3 / HDFS  │  統一 VFS_ROOT_PATH 抽象
└─────────────────────────────────────┘
  

二、 VFSUtil 核心工具類實現

package cn.gukeer.common.utils;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.Properties;

public abstract class VFSUtil {
    private static final Log log = LogFactory.getLog(VFSUtil.class);
    
    /** VFS 根路徑(無結尾分隔符),支持 Windows/Linux 雙環境 */
    private static String VFS_ROOT_PATH;
    
    static {
        try { readVFSRootPath(); } 
        catch (Exception e) { log.error("VFS 根路徑初始化失敗", e); }
    }

    /** 讀取 db.properties 中的 vfsroot.windows / vfsroot.linux */
    private static void readVFSRootPath() throws IOException {
        String key = System.getProperty("os.name").toUpperCase().contains("WINDOWS") 
                     ? "vfsroot.windows" : "vfsroot.linux";
        
        Properties p = new Properties();
        try (InputStream in = new ClassPathResource("/db.properties").getInputStream()) {
            p.load(in);
        }
        VFS_ROOT_PATH = p.getProperty(key);
        
        // 兜底默認值
        if (VFS_ROOT_PATH == null || VFS_ROOT_PATH.isEmpty()) {
            VFS_ROOT_PATH = System.getProperty("os.name").toUpperCase().contains("WINDOWS") 
                          ? "C:/gukeer/vfsroot/" : "/opt/gukeer/vfsroot/";
        }
        // 確保目錄存在
        new File(VFS_ROOT_PATH).mkdirs();
    }

    public static String getVFSRootPath() { return VFS_ROOT_PATH; }

    /** 
     * 獲取文件輸入流
     * @param fileStream true=文件流(大文件/直接傳輸) false=內存流(小文件/多次讀取)
     */
    public static InputStream getInputStream(File file, boolean fileStream) throws FileOperationException {
        if (!file.exists() || !file.isFile() || !file.canRead()) {
            throw new FileOperationException("文件不存在或不可讀: " + file.getAbsolutePath());
        }
        if (fileStream) {
            return new FileInputStream(file); // 大文件流式傳輸,避免 OOM
        } else {
            // 小文件讀入內存,支持多次讀取/關閉後復用
            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            try (FileInputStream fis = new FileInputStream(file);
                 BufferedInputStream bis = new BufferedInputStream(fis)) {
                byte[] buf = new byte[1024];
                int len;
                while ((len = bis.read(buf)) != -1) buffer.write(buf, 0, len);
            }
            return new ByteArrayInputStream(buffer.toByteArray());
        }
    }

    /** 保存上傳文件,返回相對 VFS 根路徑的相對路徑(入庫存儲) */
    public static String saveFile(MultipartFile multipartFile, String bizPath) throws IOException {
        String originalName = multipartFile.getOriginalFilename();
        String ext = originalName != null && originalName.contains(".") 
                     ? originalName.substring(originalName.lastIndexOf(".")) : "";
        String fileName = UUID.randomUUID().toString().replace("-", "") + ext;
        
        File targetDir = new File(VFS_ROOT_PATH, bizPath);
        targetDir.mkdirs();
        
        File targetFile = new File(targetDir, fileName);
        multipartFile.transferTo(targetFile); // Spring 自動處理大文件臨時文件清理
        
        return bizPath + "/" + fileName; // 返回相對路徑,如 /images/upload/20240118/abc123.jpg
    }

    /** 刪除文件 */
    public static boolean deleteFile(String relativePath) {
        File file = new File(VFS_ROOT_PATH, relativePath);
        return file.exists() && file.delete();
    }
    
    // 自定義異常
    public static class FileOperationException extends RuntimeException {
        public FileOperationException(String msg) { super(msg); }
        public FileOperationException(String msg, Throwable cause) { super(msg, cause); }
    }
}

三、 配置文件 db.properties 範例

# 數據庫配置
jdbc.schema=gk_platform
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
jdbc.username=root
jdbc.password=123456

# VFS 根路徑(Windows/Linux 分離)
vfsroot.windows=C:/platform/vfsroot/
vfsroot.linux=/opt/platform/vfsroot/

四、 Controller 端流式輸出實現(防止大文件 OOM)

@RestController
@RequestMapping("/website")
public class WebsiteController {
    private static final Logger logger = LoggerFactory.getLogger(WebsiteController.class);

    /** 圖片/文件預覽與下載統一入口 */
    @GetMapping("/showPicture")
    public void showPicture(HttpServletResponse response, @RequestParam String picPath) {
        File file = new File(VFSUtil.getVFSRootPath() + picPath);
        if (!file.exists()) {
            logger.error("文件不存在: {}", file.getAbsolutePath());
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // 根據副本名推斷 Content-Type,或統一 application/octet-stream 強制下載
        String contentType = Files.probeContentType(file.toPath());
        if (contentType == null) contentType = "application/octet-stream";
        response.setContentType(contentType);
        // 內聯預覽:inline; 附件下載:attachment;filename="xxx.jpg"
        response.setHeader("Content-Disposition", "inline;filename=\"" + file.getName() + "\"");
        response.setContentLengthLong(file.length());

        try (InputStream in = VFSUtil.getInputStream(file, true); // 文件流模式
             OutputStream out = response.getOutputStream()) {
            
            byte[] buffer = new byte[8192];
            int len;
            while ((len = in.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
            out.flush();
        } catch (IOException e) {
            logger.error("文件輸出異常: {}", e.getMessage(), e);
        }
    }
}

五、 前端調用方式

<!-- 圖片預覽 -->
<img src="<%=contextPath%>/website/showPicture?picPath=/images/upload/image/20240118/abc123.jpg" />

<!-- 文件下載 -->
<a href="<%=contextPath%>/website/showPicture?picPath=/docs/contract/20240118/contract.pdf" download>下載合同</a>

數據庫僅存儲相對路徑:/website/showPicture?picPath=/images/upload/image/20240118/abc123.jpg,前端拼接上下文路徑即可。

六、 進階擴展:遷移至分佈式存儲 (S3/MinIO/OSS)

只需替換 VFSUtil 的存儲實現,Controller/Service 層零修改:

// S3 實現示例
public class S3VFSUtil {
    private static final AmazonS3 s3Client = AmazonS3ClientBuilder.standard().build();
    private static final String BUCKET = "gukeer-vfs";

    public static String saveFile(MultipartFile file, String bizPath) {
        String key = bizPath + "/" + UUID.randomUUID() + getExt(file);
        ObjectMetadata meta = new ObjectMetadata();
        meta.setContentLength(file.getSize());
        meta.setContentType(file.getContentType());
        s3Client.putObject(BUCKET, key, file.getInputStream(), meta);
        return key; // 返回 S3 Key
    }

    public static InputStream getInputStream(String key, boolean fileStream) {
        S3Object obj = s3Client.getObject(BUCKET, key);
        return fileStream ? obj.getObjectContent() : toByteArrayInputStream(obj);
    }
}

七、 安全與運維檢查清單

  1. ✅ 路徑遍歷防護:picPath 需標準化 new File(base, picPath).getCanonicalPath() 並校驗以 base 開頭。
  2. ✅ 文件類型驗證:上傳時檢測 Magic Number(文件頭),拒絕偽裝副本名的腳本/可執行文件。
  3. ✅ 大小限制:Spring spring.servlet.multipart.max-file-size + 代碼層二次校驗。
  4. ✅ 存儲配額:單用戶/單租戶總大小限制,防止佔滿磁盤。
  5. ✅ 防盜鏈:驗證 Referer/Token,或簽名 URL 限時訪問。
  6. ✅ 監控告警:磁盤使用率、上傳失敗率、下載延遲 P99。
  7. ✅ 備份策略:定時同步至異地存儲,支持 PITR(時間點恢復)。

通過 VFS 虛擬路徑層抽象,應用實現了完全無狀態化:文件存儲與應用生命週期解耦、支持水平擴展、便於遷移雲存儲。這是構建雲原生 Java Web 應用的基礎設施模式之一。

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