通过S3协议实现通用的文件存储服务中间件
引言
在日常开发文件上传相关服务时,通常都会选择腾讯云,阿里云,七牛云等提供的oss服务作为文件存储系统,如果需要自行搭建文件存储系统,通常则会采用minio等开源项目。
但是大家有没有考虑过,不同的厂商或者开源项目提供的客户端sdk都是不同的,如果项目开发过程中,需要切换底层文件系统,那么通常情况下意味着,我们需要完全替换掉相关文件上传代码,如果微服务项目,则需要替换掉所有使用到文件上传sdk微服务的代码,这显然会带来巨大的工作量。
为了解决上面这个问题,我们有如下两个思路:
- 项目中针对文件上传写出一个单独的抽象层接口,底层不同文件存储系统,提供对应的实现即可:
这个思路很容易想到,利用门面模型向调用方屏蔽底层实现,但是其实这里还有更加简洁的实现方式。
- 基本所有云服务厂商提供的oss服务和开源的oss项目都遵循了S3协议,是Simple Storage Service的缩写,即简单存储服务,因此其实我们这里利用这一点,写出一个通用的文件中间件,利用该中间件后,我们写的客户端api就对任何实现了S3协议的oss服务进行访问。
使用演示
这里我们以Minio作为演示案例,不清楚minio的可以查看minio官方文档学习一下,下面我们先用docker方式安装一下minio:
安装minio
代码语言:javascript复制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");
}
代码语言:javascript复制 @Test
public void testUploadImg() throws IOException {
//hutool提供的spring快捷工具
OssClient ossClient = SpringUtil.getBean(OssClient.class);
ossClient.putObject("sale","dhy.img",new FileInputStream("xpy.png"));
}
代码语言:javascript复制 @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);
}