JAVA解决方案中如何处理大体积文件的上传与下载稳定性?

大文件上传系统开发指南(基于原生JS+SpringBoot)

项目背景

老哥我最近接了个"硬骨头"项目,客户要求实现20G大文件上传下载,还要支持文件夹结构保留、加密传输、断点续传,还要兼容IE9这种古董浏览器。预算只有100块,还要7×24小时技术支持…这活儿简直比相亲还难!

不过没关系,谁让我进了这个"坑"呢?下面我就把这段时间折腾出来的代码和经验分享给大家,希望能帮到同样在"水深火热"中的同行们。

技术选型

  • 前端:Vue3 CLI + 原生JavaScript(客户要求必须用原生JS实现上传核心功能)
  • 后端:SpringBoot + Tomcat
  • 数据库:MySQL(主要存用户信息和文件元数据)
  • 文件存储:直接服务器存储(简单粗暴,符合预算)
  • 加密:SM4(国密)和AES双支持

系统架构

浏览器(IE9+等) ←HTTP/HTTPS→ SpringBoot后端 ←本地文件IO→ 服务器存储
                      ↑
                    MySQL

前端实现(Vue3 + 原生JS上传核心)

1. 文件选择组件(支持文件夹)





export default {
  data() {
    return {
      fileList: [],
      isUploading: false,
      progress: 0,
      chunkSize: 5 * 1024 * 1024, // 5MB每片
      currentUploads: {}
    }
  },
  methods: {
    triggerFileInput() {
      document.getElementById('fileInput').click();
    },
    
    handleFileChange(e) {
      const files = Array.from(e.target.files);
      if (files.length === 0) return;
      
      // 处理文件夹结构
      const fileTree = {};
      files.forEach(file => {
        const path = file.webkitRelativePath || file.name;
        this.fileList.push({
          file: file,
          relativePath: path,
          size: file.size,
          chunks: Math.ceil(file.size / this.chunkSize),
          uploadedChunks: 0
        });
      });
    },
  }
}


后端实现(SpringBoot)

1. 文件上传控制器

// src/main/java/com/example/uploader/controller/FileUploadController.java
package com.example.uploader.controller;

@RestController
@RequestMapping("/api")
public class FileUploadController {
    
    @Value("${file.upload-dir}")
    private String uploadDir;
    
    // 存储上传进度信息(实际项目应该用数据库)
    private final Map progressMap = new ConcurrentHashMap<>();
    
    @PostMapping("/upload")
    public Map handleFileUpload(
            @RequestParam("file") MultipartFile file,
            @RequestParam("relativePath") String relativePath,
            @RequestParam("totalChunks") int totalChunks,
            @RequestParam("currentChunk") int currentChunk,
            @RequestParam("fileSize") long fileSize,
            @RequestParam("fileMd5") String fileMd5,
            HttpServletRequest request) throws IOException {
        
        Map result = new HashMap<>();
        
        try {
            // 创建文件存储目录(保留文件夹结构)
            Path filePath = Paths.get(uploadDir, relativePath);
            Files.createDirectories(filePath.getParent());
            
            // 如果是加密上传,这里应该先解密(示例省略)
            
            // 以追加模式写入文件块
            try (RandomAccessFile randomAccessFile = new RandomAccessFile(filePath.toFile(), "rw")) {
                randomAccessFile.seek((long) currentChunk * 5 * 1024 * 1024); // 5MB每块
                randomAccessFile.write(file.getBytes());
            }
            
            // 更新上传进度
            UploadProgress progress = progressMap.computeIfAbsent(
                fileMd5, 
                k -> new UploadProgress(fileMd5, relativePath, fileSize, totalChunks)
            );
            progress.markChunkUploaded(currentChunk);
            
            // 如果是最后一块,清理进度信息
            if (progress.isComplete()) {
                progressMap.remove(fileMd5);
                // 这里可以触发文件后处理,如加密存储等
            }
            
            result.put("success", true);
            result.put("message", "Chunk uploaded successfully");
            result.put("uploadedChunks", progress.getUploadedChunks());
        } catch (Exception e) {
            result.put("success", false);
            result.put("message", "Upload failed: " + e.getMessage());
        }
        
        return result;
    }
    
}

2. 文件下载控制器

// src/main/java/com/example/uploader/controller/FileDownloadController.java

@RestController
@RequestMapping("/api")
public class FileDownloadController {
    
    @Value("${file.upload-dir}")
    private String uploadDir;
    
    @GetMapping("/download")
    public ResponseEntity downloadFile(
            @RequestParam String relativePath,
            HttpServletRequest request) throws IOException {
        
        Path filePath = Paths.get(uploadDir, relativePath).normalize();
        Resource resource = new UrlResource(filePath.toUri());
        
        if (!resource.exists()) {
            return ResponseEntity.notFound().build();
        }
        
        // 确定内容类型
        String contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
        if (contentType == null) {
            contentType = "application/octet-stream";
        }
        
        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(contentType))
                .header(HttpHeaders.CONTENT_DISPOSITION, 
                        "attachment; filename=\"" + filePath.getFileName() + "\"")
                .body(resource);
    }
}

3. 加密工具类(简化版)

