Spring 大文件传输老中断?手把手教你解决问题!

最近在项目里搞大文件传输功能,频繁遇到传输中断的糟心事,反复踩坑调试后终于摸索出一套解决方案!这里简单记录分享一下,附上手写代码和时序图。

开发文件上传下载功能时,传输大文件就像开盲盒——要么传着传着突然中断,要么直接弹出文件大小超限的报错。其实这些问题都有迹可循,下面就结合具体场景拆解解决方案。

一、传输中断的“元凶”大盘点
  1. 服务器设置太“小气”:Spring 默认限制请求体大小,Tomcat 等容器还会自动掐断长时间没动静的连接,就像外卖小哥等太久直接取消订单。
  2. 网络“掉链子”:WiFi 信号弱、4G 信号差,都可能让传输半路夭折。
  3. 内存“爆仓”风险:直接把大文件一股脑塞进内存,分分钟触发内存溢出,程序直接“罢工”。
二、核心方案:断点续传+全链路优化

断点续传是解决传输中断的“救命稻草”,就像下载电影暂停后还能接着下。下面用代码和时序图详解实现过程。

1. 服务端分块上传处理
ClientServer发送分块文件及参数(fileName、chunkIndex等)将分块保存到临时目录在Redis记录分块上传状态检查所有分块是否上传完毕合并分块文件删除临时分块文件返回上传成功响应返回接受请求响应,继续上传alt[全部上传完成][未完成]ClientServer

核心代码实现:

@RestController
@RequestMapping("/upload")
public class FileUploadController {

