⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 网络 「网络」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

在一个微服务项目里,我们的 OSS 云存储服务常常需要配置诸如阿里云、腾讯云、minio 等多个云存储厂商的业务代码,而且后续无法确保是否会增添新的云存储厂商。

此时,倘若我们要修改具体使用的云存储厂商,就会致使 controller 层和 service 层发生变动,这并不符合低耦合的理念。

在这种情况下,我们完全可以采用适配器模式来开展项目开发!

适配器模式改造

MinioUtils和AliyunUtils被适配者类作为源接口执行原子性操作的具体逻辑各不相同,想要把多个OSS共用一个相同的接口返回,就需要使用到适配器模式。

被适配器类

@Component
publicclass 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为默认大小], 5242889L:指定文件内容长度的上限)
*/
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都可通过该接口进行操作:

/**
* 为了方便切换任何一个oss,我们将公共方法抽取为接口,由某个oss的实现类去编写具体逻辑
*/
public interface StorageAdapter {
/**
* 创建bucket
* @param bucket
*/
void createBucket(String bucket);

/**
* 上传文件
* @param multipartFile
* @param bucket
* @param objectName
*/
void uploadFile(MultipartFile multipartFile, String bucket, String objectName);

/**
* 获取文件在oss中的url
* @param bucket
* @param objectName
* @return
*/
String getUrl(String bucket, String objectName);

}

Minio适配器类:通过继承或者组合方式,将被适配者类(minioUtils)的接口与目标抽象类的接口转换起来,使得客户端可以按照目标抽象类的接口进行操作。

/**
* Minio相关操作的具体逻辑
*/
@Log4j2
publicclass MinioStorageAdapter implements StorageAdapter {

@Resource
private MinioUtil minioUtil;

@Value("${minio.url}")
private String url;

@Override
@SneakyThrows//Lombok中的注解 会在编译期补上异常处理
public void createBucket(String bucket) {
minioUtil.createBucket(bucket);
}

/**
* 上传文件
* @param multipartFile
* @param bucket
* @param objectName 为空,文件路径为根目录;不为空,文件路径为objectName目录下
*/
@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
* @param bucket
* @param objectName
* @return
*/
@Override
public String getUrl(String bucket, String objectName) {
return url + "/" + bucket + "/" + objectName;
}


}

Aliyun适配器类

/**
* 阿里云oss 具体实现逻辑
*/
publicclass 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对象(得到xxUtils jar包等,我们无法进行修改),只需新增一个xxadapter适配器类且在@Bean注解的方法中加一个else即可。

注意:这里直接使用new的方式创建实现类(实现类也不需要使用@Service注解),而不是先把所有的实现类通过注解定义出来,再直接返回对象,这样如果新增一个OSS的话,不光要加else,还需再把实现类通过直接定义出来。

@Configuration
publicclass StorageConfig {
@Value("${storage.service.type}")
private String storageType;
@Bean
public StorageAdapter storageAdapter() {
if("minio".equals(storageType)) {
returnnew MinioStorageAdapter();
} elseif("aliyun".equals(storageType)) {
returnnew AliStorageAdapter();
} else {
thrownew IllegalArgumentException("为找到对应的文件存储处理器");
}
}
}

新增FileService防腐

提高可维护性

/**
* FileService防腐层
使用fileService(相当于domain防腐层)与adapter(相当于service层只做原子性操作)进行交互、Utils相当于dao层
*/
@Component
publicclass FileService {
/**
* 通过构造函数注入
*/
privatefinal StorageAdapter storageAdapter;

public FileService(StorageAdapter storageAdapter) {
this.storageAdapter = storageAdapter;
}

/**
* 创建bucket
* @param bucket
*/
public void createBucket(String bucket) {
storageAdapter.createBucket(bucket);
}

/**
* 上传图片、返回图片在minio的地址
* @param multipartFile
* @param bucket
* @param objectName
*/
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来进行操作

@RestController
@Log4j2
publicclass FileController {
@Resource//根据名称注入
private FileService fileService;

/**
* 上传文件, 返回文件在oss中的地址
* @param uploadFile:文件, getOriginalFilename获取原始文件名
* @param bucket:桶名称
* @param objectName:上传后的文件在存储桶中的存储路径(存储目录)
* @return String: 返回文件在minio的链接地址
*/
@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搭建

1 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

    :设置Nacos服务使用的jvm参数

    • JVM_XMX:最大堆内存为 256m
    • JVM_XMS:初始堆内存为 256 m
  • –env MODE=standalone:nacos运行模式为单机模式

  • -w /home/nacos:指定容器内的工作目录为 “/home/nacos”,容器内执行的命令如果涉及到相对路径的操作,就会以这个目录作为当前工作目录的基准。

  • 8848:Nacos服务端端口

  • 9848:客户端gRPC请求服务端端口

2 引入nacos客户依赖

除了引入nacos依赖,还要引入log4j2依赖,来输出nacos日志信息

SpringCloudAlibaba 版本为2.2.6.RELEASE时,springboot版本要为2.3.8.RELEASE

<!--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>

3 编写配置文件

把nacos相关配置写入bootstrap.yml文件中,项目启动后会优先读取。

spring:
application:
name:jc-club-oss#微服务名称
profiles:
active:dev#指定环境为开发环境
cloud:
nacos:
server-addr:117.72.118.73:8848
config:
file-extension:yaml#文件后缀名

4 新增配置管理

  • dataId:jc-club-oss-dev.yaml 服务名称+开发环境.yaml
  • 配置内容:

这时spring会根据bootstrap.yml文件中的${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}作为文件id,来读取配置

5 添加@RefreshScope注解开启热更新

  • 在@Value注入的变量所在类上添加注解@RefreshScop,当配置文件内容发生变化后会重新读取
  • 当文件更新后,Bean已加入到了IOC容器,即使storageType属性值变了,Bean也无法重新加载。
  • 所以在@Bean方法上也要加入@RefreshScop注解,当文件更新后,带有此注解的Bean能够自动重新初始化
@Configuration
@RefreshScope
publicclass StorageConfig {

@Value("${storage.service.type}")
private String storageType;

@Bean
@RefreshScope
public StorageAdapter storageAdapter() {
if("minio".equals(storageType)) {
returnnew MinioStorageAdapter();
} elseif("aliyun".equals(storageType)) {
returnnew AliStorageAdapter();
} else {
thrownew IllegalArgumentException("为找到对应的文件存储处理器");
}
}
}

6 测试

1.type为阿里云

结果为:成功返回aliyun

2.修改属性为minio

结果为:图片成功上传。

在配置文件更新时,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]
文章目录
  1. 1. 适配器模式改造
  2. 2. 定义StorageConfig类来获取指定的文件适配器
  3. 3. 新增FileService防腐
  4. 4. Controller层
  5. 5. Nacos搭建
    1. 5.1. 1 Nacos部署
    2. 5.2. 2 引入nacos客户依赖
    3. 5.3. 3 编写配置文件
    4. 5.4. 4 新增配置管理
    5. 5.5. 5 添加@RefreshScope注解开启热更新
    6. 5.6. 6 测试