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这代表含义等。有需要补充的欢迎在下面补充。

创作不易,如果这篇文章对你有用,请点个赞谢谢♪(・ω・)ノ!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,451评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,172评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,782评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,709评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,733评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,578评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,320评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,241评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,686评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,878评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,992评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,715评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,336评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,912评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,040评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,173评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,947评论 2 355

推荐阅读更多精彩内容