能获取进度的阿里云OSS简单上传及线程池实现分片上传

阿里云OSS能获取进度的简单上传及线程池实现分片上传

分片上传借鉴代码自博客https://blog.csdn.net/qq_40274351/article/details/89300605?utm_medium=distribute.pc_relevant_t0.none-task-blog-OPENSEARCH-1.channel_param&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-OPENSEARCH-1.channel_param

application.yml

#阿里OSS分片上传配置
aliOss:
  multipartUpload:
    partCount: 100     # 分片数量
    threshold: 524288000 # 启用分片上传阈值:500M  500 * 1024 * 1024
    threadCount: 3     # 线程数

controller层

@RestController
@RequestMapping("/api/import")
public class ImportController {
    @Resource
    private ImportService importService;

    @Resource
    private OssService ossService;

    private static final Logger logger = LoggerFactory.getLogger(ImportController.class);

    @PostMapping("/upload")
    @ResponseBody
    public CommonResult<String> upload(MultipartFile file, HttpServletRequest request, HttpSession session) throws IOException {
        return importService.upload(file, session);
    }


    /**
     * 简单列举分片上传事件
     * @param request
     * @return
     */
    @RequestMapping ("/listMultipartUploads")
    @ResponseBody
    public CommonResult<String> listMultipartUploads(HttpServletRequest request, HttpSession session) {
        return importService.listMultipartUploads(session);
    }

    /**
     * 通过fileName获取实时长传进度
     * @param request
     * @return
     */
    @RequestMapping ("/percent")
    @ResponseBody
    public CommonResult<String> getUploadPercent(HttpServletRequest request, String fileName) {
        HttpSession session = request.getSession();
        String name = StringUtil.isNotBlank(fileName) ? fileName : "upload_percent";
        Double percent = session.getAttribute(name) == null ? 0 : Double.valueOf(session.getAttribute(name).toString());
        logger.debug(name + ":" + session.getAttribute(name));
        return new CommonResult<>(String.valueOf(percent));
    }

    /**
     * 重置上传进度
     * @param request
     * @return
     */
    @PostMapping("/percent/reset")
    public CommonResult resetPercent(HttpServletRequest request, String fileName) {
        HttpSession session = request.getSession();
        String name = StringUtil.isNotBlank(fileName) ? fileName : "upload_percent";
        session.setAttribute(name, 0);
        return new CommonResult(CommonResultEmnu.OK);
    }

    /**
     * @Description: OSS删除
     */
    @RequestMapping ("/ossDel")
    public CommonResult ossDel(String filePath) {
        ossService.OssDel(filePath);
        return new CommonResult(CommonResultEmnu.OK);
    }
}

service层

@Service
@Primary
@Slf4j
public class ImportServiceOss implements ImportService {
    @Value("${oss.filePrefix}")
    private String filePrefix;

    /**
     * 阈值,达到阈值使用分片上传
     */
    @Value("${aliOss.multipartUpload.threshold}")
    private int threshold;

    @Override
    public CommonResult<String> upload(MultipartFile file, HttpSession session) {
        if (file == null) {
            return new CommonResult<>(CommonResultEmnu.ERROR, "文件为空");
        }

        if (!file.isEmpty()) {
                String fileName = file.getOriginalFilename();
                if (fileName == null) {
                    return new CommonResult<>(CommonResultEmnu.INVALID_PARAMS, "获取文件名失败");
                }
//                InputStream inputStream = file.getInputStream();
                String suffix = fileName.substring(fileName.lastIndexOf(".") + 1);
                String newName = IdGenerator.getUUID() + "." + suffix.toLowerCase(Locale.ENGLISH);
                String filePath = filePrefix + "_upload" + ResourceUtil.createFilePath().replaceAll("/", "_") + newName;

                try {
                    //MultipartFile转File
                    File newFile = new File(fileName);
                    FileOutputStream os = new FileOutputStream(newFile);
                    os.write(file.getBytes());
                    os.close();
                    //将上传文件写入目标文件
                    file.transferTo(newFile);

                    //大于阈值走分片上传,否则直接简单上传
                    long length = newFile.length();
                    if (length < threshold) {
                        return this.simpleUpload(newFile, session, fileName, filePath);
                    } else {
                        return this.multipartUpload(newFile, session, fileName, filePath);
                    }
                } catch (Exception e) {
                    log.error("MultipartFile转File失败: ", e);
                    return new CommonResult<>(CommonResultEmnu.ERROR, "MultipartFile转File失败!");
                }

        } else {
            return new CommonResult<>(CommonResultEmnu.ERROR, "上传失败,因为文件是空的");
        }
    }


