Spring Boot 实战:腾讯云 COS 高级特性——断点续传与进度监控

前言

在上一篇文章中,我们介绍了如何在 Spring Boot 中实现基础的文件上传功能。然而,在实际生产环境中,特别是面对大文件(如视频、安装包)上传时,仅有基础上传是远远不够的。用户可能会遇到网络波动、意外关闭网页等情况,如果每次都要重新开始上传,体验将非常糟糕。

本文将深入探讨腾讯云 COS 的高级特性:断点续传 (暂停/继续)以及实时进度监控 。我们将基于 TransferManager 高级接口,实现一个可控、可视化的上传服务。

一、核心原理

要实现暂停、继续和进度监控,我们需要理解 COS SDK 中的几个关键对象:

  1. TransferManager:传输管理器,它是所有高级上传操作的入口。
  2. Upload:异步上传任务的句柄。通过它,我们可以获取上传状态、进度,以及执行暂停和取消操作。
  3. PersistableUpload :可持久化的上传信息。当调用 upload.pause() 时,会返回此对象。它包含了恢复上传所需的所有信息(如 Bucket、Key、分块信息等)。只要拥有这个对象,我们就能在任何时候(甚至重启应用后,前提是将其序列化存储)恢复上传。

二、状态管理设计

由于 HTTP 是无状态的,而上传任务是有状态的(进行中、暂停、完成),我们需要在服务端维护这些任务的状态。

为了演示方便,本文使用内存 Map 来存储任务状态。在生产环境中,建议将 PersistableUpload 序列化后存储在 Redis 或数据库中,以便支持分布式部署和应用重启后的续传。

java 复制代码
import com.qcloud.cos.transfer.PersistableUpload;
import com.qcloud.cos.transfer.Upload;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

// 存储正在进行的上传任务 (Key: taskId, Value: Upload对象)
private static final Map<String, Upload> uploadTasks = new ConcurrentHashMap<>();

// 存储已暂停的上传任务信息 (Key: taskId, Value: 恢复所需的元数据)
private static final Map<String, PersistableUpload> pausedTasks = new ConcurrentHashMap<>();

// 控制进度监控线程的开关 (Key: taskId, Value: 是否继续监控)
private static final Map<String, Boolean> progressMonitorFlags = new ConcurrentHashMap<>();

三、功能实现

1. 带进度的上传

我们需要改造之前的上传方法。不再阻塞等待结果,而是启动一个异步线程去监控进度,并将进度写入共享存储(如 Redis),供前端轮询查询。

java 复制代码
import com.qcloud.cos.model.PutObjectRequest;
import com.qcloud.cos.transfer.Transfer;
import com.qcloud.cos.transfer.TransferProgress;
import com.qcloud.cos.transfer.Upload;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class CosTransferService {

    @Autowired
    private TransferManager transferManager;
    
    @Autowired
    private RedisUtil redisUtil; // 假设有一个 Redis 工具类

    @Value("${tencent.cos.bucket-name}")
    private String bucketName;

    /**
     * 开始上传任务
     * @param file     文件对象
     * @param key      COS 文件路径
     * @param taskId   任务ID (前端生成,用于标识此次上传)
     */
    public void uploadFile(File file, String key, String taskId) {
        PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, file);
        // 开启断点续传支持 (非必须,但推荐)
        // putObjectRequest.setEnableResumableUpload(true); 

        // 1. 提交上传任务,获取 Upload 句柄
        Upload upload = transferManager.upload(putObjectRequest);
        
        // 2. 存储任务句柄,以便后续暂停/取消
        uploadTasks.put(taskId, upload);
        progressMonitorFlags.put(taskId, true);

        // 3. 启动进度监控线程
        new Thread(() -> monitorProgress(upload, taskId)).start();
    }

    /**
     * 进度监控逻辑
     */
    private void monitorProgress(Upload upload, String taskId) {
        log.info("开始监控任务进度: {}", taskId);
        
        // 循环检查上传状态
        while (!upload.isDone() && progressMonitorFlags.getOrDefault(taskId, false)) {
            try {
                // 每秒刷新一次进度
                Thread.sleep(1000); 
            } catch (InterruptedException e) {
                return;
            }

            TransferProgress progress = upload.getProgress();
            double percent = progress.getPercentTransferred();
            
            // 将进度写入 Redis: Key=upload:progress:{taskId}, Value=35.5
            redisUtil.set("upload:progress:" + taskId, percent);
            log.debug("任务 {} 进度: {}%", taskId, percent);
        }

        // 上传结束后的处理
        if (upload.isDone()) {
            try {
                // 确保获取最终结果,处理可能的异常
                upload.waitForUploadResult();
                log.info("任务 {} 上传完成", taskId);
                
                // 清理资源
                cleanup(taskId);
                redisUtil.set("upload:progress:" + taskId, 100.0);
                
            } catch (Exception e) {
                log.error("任务 {} 最终失败: {}", taskId, e.getMessage());
            }
        }
    }
    
    private void cleanup(String taskId) {
        uploadTasks.remove(taskId);
        pausedTasks.remove(taskId);
        progressMonitorFlags.remove(taskId);
    }
}

2. 暂停上传

暂停的核心是调用 upload.pause() 并保存返回的 PersistableUpload 对象。

