• 通过S3协议实现通用的文件存储服务中间件


    通过S3协议实现通用的文件存储服务中间件


    引言

    在日常开发文件上传相关服务时,通常都会选择腾讯云,阿里云,七牛云等提供的oss服务作为文件存储系统,如果需要自行搭建文件存储系统,通常则会采用minio等开源项目。

    但是大家有没有考虑过,不同的厂商或者开源项目提供的客户端sdk都是不同的,如果项目开发过程中,需要切换底层文件系统,那么通常情况下意味着,我们需要完全替换掉相关文件上传代码,如果微服务项目,则需要替换掉所有使用到文件上传sdk微服务的代码,这显然会带来巨大的工作量。

    为了解决上面这个问题,我们有如下两个思路:

    • 项目中针对文件上传写出一个单独的抽象层接口,底层不同文件存储系统,提供对应的实现即可:

    在这里插入图片描述
    这个思路很容易想到,利用门面模型向调用方屏蔽底层实现,但是其实这里还有更加简洁的实现方式。

    • 基本所有云服务厂商提供的oss服务和开源的oss项目都遵循了S3协议,是Simple Storage Service的缩写,即简单存储服务,因此其实我们这里利用这一点,写出一个通用的文件中间件,利用该中间件后,我们写的客户端api就对任何实现了S3协议的oss服务进行访问。

    在这里插入图片描述


    使用演示

    这里我们以Minio作为演示案例,不清楚minio的可以查看minio官方文档学习一下,下面我们先用docker方式安装一下minio:

    安装minio

    docker pull minio/minio
    
    docker run --name minio \
    -p 9000:9000 \
    -p 9090:9090 \
    -d --restart=always \
    -e "MINIO_ROOT_USER=admin" \
    -e "MINIO_ROOT_PASSWORD=admin123" \
    -v /usr/local/minio/data:/data \
    -v /usr/local/minio/config:/root/.minio \
    minio/minio server /data \
    --console-address '0.0.0.0:9090'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    注意,这里要单独设置console的端口,不然会报错,且无法访问

    这种安装方式 MinIO 自定义 Access 和 Secret 密钥要覆盖 MinIO 的自动生成的密钥

    登录客户端(浏览器):注意—>此处的端口,是你设置的console的端口:9090

    在这里插入图片描述

    此处的用户名密码为启动服务时,设置的用户名密码:admin admin123。

    minio基本bucket操作不再详述,和普通的oss服务一样。


    构建Starter

    • gitee仓库地址

    https://gitee.com/DaHuYuXiXi/common-oss-service

    • 模块结构
      在这里插入图片描述
    • pom配置
    <dependencies>
        <dependency>
            <groupId>com.amazonawsgroupId>
            <artifactId>aws-java-sdk-s3artifactId>
            <version>1.12.267version>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.24version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-autoconfigureartifactId>
            <version>2.3.12.RELEASEversion>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-configuration-processorartifactId>
            <version>2.7.3version>
            <optional>trueoptional>
        dependency>
    dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 构建通用的遵循S3协议的oss服务接口
    package com.oss.client;
    
    import com.amazonaws.services.s3.AmazonS3;
    import com.amazonaws.services.s3.model.PutObjectResult;
    import com.amazonaws.services.s3.model.S3Object;
    
    import java.io.IOException;
    import java.io.InputStream;
    
    /**
     * Oss 基础操作
     * 想要更复杂操作可以直接获取AmazonS3,通过AmazonS3 来进行复杂的操作
     * https://docs.aws.amazon.com/zh_cn/sdk-for-java/v1/developer-guide/examples-s3-buckets.html
     */
    public interface OssClient{
        /**
         * 创建bucket
         * @param bucketName
         */
        void createBucket(String bucketName);
    
        /**
         * 获取url
         * @param bucketName
         * @param objectName
         * @return
         */
        String getObjectURL(String bucketName, String objectName);
    
    
        /**
         * 获取存储对象信息
         * @param bucketName
         * @param objectName
         * @return
         */
        S3Object getObjectInfo(String bucketName, String objectName);
    
    
        /**
         * 上传文件
         * @param bucketName
         * @param objectName
         * @param stream
         * @param size
         * @param contextType
         * @return
         * @throws IOException
         */
        PutObjectResult putObject(String bucketName, String objectName, InputStream stream, long size, String contextType) throws IOException;
    
    
        default PutObjectResult putObject(String bucketName, String objectName, InputStream stream) throws IOException{
            return putObject(bucketName,objectName,stream, stream.available(), "application/octet-stream");
        }
    
        AmazonS3 getS3Client();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 实现类
    package com.oss.client;
    
    
    import com.amazonaws.services.s3.AmazonS3;
    import com.amazonaws.services.s3.model.ObjectMetadata;
    import com.amazonaws.services.s3.model.PutObjectRequest;
    import com.amazonaws.services.s3.model.PutObjectResult;
    import com.amazonaws.services.s3.model.S3Object;
    import lombok.RequiredArgsConstructor;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.URL;
    
    /**
     * s3 是一个协议
     * S3是Simple Storage Service的缩写,即简单存储服务
     * @author zdh
     */
    @RequiredArgsConstructor
    public class S3OssClient implements OssClient {
    
        private final AmazonS3 amazonS3;
    
    
        @Override
        public void createBucket(String bucketName) {
            if (!amazonS3.doesBucketExistV2(bucketName)) {
                amazonS3.createBucket((bucketName));
            }
        }
    
        @Override
        public String getObjectURL(String bucketName, String objectName) {
            URL url = amazonS3.getUrl(bucketName, objectName);
            return url.toString();
        }
    
        @Override
        public S3Object getObjectInfo(String bucketName, String objectName) {
            return amazonS3.getObject(bucketName, objectName);
        }
    
        @Override
        public PutObjectResult putObject(String bucketName, String objectName, InputStream stream, long size, String contextType) throws IOException {
            ObjectMetadata objectMetadata = new ObjectMetadata();
            objectMetadata.setContentLength(size);
            objectMetadata.setContentType(contextType);
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, stream, objectMetadata);
            putObjectRequest.getRequestClientOptions().setReadLimit(Long.valueOf(size).intValue() + 1);
            return amazonS3.putObject(putObjectRequest);
        }
    
        @Override
        public AmazonS3 getS3Client() {
            return amazonS3;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 对应的配置类
    package com.oss.config;
    
    import lombok.Data;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    
    @ConfigurationProperties(prefix = "oss")
    @Data
    public class OssProperties {
    
        private boolean enable = true;
    
        private String accessKey;
    
        private String accessSecret;
    
        /**
         * endpoint 配置格式为
         * 通过外网访问OSS服务时,以URL的形式表示访问的OSS资源,详情请参见OSS访问域名使用规则。OSS的URL结构为[$Schema]://[$Bucket].[$Endpoint]/[$Object]
         * 。例如,您的Region为华东1(杭州),Bucket名称为examplebucket,Object访问路径为destfolder/example.txt,
         * 则外网访问地址为https://examplebucket.oss-cn-hangzhou.aliyuncs.com/destfolder/example.txt
         * https://help.aliyun.com/document_detail/375241.html
         */
        private String endpoint;
        /**
         * refer com.amazonaws.regions.Regions;
         * 阿里云region 对应表
         * https://help.aliyun.com/document_detail/31837.htm?spm=a2c4g.11186623.0.0.695178eb0nD6jp
         */
        private String region;
    
        private boolean pathStyleAccess = true;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 自动配置类
    package com.oss;
    
    import com.amazonaws.auth.AWSCredentials;
    import com.amazonaws.auth.AWSCredentialsProvider;
    import com.amazonaws.auth.AWSStaticCredentialsProvider;
    import com.amazonaws.auth.BasicAWSCredentials;
    import com.amazonaws.client.builder.AwsClientBuilder;
    import com.amazonaws.services.s3.AmazonS3;
    import com.amazonaws.services.s3.AmazonS3Client;
    import com.oss.client.OssClient;
    import com.oss.client.S3OssClient;
    import com.oss.config.OssProperties;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    
    import java.util.Objects;
    import java.util.stream.Stream;
    
    /**
     * OSS服务自动配置类
     * @author zdh
     */
    @Configuration
    @EnableConfigurationProperties(OssProperties.class)
    public class OssAutoConfiguration {
    
        @Bean
        @ConditionalOnMissingBean(S3OssClient.class)
        public OssClient ossClient(AmazonS3 amazonS3) {
            return new S3OssClient(amazonS3);
        }
    
    
        /**
         * 参考文档
         * https://docs.aws.amazon.com/zh_cn/sdk-for-java/v1/developer-guide/credentials.html
         * 区域选择这块
         * https://docs.aws.amazon.com/zh_cn/sdk-for-java/v1/developer-guide/java-dg-region-selection.html
         * @param ossProperties
         * @return
         */
        @Bean
        @ConditionalOnMissingBean(AmazonS3.class)
        @ConditionalOnProperty(prefix = "oss", name = "enable", havingValue = "true")
        public AmazonS3 amazonS3(OssProperties ossProperties) {
            long nullSize = Stream.<String>builder()
                    .add(ossProperties.getEndpoint())
                    .add(ossProperties.getAccessSecret())
                    .add(ossProperties.getAccessKey())
                    .build()
                    .filter(s -> Objects.isNull(s))
                    .count();
            if (nullSize > 0) {
                throw new RuntimeException("oss 配置错误,请检查");
            }
            AWSCredentials awsCredentials = new BasicAWSCredentials(ossProperties.getAccessKey(),
                    ossProperties.getAccessSecret());
            AWSCredentialsProvider awsCredentialsProvider = new AWSStaticCredentialsProvider(awsCredentials);
            return AmazonS3Client.builder()
                    .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(ossProperties.getEndpoint(), ossProperties.getRegion()))
                    .withCredentials(awsCredentialsProvider)
                    .disableChunkedEncoding()
                    .withPathStyleAccessEnabled(ossProperties.isPathStyleAccess())
                    .build();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    测试

    • 将starter进行打包安装到本地仓库
    • 创建一个springboot项目,并在该工程导入该starter进行单元测试

    打包的时候,可以将starter项目里面的lombok依赖去掉

    • 添加配置属性
    #对于minio来说,配置如下
    oss:
      endpoint: http://minio服务器所在ip:9000
      access-key: admin
      access-secret: admin123
      enable: true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 编码测试
        @Test
        public void testCreateBucket(){
            //hutool提供的spring快捷工具
            OssClient ossClient = SpringUtil.getBean(OssClient.class);
            ossClient.createBucket("sale");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

        @Test
        public void testUploadImg() throws IOException {
            //hutool提供的spring快捷工具
            OssClient ossClient = SpringUtil.getBean(OssClient.class);
            ossClient.putObject("sale","dhy.img",new FileInputStream("xpy.png"));
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

        @Test
        public void testgetImgUrl() throws IOException {
            //hutool提供的spring快捷工具
            OssClient ossClient = SpringUtil.getBean(OssClient.class);
            String objectURL = ossClient.getObjectURL("sale", "dhy.img");
            System.out.println(objectURL);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述


  • 相关阅读:
    搜索技术【广度优先搜索】 - 双向广度优先搜索 【HDU No. 3085】魔鬼II Nightmare Ⅱ
    bool型的盲注
    AI与传统数据库 - ChatGPT风过之后 | 从Duet AI说开来
    Java线程周期
    华为 连接OSPF和RIP网络---OSPF和RIP网络相互引入
    【Vue五分钟】五分钟了解webpack的高级概念
    IDEA 28 个天花板技巧 + 12 款神级插件,生产力起飞...
    linuex服务器中如何安装mysql数据库(一次性完成,包含远程连接)
    Spring 项目快速整合 Hibernate
    node基础概念
  • 原文地址:https://blog.csdn.net/m0_53157173/article/details/126808223