Nacos + 適配器 動態(tài)實現(xiàn) OSS 無感切換!
在一個微服務(wù)項目里,我們的 OSS 云存儲服務(wù)常常需要配置諸如阿里云、騰訊云、minio 等多個云存儲廠商的業(yè)務(wù)代碼,而且后續(xù)無法確保是否會增添新的云存儲廠商。
此時,倘若我們要修改具體使用的云存儲廠商,就會致使 controller 層和 service 層發(fā)生變動,這并不符合低耦合的理念。
在這種情況下,我們完全可以采用適配器模式來開展項目開發(fā)!
之前也介紹過另外一種封裝,看陳某之前的文章:《企業(yè)級的OSS對象存儲服務(wù),這樣封裝萬能好用!》
一、適配器模式改造
MinioUtils和AliyunUtils被適配者類作為源接口執(zhí)行原子性操作的具體邏輯各不相同,想要把多個OSS共用一個相同的接口返回,就需要使用到適配器模式。
1. 被適配器類
@Component
publicclass MinioUtil {
@Resource
private MinioClient minioClient;
/**
* 創(chuàng)建Bucket桶(文件夾目錄)
*/
public void createBucket(String bucket) throws Exception {
boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build());
if(!exists) { //不存在創(chuàng)建
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
}
}
/**
* 上傳文件
* inputStream:處理文件的輸入流
* bucket:桶名稱
* objectName:桶中的對象名稱,也就是上傳后的文件在存儲桶中的存儲路徑和文件名。
* stream(inputStream:處理文件的輸入流,-1:指定緩沖區(qū)大小的參數(shù)[-1為默認大小], 5242889L:指定文件內(nèi)容長度的上限)
*/
public void uploadFile(InputStream inputStream, String bucket, String objectName) throws Exception {
minioClient.putObject(PutObjectArgs.builder().bucket(bucket).object(objectName)
.stream(inputStream, -1, 5242889L).build());
}
}
這是目標(biāo)接口 **(目標(biāo)抽象類,即客戶需要的方法)**,我們想要的不同OSS都可通過該接口進行操作:
/**
* 為了方便切換任何一個oss,我們將公共方法抽取為接口,由某個oss的實現(xiàn)類去編寫具體邏輯
*/
public interface StorageAdapter {
/**
* 創(chuàng)建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);
}
2. Minio適配器類
通過繼承或者組合方式,將被適配者類(minioUtils)的接口與目標(biāo)抽象類的接口轉(zhuǎn)換起來,使得客戶端可以按照目標(biāo)抽象類的接口進行操作。
/**
* Minio相關(guān)操作的具體邏輯
*/
@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;
}
}
3. Aliyun適配器類
/**
* 阿里云oss 具體實現(xiàn)邏輯
*/
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的動態(tài)配置讀取來得到當(dāng)前的storageType。
此時如果想再加入一個新的OSS對象(得到xxUtils jar包等,我們無法進行修改),只需新增一個xxadapter適配器類且在@Bean注解的方法中加一個else即可。
注意:這里直接使用new的方式創(chuàng)建實現(xiàn)類(實現(xiàn)類也不需要使用@Service注解),而不是先把所有的實現(xiàn)類通過注解定義出來,再直接返回對象,這樣如果新增一個OSS的話,不光要加else,還需再把實現(xiàn)類通過直接定義出來。
@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("為找到對應(yīng)的文件存儲處理器");
}
}
}
三、新增FileService防腐
提高可維護性:
/**
* FileService防腐層
使用fileService(相當(dāng)于domain防腐層)與adapter(相當(dāng)于service層只做原子性操作)進行交互、Utils相當(dāng)于dao層
*/
@Component
publicclass FileService {
/**
* 通過構(gòu)造函數(shù)注入
*/
privatefinal StorageAdapter storageAdapter;
public FileService(StorageAdapter storageAdapter) {
this.storageAdapter = storageAdapter;
}
/**
* 創(chuàng)建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//根據(jù)名稱注入
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部署
服務(wù)器需開啟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
(1) privileged:賦予容器擴展的特權(quán)
(2) cgroupns host:讓容器使用宿主機的 cgroup 命名空間(在資源限制方面容器會遵循宿主機規(guī)則)
(3) env:設(shè)置Nacos服務(wù)使用的jvm參數(shù)
- JVM_XMX:最大堆內(nèi)存為 256m
- JVM_XMS:初始堆內(nèi)存為 256 m
(4) env MODE=standalone:nacos運行模式為單機模式
(5) w /home/nacos:指定容器內(nèi)的工作目錄為 “/home/nacos”,容器內(nèi)執(zhí)行的命令如果涉及到相對路徑的操作,就會以這個目錄作為當(dāng)前工作目錄的基準(zhǔn)。
(6) 8848:Nacos服務(wù)端端口
(7) 9848:客戶端gRPC請求服務(wù)端端口
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相關(guān)配置寫入bootstrap.yml文件中,項目啟動后會優(yōu)先讀取。
spring:
application:
name:jc-club-oss#微服務(wù)名稱
profiles:
active:dev#指定環(huán)境為開發(fā)環(huán)境
cloud:
nacos:
server-addr:117.72.118.73:8848
config:
file-extension:yaml#文件后綴名
4. 新增配置管理
dataId:jc-club-oss-dev.yaml 服務(wù)名稱+開發(fā)環(huán)境.yaml。
配置內(nèi)容:
這時spring會根據(jù)bootstrap.yml文件中的${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}作為文件id,來讀取配置。
5. 添加@RefreshScope注解開啟熱更新
- 在@Value注入的變量所在類上添加注解@RefreshScop,當(dāng)配置文件內(nèi)容發(fā)生變化后會重新讀取
- 當(dāng)文件更新后,Bean已加入到了IOC容器,即使storageType屬性值變了,Bean也無法重新加載。
- 所以在@Bean方法上也要加入@RefreshScop注解,當(dāng)文件更新后,帶有此注解的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("為找到對應(yīng)的文件存儲處理器");
}
}
}
6. 測試
(1) type為阿里云
結(jié)果為:成功返回aliyun
(2) 修改屬性為minio
結(jié)果為:圖片成功上傳。
在配置文件更新時,nacos也會打印出對應(yīng)的日志提示:
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]