    @PostMapping(value = "/chunked", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<?> uploadChunk(
            @RequestParam("file") MultipartFile file,
            @RequestParam("fileName") String fileName,
            @RequestParam("chunkIndex") int chunkIndex,
            // 其他参数...
    ) {
        try {
            // 保存分块到临时目录
            Path tempDir = Paths.get(System.getProperty("java.io.tmpdir"), "upload");
            Files.createDirectories(tempDir);
            Path chunkPath = tempDir.resolve(fileName + "." + chunkIndex);
            file.transferTo(chunkPath.toFile());

            // 记录分块状态到Redis
            redisTemplate.opsForHash().put(fileName, "chunk_" + chunkIndex, true);

            // 检查是否全部上传完成
            if (isAllChunksUploaded(fileName, totalChunks)) {
                mergeChunks(fileName, totalChunks, tempDir);
                cleanupChunks(fileName, tempDir);
                return ResponseEntity.ok().build();
            }

            return ResponseEntity.accepted().build();
        } catch (Exception e) {
            log.error("上传分块失败", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
}
2. 客户端断点续传逻辑
ClientServer读取本地已上传分块记录生成当前分块文件发送分块文件及参数更新已上传记录删除临时分块文件执行指数退避重试alt[上传成功][上传失败]loop[遍历未上传分块]提示上传成功alt[存在未完成分块][全部上传完成]ClientServer

客户端代码片段:

public class ResumableFileUploader {

    public void uploadFile(File file) throws IOException {
        // 获取已上传的分块
        Set<Integer> uploadedChunks = getUploadedChunks(fileName);

        try (FileInputStream fis = new FileInputStream(file)) {
            for (int i = 0; i < totalChunks; i++) {
                if (uploadedChunks.contains(i)) {
                    continue;
                }

                // 创建分块
                FileSystem fs = FileSystems.getDefault();
                Path tempFilePath = fs.getPath(System.getProperty("java.io.tmpdir"), fileName + "." + i);
                try (FileOutputStream fos = new FileOutputStream(tempFilePath.toFile())) {
                    fis.transferTo(fos);
                }

                // 上传分块
                uploadChunk(fileName, i, totalChunks, fileSize, tempFilePath.toFile());

                // 记录上传成功
                markChunkAsUploaded(fileName, i);
                Files.delete(tempFilePath);
            }
        }
    }
}
三、进阶优化:异步处理+进度监控+安全防护
1. 异步处理:让上传任务“后台运行”

有时候上传大文件会卡住页面,这时就需要把上传任务丢到后台线程处理。

UserControllerAsyncServiceUploadTask发起文件上传请求调用异步上传方法启动异步线程处理上传立即返回响应,不阻塞页面执行分块上传逻辑上传完成后通知结果UserControllerAsyncServiceUploadTask

配置与代码实现:

// 启用异步支持
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(100);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("upload-");
        executor.initialize();
        return executor;
    }
}

// 异步上传服务
@Service
public class AsyncUploadService {

    @Async
    public CompletableFuture<Void> processUpload(MultipartFile file, String fileName) {
        try {
            // 处理大文件上传
            Path destination = Paths.get("/data/uploads", fileName);
            Files.copy(file.getInputStream(), destination, StandardCopyOption.REPLACE_EXISTING);
            return CompletableFuture.completedFuture(null);
        } catch (Exception e) {
            throw new RuntimeException("上传失败", e);
        }
    }
}
2. 进度监控:给用户一颗“定心丸”

上传大文件时,显示实时进度条能大大提升体验。我们可以通过自定义输入流来实现(篇幅原因具体的实现,感兴趣的伙伴可以再去搜一下,这里只简单讲述一下思路)。

UserControllerFileStream上传文件创建带进度监听的输入流读取文件数据触发进度回调更新前端进度条UserControllerFileStream

关键代码:

// 自定义带进度监听的MultipartFile
public class ProgressAwareMultipartFile implements MultipartFile {

    private final MultipartFile delegate;
    private final ProgressListener listener;
    private long bytesRead = 0;

    public ProgressAwareMultipartFile(MultipartFile delegate, ProgressListener listener) {
        this.delegate = delegate;
        this.listener = listener;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        return new ProgressAwareInputStream(delegate.getInputStream());
    }

    private class ProgressAwareInputStream extends FilterInputStream {
        protected ProgressAwareInputStream(InputStream in) {
            super(in);
        }

        @Override
        public int read() throws IOException {
            int b = super.read();
            if (b != -1) {
                bytesRead++;
                listener.onProgress(bytesRead, delegate.getSize());
            }
            return b;
        }
    }

    // 省略其他方法...
}

// 进度监听器接口
public interface ProgressListener {
    void onProgress(long bytesRead, long totalBytes);
}
3. 安全防护:给文件传输加上“防盗门”
  • 文件类型校验:只允许上传指定类型的文件,防止恶意上传可执行文件。
  • 文件名过滤:避免用户通过特殊文件名进行路径遍历攻击。
  • 并发限制:使用令牌桶算法限制同时上传的任务数,防止服务器被“压垮”。
// 示例:文件类型校验
@PostMapping("/upload")
public ResponseEntity<?> uploadFile(
    @RequestParam("file") MultipartFile file) {

    String[] allowedExtensions = {".pdf", ".docx", ".jpg"};
    String originalFilename = file.getOriginalFilename();
    boolean isValid = Arrays.stream(allowedExtensions)
            .anyMatch(originalFilename::endsWith);

    if (!isValid) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body("不支持的文件类型");
    }

    // 其他上传逻辑...
}
四、前端协作:让体验更丝滑

前端用 JavaScript 的 File API 实现分块上传,并把已上传记录存到 localStorage,即使页面刷新也能接着传。

function uploadFile(file) {
    const chunkSize = 1024 * 1024; // 1MB
    const totalChunks = Math.ceil(file.size / chunkSize);
    
    for (let i = 0; i < totalChunks; i++) {
        const start = i * chunkSize;
        const end = Math.min((i + 1) * chunkSize, file.size);
        const chunk = file.slice(start, end);
        
        const formData = new FormData();
        formData.append('file', chunk);
        // 其他参数...
        
        fetch('/upload/chunked', {
            method: 'POST',
            body: formData
        })
        .then(response => {
            if (response.ok) {
                localStorage.setItem(`upload_${file.name}_chunk_${i}`, 'done');
                console.log(`分块 ${i} 上传成功`);
            } else {
                console.error(`分块 ${i} 上传失败`);
                // 触发重试逻辑
            }
        })
        .catch(error => {
            console.error('上传异常:', error);
        });
    }
}
五、生产环境避坑指南
  1. 监控告警:实时盯着上传成功率、失败率,一旦异常立刻报警。
  2. CDN 加速:用 CDN 分担服务器压力,让传输速度起飞。
  3. 数据备份:定期备份上传文件和元数据,防止数据丢失。

搞定这些方案后,项目里的大文件传输功能稳得一批!如果你在实践中遇到问题,欢迎在评论区留言交流

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值