JAVA中JSP如何实现大文件上传的详细教程与代码分享?

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

项目概述

大家好,我是一个陕西的Java程序员,最近接了个"刺激"的外包项目 - 要开发一个支持20G文件上传下载的系统,还得兼容IE9这种古董浏览器。客户要求用原生JS实现(不能借jQuery的力),还要支持文件夹上传、加密传输、断点续传等高级功能。预算只有100元?没问题,咱们程序员最擅长的就是用爱发电!

技术选型分析

经过深思熟虑(和几根头发的牺牲),我决定采用以下技术方案:

  1. 前端:Vue3 CLI + 原生JS实现WebUploader功能(兼容IE9)
  2. 后端:SpringBoot + 阿里云OSS
  3. 数据库:MySQL(主要存用户信息和文件元数据)
  4. 加密:前端SM4(国密) + 后端AES双重加密
  5. 断点续传:基于文件分片和本地存储记录

前端实现(Vue3 + 原生JS)

1. 兼容IE9的文件夹上传组件





export default {
  name: 'FileUploader',
  data() {
    return {
      fileList: [],
      chunkSize: 5 * 1024 * 1024, // 5MB分片
      concurrent: 3 // 并发上传数
    }
  },
  methods: {
    triggerFileInput() {
      document.getElementById('fileInput').click();
    },
    
    handleFileChange(e) {
      const files = e.target.files;
      if (!files.length) return;
      
      // 处理文件夹结构
      const fileTree = this.buildFileTree(files);
      this.prepareUpload(fileTree);
    },
    
    // 构建文件树结构(保留文件夹层级)
    buildFileTree(files) {
      const tree = {};
      
      for (let i = 0; i < files.length; i++) {
        const file = files[i];
        const path = file.webkitRelativePath || file.relativePath || file.name;
        const parts = path.split('/');
        
        let currentLevel = tree;
        for (let j = 0; j < parts.length - 1; j++) {
          const dir = parts[j];
          if (!currentLevel[dir]) {
            currentLevel[dir] = { __files__: [] };
          }
          currentLevel = currentLevel[dir];
        }
        
        // 添加文件信息
        currentLevel.__files__.push({
          file: file,
          relativePath: path,
          size: file.size,
          loaded: 0,
          progress: 0,
          chunks: Math.ceil(file.size / this.chunkSize),
          uploadedChunks: 0
        });
      }
      
      return tree;
    },
    
    // 准备上传队列
    prepareUpload(fileTree) {
      const flattenFiles = [];
      
      // 扁平化文件树(保留路径信息)
      const traverse = (node, parentPath = '') => {
        for (const key in node) {
          if (key === '__files__') {
            node[key].forEach(fileItem => {
              flattenFiles.push({
                ...fileItem,
                relativePath: parentPath ? `${parentPath}/${fileItem.relativePath}` : fileItem.relativePath
              });
            });
          } else {
            const newPath = parentPath ? `${parentPath}/${key}` : key;
            traverse(node[key], newPath);
          }
        }
      };
      
      traverse(fileTree);
      this.fileList = flattenFiles;
      
      // 开始上传
      this.startUpload();
    },
    
    // 开始上传(带并发控制)
    startUpload() {
      let activeUploads = 0;
      
      const uploadNext = () => {
        if (activeUploads >= this.concurrent) return;
        
        const fileItem = this.fileList.find(f => f.progress < 100);
        if (!fileItem) {
          if (activeUploads === 0) {
            this.$emit('upload-complete');
          }
          return;
        }
        
        activeUploads++;
        this.uploadFile(fileItem).finally(() => {
          activeUploads--;
          uploadNext();
        });
        
        // 立即检查下一个
        uploadNext();
      };
      
      // 初始启动
      for (let i = 0; i < this.concurrent; i++) {
        uploadNext();
      }
    },
    
    // 分片上传文件
    async uploadFile(fileItem) {
      const file = fileItem.file;
      const totalChunks = Math.ceil(file.size / this.chunkSize);
      
      // 检查断点续传信息
      const uploadInfo = this.getUploadInfo(fileItem.relativePath);
      let startChunk = uploadInfo ? uploadInfo.uploadedChunks : 0;
      
      for (let i = startChunk; i < totalChunks; i++) {
        const start = i * this.chunkSize;
        const end = Math.min(start + this.chunkSize, file.size);
        const chunk = file.slice(start, end);
        
        // 读取分片内容(兼容IE9)
        const chunkData = await this.readFileAsArrayBuffer(chunk);
        
        // SM4加密(前端加密)
        const encryptedChunk = this.sm4Encrypt(chunkData);
        
        // 上传分片
        const formData = new FormData();
        formData.append('file', new Blob([encryptedChunk]), file.name);
        formData.append('chunkIndex', i);
        formData.append('totalChunks', totalChunks);
        formData.append('relativePath', fileItem.relativePath);
        formData.append('fileSize', file.size);
        formData.append('fileMd5', await this.calculateMD5(chunk));
        
        try {
          const response = await fetch('/api/upload/chunk', {
            method: 'POST',
            body: formData
          });
          
          if (!response.ok) throw new Error('Upload failed');
          
          // 更新进度
          fileItem.uploadedChunks = i + 1;
          fileItem.loaded = end;
          fileItem.progress = Math.round((fileItem.uploadedChunks / totalChunks) * 100);
          
          // 保存上传进度(使用localStorage)
          this.saveUploadInfo(fileItem.relativePath, {
            uploadedChunks: fileItem.uploadedChunks,
            totalChunks: totalChunks,
            fileSize: file.size
          });
          
          this.$forceUpdate();
        } catch (error) {
          console.error('Chunk upload failed:', error);
          // 失败后重试当前分片
          i--;
          await new Promise(resolve => setTimeout(resolve, 1000));
        }
      }
      
      // 所有分片上传完成,通知合并
      if (fileItem.uploadedChunks === totalChunks) {
        await fetch('/api/upload/merge', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            relativePath: fileItem.relativePath,
            fileSize: file.size,
            totalChunks: totalChunks
          })
        });
        
        // 清除本地存储的上传信息
        this.clearUploadInfo(fileItem.relativePath);
      }
    },
    
    // 兼容IE9的文件读取方法
    readFileAsArrayBuffer(file) {
      return new Promise((resolve, reject) => {
        if (typeof FileReader === 'undefined') {
          // IE9 fallback
          const reader = new ActiveXObject("Scripting.FileSystemObject");
          const stream = new ActiveXObject("ADODB.Stream");
          stream.Type = 1; // binary
          stream.Open();
          stream.LoadFromFile(file);
          const arrayBuffer = stream.Read();
          stream.Close();
          resolve(arrayBuffer);
        } else {
          const reader = new FileReader();
          reader.onload = () => resolve(reader.result);
          reader.onerror = reject;
          reader.readAsArrayBuffer(file);
        }
      });
    },
    
    // 简化的SM4加密(实际项目中应该使用成熟的加密库)
    sm4Encrypt(data) {
      // 这里应该是真正的SM4加密实现
      // 为了示例,我们只是返回原始数据(实际项目中不要这样做!)
      console.warn('实际项目中请替换为真正的SM4加密实现');
      return data;
    },
    
    // 计算MD5(用于分片校验)
    calculateMD5(file) {
      return new Promise((resolve) => {
        // 实际项目中应该使用真正的MD5计算
        // 这里简化为固定值(实际项目中不要这样做!)
        resolve('dummy-md5-hash');
      });
    },
    
    // 断点续传相关方法(使用localStorage)
    getUploadInfo(relativePath) {
      const key = `upload_progress_${relativePath}`;
      const data = localStorage.getItem(key);
      return data ? JSON.parse(data) : null;
    },
    
  }
}