// src/main/java/com/example/uploader/util/CryptoUtil.java
package com.example.uploader.util;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class CryptoUtil {
    
    private static final String AES = "AES";
    
    // 实际项目应该从安全配置中获取密钥
    private static final byte[] AES_KEY = "ThisIsASecretKey1234567890".getBytes(); // 16/24/32字节
    
    public static byte[] encryptAES(byte[] data) throws Exception {
        SecretKeySpec keySpec = new SecretKeySpec(AES_KEY, AES);
        Cipher cipher = Cipher.getInstance(AES);
        cipher.init(Cipher.ENCRYPT_MODE, keySpec);
        return cipher.doFinal(data);
    }
}

配置文件

application.properties

# 文件上传目录(确保应用有写入权限)
file.upload-dir=./uploads

# Spring Boot默认配置
server.port=8080
spring.servlet.multipart.max-file-size=10GB
spring.servlet.multipart.max-request-size=10GB

# 数据库配置(如果需要)
spring.datasource.url=jdbc:mysql://localhost:3306/uploader?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

部署说明

  1. 环境准备

    • JDK 8+
    • Maven
    • Node.js (用于前端开发)
    • MySQL (可选,如果需要数据库)
  2. 构建前端

    cd frontend
    npm install
    npm run build
    
  3. 构建后端

    mvn clean package
    
  4. 部署

    • 将前端构建产物(dist目录)复制到SpringBoot的src/main/resources/static目录
    • 运行SpringBoot应用:
      java -jar target/uploader-0.0.1-SNAPSHOT.jar
      
  5. Nginx配置(可选)
    如果需要处理大文件上传,建议使用Nginx反向代理:

    server {
        listen 80;
        server_name yourdomain.com;
        
        client_max_body_size 21G;
        
        location / {
            proxy_pass http://localhost:8080;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
    

兼容性处理

IE9兼容性方案

由于IE9不支持FormDataFile API的某些特性,我们需要做特殊处理:

  1. 前端修改
// 在FileUploader.vue中添加IE9兼容代码
methods: {
  handleFileChange(e) {
    const files = e.target.files;
    if (!files) {
      // IE9兼容处理
      this.handleIE9FileSelect();
      return;
    }
    // 原有代码...
  },
}
  1. 后端调整

对于IE9的上传请求,可能需要使用传统的multipart/form-data方式而非分块上传。

加密传输实现

如果需要端到端加密,可以在前端加密后上传:

// 在uploadFile方法中添加加密处理
async uploadFile(fileItem) {
  // ...之前的代码
  
  const chunk = file.slice(start, end);
  let encryptedChunk = chunk;
  
  // 根据配置选择加密方式
  if (this.encryptType === 'AES') {
    encryptedChunk = await this.encryptAES(chunk);
  } else if (this.encryptType === 'SM4') {
    encryptedChunk = await this.encryptSM4(chunk);
  }
  
  const formData = new FormData();
  formData.append('file', new Blob([encryptedChunk]));
  // ...其他参数
  
  // ...上传代码
},

性能优化建议

  1. 分块大小调整:根据网络情况动态调整分块大小(5MB-20MB之间)
  2. 并发控制:根据用户带宽和设备性能调整并发上传数
  3. Web Worker:将哈希计算和加密操作放到Web Worker中,避免阻塞UI
  4. 本地缓存:使用IndexedDB缓存已计算的哈希值
  5. 心跳机制:定期向服务器发送心跳,维护上传会话

完整项目结构

file-uploader/
├── frontend/                  # Vue3前端
│   ├── src/
│   │   ├── components/
│   │   │   └── FileUploader.vue
│   │   ├── App.vue
│   │   └── main.js
│   ├── package.json
│   └── vue.config.js
├── backend/                   # SpringBoot后端
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/com/example/uploader/
│   │   │   │   ├── controller/
│   │   │   │   ├── util/
│   │   │   │   └── Application.java
│   │   │   └── resources/
│   │   │       └── application.properties
│   └── pom.xml
├── uploads/                   # 文件存储目录(自动创建)
├── nginx.conf                 # Nginx配置示例
└── README.md                  # 项目说明

总结

这个项目确实是个挑战,特别是在100元预算和兼容IE9的双重限制下。不过通过分块上传、断点续传和合理的架构设计,我们还是能够实现客户的需求。

关键点总结:

  1. 前端使用原生JS实现核心上传逻辑,Vue3负责UI和状态管理
  2. 后端SpringBoot处理文件存储和进度跟踪
  3. 分块上传+本地缓存实现断点续传
  4. 保留完整的文件夹结构
  5. 通过Nginx处理大文件上传

实际项目中,你可能还需要:

  • 添加用户认证
  • 实现更完善的错误处理和重试机制
  • 添加文件预览功能
  • 实现更安全的加密方案
  • 添加管理员界面

希望这个示例能帮到你,也欢迎加入我们的QQ群(374992201)一起交流技术、合作项目。群里定期有技术分享和红包活动,还有项目合作机会哦!

最后提醒一句:这种预算的项目,记得在合同里明确需求范围和变更条款,不然很容易亏本哦!

导入项目

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

工程

image

NOSQL

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

创建数据表

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

修改数据库连接信息

image

访问页面进行测试

image

文件存储路径

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

效果预览

文件上传

文件上传

文件刷新续传

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

文件夹上传

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

下载示例

点击下载完整示例

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值