    /**
     * 简单上传
     */
    public CommonResult<String> simpleUpload(File f, HttpSession session, String oldFileName, String filePath) {
        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(OSSConfig.ENDPOINT, OSSConfig.ACCESSKEYID, OSSConfig.ACCESSKEYSECRET);

        // 异步处理,可以使用线程池
        Thread t = new Thread(() -> {
            try {
                // 带进度条的上传。
                ossClient.putObject(new PutObjectRequest(OssUtil.getBucketName(filePath), filePath, f).
                        withProgressListener(new PutObjectProgressListener(session, oldFileName)));
            } catch (Exception e) {
                log.error("上传OSS文件出现异常:", e);

            } finally {
                //保证删除服务器上临时文件
                f.delete();
                // 关闭OSSClient。
                ossClient.shutdown();
            }
        }, "简单上传-" + oldFileName);

        t.start();

        String url = OssUtil.getUrl(filePath);
        log.info("upload_url{} ", url);
        return new CommonResult<>(url);
    }

    /**
     * 分片上传
     */
    public CommonResult<String> multipartUpload(File newFile, HttpSession session, String oldFileName, String filePath) {

        // 异步处理,可以使用线程池
        Thread t = new Thread(() -> {
            try {
                AliyunOSSUtil.upLoad(newFile, session, filePath, oldFileName);
            } catch (Throwable e) {
                log.error("上传OSS文件出现异常:", e);
            } finally {
                //保证删除服务器上临时文件
                newFile.delete();
            }
        });

        t.start();

        String url = OssUtil.getUrl(filePath);
        log.info("upload_url:{} ", url);
        return new CommonResult<>(url);
    }

    /**
     * 查询所有未完成的分片上传信息
     */
    @Override
    public CommonResult<String> listMultipartUploads(HttpSession session) {
        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(OSSConfig.ENDPOINT, OSSConfig.ACCESSKEYID, OSSConfig.ACCESSKEYSECRET);

        String bucketName = OssUtil.getBucketName(filePrefix + "_");

        // 列举分片上传事件。默认列举1000个分片。
        ListMultipartUploadsRequest listMultipartUploadsRequest = new ListMultipartUploadsRequest(bucketName);
        MultipartUploadListing multipartUploadListing = ossClient.listMultipartUploads(listMultipartUploadsRequest);

        for (MultipartUpload multipartUpload : multipartUploadListing.getMultipartUploads()) {
            // 获取uploadId。
            multipartUpload.getUploadId();
            // 获取Key。
            multipartUpload.getKey();
            // 获取分片上传的初始化时间。
            multipartUpload.getInitiated();
        }
        // 关闭OSSClient。
        ossClient.shutdown();
        return null;
    }
简单上传的自定义监听器,用于获取进度
@Slf4j
public class PutObjectProgressListener implements ProgressListener {
    private long bytesWritten = 0;
    private long totalBytes = -1;
    private boolean succeed = false;
    private HttpSession session;
    private String fileName;

    private static final Logger logger = LoggerFactory.getLogger(PutObjectProgressListener.class);

    //构造方法中加入session
    public PutObjectProgressListener() {
    }

    public PutObjectProgressListener(HttpSession mSession, String fileName) {
        this.session = mSession;
        int percent = 0;
        this.fileName = StringUtil.isNotBlank(fileName) ? fileName : "upload_percent";
        session.setAttribute(this.fileName, percent);
    }

    @Override
    public void progressChanged(ProgressEvent progressEvent) {
        long bytes = progressEvent.getBytes();
        ProgressEventType eventType = progressEvent.getEventType();
        switch (eventType) {
            case TRANSFER_STARTED_EVENT:
                logger.debug("Start to upload......");
                break;
            case REQUEST_CONTENT_LENGTH_EVENT:
                this.totalBytes = bytes;
                logger.debug(this.totalBytes + " bytes in total will be uploaded to OSS");
                break;
            case REQUEST_BYTE_TRANSFER_EVENT:
                this.bytesWritten += bytes;
                if (this.totalBytes != -1) {
                    int percent = (int) (this.bytesWritten * 100.0 / this.totalBytes);
                    //将进度percent放入session中 官网demo中没有放入session这一步
                    session.setAttribute(this.fileName, percent);
                    logger.debug(bytes + " bytes have been written at this time, upload progress: " + percent + "%(" + this.bytesWritten + "/" + this.totalBytes + ")");
                } else {
                    logger.debug(bytes + " bytes have been written at this time, upload ratio: unknown" + "(" + this.bytesWritten + "/...)");
                }
                break;
            case TRANSFER_COMPLETED_EVENT:
                this.succeed = true;
                logger.debug("Succeed to upload, " + this.bytesWritten + " bytes have been transferred in total");
                break;
            case TRANSFER_FAILED_EVENT:
                logger.debug("Failed to upload, " + this.bytesWritten + " bytes have been transferred");
                break;
            default:
                break;
        }
    }