后端实现(SpringBoot)

1. 文件上传控制器

@RestController
@RequestMapping("/api/upload")
public class FileUploadController {
    
    @Autowired
    private FileChunkService fileChunkService;
    
    @Autowired
    private OSSClient ossClient;
    
    @Value("${oss.bucketName}")
    private String bucketName;
    
    // 上传分片
    @PostMapping("/chunk")
    public ResponseEntity uploadChunk(
            @RequestParam("file") MultipartFile file,
            @RequestParam("chunkIndex") int chunkIndex,
            @RequestParam("totalChunks") int totalChunks,
            @RequestParam("relativePath") String relativePath,
            @RequestParam("fileSize") long fileSize,
            @RequestParam("fileMd5") String fileMd5) {
        
        try {
            // AES解密(后端解密)
            byte[] decryptedBytes = AesUtil.decrypt(file.getBytes(), "your-secret-key-123");
            
            // 保存分片到临时目录
            String tempDir = System.getProperty("java.io.tmpdir") + "/upload_chunks/" + fileMd5;
            File chunkFile = new File(tempDir + "/" + chunkIndex);
            Files.createParentDirs(chunkFile);
            Files.write(decryptedBytes, chunkFile);
            
            // 记录分片信息到数据库
            FileChunk chunk = new FileChunk();
            chunk.setFileMd5(fileMd5);
            chunk.setChunkIndex(chunkIndex);
            chunk.setTotalChunks(totalChunks);
            chunk.setRelativePath(relativePath);
            chunk.setFileSize(fileSize);
            chunk.setUploadTime(new Date());
            fileChunkService.save(chunk);
            
            return ResponseEntity.ok().build();
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
        }
    }
    
