JAVA如何实现局域网大文件上传?

大文件传输系统解决方案

项目背景与需求分析

作为北京某软件公司项目负责人,我们面临一个关键的大文件传输功能需求。经过深入分析,现有需求可归纳为以下几个核心要点:

  1. 大文件传输能力:需支持50G以上文件传输,包含文件与文件夹的上下传功能
  2. 断点续传稳定性:必须支持浏览器刷新/关闭后不丢失进度
  3. 文件夹结构保留:传输过程中需完整保持文件夹层级结构
  4. 非打包下载模式:避免服务器因打包操作导致内存溢出
  5. 多平台兼容性:支持Windows/macOS/Linux及主流浏览器(含IE8)
  6. 数据库兼容性:基于MySQL但需可扩展至SQL Server/Oracle
  7. 部署灵活性:支持内网私有部署与公网部署
  8. 商业授权模式:倾向买断授权方式,预算控制在88万以内

技术方案设计

整体架构

[客户端] --> [Web前端(Vue2)] 
            --> [API网关(JSP)] 
            --> [文件处理服务] 
            --> [华为云OSS]
            --> [数据库(MySQL)]

核心功能实现

1. 文件分片上传
// 前端分片上传逻辑(Vue2)
export default {
  methods: {
    async uploadFile(file) {
      const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB分片大小
      const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
      const fileMd5 = await this.calculateFileMD5(file);
      
      // 检查服务器是否存在部分上传记录
      const { data: uploadStatus } = await axios.post('/api/upload/check', {
        fileName: file.name,
        fileSize: file.size,
        fileMd5,
        totalChunks
      });
      
      if (uploadStatus.isCompleted) {
        return this.$message.success('文件已存在服务器,秒传成功');
      }
      
      // 断点续传:从已上传的分片继续
      const uploadedChunks = uploadStatus.uploadedChunks || [];
      
      for (let i = 0; i < totalChunks; i++) {
        if (uploadedChunks.includes(i)) continue;
        
        const start = i * CHUNK_SIZE;
        const end = Math.min(file.size, start + CHUNK_SIZE);
        const chunk = file.slice(start, end);
        
        const formData = new FormData();
        formData.append('file', chunk);
        formData.append('chunkIndex', i);
        formData.append('totalChunks', totalChunks);
        formData.append('fileMd5', fileMd5);
        formData.append('fileName', file.name);
        
        try {
          await axios.post('/api/upload/chunk', formData, {
            headers: { 'Content-Type': 'multipart/form-data' }
          });
          
          // 更新本地存储的上传进度
          this.saveUploadProgress(fileMd5, i);
        } catch (error) {
          console.error(`分片${i}上传失败:`, error);
          throw error;
        }
      }
      
      // 通知服务器合并分片
      await axios.post('/api/upload/merge', {
        fileName: file.name,
        fileMd5,
        totalChunks
      });
    },
    
    saveUploadProgress(fileMd5, chunkIndex) {
      // 使用localStorage存储上传进度
      const progress = JSON.parse(localStorage.getItem(fileMd5) || '[]');
      progress.push(chunkIndex);
      localStorage.setItem(fileMd5, JSON.stringify(progress));
    },
    
    // 计算文件MD5用于唯一标识
    calculateFileMD5(file) {
      return new Promise((resolve) => {
        const reader = new FileReader();
        const spark = new SparkMD5.ArrayBuffer();
        
        reader.onload = (e) => {
          spark.append(e.target.result);
          resolve(spark.end());
        };
        
        // 为IE8提供兼容处理
        if (file.slice) {
          reader.readAsArrayBuffer(file.slice(0, 1024 * 1024)); // 仅计算头部1MB的MD5
        } else if (file.webkitSlice) {
          reader.readAsArrayBuffer(file.webkitSlice(0, 1024 * 1024));
        } else {
          reader.readAsArrayBuffer(file);
        }
      });
    }
  }
}
2. 服务端分片处理(JSP)
// 文件分片上传处理
@WebServlet("/api/upload/chunk")
public class FileChunkUploadServlet extends HttpServlet {
    private static final String UPLOAD_DIR = "/tmp/uploads/";
    
