news 2026/4/19 23:58:20

SpringBoot整合阿里云OSS:手把手教你实现大文件分片上传与断点续传(附完整前后端代码)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot整合阿里云OSS:手把手教你实现大文件分片上传与断点续传(附完整前后端代码)

SpringBoot与阿里云OSS深度整合:大文件分片上传实战指南

当用户需要上传1GB以上的视频文件时,传统的单次上传方式往往会因为网络波动、服务器超时等问题导致失败。这种场景下,分片上传技术成为解决大文件传输难题的关键方案。本文将带你从零开始构建一个完整的SpringBoot应用,整合阿里云OSS实现高效稳定的分片上传与断点续传功能。

1. 环境准备与基础配置

在开始编码之前,我们需要完成一些基础准备工作。首先确保你的开发环境满足以下要求:

  • JDK 1.8或更高版本
  • Maven 3.6+
  • SpringBoot 2.5.x
  • Redis 5.0+(用于断点续传状态管理)

核心依赖配置

<dependencies> <!-- SpringBoot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 阿里云OSS SDK --> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.13.0</version> </dependency> <!-- Redis集成 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- Lombok简化代码 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

阿里云OSS配置类

@Configuration public class OssConfig { @Value("${aliyun.oss.endpoint}") private String endpoint; @Value("${aliyun.oss.accessKeyId}") private String accessKeyId; @Value("${aliyun.oss.accessKeySecret}") private String accessKeySecret; @Value("${aliyun.oss.bucketName}") private String bucketName; @Bean public OSS ossClient() { return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); } @Bean public String bucketName() { return bucketName; } }

提示:建议将敏感配置信息如accessKeyId等放在配置中心或环境变量中,不要直接硬编码在代码里

2. 分片上传核心设计

2.1 分片策略与参数设计

分片上传的核心在于将大文件分割为多个小块独立上传,最后在云端合并。我们需要设计合理的分片策略:

