SpringBoot框架中使用云存儲(chǔ),如何優(yōu)雅地在亞馬遜、華為、阿里、騰...
Hello,各位小伙伴,好久又沒有更新文章了,不是我不更新,而是產(chǎn)品經(jīng)理太懶了。
最近產(chǎn)品經(jīng)理又與我扛上了,因?yàn)橄到y(tǒng)使用云環(huán)境不同,產(chǎn)品經(jīng)理提出按環(huán)境變化的一個(gè)需求:
我們系統(tǒng)有些資料,比如文件、圖片要使用云存儲(chǔ),測(cè)試環(huán)境使用的是亞馬遜云存儲(chǔ);生產(chǎn)環(huán)境要使用華為云存儲(chǔ)。所以系統(tǒng)必須要實(shí)現(xiàn)這2套存儲(chǔ),而且還要按環(huán)境動(dòng)態(tài)切換。如果后面要實(shí)用阿里或者騰訊云,都可以動(dòng)態(tài)切換。讓系統(tǒng)不做太多的改變。
當(dāng)時(shí)聽到這需求,心想不是很簡(jiǎn)單嘛,只需要下面三步:
1、定義一個(gè)接口
2、按不同的云廠商實(shí)現(xiàn)即可
3、在使用的時(shí)候,根據(jù)使用的廠商名字注入 Service 即可
可后來(lái)仔細(xì)一想,這樣雖然能完成產(chǎn)品經(jīng)理提出的功能需求,可每次都要修改注入的 Service 代碼,還需要重新打包,實(shí)在很麻煩。作為一名 JAVA 懶漢編程人員,這肯定不是我想要的。
有沒有更好的方式呢?
有!
作為開發(fā)人員一定要干翻產(chǎn)品經(jīng)理,不能丟了開發(fā)人員的顏面,只要你提的出來(lái),我就有對(duì)策。所以在產(chǎn)品經(jīng)理提出需要的時(shí)候,我想也沒想就回答有。
回答雖然很爽快,但要怎么去實(shí)現(xiàn)呢?
整理下面的過(guò)程:
1、定義一個(gè)接口
2、按廠商實(shí)現(xiàn)接口
3、根據(jù)廠商注入接口實(shí)現(xiàn)
So Easy! ?簡(jiǎn)單三步完成,下面一起來(lái)看代碼實(shí)現(xiàn):
定義接口:
package com.hx.module.system.v2.service;import org.springframework.web.multipart.MultipartFile;import java.io.IOException;import java.net.URISyntaxException;public interface AmazonS3OperationService {/*** 上傳文件到S3服務(wù)器** key唯一,否則會(huì)變成覆蓋相同key的內(nèi)容*** @param file /* @param key /* @throws IOException /*/void uploadFile(MultipartFile file, String key) throws IOException;/*** 下載文件** @param key /*/byte[] downloadFile(String key) throws IOException;/*** 刪除文件** @param key /*/void deleteFile(String key);/*** 獲取文件匿名訪問(wèn)URL** @param key /* @return /* @throws URISyntaxException /*/String getFileUrl(String key) throws URISyntaxException;}
我們現(xiàn)在只有亞馬遜、華為云,其實(shí)現(xiàn)如下:
亞馬遜云實(shí)現(xiàn):
package com.hx.module.system.v2.service.impl;import com.hx.module.system.v2.service.AmazonS3OperationService;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;import org.springframework.context.annotation.Configuration;import org.springframework.web.multipart.MultipartFile;import software.amazon.awssdk.core.ResponseInputStream;import software.amazon.awssdk.core.sync.RequestBody;import software.amazon.awssdk.services.s3.S3Client;import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;import software.amazon.awssdk.services.s3.model.GetObjectRequest;import software.amazon.awssdk.services.s3.model.GetObjectResponse;import software.amazon.awssdk.services.s3.model.PutObjectRequest;import software.amazon.awssdk.services.s3.presigner.S3Presigner;import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;import javax.validation.constraints.NotBlank;import java.io.IOException;import java.net.URISyntaxException;import java.time.Duration;import java.util.Objects;public class AmazonS3OperationServiceImpl implements AmazonS3OperationService {private String TEST_BUCKET = "hx-aqgl-test";private Integer URL_OUT_TIME = 10;private final S3Client client;private final S3Presigner presigner;public AmazonS3OperationServiceImpl(S3Client client, S3Presigner presigner) {this.client = client;this.presigner = presigner;}public void uploadFile(MultipartFile file, String key) throws IOException {if (Objects.isNull(file)) {return;}String originalFilename = file.getOriginalFilename();if (Objects.isNull(originalFilename)) {return;}// int indexOf = originalFilename.lastIndexOf(".");// String suffix = originalFilename.substring(indexOf);// key = key + suffix;PutObjectRequest objectRequest = PutObjectRequest.builder().bucket(TEST_BUCKET).key(key).build();client.putObject(objectRequest, RequestBody.fromBytes(file.getBytes()));}public byte[] downloadFile( String key) throws IOException {GetObjectRequest getObjectRequest = GetObjectRequest.builder().bucket(TEST_BUCKET).key(key).build();ResponseInputStreamobject = client.getObject(getObjectRequest); return object.readAllBytes();}public void deleteFile( String key) {DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder().bucket(TEST_BUCKET).key(key).build();client.deleteObject(deleteObjectRequest);}public String getFileUrl( String key) throws URISyntaxException {GetObjectRequest getObjectRequest = GetObjectRequest.builder().bucket(TEST_BUCKET).key(key).build();GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder().signatureDuration(Duration.ofMinutes(URL_OUT_TIME)).getObjectRequest(getObjectRequest).build();PresignedGetObjectRequest presignedGetObjectRequest =presigner.presignGetObject(getObjectPresignRequest);return presignedGetObjectRequest.url().toURI().toString();}}
華為云實(shí)現(xiàn):
package com.hx.module.system.v2.service.impl;import com.hx.config.ObsConfig;import com.hx.module.system.utils.UploadFileUtil;import com.hx.module.system.v2.service.AmazonS3OperationService;import com.obs.services.ObsClient;import com.obs.services.model.*;import lombok.extern.slf4j.Slf4j;import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;import org.springframework.context.annotation.Configuration;import org.springframework.web.multipart.MultipartFile;import javax.annotation.Resource;import java.io.IOException;import java.io.InputStream;public class HuaWeiServerImpl implements AmazonS3OperationService {private ObsConfig obsConfig;public void uploadFile(MultipartFile file, String key) throws IOException {PutObjectResult putObjectResult = UploadFileUtil.uploadNetworkStream(obsConfig.createObsClient(), obsConfig.getBucketName(), file.getInputStream(), key);log.info("上傳文件:{}", putObjectResult);}public byte[] downloadFile(String key) throws IOException {ObsObject obsObject = obsConfig.createObsClient().getObject(obsConfig.getBucketName(), key);InputStream input = obsObject.getObjectContent();log.info("下載文件:{}", input.toString());return input.readAllBytes();}public void deleteFile(String key) {DeleteObjectResult deleteObjectResult = obsConfig.createObsClient().deleteObject( obsConfig.getBucketName(), key);log.info("刪除文件:{}", deleteObjectResult);}public String getFileUrl(String key) {TemporarySignatureRequest request = new TemporarySignatureRequest(HttpMethodEnum.PUT, obsConfig.getExpireSeconds() > 60 ? obsConfig.getExpireSeconds() : 300L);request.setBucketName(obsConfig.getBucketName());request.setObjectKey(key);// 創(chuàng)建ObsClient實(shí)例ObsClient obsClient = obsConfig.createObsClient();TemporarySignatureResponse response = obsClient.createTemporarySignature(request);return response.getSignedUrl();}}
不同廠商的云實(shí)現(xiàn)已經(jīng)有了,現(xiàn)在需要在配制文件里添加配制參數(shù):
## 配制亞馬遜存儲(chǔ)或者華為云存儲(chǔ)service:# Amazon 亞馬遜存儲(chǔ)# huaWei 華為云存儲(chǔ)type: Amazon
最后根據(jù)參數(shù)修改亞馬遜云、華為云實(shí)現(xiàn)的注解:
亞馬遜云實(shí)現(xiàn):?
// 將 @Service 替換為(name = "service.type", havingValue="Amazon”)
華為云實(shí)現(xiàn):?
// 將 @Service 替換為(name = "service.type", havingValue="HuaWei”)
現(xiàn)在使用的?亞馬遜云 啟動(dòng)系統(tǒng):

OK,項(xiàng)目啟動(dòng)成功??!
后面即使使用阿里去,騰訊云或者其它,我只需要pugm實(shí)現(xiàn)一個(gè)接口,在修改配制文件即可啟動(dòng)對(duì)應(yīng)的云Servie
總結(jié):
動(dòng)態(tài)切換主要用到了2個(gè)注解:
1、@Configuration ? 是?Spring-conten 中的一個(gè)注解,將該類聲明為一個(gè)配置類
2、@ConditionalOnProperty ?是 spring-boot-autoconfigure 中的一個(gè)注解,根據(jù)注解從配制文件中讀取到的值,決定是否把當(dāng)前類注入到 Spring 中
擴(kuò)展, 以 Conditional 開頭的還有以下注解:
@ConditionalOnBean:僅在當(dāng)前上下文中存在某個(gè)對(duì)象時(shí),才會(huì)實(shí)例化一個(gè)Bean。
@ConditionalOnClass:某個(gè)class位于類路徑上,才會(huì)實(shí)例化一個(gè)Bean。
@ConditionalOnExpression:當(dāng)表達(dá)式值為true的時(shí)候,才會(huì)實(shí)例化一個(gè)Bean。
@ConditionalOnMissingBean:僅僅在當(dāng)前上下文中不存在某個(gè)對(duì)象時(shí),才會(huì)實(shí)例化一個(gè)Bean。
@ConditionalOnMissingClass:某個(gè)class類路徑上不存在的時(shí)候,才會(huì)實(shí)例化一個(gè)Bean。
@ConditionalOnNotWebApplication:非web應(yīng)用,才會(huì)實(shí)例化一個(gè)Bean。
@ConditionalOnBean:當(dāng)容器中有指定Bean的條件下進(jìn)行實(shí)例化。
@ConditionalOnMissingBean:當(dāng)容器里沒有指定Bean的條件下進(jìn)行實(shí)例化。
@ConditionalOnClass:當(dāng)classpath類路徑下有指定類的條件下進(jìn)行實(shí)例化。
@ConditionalOnMissingClass:當(dāng)類路徑下沒有指定類的條件下進(jìn)行實(shí)例化。
@ConditionalOnWebApplication:當(dāng)項(xiàng)目是一個(gè)Web項(xiàng)目時(shí)進(jìn)行實(shí)例化。
@ConditionalOnNotWebApplication:當(dāng)項(xiàng)目不是一個(gè)Web項(xiàng)目時(shí)進(jìn)行實(shí)例化。
@ConditionalOnProperty:當(dāng)指定的屬性有指定的值時(shí)進(jìn)行實(shí)例化。
@ConditionalOnExpression:基于SpEL表達(dá)式的條件判斷。
@ConditionalOnJava:當(dāng)JVM版本為指定的版本范圍時(shí)觸發(fā)實(shí)例化。
@ConditionalOnResource:當(dāng)類路徑下有指定的資源時(shí)觸發(fā)實(shí)例化。
@ConditionalOnJndi:在JNDI存在的條件下觸發(fā)實(shí)例化。
@ConditionalOnSingleCandidate:當(dāng)指定的Bean在容器中只有一個(gè),或者有多個(gè)但是指定了首選的Bean時(shí),才會(huì)觸發(fā)實(shí)例化。
