oss模块设计之适配器模式改造minio
在进行本节的笔记之前,我们先进行对oss服务与minio做一个简单介绍,方便大家更便于理解;
OSS服务(Object Storage Service)
OSS服务,即对象存储服务,是一种用于云端的大规模、高可用、安全、低成本的数据存储服务。它主要用于存储非结构化的数据,如图片、音频、视频、文档等文件。OSS服务具有以下特点:
- 海量存储:能够存储几乎无限量的数据。
- 高可用性:通常采用冗余存储策略,保证数据的持久性和高可用性。
- 弹性扩展:根据需求自动扩展存储容量和带宽。
- 安全性:提供数据加密、访问控制和审计等功能,保障数据安全。
- 成本效益:按需付费,无需预先购买硬件或担心硬件维护。
MinIO
MinIO是一个高性能、分布式、兼容Amazon S3 API的对象存储服务器。它被设计用于云基础设施中,提供与OSS服务类似的功能,但作为一个开源项目,它允许用户在自己的基础设施上部署和管理对象存储服务。MinIO的特点包括:
- 高性能:利用现代硬件的特性,如多核CPU和高速网络,提供极高的I/O性能。
- 兼容S3 API:这使得现有的S3客户端可以直接与MinIO交互,无需修改代码。
- 可扩展性:支持水平扩展,可以跨多个节点和数据中心部署。
- 安全性:支持数据加密、身份验证和授权机制。
- 开源:遵循Apache License 2.0,可以自由下载、修改和分发。
作用
OSS服务和MinIO的主要作用是提供一种简单、高效的方式存储和检索大量非结构化数据。它们可以应用于各种场景,包括但不限于:
- 备份和归档:长期保存重要数据。
- 内容分发:存储和分发网站的静态资源,如图片和视频。
- 大数据分析:作为Hadoop、Spark等大数据处理框架的数据仓库。
- 媒体应用:存储和处理流媒体文件。
首先,我们先写一个简单的oss模块调用minio的实现步骤
docker部署minio
docker pull minio/minio docker run -p 9000:9000 -p 9090:9090 \ --name minio \ -d --restart=always \ -e "MINIO_ACCESS_KEY=minioadmin" \ -e "MINIO_SECRET_KEY=minioadmin" \ -v /mydata/minio/data:/data \ minio/minio server \ /data --console-address ":9090" -address ":9000"
docker部署指令如上。关于docker镜像拉取问题可以看博主前面的文章上有具体的换源介绍。
application如上
首先封装一个fileinfo类
import lombok.Data; /** * 文件类 */ @Data public class FileInfo { private String fileName; private Boolean directoryFlag; private String etag; }
然后实现minio的工具类
import com.jingdianjichi.oss.entity.FileInfo; import io.minio.*; import io.minio.http.Method; import io.minio.messages.Bucket; import io.minio.messages.Item; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.io.InputStream; import java.util.LinkedList; import java.util.List; import java.util.stream.Collectors; /** * minio文件操作工具 */ @Component public class MinioUtil { @Resource private MinioClient minioClient; /** * 创建bucket桶 */ public void createBucket(String bucket) throws Exception { boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build()); if (!exists) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build()); } } /** * 上传文件 */ public void uploadFile(InputStream inputStream, String bucket, String objectName) throws Exception { ObjectWriteResponse objectWriteResponse = minioClient.putObject(PutObjectArgs.builder() .bucket(bucket) .object(objectName) .stream(inputStream, -1, Integer.MAX_VALUE) .build()); } /** * 列出所有桶 */ public List<String> getAllBucket() throws Exception { List<Bucket> buckets = minioClient.listBuckets(); return buckets.stream().map(Bucket::name).collect(Collectors.toList()); } /** * 列出当前桶及文件 */ public List<FileInfo> getAllFile(String bucket) throws Exception { Iterable<Result<Item>> results = minioClient.listObjects( ListObjectsArgs.builder().bucket(bucket).build()); List<FileInfo> fileInfoList = new LinkedList<>(); for (Result<Item> result : results) { FileInfo fileInfo = new FileInfo(); Item item = result.get(); fileInfo.setFileName(item.objectName()); fileInfo.setDirectoryFlag(item.isDir()); fileInfo.setEtag(item.etag()); fileInfoList.add(fileInfo); } return fileInfoList; } /** * 下载文件 */ public InputStream downLoad(String bucket, String objectName) throws Exception { return minioClient.getObject( GetObjectArgs.builder().bucket(bucket).object(objectName).build() ); } /** * 删除桶 */ public void deleteBucket(String bucket) throws Exception { minioClient.removeBucket( RemoveBucketArgs.builder().bucket(bucket).build() ); } /** * 删除文件 */ public void deleteObject(String bucket, String objectName) throws Exception { minioClient.removeObject( RemoveObjectArgs.builder().bucket(bucket).object(objectName).build() ); } /** * 获取文件url */ public String getPreviewFileUrl(String bucketName, String objectName) throws Exception { GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(bucketName).object(objectName).build(); return minioClient.getPresignedObjectUrl(args); } }
写一个配置类用于接收我们minio的application的具体参数。
import io.minio.MinioClient; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * minio配置管理 */ @Configuration public class MinioConfig { /** * miniourl */ @Value("${minio.url}") private String url; /** * minio账户 */ @Value("${minio.accessKey}") private String accessKey; /** * minio密码 */ @Value("${minio.secretKey}") private String secretKey; /** * 构造minio客户端 * * @return */ @Bean public MinioClient getMinioClient() { return MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build(); } }
为了不直接对我们的MinioUtil进行直接的与controller进行关联,降低代码耦合度,我们引入一个接口类,便于后期业务的变化以及无需直接处理底层存储服务的复杂细节
如果需要更换对象存储服务提供商,只需要实现相同的接口即可,而无需修改调用这些方法的业务逻辑代码。
构建接口类
import com.jingdianjichi.oss.entity.FileInfo; import org.springframework.web.multipart.MultipartFile; import java.io.InputStream; import java.util.List; public interface StorageService { /** * 创建bucket桶 */ void createBucket(String bucket); /** * 上传文件 */ void uploadFile(MultipartFile uploadFile, String bucket, String objectName); /** * 列出所有桶 */ List<String> getAllBucket(); /** * 列出当前桶及文件 */ List<FileInfo> getAllFile(String bucket); /** * 下载文件 */ InputStream downLoad(String bucket, String objectName); /** * 删除桶 */ void deleteBucket(String bucket); /** * 删除文件 */ void deleteObject(String bucket, String objectName); }
构建两个实现接口类
import com.jingdianjichi.oss.entity.FileInfo; import com.jingdianjichi.oss.service.StorageService; import com.jingdianjichi.oss.util.MinioUtil; import lombok.SneakyThrows; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.io.InputStream; import java.util.List; /** * @author: zwz * @date: 2024 * @description: */ @Service("miniostorageServiceImpl") public class MinioStorageServiceImpl implements StorageService { @Resource private MinioUtil minioUtil; @Override @SneakyThrows public void createBucket(String bucket) { minioUtil.createBucket(bucket); } @Override @SneakyThrows public void uploadFile(MultipartFile uploadFile, String bucket, String objectName) { minioUtil.createBucket(bucket); if (objectName != null) { minioUtil.uploadFile(uploadFile.getInputStream(), bucket, objectName + "/" + uploadFile.getName()); } else { minioUtil.uploadFile(uploadFile.getInputStream(), bucket, uploadFile.getName()); } } @Override @SneakyThrows public List<String> getAllBucket() { return minioUtil.getAllBucket(); } @Override @SneakyThrows public List<FileInfo> getAllFile(String bucket) { return minioUtil.getAllFile(bucket); } @Override @SneakyThrows public InputStream downLoad(String bucket, String objectName) { return minioUtil.downLoad(bucket, objectName); } @Override @SneakyThrows public void deleteBucket(String bucket) { minioUtil.deleteBucket(bucket); } @Override @SneakyThrows public void deleteObject(String bucket, String objectName) { minioUtil.deleteObject(bucket, objectName); } }
import com.jingdianjichi.oss.entity.FileInfo; import com.jingdianjichi.oss.service.StorageService; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.InputStream; import java.util.LinkedList; import java.util.List; @Service("aliStorageServiceImpl") public class AliStorageServiceImpl implements StorageService { @Override public void createBucket(String bucket) { } @Override public void uploadFile(MultipartFile uploadFile, String bucket, String objectName) { } @Override public List<String> getAllBucket() { List<String> bucketNameList = new LinkedList<>(); bucketNameList.add("aliyun"); return bucketNameList; } @Override public List<FileInfo> getAllFile(String bucket) { return null; } @Override public InputStream downLoad(String bucket, String objectName) { return null; } @Override public void deleteBucket(String bucket) { } @Override public void deleteObject(String bucket, String objectName) { } }
controller层
import com.jingdianjichi.oss.service.StorageService; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.List; @RestController public class FileController { @Resource private StorageService miniostorageServiceImpl; @RequestMapping("/testGetAllBuckets") public String testGetAllBuckets() throws Exception { List<String> allBucket = miniostorageServiceImpl.getAllBucket(); return allBucket.get(0); } }
当完成此步骤时,我们在controller层通过调用接口类便可以进行更好的更换对象存储服务提供商来实现oss模块的具体处理。
但是,仍然存在一个问题便是,当我们新加入新的oss服务提供商的时候,
@Resource private StorageService miniostorageServiceImpl;
miniostorageServiceImpl.getAllBucket();
我们需要不断的进行修改此类部分内容,这仍然需要较大的任务量以及设计了内部逻辑的修改。
因此,我们可以在StorageService与controller层再次抽出新的一层FileService层使得前面两层之间降低耦合度,同时假如StorageConfig进行业务的选取
import com.jingdianjichi.oss.service.StorageService; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.annotation.Resource; @Configuration public class StorageConfig { @Value("${storage.service.type}") private String storageType; @Resource private StorageService aliStorageServiceImpl; @Resource private StorageService miniostorageServiceImpl; @Bean public StorageService storageService() { if ("minio".equals(storageType)) { return miniostorageServiceImpl; } else if ("aliyun".equals(storageType)) { return aliStorageServiceImpl; } else { throw new IllegalArgumentException("未找到相应的文件存储管理器"); } } }
package com.jingdianjichi.oss.service; import org.springframework.stereotype.Service; import java.util.List; @Service public class FileService { private final StorageService storageService; public FileService(StorageService storageService) { this.storageService = storageService; } /** * 列出所有存储桶 */ public List<String> getAllBucket() { return storageService.getAllBucket(); } }
现在,我们可以直接通过修改type类便进行不同供应商的选取。
但是,这样的解决方案就是最佳的了么?
如果当我需要引入新的业务商的时候,依旧需要修改多个地方,比如以下内容
因此,笔者决定采用适配器的模式再次优化此方案设计。
首先,我们先简单介绍一下适配器模式:
适配器模式(Adapter Pattern)是面向对象设计模式中的一种,它允许不兼容的接口之间可以协同工作。适配器模式通常用于“适配”一个已有类的接口,使其看起来像是另一个接口。这样做的目的是为了复用现有类的功能,但又不想或者不能修改现有类的代码。
适配器模式的基本概念
适配器模式涉及到以下几种角色:
-
目标(Target):这是期望的接口,即客户端希望使用的接口。它定义了客户端可以调用的方法。
-
适配者(Adaptee):这是已有接口,拥有客户端不能直接使用的接口。适配器模式的目的就是让适配者接口能够与目标接口兼容。
-
适配器(Adapter):这是关键的部分,它持有适配者对象的实例,并将适配者接口转换成目标接口。适配器对外呈现目标接口,内部则调用适配者的功能。
在本文中,以oss模块举例,我们更换服务商或者业务目标不同时,可能并不需要下面图片上所展示的这些方法
因此,通过适配器模式,将彼此间业务不兼容或者不同的接口彼此都可以使用,根据需求选择不同的接口而不用修改过多代码是十分有必要的。
首先重构StorageService如下
package com.jingdianjichi.oss.service; import com.jingdianjichi.oss.entity.FileInfo; import org.springframework.web.multipart.MultipartFile; import java.io.InputStream; import java.util.List; public interface StorageAdapter { /** * 创建bucket桶 */ void createBucket(String bucket); /** * 上传文件 */ void uploadFile(MultipartFile uploadFile, String bucket, String objectName); /** * 列出所有桶 */ List<String> getAllBucket(); /** * 列出当前桶及文件 */ List<FileInfo> getAllFile(String bucket); /** * 下载文件 */ InputStream downLoad(String bucket, String objectName); /** * 删除桶 */ void deleteBucket(String bucket); /** * 删除文件 */ void deleteObject(String bucket, String objectName); }
删除原接口实现类的注解
修改storageConfig配置类
package com.jingdianjichi.oss.config; import com.jingdianjichi.oss.service.StorageAdapter; import com.jingdianjichi.oss.service.impl.AliStorageAdapter; import com.jingdianjichi.oss.service.impl.MinioStorageAdapter; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class StorageConfig { @Value("${storage.service.type}") private String storageType; @Bean public StorageAdapter storageService() { if ("minio".equals(storageType)) { return new MinioStorageAdapter(); } else if ("aliyun".equals(storageType)) { return new AliStorageAdapter(); } else { throw new IllegalArgumentException("未找到相应的文件存储管理器"); } } }
我们可以清楚的看到,经过·适配器模式的改造后,引入新的服务商提供类,我们只需要再次添加一个else if 语句即可。当然,可能有的小伙伴会说,为什么不采用枚举或者mapper映射注入的形式呢?
当然,这些解决方案都是可以的,但是目前在我们的oss模块中,数量上也不会太多,不必过度进行设计。
经过重构后,代码整体架构如下