java中oss分片上传(包含业务和详细讲解)
背景:
前端上传音视频文件过大大于100MB。讨论后决定采用oss分片上传。
业务流程:
前端先调用一次初始化接口拿到本次分片任务的唯一分片id。前端负责分片,传参:总片数、第几片,唯一分片id等数据,这些需要传给后台,后台才能够以此判断。下面是demo:
导maven包: 注意需要3以上的版本
<!-- 阿里云对象存储服务 --> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.15.1</version> </dependency>
配置:
####################################### 阿里云对象存储配置 ####################################### oss.endpoint.ext = oss-cn-zhangjiakou.aliyuncs.com oss.endpoint.internal = oss-cn-zhangjiakou.aliyuncs.com oss.accessKeyId = LTAI4FcG6N5FEUt38DQdHGE1 oss.accessKeySecret = KEBGjLMlLLvMLMi2FQ1GRZ825rUywh oss.bucketName = dev-iot-services-public
业务实现: 使用了ossUtil工具类
/** * 我们先初始化拿到分片唯一ID,返回给前端 * @param param * @return */ @ApiOperation("oss初始化分片") @PostMapping("/initTest") public UmsAdminLoginLogDO testInitControl(@RequestBody UmsAdminLoginLogDO param) { //分片上传 UmsAdminLoginLogDO result = new UmsAdminLoginLogDO(); // 生成任务id String taskId = UUID.randomUUID().toString().replaceAll("-", ""); result.setTaskId(taskId); //生成任务名称,建议使用各种ID拼接 String taskKey = param.getFileName() + taskId; // 请求阿里云oss获取分片唯一ID String ossSlicesId = ossUtil.getUploadId(taskKey); result.setOssSlicesId(ossSlicesId); //每一片的大小 result.setMinSliceSize("100k"); redisUtil.set(ossSlicesId,result); return result; }
分片上传:
/** * 有些必传的参数比如分片id,总片数,第几片,文件流数据源 * @param param * @throws Exception */ @ApiOperation("oss分片上传") @PostMapping("/uploadTest") public void testControl(@RequestBody UmsAdminLoginLogDO param) throws Exception { //必须求出redis中的PartETags,在分片合成文件中需要以此为依据,合并文件返回最终地址 UmsAdminLoginLogDO redisParam = (UmsAdminLoginLogDO) redisUtil.get(param.getOssSlicesId()); if (redisParam !=null) { param.setPartETags(redisParam.getPartETags()); } int sliceNo = param.getSliceNo(); int fileSlicesNum = param.getFileSlicesNum(); String ossSlicesId = param.getOssSlicesId(); //字节流转换 InputStream inputStream = new ByteArrayInputStream(param.getContent()); Map<Integer, PartETag> partETags = param.getPartETags(); //分片上传 try { //每次上传分片之后,OSS的返回结果会包含一个PartETag PartETag partETag = ossUtil.partUploadFile(param.getFileName(), inputStream, ossSlicesId, param.getFileMD5(), param.getSliceNo(), param.getContent().length); partETags.put(param.getSliceNo(), partETag); //分片编号等于总片数的时候合并文件,如果符合条件则合并文件,否则继续等待 if (fileSlicesNum==sliceNo) { //合并文件,注意:partETags必须是所有分片的所以必须存入redis,然后取出放入集合 String url = ossUtil.completePartUploadFile(param.getFileName(), ossSlicesId, new ArrayList<>(partETags.values())); //oss地址返回后存入并清除redis param.setFileUrl(url); redisUtil.del(ossSlicesId); }else { redisUtil.set(param.getOssSlicesId(), param); } } catch (Exception e) { throw new Exception(ErrorCodeEnum.SYSTEM_ERROR.getMsg()); } }
工具类:
package com.macro.mall.tiny.demo.utils; import com.aliyun.oss.OSSClientBuilder; import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClient; import com.aliyun.oss.model.*; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.io.IOException; import java.io.InputStream; import java.util.List; /** * @author zhangtonghao * @create 2022-08-30 16:26 */ @Component public class OSSUtil { private static Logger logger = LoggerFactory.getLogger(OSSUtil.class); // private OSSClient ossClient; @Value("${oss.endpoint.ext}") private String endpoint; @Value("${oss.endpoint.internal}") private String internalEndpoint; @Value("${oss.accessKeyId}") private String accessKeyId; @Value("${oss.accessKeySecret}") private String accessKeySecret; @Value("${oss.bucketName}") private String bucketName; private OSS ossClient; @PostConstruct public void init() { ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); } /** * 分块上传完成获取结果 */ public String completePartUploadFile(String fileKey, String uploadId, List<PartETag> partETags) { CompleteMultipartUploadRequest request = new CompleteMultipartUploadRequest(bucketName, fileKey, uploadId, partETags); ossClient.completeMultipartUpload(request); String downLoadUrl = getDownloadUrl(fileKey, bucketName); logger.debug("-------------- 文件的下载URL ------------" + downLoadUrl); return downLoadUrl; } /** * * @param fileKey 文件名称 * @param is 文件流数据 * @param uploadId oss唯一分片id * @param fileMd5 文件的md5值(非必传) * @param partNum 第几片 * @param partSize 总片数 * @return */ public PartETag partUploadFile(String fileKey, InputStream is, String uploadId, String fileMd5, int partNum, long partSize) { UploadPartRequest uploadPartRequest = new UploadPartRequest(); uploadPartRequest.setBucketName(bucketName); uploadPartRequest.setUploadId(uploadId); uploadPartRequest.setPartNumber(partNum); uploadPartRequest.setPartSize(partSize); uploadPartRequest.setInputStream(is); uploadPartRequest.setKey(fileKey); uploadPartRequest.setMd5Digest(fileMd5); UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest); return uploadPartResult.getPartETag(); } /** * 分块上传完成获取结果 */ public String getUploadId(String fileKey) { InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, fileKey); // 初始化分片 InitiateMultipartUploadResult unrest = ossClient.initiateMultipartUpload(request); // 返回uploadId,它是分片上传事件的唯一标识,您可以根据这个ID来发起相关的操作,如取消分片上传、查询分片上传等。 String uploadId = unrest.getUploadId(); return uploadId; } /** * 获取bucket文件的下载链接 * * @param pathFile 首字母不带/的路径和文件 * @param bucketName * @return 上报返回null, 成功返回地址 */ public String getDownloadUrl(String pathFile, String bucketName) { if (bucketName == null || "".equals(bucketName)) { bucketName = bucketName; } StringBuffer url = new StringBuffer(); url.append("http://").append(bucketName).append(endpoint).append("/"); if (pathFile != null && !"".equals(pathFile)) { url.append(pathFile); } return url.toString(); } /** * 上传文件到阿里云,并生成url * * @param filedir (key)文件名(不包括后缀) * @param in 文件字节流 * @return String 生成的文件url */ public String uploadToAliyun(String filedir, InputStream in, String fileName, boolean isRandomName) { String suffix = fileName.substring(fileName.lastIndexOf(".") + 1); if (isRandomName) { fileName = UUIDGenerator.generateCommonUUID() + "." + suffix; } logger.debug("------------>文件名称为: " + fileName); OSSClient ossClient = new OSSClient(internalEndpoint, accessKeyId, accessKeySecret); String url = null; try { // 创建上传Object的Metadata ObjectMetadata objectMetadata = new ObjectMetadata(); objectMetadata.setContentLength(in.available()); objectMetadata.setCacheControl("no-cache");// 设置Cache-Control请求头,表示用户指定的HTTP请求/回复链的缓存行为:不经过本地缓存 objectMetadata.setHeader("Pragma", "no-cache");// 设置页面不缓存 objectMetadata.setContentType(getcontentType(suffix)); objectMetadata.setContentDisposition("inline;filename=" + fileName); // 上传文件 ossClient.putObject(bucketName, filedir + "/" + fileName, in, objectMetadata); url = buildUrl(filedir + "/" + fileName); } catch (IOException e) { logger.error("error", e); } finally { ossClient.shutdown(); try { if (in != null) { in.close(); } } catch (IOException e) { logger.error("error", e); } } return url; } private String buildUrl(String fileDir) { StringBuffer url = new StringBuffer(); if (org.apache.commons.lang3.StringUtils.isEmpty(bucketName)) { logger.error("bucketName为空"); return null; } if (org.apache.commons.lang3.StringUtils.isEmpty(endpoint)) { logger.error("endpoint为空"); return null; } if (StringUtils.isEmpty(endpoint)) { logger.error("上传文件目录为空"); return null; } url.append("https://").append(bucketName).append(".").append(endpoint).append("/").append(fileDir); return url.toString(); } /** * 删除图片 * * @param key */ public void deletePicture(String key) { OSSClient ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret); ossClient.deleteObject(bucketName, key); ossClient.shutdown(); } /** * Description: 判断OSS服务文件上传时文件的contentType * * @param suffix 文件后缀 * @return String HTTP Content-type */ public String getcontentType(String suffix) { if (suffix.equalsIgnoreCase("bmp")) { return "image/bmp"; } else if (suffix.equalsIgnoreCase("gif")) { return "image/gif"; } else if (suffix.equalsIgnoreCase("jpeg") || suffix.equalsIgnoreCase("jpg")) { return "image/jpeg"; } else if (suffix.equalsIgnoreCase("png")) { return "image/png"; } else if (suffix.equalsIgnoreCase("html")) { return "text/html"; } else if (suffix.equalsIgnoreCase("txt")) { return "text/plain"; } else if (suffix.equalsIgnoreCase("vsd")) { return "application/vnd.visio"; } else if (suffix.equalsIgnoreCase("pptx") || suffix.equalsIgnoreCase("ppt")) { return "application/vnd.ms-powerpoint"; } else if (suffix.equalsIgnoreCase("docx") || suffix.equalsIgnoreCase("doc")) { return "application/msword"; } else if (suffix.equalsIgnoreCase("xls") || suffix.equalsIgnoreCase("xlsx")) { return "application/vnd.ms-excel"; } else if (suffix.equalsIgnoreCase("xml")) { return "text/xml"; } else if (suffix.equalsIgnoreCase("mp3")) { return "audio/mp3"; } else if (suffix.equalsIgnoreCase("amr")) { return "audio/amr"; } else if (suffix.equalsIgnoreCase("pdf")) { return "application/pdf"; } else { return "text/plain"; } } }
实体类对象:
package com.macro.mall.tiny.demo.model.po.mall; import com.aliyun.oss.model.PartETag; import lombok.Data; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * @author zhangtonghao * @create 2022-04-06 15:08 */ @Data public class UmsAdminLoginLogDO { /** * 初始化任务id */ private String taskId; /** * 上传文件类型 */ private String fileType; /** * 文件总片数 */ private Integer fileSlicesNum; /** * 分片编号(1-10000有序的编号,越大的编号位置越靠后) */ private Integer sliceNo; /** * 本次请求文件的md5值 */ private String fileMD5; /** *文件流数据 */ private byte[] content; /** * 文件名称 */ private String fileName; /** * oss初始化分片id */ private String ossSlicesId; /** * 最小分片大小(分片上传是除最后一片外,其他文件不得小于该值) */ private String minSliceSize; Map<Integer, PartETag> partETags = new HashMap<>(16); }
文件流数据:content,可以换成file等类型,最后转换成oss所需文件流即可,合格的程序员应当学会灵活应变相关代码,哈哈哈。
结语:其实分片上传和普通的上传只是多了一个合并文件的步骤,其他的都是差不多;因为研究时间较短,还有些资料没有查出,比如PartETag这代表含义等。有需要补充的欢迎在下面补充。
创作不易,如果这篇文章对你有用,请点个赞谢谢♪(・ω・)ノ!
#java##技术##实战##工作##oss#技术 文章被收录于专栏
不秃头