前言
在上一篇文章中,我们介绍了如何在 Spring Boot 中实现基础的文件上传功能。然而,在实际生产环境中,特别是面对大文件(如视频、安装包)上传时,仅有基础上传是远远不够的。用户可能会遇到网络波动、意外关闭网页等情况,如果每次都要重新开始上传,体验将非常糟糕。
本文将深入探讨腾讯云 COS 的高级特性:断点续传 (暂停/继续)以及实时进度监控 。我们将基于 TransferManager 高级接口,实现一个可控、可视化的上传服务。
一、核心原理
要实现暂停、继续和进度监控,我们需要理解 COS SDK 中的几个关键对象:
TransferManager:传输管理器,它是所有高级上传操作的入口。Upload:异步上传任务的句柄。通过它,我们可以获取上传状态、进度,以及执行暂停和取消操作。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; // 示例返回
}
}
五、注意事项与优化
- 临时文件管理 :在使用断点续传时,本地的临时文件(
File)必须保留,直到上传完全结束。如果用户暂停后清理了临时文件,续传将会失败。 - 分布式支持 :本文示例使用的是内存 Map (
ConcurrentHashMap)。在微服务多实例部署下,必须将PersistableUpload序列化为字符串(它实现了 Serializable 接口)存储到 Redis 或数据库中。恢复时,从 Redis 读取字符串反序列化即可。 - 分块大小 :在
TransferManagerConfiguration中设置合理的分块大小(如 1MB 或 5MB)。分块越小,暂停/恢复的粒度越细,但网络请求次数越多。
六、总结
通过集成 TransferManager 的暂停与恢复功能,我们大大提升了用户在弱网环境下的上传体验。结合 Redis 实现的实时进度监控,更是让文件传输过程透明化、可视化。掌握这些高级特性,能让你的 Spring Boot 文件服务更加健壮和专业。