一、 前言
在一个微服务项目里,我们的 OSS 云存储服务常常需要配置诸如阿里云、腾讯云、minio 等多个云存储厂商的业务代码,而且后续无法确保是否会增添新的云存储厂商。此时,倘若我们要修改具体使用的云存储厂商,就会致使 controller 层和 service 层发生变动,这并不符合低耦合的理念。在这种情况下,我们完全可以采用适配器模式来开展项目开发!
二、适配器模式改造
MinioUtils和AliyunUtils被适配者类作为源接口执行原子性操作的具体逻辑各不相同,想要把多个OSS共用一个相同的接口返回,就需要使用到适配器模式。
被适配器类
scss
@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());
}
}
/**
* 上传文件
* inputStream:处理文件的输入流
* bucket:桶名称
* objectName:桶中的对象名称,也就是上传后的文件在存储桶中的存储路径和文件名。
* stream(inputStream:处理文件的输入流,-1:指定缓冲区大小的参数[-1为默认大小], Integer.MAX_VALUE:指定文件内容长度的上限)
*/
public void uploadFile(InputStream inputStream, String bucket, String objectName) throws Exception {
minioClient.putObject(PutObjectArgs.builder().bucket(bucket).object(objectName)
.stream(inputStream, -1, Integer.MAX_VALUE).build());
}
}
这是目标接口 (目标抽象类,即客户需要的方法),我们想要的不同OSS都可通过该接口进行操作:
typescript
public interface StorageAdapter {
/**
* 创建bucket
* @param bucket
*/
void createBucket(String bucket);
/**
* 上传文件
* @param multipartFile
* @param bucket
* @param objectName
*/
void uploadFile(MultipartFile multipartFile, String bucket, String objectName);
}
Minio适配器类:通过继承或者组合方式,将被适配者类(minioUtils)的接口与目标抽象类的接口转换起来,使得客户端可以按照目标抽象类的接口进行操作。
typescript
public class MinioStorageAdapter implements StorageAdapter {
@Resource
private MinioUtil minioUtil;
@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);
//对象名不是必填, 不填就是(multipartFile.getName())上传的文件名
if(objectName != null) {
minioUtil.uploadFile(multipartFile.getInputStream(), bucket, objectName + "/" + multipartFile.getName());
} else {
minioUtil.uploadFile(multipartFile.getInputStream(), bucket, multipartFile.getName());
}
}
}
Aliyun适配器类
typescript
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) {
}
}
三、定义StorageConfig类来获取指定的文件适配器
通过Nacos的动态配置读取来得到当前的storageType
此时如果想再加入一个新的OSS对象(得到xxUtils jar包等,我们无法进行修改),只需新增一个xxadapter适配器类且在@Bean注解的方法中加一个else即可。
注意:这里直接使用new的方式创建实现类(实现类也不需要使用@Service注解),而不是先把所有的实现类通过注解定义出来,再直接返回对象,这样如果新增一个OSS的话,不光要加else,还需再把实现类通过直接定义出来。
typescript
@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防腐
提高可维护性
typescript
@Component
public class FileService {
/**
* 通过构造函数注入
*/
private final StorageAdapter storageAdapter;
public FileService(StorageAdapter storageAdapter) {
this.storageAdapter = storageAdapter;
}
/**
* 创建bucket
* @param bucket
*/
public void createBucket(String bucket) {
storageAdapter.createBucket(bucket);
}
/**
* 上传图片
* @param multipartFile
* @param bucket
* @param objectName
*/
public void uploadFile(MultipartFile multipartFile, String bucket, String objectName) {
storageAdapter.uploadFile(multipartFile, bucket, objectName);
}
}
五、Controller层
Controller层通过注入FileService来进行操作
java
@RestController
public class FileController {
@Resource //根据名称注入
private FileService fileService;
@PostMapping("/upload")
public void test() throws Exception {
fileService.createBucket("ssm");
}
}
六、Nacos搭建
6.1 Nacos部署
服务器需开启8848、9848端口
bash
docker search nacos
docker pull nacos/nacos-server
# 镜像拉完之后,启动脚本
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:赋予容器扩展的特权
- --cgroupns host:让容器使用宿主机的 cgroup 命名空间(在资源限制方面容器会遵循宿主机规则)
- --env :设置Nacos服务使用的jvm参数
- JVM_XMX:最大堆内存为 256m
- JVM_XMS:初始堆内存为 256 m
- --env MODE=standalone:nacos运行模式为单机模式
- -w /home/nacos:指定容器内的工作目录为 "/home/nacos",容器内执行的命令如果涉及到相对路径的操作,就会以这个目录作为当前工作目录的基准。
- 8848:Nacos服务端端口
- 9848:客户端gRPC请求服务端端口
6.2 引入nacos客户依赖
除了引入nacos依赖,还要引入log4j2依赖,来输出nacos日志信息
SpringCloudAlibaba 版本为2.2.6.RELEASE时,springboot版本要为2.3.8.RELEASE
xml
<!--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 编写配置文件
把nacos相关配置写入bootstrap.yml
文件中,项目启动后会优先读取。
yaml
spring:
application:
name: jc-club-oss #微服务名称
profiles:
active: dev #指定环境为开发环境
cloud:
nacos:
server-addr: 117.72.118.73:8848
config:
file-extension: yaml #文件后缀名
6.4 新增配置管理
- dataId:
jc-club-oss-dev.yaml
服务名称+开发环境.yaml - 配置内容:
这时spring会根据bootstrap.yml文件中的${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
作为文件id,来读取配置
6.5 添加@RefreshScope
注解开启热更新
-
在@Value注入的变量所在类上添加注解
@RefreshScop
,当配置文件内容发生变化后会重新读取 -
当文件更新后,Bean已加入到了IOC容器,即使storageType属性值变了,Bean也无法重新加载。
-
所以在@Bean方法上也要加入
@RefreshScop
注解,当文件更新后,带有此注解的Bean能够自动重新初始化
less
@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 测试
1.type为阿里云
结果为:
2.修改属性为minio
结果为:bucket上传成功!
在配置文件更新时,nacos也会打印出对应的日志提示:
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]