    // 合并分片
    @PostMapping("/merge")
    public ResponseEntity mergeChunks(@RequestBody MergeRequest request) {
        try {
            String fileMd5 = request.getFileMd5();
            String tempDir = System.getProperty("java.io.tmpdir") + "/upload_chunks/" + fileMd5;
            
            // 检查所有分片是否已上传
            List chunks = fileChunkService.findByFileMd5(fileMd5);
            if (chunks.size() != request.getTotalChunks()) {
                return ResponseEntity.badRequest().body("Not all chunks uploaded");
            }
            
            // 创建临时合并文件
            File mergedFile = new File(tempDir + "/merged_" + System.currentTimeMillis());
            try (FileOutputStream fos = new FileOutputStream(mergedFile);
                 BufferedOutputStream mergingStream = new BufferedOutputStream(fos)) {
                
                // 按顺序合并分片
                for (int i = 0; i < request.getTotalChunks(); i++) {
                    File chunkFile = new File(tempDir + "/" + i);
                    Files.copy(chunkFile, mergingStream);
                }
            }
            
            // 计算合并后文件的MD5(校验用)
            String actualMd5 = DigestUtils.md5DigestAsHex(new FileInputStream(mergedFile));
            if (!actualMd5.equals(fileMd5)) {
                return ResponseEntity.badRequest().body("File MD5 mismatch");
            }
            
            // 上传到OSS(保留路径结构)
            String ossKey = "uploads/" + request.getRelativePath();
            ossClient.putObject(bucketName, ossKey, mergedFile);
            
            // 保存文件元数据到数据库
            FileInfo fileInfo = new FileInfo();
            fileInfo.setRelativePath(request.getRelativePath());
            fileInfo.setFileSize(request.getFileSize());
            fileInfo.setOssKey(ossKey);
            fileInfo.setUploadTime(new Date());
            fileInfo.setLastModified(new Date());
            fileInfoService.save(fileInfo);
            
            // 清理临时文件
            FileUtils.deleteDirectory(new File(tempDir));
            
            return ResponseEntity.ok().build();
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
        }
    }
}

2. 文件下载控制器

@RestController
@RequestMapping("/api/download")
public class FileDownloadController {
    
    @GetMapping("/info")
    public ResponseEntity> getFileInfo(@RequestParam String path) {
        List files = fileInfoService.findByPathPrefix(path);
        return ResponseEntity.ok(files.stream()
            .map(this::convertToDTO)
            .collect(Collectors.toList()));
    }
    
    // 分片下载文件(大文件支持)
    @GetMapping("/file")
    public ResponseEntity downloadFile(
            @RequestParam String ossKey,
            @RequestParam(required = false) Long start,
            @RequestParam(required = false) Long end) {
        
        try {
            // 从OSS获取文件对象
            OSSObject ossObject = ossClient.getObject(bucketName, ossKey);
            
            // 如果请求了范围下载
            if (start != null && end != null) {
                InputStream inputStream = ossObject.getObjectContent();
                long contentLength = end - start + 1;
                
                // 跳过前面的字节
                inputStream.skip(start);
                
                // 创建限制长度的输入流
                InputStream limitedStream = new LimitedInputStream(inputStream, contentLength);
                
                return ResponseEntity.ok()
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + URLEncoder.encode(ossKey.substring(ossKey.lastIndexOf('/') + 1), "UTF-8") + "\"")
                    .header(HttpHeaders.CONTENT_RANGE, "bytes " + start + "-" + end + "/*")
                    .header(HttpHeaders.ACCEPT_RANGES, "bytes")
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .contentLength(contentLength)
                    .body(new InputStreamResource(limitedStream));
            } else {
                // 完整文件下载
                return ResponseEntity.ok()
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + URLEncoder.encode(ossKey.substring(ossKey.lastIndexOf('/') + 1), "UTF-8") + "\"")
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .body(new InputStreamResource(ossObject.getObjectContent()));
            }
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
}

