在一个微服务项目中,我们常常需要接入阿里云、腾讯云、MinIO 等多个对象存储服务。并且,未来可能还会引入新的存储厂商。
如果每次切换存储服务,都需要修改 Controller 和 Service 层的代码,无疑会破坏代码的低耦合性。这时,采用适配器模式来进行开发就是一个非常优雅的解决方案。
二、使用适配器模式进行改造
MinioUtils 和 AliyunUtils 作为被适配者类,它们执行原子操作的内部逻辑各不相同。为了让不同的 OSS 服务对外提供统一的接口,适配器模式正好派上用场。
被适配者类 (MinioUtil示例)
@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 {
minioClient.putObject(PutObjectArgs.builder().bucket(bucket).object(objectName)
.stream(inputStream, -1, 5242889L).build());
}
}
接下来,我们定义一个目标接口 StorageAdapter,所有 OSS 服务都将通过这个统一的接口进行操作。
/**
* 为了方便切换任何一个oss,我们将公共方法抽取为接口
*/
public interface StorageAdapter {
/**
* 创建bucket
*/
void createBucket(String bucket);
/**
* 上传文件
*/
void uploadFile(MultipartFile multipartFile, String bucket, String objectName);
/**
* 获取文件在oss中的url
*/
String getUrl(String bucket, String objectName);
}
Minio适配器类: 通过组合的方式,将 MinioUtil 的接口适配成 StorageAdapter 接口。
@Log4j2
public class MinioStorageAdapter implements StorageAdapter {
@Resource
private MinioUtil minioUtil;
@Value("${minio.url}")
private String url;
@Override
@SneakyThrows //Lombok中的注解 会在编译期补上异常处理
public void createBucket(String bucket) {
minioUtil.createBucket(bucket);
}
/**
* 上传文件
*/
@Override
@SneakyThrows
public void uploadFile(MultipartFile multipartFile, String bucket, String objectName) {
minioUtil.createBucket(bucket);
if(objectName != null) {
minioUtil.uploadFile(multipartFile.getInputStream(), bucket, objectName + "/" + multipartFile.getOriginalFilename());
} else {
minioUtil.uploadFile(multipartFile.getInputStream(), bucket, multipartFile.getOriginalFilename());
}
}
/**
* 获取文件在oss中的url
*/
@Override
public String getUrl(String bucket, String objectName) {
return url + "/" + bucket + "/" + objectName;
}
}
Aliyun适配器类 (示例)
/**
* 阿里云oss 具体实现逻辑
*/
public class AliStorageAdapter implements StorageAdapter {
@Override
public void createBucket(String bucket) {
System.out.println("aliyun");
}
@Override
public void uploadFile(MultipartFile multipartFile, String bucket, String objectName) {
// 阿里云OSS具体上传逻辑
}
@Override
public String getUrl(String bucket, String objectName) {
return "aliyun";
}
}
三、定义 StorageConfig 类动态获取适配器
我们通过读取 Nacos 中的动态配置来决定当前使用哪个存储适配器。这样做的好处是,未来如果需要新增一个 OSS 服务,只需新增一个对应的适配器类,并在下面的配置工厂方法中添加一个分支即可。
注意:这里采用在 @Bean 方法中直接 new 出实现类的方式,而不是依赖 Spring 容器自动注入所有实现类。这样做避免了因新增实现类而需要修改注解配置的麻烦。
@Configuration
public class StorageConfig {
@Value("${storage.service.type}")
private String storageType;
@Bean
public StorageAdapter storageAdapter() {
if("minio".equals(storageType)) {
return new MinioStorageAdapter();
} else if("aliyun".equals(storageType)) {
return new AliStorageAdapter();
} else {
throw new IllegalArgumentException("未找到对应的文件存储处理器");
}
}
}
四、引入 FileService 作为防腐层
引入一个 Service 层,可以进一步提高代码的可维护性和清晰度,它相当于领域模型中的防腐层。
/**
* FileService防腐层
*/
@Component
public class FileService {
/**
* 通过构造函数注入
*/
private final StorageAdapter storageAdapter;
public FileService(StorageAdapter storageAdapter) {
this.storageAdapter = storageAdapter;
}
/**
* 创建bucket
*/
public void createBucket(String bucket) {
storageAdapter.createBucket(bucket);
}
/**
* 上传图片、返回图片在oss的地址
*/
public String uploadFile(MultipartFile multipartFile, String bucket, String objectName) {
storageAdapter.uploadFile(multipartFile, bucket, objectName);
objectName = (StringUtils.isEmpty(objectName) ? "" : objectName + "/") + multipartFile.getOriginalFilename();
return storageAdapter.getUrl(bucket, objectName);
}
}
五、Controller 层调用
Controller 层只需注入统一的 FileService 即可,完全与具体的 OSS 实现解耦。
@RestController
@Log4j2
public class FileController {
@Resource //根据名称注入
private FileService fileService;
/**
* 上传文件,返回文件在oss中的地址
*/
@PostMapping("/upload")
public Result<String> upload(MultipartFile uploadFile, String bucket, String objectName) throws Exception {
try {
Preconditions.checkArgument(!ObjectUtils.isEmpty(uploadFile), "文件不能为空");
Preconditions.checkArgument(!StringUtils.isEmpty(bucket), "bucket桶名称不能为空");
if(log.isInfoEnabled()) {
log.info("FileController.upload.uploadFile:{}, bucket:{}, objectName:{}", uploadFile.getOriginalFilename(), bucket, objectName);
}
String url = fileService.uploadFile(uploadFile, bucket, objectName);
return Result.ok(url);
} catch (Exception e) {
log.info("FileController.upload.error:{}", e.getMessage(), e);
return Result.fail("上传文件失败");
}
}
}
六、整合 Nacos 实现动态配置
6.1 Nacos 服务部署
使用 Docker 快速部署一个 Nacos 服务。确保服务器开放了 8848 和 9848 端口。
docker run -d \
--name nacos \
--privileged \
--cgroupns host \
--env JVM_XMX=256m \
--env MODE=standalone \
--env JVM_XMS=256m \
-p 8848:8848/tcp \
-p 9848:9848/tcp \
--restart=always \
-w /home/nacos \
nacos/nacos-server
--privileged:赋予容器扩展的特权。
--env MODE=standalone:指定以单机模式运行。
8848:Nacos 服务端端口。
9848:客户端 gRPC 请求服务端端口。
6.2 引入 Nacos 客户端依赖
在 pom.xml 中引入 Nacos Config 和 Log4j2 依赖(用于查看 Nacos 日志)。请注意 Spring Cloud Alibaba 与 Spring Boot 的版本兼容性。
<!--nacos依赖(配合日志,打印nacos信息)-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
<version>2.4.2</version>
</dependency>
6.3 编写 bootstrap.yml 配置文件
项目启动时会优先读取 bootstrap.yml 文件来获取 Nacos 配置中心的地址。
spring:
application:
name: jc-club-oss #微服务名称
profiles:
active: dev #指定环境为开发环境
cloud:
nacos:
server-addr: 117.72.118.73:8848
config:
file-extension: yaml #文件后缀名
图:项目启动时的配置加载流程图
6.4 在 Nacos 控制台添加配置
在 Nacos 控制台新建一个配置,Data ID 的命名规则为:${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension},即 jc-club-oss-dev.yaml。
配置内容示例:
storage:
service:
type: aliyun # 指定当前使用的存储服务类型
图:Nacos 控制台中的配置文件内容
6.5 添加 @RefreshScope 注解开启热更新
为了让 StorageConfig 中的 storageType 值能随 Nacos 配置动态更新,需要在类和方法上都添加 @RefreshScope 注解。
@Configuration
@RefreshScope
public class StorageConfig {
@Value("${storage.service.type}")
private String storageType;
@Bean
@RefreshScope
public StorageAdapter storageAdapter() {
if("minio".equals(storageType)) {
return new MinioStorageAdapter();
} else if("aliyun".equals(storageType)) {
return new AliStorageAdapter();
} else {
throw new IllegalArgumentException("未找到对应的文件存储处理器");
}
}
}
6.6 功能测试
-
初始配置为阿里云 (type: aliyun)
调用上传接口,返回结果为 “aliyun”,符合 AliStorageAdapter 的模拟逻辑。
图:API测试工具中阿里云配置下的请求与响应结果
-
动态修改配置为 MinIO (type: minio)
在 Nacos 控制台将 storage.service.type 的值修改为 minio 并发布。
图:修改后的 Nacos 配置文件内容
再次调用上传接口,图片成功上传至 MinIO,并返回可访问的 URL。
图:API测试工具中MinIO配置下的请求与响应结果
图:MinIO控制台中已成功上传的文件列表
同时,应用日志会打印出配置刷新的提示:
2024-12-03 17:05:50.719 INFO 35932 --- [.72.118.73_8848] o.s.c.e.e.RefreshEventListener : Refresh keys changed: [storage.service.type]
图:应用启动时的Nacos配置加载日志
通过这种方式,我们利用适配器模式统一了不同 OSS 服务的接口,并结合 Nacos 的动态配置能力,实现了存储服务的无感、热切换。整个架构清晰解耦,扩展新的存储服务也变得非常容易。如果你对更多系统架构和设计模式的实战应用感兴趣,欢迎到云栈社区的Java或数据库/中间件板块进行深入交流。