SpringBoot框架中使用云存储,如何优雅地在亚马逊、华为、阿里、腾...
Hello,各位小伙伴,好久又没有更新文章了,不是我不更新,而是产品经理太懒了。
最近产品经理又与我扛上了,因为系统使用云环境不同,产品经理提出按环境变化的一个需求:
我们系统有些资料,比如文件、图片要使用云存储,测试环境使用的是亚马逊云存储;生产环境要使用华为云存储。所以系统必须要实现这2套存储,而且还要按环境动态切换。如果后面要实用阿里或者腾讯云,都可以动态切换。让系统不做太多的改变。
当时听到这需求,心想不是很简单嘛,只需要下面三步:
1、定义一个接口
2、按不同的云厂商实现即可
3、在使用的时候,根据使用的厂商名字注入 Service 即可
可后来仔细一想,这样虽然能完成产品经理提出的功能需求,可每次都要修改注入的 Service 代码,还需要重新打包,实在很麻烦。作为一名 JAVA 懒汉编程人员,这肯定不是我想要的。
有没有更好的方式呢?
有!
作为开发人员一定要干翻产品经理,不能丢了开发人员的颜面,只要你提的出来,我就有对策。所以在产品经理提出需要的时候,我想也没想就回答有。
回答虽然很爽快,但要怎么去实现呢?
整理下面的过程:
1、定义一个接口
2、按厂商实现接口
3、根据厂商注入接口实现
So Easy! 简单三步完成,下面一起来看代码实现:
定义接口:
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服务器** key唯一,否则会变成覆盖相同key的内容*** @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);/*** 获取文件匿名访问URL** @param key /* @return /* @throws URISyntaxException /*/String getFileUrl(String key) throws URISyntaxException;}
我们现在只有亚马逊、华为云,其实现如下:
亚马逊云实现:
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();}}
华为云实现:
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);// 创建ObsClient实例ObsClient obsClient = obsConfig.createObsClient();TemporarySignatureResponse response = obsClient.createTemporarySignature(request);return response.getSignedUrl();}}
不同厂商的云实现已经有了,现在需要在配制文件里添加配制参数:
## 配制亚马逊存储或者华为云存储service:# Amazon 亚马逊存储# huaWei 华为云存储type: Amazon
最后根据参数修改亚马逊云、华为云实现的注解:
亚马逊云实现:
// 将 @Service 替换为(name = "service.type", havingValue="Amazon”)
华为云实现:
// 将 @Service 替换为(name = "service.type", havingValue="HuaWei”)
现在使用的 亚马逊云 启动系统:

OK,项目启动成功!!
后面即使使用阿里去,腾讯云或者其它,我只需要pugm实现一个接口,在修改配制文件即可启动对应的云Servie
总结:
动态切换主要用到了2个注解:
1、@Configuration 是 Spring-conten 中的一个注解,将该类声明为一个配置类
2、@ConditionalOnProperty 是 spring-boot-autoconfigure 中的一个注解,根据注解从配制文件中读取到的值,决定是否把当前类注入到 Spring 中
扩展, 以 Conditional 开头的还有以下注解:
@ConditionalOnBean:仅在当前上下文中存在某个对象时,才会实例化一个Bean。
@ConditionalOnClass:某个class位于类路径上,才会实例化一个Bean。
@ConditionalOnExpression:当表达式值为true的时候,才会实例化一个Bean。
@ConditionalOnMissingBean:仅仅在当前上下文中不存在某个对象时,才会实例化一个Bean。
@ConditionalOnMissingClass:某个class类路径上不存在的时候,才会实例化一个Bean。
@ConditionalOnNotWebApplication:非web应用,才会实例化一个Bean。
@ConditionalOnBean:当容器中有指定Bean的条件下进行实例化。
@ConditionalOnMissingBean:当容器里没有指定Bean的条件下进行实例化。
@ConditionalOnClass:当classpath类路径下有指定类的条件下进行实例化。
@ConditionalOnMissingClass:当类路径下没有指定类的条件下进行实例化。
@ConditionalOnWebApplication:当项目是一个Web项目时进行实例化。
@ConditionalOnNotWebApplication:当项目不是一个Web项目时进行实例化。
@ConditionalOnProperty:当指定的属性有指定的值时进行实例化。
@ConditionalOnExpression:基于SpEL表达式的条件判断。
@ConditionalOnJava:当JVM版本为指定的版本范围时触发实例化。
@ConditionalOnResource:当类路径下有指定的资源时触发实例化。
@ConditionalOnJndi:在JNDI存在的条件下触发实例化。
@ConditionalOnSingleCandidate:当指定的Bean在容器中只有一个,或者有多个但是指定了首选的Bean时,才会触发实例化。