    public boolean isSucceed() {
        return succeed;
    }

可以获取进度的分片上传方法实现

@Component
public class AliyunOSSUtil {
    /**
     * 分片数量,给静态变量用@Value赋值  需要写在set方法上
     */
    private static int defaultPartCount;

    /**
     * 线程数量
     */
    private static int threadCount;

    private static final org.slf4j.Logger logger = LoggerFactory.getLogger(AliyunOSSUtil.class);


    /**
     * 上传文件
     **/
    public static void upLoad(File file, HttpSession session, String filePath, String oldFileName) {

        //创建一个可重用固定线程数的无界队列线程池  最多一百零一个任务,无需担心队列存放任务太多
        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

        //判断文件
        if (file == null) {
            return;
        }

        ClientBuilderConfiguration conf = new ClientBuilderConfiguration();
        //连接空闲超时时间,超时则关闭
        conf.setIdleConnectionTime(2000);
        //创建OSSClient实例,需要放到这里,不能把OSS作为静态变量,如果多个客户同时上传,客户A关闭ossClient,而客户B正用着ossClient
        OSS ossClient = new OSSClientBuilder().build(OSSConfig.ENDPOINT, OSSConfig.ACCESSKEYID, OSSConfig.ACCESSKEYSECRET, conf);
        //记录上传片数,线程安全,不能静态变量,客户A和客户B同时上传,片数到最后大于partSize
        AtomicInteger uploadedPart = new AtomicInteger(0);
        /*分片上传*/
        /*1.初始化一个分片上传事件*/
        InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(OssUtil.getBucketName(filePath), filePath);
        InitiateMultipartUploadResult result = ossClient.initiateMultipartUpload(request);
        String uploadId = result.getUploadId();
//        session.setAttribute("uploadId", uploadId);
        try {
            /*2.上传分片**/
            long partSize;
            long fileLength = file.length();

            //按默认片数计算每片大小
            partSize = (int) fileLength / defaultPartCount;

            //如果100片不能分完,就是101片
            int partCount;
            if (fileLength % partSize != 0) {
                partCount = defaultPartCount + 1;
            } else {
                partCount = defaultPartCount;
            }

            //返回uploadId,它是分片上传事件的唯一标识,可以根据这个ID来发起相关操作,如取消分片上传、查询分片上传等
            // 装饰者模式加锁,保证线程安全
            List<PartETag> partETags = Collections.synchronizedList(new ArrayList<>(partCount));
  
            //遍历分片上传
            for (int i = 0; i < partCount; i++) {
                long startPos = i * partSize;
                //是否为最后一块分片
                long curPartSize = (i + 1 == partCount) ? (fileLength - startPos) : partSize;
                //getBucketName(filePath)是Bucket的名字
                executorService.execute(new PartUploader(ossClient, OssUtil.getBucketName(filePath), file, startPos, curPartSize, i + 1, uploadId, partETags, partCount, session, filePath, oldFileName,uploadedPart));
            }
            //等待所有的分片完成    执行后不再接收新任务,如果里面有任务,就执行完
            executorService.shutdown();
            while (!executorService.isTerminated()) {
                try {
                    executorService.awaitTermination(5, TimeUnit.SECONDS);
                } catch (InterruptedException e) {
                    logger.error(e.getMessage());
                }
            }

            //重新设为0片
            uploadedPart.set(0);

            //验证是否所有的分片都完成
            if (partETags.size() != partCount) {
                throw new IllegalStateException("文件的某些部分上传失败!");
            }

            /*3.完成分片上传**/
            //排序。partETags必须按分片号升序排列
            partETags.sort(new Comparator<PartETag>() {
                @Override
                public int compare(PartETag o1, PartETag o2) {
                    return o1.getPartNumber() - o2.getPartNumber();
                }
            });
            //在执行该操作时,需要提供所有有效的partETags。OSS收到提交的partETags后,会逐一验证每个分片的有效性。当所有的数据分片验证通过后,OSS将把这些分片组合成一个完整的文件。
            CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest(OssUtil.getBucketName(filePath), filePath, uploadId, partETags);
            ossClient.completeMultipartUpload(completeMultipartUploadRequest);
            /*//设置权限
            ossClient.setBucketAcl(OssUtil.getBucketName(filePath), CannedAccessControlList.PublicRead);*/

        } catch (OSSException oe) {
            logger.error(oe.getMessage());
        } catch (ClientException ce) {
            logger.error(ce.getErrorMessage());
        } finally {
            //关闭OSSClient
            ossClient.shutdown();
        }
    }

