找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1538

积分

0

好友

193

主题
发表于 2025-12-31 04:55:23 | 查看: 23| 回复: 0

在一个微服务项目中,我们常常需要接入阿里云、腾讯云、MinIO 等多个对象存储服务。并且,未来可能还会引入新的存储厂商。

如果每次切换存储服务,都需要修改 Controller 和 Service 层的代码,无疑会破坏代码的低耦合性。这时,采用适配器模式来进行开发就是一个非常优雅的解决方案。

二、使用适配器模式进行改造

MinioUtilsAliyunUtils 作为被适配者类,它们执行原子操作的内部逻辑各不相同。为了让不同的 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 功能测试

  1. 初始配置为阿里云 (type: aliyun)
    调用上传接口,返回结果为 “aliyun”,符合 AliStorageAdapter 的模拟逻辑。
    图:API测试工具中阿里云配置下的请求与响应结果

  2. 动态修改配置为 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数据库/中间件板块进行深入交流。




上一篇:SkyWalking分布式链路追踪实践:Spring Boot微服务监控指南
下一篇:ProxmoxVE安装与集群配置教程:开源虚拟化平台的VMware替代方案
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-10 18:36 , Processed in 0.438126 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表