S3 文件操作使用实践

目录

1.引入 AWS SDK for Java2.x

2.创建 S3Client

3.上传对象

3.1 直接上传对象

3.2 分段上传对象

 4.下载对象

4.1 下载对象到指定目录

4.2 完整性验证

4.2.1 未分段对象的验证

4.2.2 分段对象的验证

5.验证对象是否存在

6.风险和优化


1.引入 AWS SDK for Java2.x

通过 Maven 引入依赖,截至 2021 年 12 月 22 日,最新版本:2.17.101

<properties>
    <awssdk.version>2.17.101</awssdk.version>
</properties>
<dependencyManagement>
    <dependencies>
        .........
        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>bom</artifactId>
            <version>${awssdk.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependencies>
    .........
    <!-- S3 dependencies start -->
    <dependency>
        <groupId>software.amazon.awssdk</groupId>
        <artifactId>s3</artifactId>
    </dependency>
    <dependency>
        <groupId>software.amazon.awssdk</groupId>
        <artifactId>sts</artifactId>
    </dependency>
    <!-- S3 dependencies end -->
    ....
</dependencies>

2.创建 S3Client

  • 鉴权问题
  • 初始化的时候必须指定区域 Region
  • S3Client 使用完毕之后,要调用它的 close 方法,释放资源,推荐使用显式关闭的方式,以免遗漏
/**
     * @param region  区域
     * @param roleARN 如果有值,则使用 roleARN 鉴权方式。如果没有,则使用默认鉴权方式
     * @return S3Client
     */
    public static S3Client createS3Client(String region, String roleARN) {
        var builder = S3Client.builder().region(Region.of(region));

        if (StringUtils.isNotBlank(roleARN)) {
            var assumeRoleRequest = AssumeRoleRequest.builder()
                    .roleArn(roleARN)
                    .roleSessionName(SESSION_NAME_PREFIX + System.nanoTime())
                    .build();
            var provider = StsAssumeRoleCredentialsProvider.builder()
                    .stsClient(StsClient.create())
                    .refreshRequest(assumeRoleRequest)
                    .asyncCredentialUpdateEnabled(true)
                    .build();
            builder.credentialsProvider(provider);
        }

        return builder.build();
    }

3.上传对象

  • 上传对象的方式分为直接上传,以及,将大文件分段上传。AWS 官方建议超过 100MB 的文件就可以考虑分段上传,请参考分段上传的官方文档

3.1 直接上传对象

通过 contentMD5() 方法应该加入上传对象的 MD5 校验:

private static void wholeUpload(String bucketName, String objectKey, 
                                File file, S3Client s3Client, 
                                FileInputStream fis) throws IOException {
    String md5 = new String(Base64.encodeBase64(DigestUtils.md5(fis)));
    PutObjectRequest objectRequest = PutObjectRequest.builder()
            .bucket(bucketName)
            .key(objectKey)
            .contentMD5(md5)
            .build();
    RequestBody requestBody = RequestBody.fromFile(file);
    s3Client.putObject(objectRequest, requestBody);
}

3.2 分段上传对象

  • 应该给每一段都加上各自的 md5 校验分段上传的完整性
  • 对于一个大文件,如果我们不分段,可以直接使用文件输入流计算 md5 和上传,这样不会有内存压力;但是使用分段上传之后,要计算每一段内容的 md5 值,就不能直接使用文件输入流了,得按照设置的分段大小从文件输入流中读取每一段,再计算每一段的 md5 值,这样就把分段大小的内容读取到了内存中,会对内容增加分段大小这么多的压力。串行的情况下,每次读取一段,只会增加一段内容的内存压力,如果程序串行的话,就相当于把整个文件加载到了内存中,压力会更大
  • 如果上传发生异常,应该主动关闭分段上传,尝试重新上传
  • 建议对 s3 bucket 启用 AbortIncompleteMultipartUploadAmazon 生命周期规则,该规则指示 S3 中止没有在指定天数内完成的分段上传,并删除未完成的上传数据,参考关于 S3 生命周期的官方文档
private static void multipartUpload(String bucketName, String objectKey, File file, S3Client s3Client, FileInputStream fis) {
    CreateMultipartUploadRequest createMultipartUploadRequest = CreateMultipartUploadRequest.builder()
            .bucket(bucketName)
            .key(objectKey)
            .build();

    CreateMultipartUploadResponse response = s3Client.createMultipartUpload(createMultipartUploadRequest);
    String uploadId = response.uploadId();
    try {
        List<CompletedPart> completedParts = new ArrayList<>();
        final long fileLength = file.length();
        final long partNumber = getPartNumber(fileLength);
        log.info("multipartUpload fileLength={}, partNumber={},uploadId={}", fileLength, partNumber, uploadId);
        for (int i = 1; i <= partNumber; i++) {
            final byte[] bytes = fis.readNBytes((int) PART_SIZE);
            String md5 = new String(Base64.encodeBase64(DigestUtils.md5(bytes)));
            UploadPartRequest uploadPartRequest = UploadPartRequest.builder()
                    .bucket(bucketName)
                    .key(objectKey)
                    .uploadId(uploadId)
                    .partNumber(i)
                    .contentMD5(md5)
                    .build();
            final RequestBody requestBody = RequestBody.fromBytes(bytes);
            UploadPartResponse uploadPartResponse = s3Client.uploadPart(uploadPartRequest, requestBody);
            String eTag = uploadPartResponse.eTag();
            CompletedPart part = CompletedPart.builder().partNumber(i).eTag(eTag).build();
            completedParts.add(part);
        }

        CompletedMultipartUpload completedMultipartUpload = CompletedMultipartUpload.builder()
                .parts(completedParts)
                .build();

        CompleteMultipartUploadRequest completeMultipartUploadRequest =
                CompleteMultipartUploadRequest.builder()
                        .bucket(bucketName)
                        .key(objectKey)
                        .uploadId(uploadId)
                        .multipartUpload(completedMultipartUpload)
                        .build();

        s3Client.completeMultipartUpload(completeMultipartUploadRequest);
    } catch (Exception e) {
        log.error("S3 multipartUpload fail!", e);
        // 停止正在进行的分段上传,清理已上传分段
        s3Client.abortMultipartUpload(AbortMultipartUploadRequest.builder()
                .bucket(bucketName)
                .key(objectKey)
                .uploadId(uploadId)
                .build());
    }

}

 4.下载对象

  • 下载对象到指定的临时目录,在根据返回的 eTag 值校验文件的完整性
  • 如果验证失败,删除已下载的缺损文件

4.1 下载对象到指定目录

public static boolean getObjectByKey(String bucketName, String objectKey, 
                                     String path) {
    log.info("S3Utils getObjectByKey key={}", objectKey);
    // 1.从 S3 获取文件
    GetObjectRequest objectRequest = GetObjectRequest
            .builder()
            .key(objectKey)
            .bucket(bucketName)
            .build();
    try (S3Client s3Client = S3Utils.createS3Client(S3Utils.REGION, null);
         ResponseInputStream<GetObjectResponse> responseInputStream = s3Client.getObject(objectRequest);
         FileOutputStream fileOutputStream = new FileOutputStream(path)) {
        String eTag = responseInputStream.response().eTag().replaceAll("\"", "");
        log.info("eTag={}", eTag);
        // 2.下载文件到临时目录
        byte[] bufferByte = new byte[STREAM_BUFFER_LENGTH];
        int len;
        while ((len = responseInputStream.read(bufferByte)) != -1) {
            fileOutputStream.write(bufferByte, 0, len);
        }
        fileOutputStream.flush();
        // 3.验证文件的完整性
        boolean validateResult = validateFile(path, eTag);
        if (validateResult) {
            return true;
        }
    } catch (Exception e) {
        log.error("deleteObjectByKey error", e);
    }
    // 4.如果验证失败,删除文件
    FileUtils.deleteFile(path);
    return false;
}

4.2 完整性验证

验证方法,根据返回对象的 eTag 进行验证

4.2.1 未分段对象的验证

未分段对象的 eTag = DigestUtils.md5Hex(inputStream) 示例:

"ETag": "\"f7225931e6cc461dd14879fd340aba44\""

只需要 eTag 与 DigestUtils.md5Hex(inputStream) 的值相等即可

4.2.2 分段对象的验证

分段对象的 eTag = DigestUtils.md5Hex((DigestUtils.md5(part1) + DigestUtils.md5(part2) + ........+ DigestUtils.md5(partn))) + "-n" ,示例:

"ETag": "\"6be8dea194cee773daf9f07446f3a520-3\""

,”-3“ 表示这个对象分了三段

4.2.1 和 4.2.2 的验证示例代码

private static boolean validateFile(String path, String eTag) throws IOException {
    try (InputStream inputStream = new FileInputStream(path);
         ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
        String localMd5Hex;
        final String[] eTags = eTag.split(S3_ETAG_SEPARATOR);
        if (eTags.length > 1) { // 分段上传的文件
            int parts = Integer.parseInt(eTags[1]);
            for (int i = 0; i < parts; i++) {
                byte[] bytes = inputStream.readNBytes((int) PART_SIZE);
                byte[] md5Bytes = DigestUtils.md5(bytes);
                byteArrayOutputStream.write(md5Bytes);
            }
            localMd5Hex = DigestUtils.md5Hex(byteArrayOutputStream.toByteArray());
        }else{
            localMd5Hex = DigestUtils.md5Hex(inputStream);
        }
        log.info("localMd5Hex={}", localMd5Hex);
        if (eTags[0].equals(localMd5Hex)) { // 文件完整性校验
            return true;
        }
    } catch (Exception e) {
        log.error("validateFile error", e);
    }
    return false;
}

5.验证对象是否存在

  • sdk-java v1 版本可以用 s3Client.doesObjectExist() ,但是 sdk-java v2 里取消了这个方法,建议使用 s3Client.headObject(),如果对象不存在,会报 NoSuchKeyException。更多 sdk 服务变更请参考 S3 sdk Service Changes
  • 示例代码
public static boolean doesObjectExist(String bucketName, String objectKey) {
    if (StringUtils.isAnyBlank(bucketName, objectKey)) {
        return false;
    }
    S3Client s3Client = null;
    try {
        s3Client = createS3Client(REGION, null);
        HeadObjectRequest objectRequest = HeadObjectRequest
                .builder()
                .key(objectKey)
                .bucket(bucketName)
                .build();

        s3Client.headObject(objectRequest);
        return true;
    } catch (NoSuchKeyException e) {
        // 如果 key 不存在,会报 NoSuchKeyException
        log.error("noSuchKey bucketName={}, objectKey={}", bucketName, objectKey);
    } catch (S3Exception e) {
        log.error("S3Exception", e);
    } finally {
        if(s3Client != null){
            s3Client.close();
        }
    }
    return false;
}

6.风险和优化

  • 分段上传的完整性校验和分段对象下载的完整性校验,存在内存方面的忧虑,可以改良代码,以一种比较平滑的方式来计算
  • 分段上传可以并行进行,这样可以充分利用带宽,加快对象上传速度。但在内存压力问题没有解决之前还是不要这么做
  • 校验时无法知道它以前是按多大分段的,如果程序内分段大小参数改变了,再下载以前上传的分段对象,校验的时候肯定要出错了

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Java 2实用教程(第三版)实验指导与习题解答 清华大学出版社 (编著 耿祥义 张跃平) 实验模版代码 建议使用文档结构图 (选择Word菜单→视图→文档结构图) 上机实践1 初识Java 4 实验1 一个简单的应用程序 4 实验2 一个简单的Java Applet程序 4 实验3 联合编译 5 上机实践2 基本数据类型与控制语句 6 实验1 输出希腊字母表 6 实验2 回文数 6 实验3 猜数字游戏 8 上机实践3 类与对象 9 实验1 三角形、梯形和圆形的类封装 9 实验2 实例成员与类成员 12 实验3 使用package语句与import语句 13 上机实践4 继承与接口 15 实验1 继承 15 实验2 上转型对象 17 实验3 接口回调 18 上机实践5 字符串、时间与数字 19 实验1 String类的常用方法 19 实验2 比较日期的大小 21 实验3 处理大整数 22 上机实践6 组件及事件处理 23 实验1 算术测试 23 实验2 信号灯 25 实验3 布局与日历 28 上机实践7 组件及事件处理2 31 实验1 方程求根 31 实验2 字体对话框 34 实验3 英语单词拼写训练 37 上机实践8 多线程 41 实验1 汉字打字练习 41 实验2 旋转的行星 43 实验3 双线程接力 47 上机实践9 输入输出流 50 实验1 学读汉字 50 实验2 统计英文单词字 53 实验2 读取Zip文件 56 上机实践10 Java 中的网络编程 57 实验1 读取服务器端文件 57 实验2 使用套接字读取服务器端对象 59 实验3 基于UDP的图像传输 62 上机实践11 数据结构 66 实验1 扫雷小游戏 66 实验2 排序与查找 70 实验3 使用TreeSet排序 72 上机实践12 java Swing 74 实验1 JLayeredPane分层窗格 74 实验2 使用表格显示日历 75 实验3 多文档界面(MDI) 78 上机实践1 初识Java 实验1 一个简单的应用程序 2.模板代码 Hello.java package 实验一; public class Hello { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub System.out.println("你好,很高兴学习Java"); //命令行窗口输出"你好,很高兴学习Java" A a=new A(); a.fA(); } } class A { void fA() {System.out.println("we are student"); } } 实验2 一个简单的Java Applet程序 2.模板代码 FirstApplet.java import java.applet.*; import java.awt.*; public class FirstApplet extends Applet { public void paint(Graphics g) { g.setColor(Color.blue); g.drawString("这是一个Java Applet 程序",10,30);//在Java Applet中绘制一行文字:“这是一个Java Applet 程序” g.setColor(Color.red); g.setFont(new Font("宋体",Font.BOLD,36)); g.drawString("我改变了字体",20,50);//在Java Applet中绘制一行文字:“我改变了字体” } }实验3 联合编译 2.模板代码 public class MainClass { public static void main (String args[ ]) { System.out.println("你好,只需编译我") ; //命令行窗口输出"你好,只需编译我" A a=new A(); a.fA(); B b=new B(); b.fB(); } } public class A { void fA() {

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值