    /**
     * 实现并启动线程
     **/
    private static class PartUploader implements Runnable {
        private final String bucketName;
        private final int partCount;
        private final File localFile;
        private final long startPos;
        private final long partSize;
        private final int partNumber;
        private final String uploadId;
        private List<PartETag> partETags;
        private final HttpSession session;
        private final String key;
        private final String oldFileName;
        private final OSS ossClient;
        private AtomicInteger uploadedPart;

        public PartUploader(OSS ossClient, String bucketName, File localFile, long startPos, long partSize, int partNumber, String uploadId, List<PartETag> partETags, int partCount, HttpSession session, String key, String oldFileName, AtomicInteger uploadedPart) {
            this.ossClient = ossClient;
            this.bucketName = bucketName;
            this.localFile = localFile;
            this.startPos = startPos;
            this.partNumber = partNumber;
            this.uploadId = uploadId;
            this.partSize = partSize;
            this.partETags = partETags;
            this.partCount = partCount;
            this.session = session;
            this.key = key;
            this.oldFileName = oldFileName;
            this.uploadedPart = uploadedPart;
        }

        @Override
        public void run() {
            Thread.currentThread().setName("文件分片上传pool-" + oldFileName + "-" + partNumber);
            InputStream inputStream = null;
            try {
                inputStream = new FileInputStream(this.localFile);
                //跳过已经上传的分片
                inputStream.skip(startPos);
                UploadPartRequest uploadPartRequest = new UploadPartRequest();
                uploadPartRequest.setBucketName(bucketName);
                uploadPartRequest.setKey(key);
                uploadPartRequest.setUploadId(this.uploadId);
                uploadPartRequest.setInputStream(inputStream);
                //设置分片大小。除了最后一个分片没有大小限制,其他分片最小为100KB
                uploadPartRequest.setPartSize(this.partSize);
                //设置分片号。每一个上传的分片都有一个分片号,取值范围是1~10000,如果超出这个范围,OSS将返回InvalidArgum的错误码
                uploadPartRequest.setPartNumber(this.partNumber);
                //每个分片不需要按顺序上传,甚至可以在不同客户端上传,OSS会根据分片号排序组成完整的文件。
                UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
//                System.out.println("Part#" + this.partNumber + "done");
//                System.out.println();
                //已经上传的片数加1
                uploadedPart.incrementAndGet();
                //每次上传分片之后,OSS的返回结果会包含一个PartETag。PartETag将被保存到PartETags中。
                this.partETags.add(uploadPartResult.getPartETag());

                //计算进度
                int percent = (int) (uploadedPart.intValue() * 100.0 / partCount);
                session.setAttribute(oldFileName, percent);

            } catch (Exception e) {
                logger.error(e.getMessage());
            } finally {
                if (inputStream != null) {
                    try {
                        inputStream.close();
                    } catch (IOException e) {
                        logger.error(e.getMessage());
                    }
                }
            }
        }
    }


    @Value("${aliOss.multipartUpload.partCount}")
    public void setDefaultPartCount(int defaultPartCount) {
        AliyunOSSUtil.defaultPartCount = defaultPartCount;
    }

    @Value("${aliOss.multipartUpload.threadCount}")
    public void setThreadCount(int threadCount) {
        AliyunOSSUtil.threadCount = threadCount;
    }

获取进度条接口,前端可以循环调用以下接口,传入文件名,获取进度

    @RequestMapping ("/percent")
    @ResponseBody
    public CommonResult<Object> getUploadPercent(HttpServletRequest request, String fileName) {
        HttpSession session = request.getSession();
        String name = StringUtil.isNotBlank(fileName) ? fileName : "upload_percent";
        return new CommonResult<>(session.getAttribute(name));
    }
  • 7
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值