@Data public class ChunkUploadParam { // 文件唯一标识(通常使用MD5) private String fileIdentifier; // 原始文件名 private String originalFilename; // 当前分片序号(从1开始) private Integer chunkNumber; // 分片大小(字节) private Long chunkSize; // 当前分片实际大小 private Long currentChunkSize; // 总分片数 private Integer totalChunks; // 上传任务ID(OSS返回的uploadId) private String uploadId; // 分片文件内容 private MultipartFile file; }

分片大小选择建议

文件大小范围推荐分片大小适用场景
<100MB1MB小文件快速上传
100MB-1GB5MB中等文件平衡上传
1GB-10GB10MB大文件稳定上传
>10GB20MB超大文件分片上传

2.2 上传状态管理

断点续传的关键是记录上传状态,我们使用Redis存储已上传的分片信息:

@Service @RequiredArgsConstructor public class UploadStatusService { private final StringRedisTemplate redisTemplate; private static final String UPLOAD_PREFIX = "oss:upload:"; // 记录已上传分片 public void recordUploadedChunk(String uploadId, int partNumber, String eTag) { redisTemplate.opsForHash().put( UPLOAD_PREFIX + uploadId, String.valueOf(partNumber), eTag ); } // 获取已上传分片列表 public Map<Integer, String> getUploadedParts(String uploadId) { Map<Object, Object> entries = redisTemplate.opsForHash() .entries(UPLOAD_PREFIX + uploadId); return entries.entrySet().stream() .collect(Collectors.toMap( e -> Integer.parseInt(e.getKey().toString()), e -> e.getValue().toString() )); } // 清除上传状态 public void cleanUploadStatus(String uploadId) { redisTemplate.delete(UPLOAD_PREFIX + uploadId); } }

3. OSS分片上传实现

3.1 初始化分片上传任务

@Service @RequiredArgsConstructor public class OssUploadService { private final OSS ossClient; private final String bucketName; private final UploadStatusService uploadStatusService; public String initiateMultipartUpload(String objectKey) { InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest( bucketName, objectKey); InitiateMultipartUploadResult result = ossClient.initiateMultipartUpload(request); return result.getUploadId(); } }

3.2 分片上传核心逻辑

public PartETag uploadPart(String uploadId, String objectKey, int partNumber, InputStream inputStream, long partSize) { UploadPartRequest request = new UploadPartRequest(); request.setBucketName(bucketName); request.setKey(objectKey); request.setUploadId(uploadId); request.setPartNumber(partNumber); request.setPartSize(partSize); request.setInputStream(inputStream); UploadPartResult result = ossClient.uploadPart(request); return result.getPartETag(); } public void completeMultipartUpload(String uploadId, String objectKey, List<PartETag> partETags) { CompleteMultipartUploadRequest request = new CompleteMultipartUploadRequest( bucketName, objectKey, uploadId, partETags); ossClient.completeMultipartUpload(request); }

3.3 断点续传处理流程

public Map<String, Object> handleChunkUpload(ChunkUploadParam param) throws IOException { Map<String, Object> result = new HashMap<>(); // 检查是否已完成上传 if (ossClient.doesObjectExist(bucketName, param.getFileIdentifier())) { result.put("status", "COMPLETED"); result.put("url", generateUrl(param.getFileIdentifier())); return result; } // 初始化上传任务 if (StringUtils.isEmpty(param.getUploadId())) { String uploadId = initiateMultipartUpload(param.getFileIdentifier()); result.put("uploadId", uploadId); result.put("uploadedParts", Collections.emptyList()); return result; } // 获取已上传分片 Map<Integer, String> uploadedParts = uploadStatusService .getUploadedParts(param.getUploadId()); // 如果是查询已上传分片请求 if (param.getFile() == null) { result.put("uploadedParts", uploadedParts.keySet()); return result; } // 上传当前分片 PartETag partETag = uploadPart( param.getUploadId(), param.getFileIdentifier(), param.getChunkNumber(), param.getFile().getInputStream(), param.getCurrentChunkSize() ); // 记录上传状态 uploadStatusService.recordUploadedChunk( param.getUploadId(), param.getChunkNumber(), partETag.getETag() ); // 检查是否全部完成 if (uploadedParts.size() + 1 == param.getTotalChunks()) { List<PartETag> partETags = new ArrayList<>(); for (int i = 1; i <= param.getTotalChunks(); i++) { partETags.add(new PartETag(i, uploadedParts.get(i))); } completeMultipartUpload( param.getUploadId(), param.getFileIdentifier(), partETags ); uploadStatusService.cleanUploadStatus(param.getUploadId()); result.put("status", "COMPLETED"); result.put("url", generateUrl(param.getFileIdentifier())); } return result; }

4. 前端集成与优化

4.1 前端分片处理逻辑

async function uploadByPieces({ file, chunkSize = 5 * 1024 * 1024 }) { const chunkCount = Math.ceil(file.size / chunkSize); const fileMd5 = await calculateFileMd5(file); // 1. 检查文件是否已存在 const checkResult = await api.checkFileExist(fileMd5); if (checkResult.exists) { return checkResult.url; } // 2. 初始化上传任务 let uploadId = checkResult.uploadId; if (!uploadId) { const initResult = await api.initUpload(fileMd5, file.name); uploadId = initResult.uploadId; } // 3. 获取已上传分片 const uploadedParts = await api.getUploadedParts(uploadId); // 4. 上传未完成的分片 const uploadPromises = []; for (let i = 0; i < chunkCount; i++) { if (!uploadedParts.includes(i + 1)) { const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize); uploadPromises.push( api.uploadChunk({ uploadId, fileIdentifier: fileMd5, chunkNumber: i + 1, chunkSize, currentChunkSize: chunk.size, totalChunks: chunkCount, file: chunk }) ); } } // 5. 并行上传分片 await Promise.all(uploadPromises); // 6. 完成上传 const completeResult = await api.completeUpload(uploadId, fileMd5); return completeResult.url; }

4.2 上传优化策略

并发控制方案对比

策略类型优点缺点适用场景
顺序上传实现简单,服务器压力小上传速度慢小文件或网络环境差
固定并发数平衡速度与资源消耗需要调优并发数大多数常规场景
动态并发调整根据网络状况自动调整实现复杂网络波动大的移动环境

推荐的上传进度计算方式

// 更精确的进度计算 function calculateProgress(uploadedChunks, totalChunks, currentChunkProgress) { const baseProgress = (uploadedChunks.length / totalChunks) * 100; const currentChunkRatio = currentChunkProgress / 100 * (1 / totalChunks); return Math.min(99, baseProgress + currentChunkRatio * 100); }

5. 高级功能与异常处理

5.1 秒传实现原理

秒传通过文件内容指纹(通常是MD5)实现:

public boolean checkFileExist(String fileMd5) { // 1. 检查Redis中是否有完整文件记录 if (redisTemplate.hasKey("oss:file:" + fileMd5)) { return true; } // 2. 检查OSS中是否存在 boolean exists = ossClient.doesObjectExist(bucketName, fileMd5); if (exists) { redisTemplate.opsForValue().set( "oss:file:" + fileMd5, generateUrl(fileMd5), Duration.ofDays(7) ); } return exists; }

5.2 异常处理与重试机制

常见错误处理方案

错误类型处理策略重试次数
网络超时指数退避重试3-5次
分片校验失败重新上传该分片2-3次
OSS服务不可用暂停并等待恢复按业务需求
客户端中断记录已上传分片无限制

健壮的重试实现

public PartETag uploadPartWithRetry(String uploadId, String objectKey, int partNumber, InputStream inputStream, long partSize, int maxRetries) { int attempt = 0; while (attempt <= maxRetries) { try { inputStream.reset(); // 重置流以便重试 return uploadPart(uploadId, objectKey, partNumber, inputStream, partSize); } catch (Exception e) { attempt++; if (attempt > maxRetries) { throw new RuntimeException("Upload part failed after retries", e); } try { Thread.sleep((long) (1000 * Math.pow(2, attempt))); // 指数退避 } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new RuntimeException("Upload interrupted", ie); } } } throw new IllegalStateException("Should not reach here"); }

6. 性能优化实践

6.1 客户端优化技巧

  1. 文件预检:上传前计算文件MD5,实现秒传可能性检查
  2. 分片并行化:使用Web Worker在浏览器中并行处理分片
  3. 内存管理:及时释放已上传分片的内存引用
  4. 上传暂停/恢复:利用localStorage保存上传状态

6.2 服务端性能调优

OSS客户端配置优化

@Bean public OSS ossClient() { ClientConfiguration config = new ClientConfiguration(); // 设置最大连接数 config.setMaxConnections(200); // 设置超时时间 config.setConnectionTimeout(5000); config.setSocketTimeout(30000); // 开启失败请求重试 config.setRetryStrategy(new DefaultRetryStrategy(3)); return new OSSClientBuilder() .build(endpoint, accessKeyId, accessKeySecret, config); }

Redis缓存策略优化

  1. 使用Hash结构存储分片状态,减少内存占用
  2. 设置合理的TTL,自动清理过期上传状态
  3. 对超大文件(>100GB)采用分页查询已上传分片

7. 安全防护措施

7.1 上传安全控制

关键安全措施

  1. 内容校验:对上传文件进行病毒扫描和内容类型验证
  2. 权限控制:使用STS临时凭证代替永久AccessKey
  3. 流量限制:对单个IP的上传速率进行限制
  4. 文件隔离:不同用户文件存储在不同目录

7.2 临时访问凭证实现

public String generatePresignedUrl(String objectKey) { // 设置URL过期时间为1小时 Date expiration = new Date(System.currentTimeMillis() + 3600 * 1000); GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest( bucketName, objectKey, HttpMethod.GET); request.setExpiration(expiration); // 设置响应头,强制下载而非预览 ResponseHeaderOverrides headers = new ResponseHeaderOverrides(); headers.setContentDisposition("attachment; filename=\"" + objectKey + "\""); request.setResponseHeaders(headers); return ossClient.generatePresignedUrl(request).toString(); }

在实际项目中,我们团队发现分片大小设置为5MB时能在大多数网络环境下取得较好的平衡。对于特别不稳定的移动网络,动态调整分片大小的策略能显著提升上传成功率。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/19 23:57:04

程序员的心理学学习笔记 - 逆火效应

逆火效应 1、基本介绍 逆火效应指的是当人们遇到与自己坚定信念相矛盾的证据时&#xff0c;不但不会改变想法&#xff0c;反而会更加坚信自己原来的观点&#xff0c;有如下原因威胁感&#xff1a;挑战某个信念等于挑战自我认同&#xff0c;大脑会启动防御认知失调&#xff1a;矛…

作者头像 李华
网站建设 2026/4/19 23:55:07

从Visio画图到MagicDraw建模:我的MBSE工具升级踩坑实录

从Visio画图到MagicDraw建模&#xff1a;我的MBSE工具升级踩坑实录 第一次接触MagicDraw时&#xff0c;我盯着屏幕上那些看似熟悉却又陌生的图表元素&#xff0c;突然意识到自己过去十年用Visio画的"系统架构图"可能只是一堆漂亮的涂鸦。作为团队里最早接触MBSE的工程…

作者头像 李华
网站建设 2026/4/19 23:53:08

从 strtok 到 stringstream:C++ 字符串分割的‘现代化’升级指南

从 strtok 到 stringstream&#xff1a;C 字符串分割的现代化升级指南 在C开发中&#xff0c;字符串处理是最基础却也是最容易出问题的环节之一。许多从C语言转向C的开发者&#xff0c;往往带着strtok等传统字符串处理函数的使用习惯。然而&#xff0c;随着C标准库的不断进化&…

作者头像 李华
网站建设 2026/4/19 23:47:55

《用AI轻松搞定投资》读书笔记:你的第一个智能投资助手

一、核心观点&#xff1a;从“算力平权”到“投资平权”这本书提出了一个激动人心的观点&#xff1a;人工智能正在彻底改变投资领域&#xff0c;让普通投资者也能拥有以往只有专业机构才具备的投研能力。作者认为&#xff0c;随着国产大模型&#xff08;如DeepSeek、Kimi、智谱…

作者头像 李华