java 复制代码
    /**
     * 暂停上传
     */
    public boolean pauseUpload(String taskId) {
        Upload upload = uploadTasks.get(taskId);
        if (upload == null) {
            return false;
        }

        // 1. 停止进度监控线程
        progressMonitorFlags.put(taskId, false);

        try {
            // 2. 执行暂停,获取恢复点信息
            PersistableUpload persistableUpload = upload.pause();
            
            if (persistableUpload != null) {
                // 3. 存储恢复点信息 (生产环境应存入 Redis/DB)
                pausedTasks.put(taskId, persistableUpload);
                // 移除内存中的 Upload 对象,因为它已经失效
                uploadTasks.remove(taskId);
                log.info("任务 {} 已暂停", taskId);
                return true;
            }
        } catch (Exception e) {
            log.error("暂停任务失败", e);
        }
        return false;
    }

3. 继续上传 (断点续传)

恢复上传时,我们需要拿出之前保存的 PersistableUpload,交给 TransferManager 重新生成一个 Upload 对象。

java 复制代码
    /**
     * 继续上传
     */
    public boolean resumeUpload(String taskId) {
        // 1. 获取之前的恢复点信息
        PersistableUpload persistableUpload = pausedTasks.get(taskId);
        if (persistableUpload == null) {
            log.warn("未找到任务 {} 的暂停记录", taskId);
            return false;
        }

        try {
            // 2. 恢复上传,获取新的 Upload 句柄
            Upload newUpload = transferManager.resumeUpload(persistableUpload);
            
            // 3. 更新状态
            uploadTasks.put(taskId, newUpload);
            pausedTasks.remove(taskId); // 移除暂停记录
            progressMonitorFlags.put(taskId, true);

            // 4. 重新启动进度监控
            new Thread(() -> monitorProgress(newUpload, taskId)).start();
            
            log.info("任务 {} 已恢复上传", taskId);
            return true;
        } catch (Exception e) {
            log.error("恢复任务失败", e);
            return false;
        }
    }

四、接口设计 (Controller)

最后,我们需要暴露接口供前端控制上传流程。

java 复制代码
@RestController
@RequestMapping("/api/upload")
public class UploadController {

    @Autowired
    private CosTransferService transferService;

    // 开始上传 (通常接收 MultipartFile 并转存为 File)
    @PostMapping("/start")
    public String start(@RequestParam("file") MultipartFile file, String taskId) {
        // ... MultipartFile 转 File 的逻辑 (参考上一篇文章) ...
        // transferService.uploadFile(tempFile, "video/" + file.getOriginalFilename(), taskId);
        return "上传已开始";
    }

    // 暂停
    @PostMapping("/pause")
    public String pause(@RequestParam String taskId) {
        boolean success = transferService.pauseUpload(taskId);
        return success ? "暂停成功" : "暂停失败";
    }

    // 继续
    @PostMapping("/resume")
    public String resume(@RequestParam String taskId) {
        boolean success = transferService.resumeUpload(taskId);
        return success ? "继续上传成功" : "继续上传失败";
    }

    // 获取进度 (前端轮询)
    @GetMapping("/progress")
    public Double getProgress(@RequestParam String taskId) {
        // return redisUtil.get("upload:progress:" + taskId);
        return 0.0; // 示例返回
    }
}

五、注意事项与优化

  1. 临时文件管理 :在使用断点续传时,本地的临时文件(File必须保留,直到上传完全结束。如果用户暂停后清理了临时文件,续传将会失败。
  2. 分布式支持 :本文示例使用的是内存 Map (ConcurrentHashMap)。在微服务多实例部署下,必须将 PersistableUpload 序列化为字符串(它实现了 Serializable 接口)存储到 Redis 或数据库中。恢复时,从 Redis 读取字符串反序列化即可。
  3. 分块大小 :在 TransferManagerConfiguration 中设置合理的分块大小(如 1MB 或 5MB)。分块越小,暂停/恢复的粒度越细,但网络请求次数越多。

六、总结

通过集成 TransferManager 的暂停与恢复功能,我们大大提升了用户在弱网环境下的上传体验。结合 Redis 实现的实时进度监控,更是让文件传输过程透明化、可视化。掌握这些高级特性,能让你的 Spring Boot 文件服务更加健壮和专业。

相关推荐
wb0430720112 小时前
使用 Java 开发 MCP 服务并发布到 Maven 中央仓库完整指南
java·开发语言·spring boot·ai·maven
nbwenren13 小时前
Springboot中SLF4J详解
java·spring boot·后端
helx8214 小时前
SpringBoot中自定义Starter
java·spring boot·后端
rleS IONS15 小时前
SpringBoot获取bean的几种方式
java·spring boot·后端
R***z10116 小时前
Spring Boot 整合 MyBatis 与 PostgreSQL 实战指南
spring boot·postgresql·mybatis
赵丙双17 小时前
spring boot AutoConfiguration.replacements 文件的作用
java·spring boot
计算机学姐18 小时前
基于SpringBoot的兴趣家教平台系统
java·spring boot·后端·spring·信息可视化·tomcat·intellij-idea
bearpping19 小时前
Spring Boot + Vue 全栈开发实战指南
vue.js·spring boot·后端
__土块__20 小时前
一次 Spring Boot 自动装配机制源码走读:从误用 @Component 到理解 Bean 生命周期
spring boot·源码分析·自动装配·bean生命周期·@configuration·configurationclasspostprocessor·cglib代理
回到原点的码农1 天前
Spring Boot 3.3.4 升级导致 Logback 之前回滚策略配置不兼容问题解决
java·spring boot·logback