Java案例怎么分片上传文件?

wen java案例 14

本文目录导读:

Java案例怎么分片上传文件?

  1. 核心流程
  2. 前端代码示例(HTML + JS)
  3. 后端代码示例(Spring Boot)
  4. 关键优化与注意事项
  5. 测试示例
  6. 常见问题

在Java中实现文件分片上传,通常用于解决大文件上传超时、断点续传等问题,下面是一个完整的前后端配合方案,包含核心逻辑、代码示例和注意事项。

核心流程

  1. 前端切片:将大文件切成固定大小的块(如5MB),计算每块的MD5
  2. 上传分片:按顺序上传每个分片,携带分片序号和分片总数
  3. 服务端拼接:接收所有分片后,按序号合并成完整文件
  4. 断点续传:上传前先查询已上传的分片,跳过已完成的部分

前端代码示例(HTML + JS)

<!DOCTYPE html>
<html>
<body>
    <input type="file" id="fileInput" />
    <button onclick="uploadFile()">上传</button>
    <progress id="progress" value="0" max="100"></progress>
    <div id="status"></div>
    <script>
        const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
        function uploadFile() {
            const file = document.getElementById('fileInput').files[0];
            const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
            const fileName = file.name;
            let uploadedChunks = [];
            // 1. 先查询已上传的分片(断点续传)
            checkUploadedChunks(fileName, totalChunks).then(existing => {
                uploadedChunks = existing;
                // 2. 开始上传未完成的分片
                uploadChunks(file, fileName, totalChunks, uploadedChunks);
            });
        }
        // 查询已上传的分片列表
        async function checkUploadedChunks(fileName, totalChunks) {
            const res = await fetch(`/check?fileName=${fileName}&totalChunks=${totalChunks}`);
            const data = await res.json();
            return data.uploadedChunks || [];
        }
        // 递归上传所有分片
        async function uploadChunks(file, fileName, totalChunks, uploadedChunks) {
            for (let i = 0; i < totalChunks; i++) {
                if (uploadedChunks.includes(i)) continue; // 跳过已上传的
                const start = i * CHUNK_SIZE;
                const end = Math.min(start + CHUNK_SIZE, file.size);
                const chunk = file.slice(start, end);
                const formData = new FormData();
                formData.append('file', chunk);
                formData.append('fileName', fileName);
                formData.append('chunkIndex', i);
                formData.append('totalChunks', totalChunks);
                // 上传当前分片
                const res = await fetch('/upload', { method: 'POST', body: formData });
                if (!res.ok) throw new Error(`分片${i}上传失败`);
                // 更新进度
                const progress = Math.round(((i + 1) / totalChunks) * 100);
                document.getElementById('progress').value = progress;
                document.getElementById('status').innerText = `上传中: ${i+1}/${totalChunks}`;
            }
            // 3. 所有分片上传完成,通知服务端合并
            const mergeRes = await fetch('/merge', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ fileName, totalChunks })
            });
            const result = await mergeRes.json();
            alert('上传完成: ' + result.url);
        }
    </script>
</body>
</html>

后端代码示例(Spring Boot)

依赖引入(pom.xml)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>