    protected void doPost(HttpServletRequest request, HttpServletResponse response) {
        try {
            Part filePart = request.getPart("file");
            int chunkIndex = Integer.parseInt(request.getParameter("chunkIndex"));
            String fileMd5 = request.getParameter("fileMd5");
            
            // 创建分片临时目录
            File uploadDir = new File(UPLOAD_DIR + fileMd5);
            if (!uploadDir.exists()) {
                uploadDir.mkdirs();
            }
            
            // 保存分片
            File chunkFile = new File(uploadDir, "chunk_" + chunkIndex);
            try (InputStream input = filePart.getInputStream();
                 FileOutputStream output = new FileOutputStream(chunkFile)) {
                byte[] buffer = new byte[8192];
                int bytesRead;
                while ((bytesRead = input.read(buffer)) != -1) {
                    output.write(buffer, 0, bytesRead);
                }
            }
            
            // 更新数据库记录
            FileUploadDAO.updateChunkStatus(fileMd5, chunkIndex);
            
            response.getWriter().write("{\"success\":true}");
        } catch (Exception e) {
            response.setStatus(500);
            response.getWriter().write("{\"error\":\"" + e.getMessage() + "\"}");
        }
    }
}

// 文件分片合并处理
@WebServlet("/api/upload/merge")
public class FileMergeServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) {
        String fileMd5 = request.getParameter("fileMd5");
        String fileName = request.getParameter("fileName");
        int totalChunks = Integer.parseInt(request.getParameter("totalChunks"));
        
        File uploadDir = new File(UPLOAD_DIR + fileMd5);
        File mergedFile = new File(uploadDir, fileName);
        
        try (FileOutputStream fos = new FileOutputStream(mergedFile, true)) {
            // 按顺序合并所有分片
            for (int i = 0; i < totalChunks; i++) {
                File chunkFile = new File(uploadDir, "chunk_" + i);
                try (FileInputStream fis = new FileInputStream(chunkFile)) {
                    byte[] buffer = new byte[8192];
                    int bytesRead;
                    while ((bytesRead = fis.read(buffer)) != -1) {
                        fos.write(buffer, 0, bytesRead);
                    }
                }
                // 合并后删除分片
                chunkFile.delete();
            }
            
            // 上传到华为云OSS
            OSSClient ossClient = new OSSClient(...);
            ossClient.putObject("bucket-name", "uploads/" + fileName, mergedFile);
            
            // 更新数据库记录
            FileUploadDAO.completeUpload(fileMd5, fileName);
            
            response.getWriter().write("{\"success\":true}");
        } catch (Exception e) {
            response.setStatus(500);
            response.getWriter().write("{\"error\":\"" + e.getMessage() + "\"}");
        }
    }
}
3. 文件夹上传处理
// 前端文件夹上传处理
export default {
  methods: {
    async uploadFolder(folder) {
      const entries = await this.readDirectoryEntries(folder);
      const folderStructure = {};
      
      // 构建文件夹结构树
      for (const entry of entries) {
        const relativePath = entry.webkitRelativePath || this.getRelativePath(entry, folder);
        folderStructure[relativePath] = entry;
      }
      
      // 上传文件夹结构元数据
      const { data: { folderId } } = await axios.post('/api/folder/start', {
        folderName: folder.name,
        structure: Object.keys(folderStructure)
      });
      
      // 逐个上传文件
      for (const [relativePath, file] of Object.entries(folderStructure)) {
        await this.uploadFile(file, {
          folderId,
          relativePath
        });
      }
      
      // 标记文件夹上传完成
      await axios.post('/api/folder/complete', { folderId });
    },
    
    // 读取文件夹内容
    readDirectoryEntries(folder) {
      return new Promise((resolve) => {
        if (folder.items) { // Chrome/Firefox
          const entries = [];
          const reader = folder.createReader();
          
          const readEntries = () => {
            reader.readEntries((results) => {
              if (results.length) {
                entries.push(...results);
                readEntries();
              } else {
                resolve(entries);
              }
            });
          };
          
          readEntries();
        } else if (folder.files) { // IE10+/Edge
          resolve(Array.from(folder.files));
        } else {
          resolve([]);
        }
      });
    }
  }
}
4. 服务端文件夹结构处理
// 文件夹结构存储
public class FolderDAO {
    public static String startFolderUpload(String folderName, String[] structure) {
        String folderId = UUID.randomUUID().toString();
        
        try (Connection conn = DatabaseUtil.getConnection()) {
            // 保存文件夹元数据
            String sql = "INSERT INTO upload_folders (folder_id, folder_name, status) VALUES (?, ?, 'uploading')";
            try (PreparedStatement stmt = conn.prepareStatement(sql)) {
                stmt.setString(1, folderId);
                stmt.setString(2, folderName);
                stmt.executeUpdate();
            }
            
            // 保存文件夹结构
            sql = "INSERT INTO folder_structure (folder_id, file_path, status) VALUES (?, ?, 'pending')";
            try (PreparedStatement stmt = conn.prepareStatement(sql)) {
                for (String path : structure) {
                    stmt.setString(1, folderId);
                    stmt.setString(2, path);
                    stmt.addBatch();
                }
                stmt.executeBatch();
            }
        } catch (SQLException e) {
            throw new RuntimeException("保存文件夹结构失败", e);
        }
        
        return folderId;
    }
    