数据库设计

-- 文件信息表
CREATE TABLE file_info (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    relative_path VARCHAR(1000) NOT NULL COMMENT '文件相对路径(保留层级结构)',
    file_size BIGINT NOT NULL COMMENT '文件大小(字节)',
    oss_key VARCHAR(500) NOT NULL COMMENT 'OSS存储key',
    upload_time DATETIME NOT NULL COMMENT '上传时间',
    last_modified DATETIME NOT NULL COMMENT '最后修改时间',
    UNIQUE KEY uk_relative_path (relative_path(255))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 文件分片表(用于断点续传)
CREATE TABLE file_chunk (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    file_md5 VARCHAR(32) NOT NULL COMMENT '文件MD5(作为唯一标识)',
    chunk_index INT NOT NULL COMMENT '分片索引',
    total_chunks INT NOT NULL COMMENT '总分片数',
    relative_path VARCHAR(1000) NOT NULL COMMENT '文件相对路径',
    file_size BIGINT NOT NULL COMMENT '文件总大小',
    upload_time DATETIME NOT NULL COMMENT '上传时间',
    INDEX idx_file_md5 (file_md5)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

项目部署说明

  1. 前端部署

    • 使用Vue CLI构建生产版本:npm run build
    • 将生成的dist目录内容部署到Nginx或Apache
  2. 后端部署

    • 使用Maven打包:mvn clean package
    • 生成JAR文件后上传到阿里云ECS
    • 使用java -jar命令运行,或配置为系统服务
  3. Nginx配置示例(支持大文件上传):

server {
    listen 80;
    server_name your-domain.com;
    
    client_max_body_size 102400m; # 100GB
    proxy_read_timeout 600s;
    proxy_send_timeout 600s;
    
    location / {
        root /path/to/frontend/dist;
        try_files $uri $uri/ /index.html;
    }
    
    location /api {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

兼容性处理要点

  1. IE9兼容性

    • 使用ActiveXObject替代FileReader
    • 避免使用ES6+语法
    • 使用fetch的polyfill或改用XMLHttpRequest
  2. 文件夹上传

    • 利用webkitdirectory属性(Chrome等)
    • IE中使用``并手动解析路径
  3. 加密处理

    • 前端使用SM4(国密算法)加密
    • 后端使用AES二次加密
    • 提供加密开关配置

开发建议

  1. 分阶段开发

    • 第一阶段:实现基本文件上传下载
    • 第二阶段:添加文件夹支持
    • 第三阶段:实现断点续传
    • 第四阶段:添加加密功能
  2. 测试要点

    • 大文件上传(>5GB)
    • 网络中断后恢复上传
    • 文件夹层级结构验证
    • 跨浏览器兼容性测试
  3. 性能优化

    • 分片大小调整(5MB-10MB比较合适)
    • 并发上传数控制(3-5个并发)
    • 使用Web Worker处理加密计算

完整项目结构

large-file-upload/
├── frontend/                # 前端Vue3项目
│   ├── src/
│   │   ├── components/
│   │   │   └── FileUploader.vue
│   │   ├── App.vue
│   │   └── main.js
│   ├── public/
│   └── package.json
├── backend/                 # 后端SpringBoot项目
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/com/example/upload/
│   │   │   │   ├── controller/
│   │   │   │   ├── service/
│   │   │   │   ├── model/
│   │   │   │   └── Application.java
│   │   │   └── resources/
│   │   │       ├── application.yml
│   │   │       └── application-dev.yml
│   └── pom.xml
├── docs/                    # 开发文档
│   ├── api.md
│   ├── deployment.md
│   └── compatibility.md
└── README.md

最后的话

兄弟,这个项目确实有点挑战性,特别是100元预算还要兼容IE9。不过咱们程序员不就是喜欢挑战吗?我建议:

  1. 先实现核心功能(文件上传下载)
  2. 再逐步添加高级功能
  3. 重点测试断点续传和文件夹结构保留
  4. 加密功能可以先用简化版,后续再完善

我已经提供了核心代码框架,你可以基于这个继续开发。如果遇到具体问题,欢迎加入我们的QQ群(374992201)交流,群里大佬众多,说不定能找到帮你调试的兄弟。

记住,咱们虽然预算有限,但志气不能限!用爱发电,照亮代码之路!💪🔥

导入项目

导入到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、付费专栏及课程。

余额充值