控制器(UploadController.java)

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
@RestController
public class UploadController {
    @Value("${upload.dir:/tmp/uploads}")
    private String uploadDir;
    // 临时分片存储目录
    private String getChunkDir(String fileName) {
        return uploadDir + "/chunks/" + fileName;
    }
    // 1. 查询已上传的分片
    @GetMapping("/check")
    public Map<String, Object> check(@RequestParam String fileName, 
                                     @RequestParam int totalChunks) {
        Map<String, Object> result = new HashMap<>();
        List<Integer> uploaded = new ArrayList<>();
        String chunkDir = getChunkDir(fileName);
        File dir = new File(chunkDir);
        if (dir.exists()) {
            File[] files = dir.listFiles();
            if (files != null) {
                for (File f : files) {
                    try {
                        uploaded.add(Integer.parseInt(f.getName()));
                    } catch (NumberFormatException ignored) {}
                }
            }
        }
        result.put("uploadedChunks", uploaded);
        return result;
    }
    // 2. 上传分片
    @PostMapping("/upload")
    public String uploadChunk(@RequestParam("file") MultipartFile file,
                              @RequestParam("fileName") String fileName,
                              @RequestParam("chunkIndex") int chunkIndex,
                              @RequestParam("totalChunks") int totalChunks) throws Exception {
        String chunkDir = getChunkDir(fileName);
        File dir = new File(chunkDir);
        if (!dir.exists()) dir.mkdirs();
        // 保存分片到独立文件
        File chunkFile = new File(chunkDir, String.valueOf(chunkIndex));
        file.transferTo(chunkFile);
        return "ok";
    }
    // 3. 合并分片
    @PostMapping("/merge")
    public Map<String, Object> merge(@RequestBody Map<String, Object> params) throws Exception {
        String fileName = (String) params.get("fileName");
        int totalChunks = (int) params.get("totalChunks");
        String chunkDir = getChunkDir(fileName);
        String finalPath = uploadDir + "/final/" + fileName;
        File finalFile = new File(finalPath);
        // 使用RandomAccessFile进行高效合并
        try (RandomAccessFile raf = new RandomAccessFile(finalFile, "rw")) {
            for (int i = 0; i < totalChunks; i++) {
                File chunk = new File(chunkDir, String.valueOf(i));
                if (!chunk.exists()) {
                    throw new RuntimeException("分片 " + i + " 缺失");
                }
                byte[] bytes = Files.readAllBytes(chunk.toPath());
                raf.write(bytes);
            }
        }
        // 清理临时分片
        deleteDirectory(new File(chunkDir));
        Map<String, Object> result = new HashMap<>();
        result.put("url", "/files/" + fileName);
        return result;
    }
    // 辅助删除目录
    private void deleteDirectory(File dir) {
        if (dir.isDirectory()) {
            File[] children = dir.listFiles();
            if (children != null) {
                for (File child : children) {
                    deleteDirectory(child);
                }
            }
        }
        dir.delete();
    }
}

静态文件映射(可选,用于下载)

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/files/**")
                .addResourceLocations("file:/tmp/uploads/final/");
    }
}

application.yml 配置

server:
  port: 8080
upload:
  dir: /tmp/uploads
spring:
  servlet:
    multipart:
      max-file-size: -1
      max-request-size: -1

关键优化与注意事项

并发控制

  • 可以允许前端并发上传多个分片(使用 Promise.all),但服务端需要处理并发写入问题
  • 建议分片写入独立文件,合并时按顺序读取,天然支持并发

分片校验

  • 前端计算每个分片的 MD5,服务端校验,防止数据损坏
  • 示例(前端用 SparkMD5 库,服务端用 MessageDigest)

断点续传实现

  • 前端上传前调用 /check 接口
  • 服务端记录已上传的分片索引(可用 Redis 或数据库)

生产环境建议

  • 使用 Redis 存储分片状态和文件关联信息
  • 大文件合并时使用 RandomAccessFileFileChannel,避免 OOM
  • 添加分片过期删除机制(24 小时未完成自动清理)

安全性

  • 限制文件名,防止路径穿越(如 ../../etc/passwd
  • 对文件类型进行校验
  • 添加鉴权(JWT Token 等)

测试示例

使用 curl 手动测试:

# 1. 检查已上传分片
curl "http://localhost:8080/check?fileName=test.txt&totalChunks=3"
# 2. 上传分片1
curl -X POST http://localhost:8080/upload \
  -F "file=@test.part0" \
  -F "fileName=test.txt" \
  -F "chunkIndex=0" \
  -F "totalChunks=3"
# 3. 上传分片2
curl -X POST http://localhost:8080/upload \
  -F "file=@test.part1" \
  -F "fileName=test.txt" \
  -F "chunkIndex=1" \
  -F "totalChunks=3"
# 4. 上传分片3
curl -X POST http://localhost:8080/upload \
  -F "file=@test.part2" \
  -F "fileName=test.txt" \
  -F "chunkIndex=2" \
  -F "totalChunks=3"
# 5. 合并
curl -X POST http://localhost:8080/merge \
  -H "Content-Type: application/json" \
  -d '{"fileName":"test.txt","totalChunks":3}'

常见问题

Q1: 分片大小如何选择?

  • 推荐 1MB~10MB,太大会失去分片优势,太小导致请求过多
  • 根据网络状况动态调整(如检测到弱网自动减小分片)

Q2: 如何防止重复上传?

  • 前端生成文件唯一标识(MD5+文件名+大小)
  • 服务端检查文件是否已存在,直接返回已有 URL

Q3: 内存溢出怎么办?

  • 使用流式写入,避免一次加载整个文件到内存
  • 合并时使用 Files.write(Path, byte[]) 会被加载到内存,推荐 RandomAccessFile

这个方案实现了从零开始的分片上传、断点续传和合并功能,可根据实际需求进行扩展(如增加进度通知、暂停/恢复、秒传等功能)。

抱歉,评论功能暂时关闭!