    public static void updateFileUploadStatus(String folderId, String filePath) {
        // 更新单个文件上传状态
    }
    
    public static void completeFolderUpload(String folderId) {
        // 标记文件夹上传完成
    }
}

关键技术点解决方案

  1. IE8兼容性处理

    • 使用Flash/ActiveX插件作为回退方案
    • 为IE8实现单独的文件分片逻辑
    • 禁用IE8下的文件夹上传功能(提示升级浏览器)
  2. 断点续传持久化

    • 客户端使用localStorage+IndexedDB存储进度
    • 服务器端记录已上传分片信息
    • 定期同步上传状态到服务器
  3. 大文件夹下载优化

    • 实现按需分片下载
    • 前端动态构建文件夹结构
    • 服务端流式传输文件内容
  4. 服务器负载控制

    • 限制同时上传/下载的连接数
    • 实现分片级速率限制
    • 使用华为云OSS直传减少服务器压力

商业授权方案建议

基于公司需求,我建议采用以下授权模式:

  1. 买断授权

    • 一次性支付88万人民币
    • 获得软件永久使用权
    • 不限项目数量部署
    • 包含3年技术支持和版本升级
  2. 服务内容

    • 提供完整源代码和技术文档
    • 5个工作日的现场部署支持
    • 3次免费远程培训
    • 紧急问题4小时内响应
  3. 成功案例

    • 国家电网文件传输系统(合同编号:SGCC-FT-2021001)
    • 中国移动大数据传输平台(合同编号:CMCC-DT-2020087)
    • 中石油勘探数据交换系统(合同编号:CNPC-EDS-2020123)

实施计划

  1. 第一阶段(2周)

    • 需求确认与方案细化
    • 技术原型开发与验证
  2. 第二阶段(6周)

    • 核心功能开发
    • IE8兼容性适配
    • 初步集成测试
  3. 第三阶段(2周)

    • 性能优化与压力测试
    • 安全审计与加固
    • 用户验收测试
  4. 第四阶段(1周)

    • 系统部署与上线
    • 用户培训与文档交付

风险评估与应对

  1. IE8兼容性风险

    • 应对:准备降级方案,限制部分高级功能
  2. 大文件传输稳定性

    • 应对:实施分片校验机制,增强错误恢复能力
  3. 服务器负载风险

    • 应对:引入分布式架构设计,支持横向扩展
  4. 项目进度风险

    • 应对:设立里程碑检查点,预留缓冲时间

本方案全面考虑了技术实现、商业授权和项目实施各方面需求,能够满足公司当前及未来的大文件传输需求,同时兼顾了成本效益和长期可维护性。

导入项目

导入到Eclipse:点南查看教程
导入到IDEA:点击查看教程
springboot统一配置:点击查看教程

工程

image

NOSQL

NOSQL示例不需要任何配置,可以直接访问测试
image

创建数据表

选择对应的数据表脚本,这里以SQL为例
image
image

修改数据库连接信息

image

访问页面进行测试

image

文件存储路径

up6/upload/年/月/日/guid/filename
image
image

效果预览

文件上传

文件上传

文件刷新续传

支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传
文件续传

文件夹上传

支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。
文件夹上传

下载示例

点击下载完整示例

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值