通过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'

注意,这里要单独设置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.amazonaws</groupId>
        <artifactId>aws-java-sdk-s3</artifactId>
        <version>1.12.267</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.24</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure</artifactId>
        <version>2.3.12.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <version>2.7.3</version>
        <optional>true</optional>
    </dependency>
</dependencies>
  • 构建通用的遵循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();
}

  • 实现类
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;
    }
}

  • 对应的配置类
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;

  • 自动配置类
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();
    }
}


测试

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

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

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

在这里插入图片描述

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

在这里插入图片描述

    @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);
    }

在这里插入图片描述


Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