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 文件服务更加健壮和专业。

相关推荐
你才是臭弟弟5 小时前
SpringBoot 集成MinIo(根据上传文件.后缀自动归类)
java·spring boot·后端
what丶k5 小时前
SpringBoot3 配置文件使用全解析:从基础到实战,解锁灵活配置新姿势
java·数据库·spring boot·spring·spring cloud
RwTo6 小时前
【源码】- SpringBoot启动
java·spring boot·spring
Elieal6 小时前
JWT 登录校验机制:5 大核心类打造 Spring Boot 接口安全屏障
spring boot·后端·安全
czlczl200209256 小时前
Spring Boot Filter :doFilter 与 doFilterInternal 的差异
java·spring boot·后端
码界奇点6 小时前
基于Spring Boot和Activiti6的工作流OA系统设计与实现
java·spring boot·后端·车载系统·毕业设计·源代码管理
yangminlei6 小时前
Spring Boot拦截器(Interceptor)与过滤器(Filter)深度解析:区别、实现与实战指南
java·spring boot·后端
czlczl200209257 小时前
Spring Boot :彻底解决 HttpServletRequest 输入流只能读取一次的问题
java·spring boot·后端
小信丶7 小时前
@MappedJdbcTypes 注解详解:应用场景与实战示例
java·数据库·spring boot·后端·mybatis
奋进的芋圆7 小时前
Spring Boot 3 高并发事务与分布式事务企业级完整解决方案
spring boot·分布式