适配器模式 + Nacos动态配置 实现 OSS 无感切换

一、 前言

在一个微服务项目里,我们的 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]

相关推荐
Query*12 小时前
Java 设计模式——代理模式:从静态代理到 Spring AOP 最优实现
java·设计模式·代理模式
梵得儿SHI12 小时前
Java 反射机制深度解析:从对象创建到私有成员操作
java·开发语言·class对象·java反射机制·操作类成员·三大典型·反射的核心api
JAVA学习通12 小时前
Spring AI 核心概念
java·人工智能·spring·springai
望获linux12 小时前
【实时Linux实战系列】实时 Linux 在边缘计算网关中的应用
java·linux·服务器·前端·数据库·操作系统
绝无仅有12 小时前
面试真实经历某商银行大厂数据库MYSQL问题和答案总结(二)
后端·面试·github
绝无仅有12 小时前
通过编写修复脚本修复 Docker 启动失败(二)
后端·面试·github
..Cherry..12 小时前
【java】jvm
java·开发语言·jvm
老K的Java兵器库12 小时前
并发集合踩坑现场:ConcurrentHashMap size() 阻塞、HashSet 并发 add 丢数据、Queue 伪共享
java·后端·spring
冷冷的菜哥13 小时前
go邮件发送——附件与图片显示
开发语言·后端·golang·邮件发送·smtp发送邮件
向葭奔赴♡13 小时前
Spring Boot 分模块:从数据库到前端接口
数据库·spring boot·后端