前言
在微服务架构中,OSS云存储服务通常需要集成多个存储厂商(如阿里云、腾讯云、Minio等),且未来可能新增其他厂商。如果直接修改存储厂商,会导致Controller层和Service层代码变更,违背了低耦合原则。此时,采用适配器模式进行项目开发是最佳选择!
适配器模式改造
MinioUtils和AliyunUtils作为被适配者类,各自实现原子性操作逻辑。为了统一多个OSS的接口返回,需要使用适配器模式。
被适配者类
@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());
}
}
定义目标接口,统一不同OSS的操作方法:
public interface StorageAdapter {
void createBucket(String bucket);
void uploadFile(MultipartFile multipartFile, String bucket, String objectName);
String getUrl(String bucket, String objectName);
}
Minio适配器类
通过实现目标接口,转换被适配者类的接口:
@Log4j2
public class MinioStorageAdapter implements StorageAdapter {
@Resource
private MinioUtil minioUtil;
@Value("${minio.url}")
private String url;
@Override
@SneakyThrows
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());
}
}
@Override
public String getUrl(String bucket, String objectName) {
return url + "/" + bucket + "/" + objectName;
}
}
阿里云适配器类
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) {
}
@Override
public String getUrl(String bucket, String objectName) {
return "aliyun";
}
}
定义StorageConfig类
通过Nacos动态配置读取当前storageType值。新增OSS厂商时,只需添加对应的适配器类并在@Bean方法中扩展条件判断。
@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("未找到对应的文件存储处理器");
}
}
}
新增FileService防腐层
提升可维护性
@Component
public class FileService {
private final StorageAdapter storageAdapter;
public FileService(StorageAdapter storageAdapter) {
this.storageAdapter = storageAdapter;
}
public void createBucket(String bucket) {
storageAdapter.createBucket(bucket);
}
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层
通过注入FileService进行操作:
@RestController
@Log4j2
public class FileController {
@Resource
private FileService fileService;
@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搭建
Nacos部署
服务器需开启8848、9848端口:
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 MODE=standalone:单机模式运行
- 8848:Nacos服务端端口
- 9848:客户端gRPC请求端口
引入Nacos客户端依赖
需引入nacos-config和log4j2依赖:
<!-- 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>
配置文件设置
在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
配置管理
在Nacos控制台添加配置:
- DataId:
jc-club-oss-dev.yaml
- 配置内容包含storage.service.type参数
热更新配置
在配置类添加@RefreshScope注解,实现配置动态更新:
@Configuration
@RefreshScope
public class StorageConfig {
// 配置内容
}
测试验证
- 配置storage.service.type为aliyun时,返回阿里云标识
- 动态修改为minio后,文件成功上传至Minio
- Nacos日志显示配置更新记录:
Refresh keys changed: [storage.service.type]
通过SpringBoot框架与Nacos的集成,实现了OSS存储的无感动态切换,提升了系统的扩展性和维护性。