一、思路
- 我们定义一个接口(就比如接下来要实现的文件上传接口)
- 我们定义所需要实现的策略实现类 A、B、C、D(也就是项目中所使用的四种策略阿里云Oss上传、腾讯云Cos上传、七牛云Kodo上传、本地上传)
- 我们通过策略上下文来调用策略接口,并选择所需要使用的策略
二、策略模式的具体实现
2.1、策略接口的编写
首先我们新建一个名称为
strategy
的文件夹(在代码规范中,使用设计模式要明确的体现出来,便于后期维护)
如下就是我们的策略接口了,接下来我们去编写对应的实现类。
/**
* 上传策略
*
* @author DarkClouds
* @date 2023/05/13
*/
public interface UploadStrategy {
/**
* 上传文件
*
* @param file 文件
* @param path 上传路径
* @return {@link String} 文件地址
*/
String uploadFile(MultipartFile file, String path);
}
2.3、完善配置文件
在编写对象存储实现类之前,我门会发现一个问题。我们需要去对应的云服务厂商开通对象存储服务,然后获取到accessKey、accessKeySecret、endpoint、bucket、domainUrl等必须的参数。
因为这些信息基本是不会发生改变,所以我们可以将这些信息存储在配置文件中。
除此之外我们还需要对文件上传进行配置,设置为最大文件为100MB
server:
port: 8080
spring:
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
# 文件上传策略 local、oss、cos
upload:
strategy: oss
local:
# nginx映射本地文件路径
url: http://127.0.0.1:8800
# 本地文件存储路径
#path: /usr/local/upload
path: D:\img\upload
# oss存储
oss:
url: http://Bucket域名
endpoint: OSS配置endpoint
bucketName: OSS配置bucketName
accessKeyId: OSS配置accessKeyId
accesskeySecret: OSS配置accesskeySecret
# cos存储
cos:
url: https://Bucket域名
secretId: COS配置secretId
secretKey: COS配置secretKey
region: COS配置region
bucketName: COS配置bucketName
配置文件的格式如上,我们获取配置文件的时候可以使用@Value()的注解进行获取。
我们使用@ConfigurationProperties()的方式来获取配置文件的内容。
引入自定义配置依赖 以及 云服务依赖
<!--============== 项目版本号规定 ===============-->
<properties>
<!--============== 对象存储依赖 ==================-->
<cos.version>5.6.89</cos.version>
<kodo.version>[7.7.0, 7.10.99]</kodo.version>
<oss.version>3.15.1</oss.version>
</properties>
<dependencies>
<!-- 自定义配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<!--================== 对象存储依赖 =======================-->
<!-- 腾讯云Cos对象存储 -->
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>${cos.version}</version>
</dependency>
<!-- 七牛云Kodo对象存储 -->
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>${kodo.version}</version>
</dependency>
<!--阿里云Oss对象存储-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>${oss.version}</version>
</dependency>
</dependencies>
我们编写properties实体类,通过@ConfigurationProperties()注解可以将配置文件中的内容读取到实体类中。
实体类中由于类继承关系不要使用@Data注解
,而要使用@Getter和@Setter
,某则可能会出现问题。
除此之外还要注意配置目录的对应关系
。
/**
* oss配置属性
*
* @author DarkClouds
* @date 2023/05/13
*/
@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "upload.oss")
public class OssProperties {
/**
* oss域名
*/
private String url;
/**
* 终点
*/
private String endpoint;
/**
* 访问密钥id
*/
private String accessKeyId;
/**
* 访问密钥密码
*/
private String accessKeySecret;
/**
* bucket名称
*/
private String bucketName;
}
2.4、策略实现类内部实现
我们在进行具体文件上传策略实现之前总结一下所涉及到的功能。
- 上传对象初始化
- 文件是否已经存在
- 文件上传
- 获取访问路径
我们会发现无论是通过哪个平台进行文件的上传,基本上都会使用到上述的步骤,也就是说都会使用到上述的方法。
所以在这里我们定义一个抽象类来规定具体所需要使用的方法,然后各个具体实现来继承我们的抽象类即可。
/**
* 抽象上传模板
*
* @author DarkClouds
* @date 2023/05/13
*/
@Service
public abstract class AbstractUploadStrategyImpl implements UploadStrategy {
@Override
public String uploadFile(MultipartFile file, String path) {
try {
// 获取文件md5值
String md5 = FileUtils.getMd5(file.getInputStream());
// 获取文件扩展名
String extName = FileUtils.getExtension(file);
// 重新生成文件名
String fileName = md5 + "." + extName;
// 判断文件是否已存在
if (!exists(path + fileName)) {
// 不存在则继续上传
upload(path, fileName, file.getInputStream());
}
// 返回文件访问路径
return getFileAccessUrl(path + fileName);
} catch (Exception e) {
e.printStackTrace();
throw new ServiceException("文件上传失败");
}
}
/**
* 判断文件是否存在
*
* @param filePath 文件路径
* @return {@link Boolean}
*/
public abstract Boolean exists(String filePath);
/**
* 上传
*
* @param path 路径
* @param fileName 文件名
* @param inputStream 输入流
* @throws IOException io异常
*/
public abstract void upload(String path, String fileName, InputStream inputStream) throws IOException;
/**
* 获取文件访问url
*
* @param filePath 文件路径
* @return {@link String} 文件url
*/
public abstract String getFileAccessUrl(String filePath);
}
2.4.1、Oss上传策略具体实现
我们在
OssUploadStrategyImpl
实现文件上传至Oss平台,具体如何上传代码至阿里云Oss平台可以去看阿里云官方文档
/**
* oss上传策略
*
* @author DarkClouds
* @date 2023/05/13
*/
@Slf4j
@RequiredArgsConstructor
@Service("ossUploadStrategyImpl")
public class OssUploadStrategyImpl extends AbstractUploadStrategyImpl {
//构造器注入bean
private final OssProperties ossProperties;
@Override
public Boolean exists(String filePath) {
return getOssClient().doesObjectExist(ossProperties.getBucketName(), filePath);
}
@Override
public void upload(String path, String fileName, InputStream inputStream) {
OSS ossClient = getOssClient();
try {
//OSS上传不能以"/"开头,如果路径只有"/"则把路径设置为空
String uploadPath = "/".equals(path) ? "" : path.split("/")[1] + "/";
// 调用oss方法上传
ossClient.putObject(ossProperties.getBucketName(), uploadPath + fileName, inputStream);
} catch (OSSException oe) {
log.error("Error Message:" + oe.getErrorMessage());
log.error("Error Code:" + oe.getErrorCode());
log.info("Request ID:" + oe.getRequestId());
log.info("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
log.error("Caught an ClientException, Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
@Override
public String getFileAccessUrl(String filePath) {
return ossProperties.getUrl() + filePath;
}
/**
* 获取ossClient
*
* @return {@link OSS} ossClient
*/
private OSS getOssClient() {
return new OSSClientBuilder().build(ossProperties.getEndpoint(), ossProperties.getAccessKeyId(), ossProperties.getAccessKeySecret());
}
}
2.4.2、Cos上传策略具体实现
我们在
CosUploadStrategyImpl
实现文件上传至Cos平台,具体如何上传代码至腾讯云Cos平台可以去看腾讯云官方文档。
/**
* cos上传策略
*
* @author DarkClouds
* @date 2023/05/13
*/
@Slf4j
@RequiredArgsConstructor
@Service("cosUploadStrategyImpl")
public class CosUploadStrategyImpl extends AbstractUploadStrategyImpl {
private final CosProperties cosProperties;
@Override
public Boolean exists(String filePath) {
return getCosClient().doesObjectExist(cosProperties.getBucketName(), filePath);
}
@Override
public void upload(String path, String fileName, InputStream inputStream) {
COSClient cosClient = getCosClient();
try {
ObjectMetadata objectMetadata = new ObjectMetadata();
// 上传的流如果能够获取准确的流长度,则推荐一定填写 content-length
objectMetadata.setContentLength(inputStream.available());
// 调用cos方法上传
cosClient.putObject(cosProperties.getBucketName(), path + fileName, inputStream, objectMetadata);
} catch (CosServiceException e) {
log.error("Error Message:" + e.getErrorMessage());
log.error("Error Code:" + e.getErrorCode());
log.info("Request ID:" + e.getRequestId());
} catch (CosClientException e) {
log.error("Caught an CosClientException, Error Message:" + e.getMessage());
} catch (IOException e) {
log.error("Caught an IOException, Error Message:" + e.getMessage());
} finally {
cosClient.shutdown();
}
}
@Override
public String getFileAccessUrl(String filePath) {
return cosProperties.getUrl() + filePath;
}
/**
* 获取cosClient
*
* @return {@link COSClient} cosClient
*/
private COSClient getCosClient() {
// 1 初始化用户身份信息(secretId, secretKey)。
COSCredentials cred = new BasicCOSCredentials(cosProperties.getSecretId(), cosProperties.getSecretKey());
// 2 设置 bucket 的地域, COS 地域的简称请参照 https://cloud.tencent.com/document/product/436/6224
Region region = new Region(cosProperties.getRegion());
ClientConfig clientConfig = new ClientConfig(region);
// 这里建议设置使用 https 协议
// 从 5.6.54 版本开始,默认使用了 https
clientConfig.setHttpProtocol(HttpProtocol.https);
// 3 生成 cos 客户端。
return new COSClient(cred, clientConfig);
}
}
2.4.3、Kodo上传策略具体实现
我们在
KodoUploadStrategyImpl
实现文件上传至七牛云平台,具体如何上传代码至七牛云Kodo平台可以去看七牛云官方文档。
@Slf4j
@RequiredArgsConstructor
@Service("kodoUploadServiceImpl")
public class KodoUploadStrategyImpl extends AbstractUploadStrategyImpl {
/**
* 构造器注入Bean
*/
private final ObjectStoreProperties properties;
/**
* upToken
*/
private String upToken;
/**
* 上传Manger
*/
private UploadManager uploadManager;
/**
* 存储桶Manger
*/
private BucketManager bucketManager;
@Override
public void initClient() {
Auth auth = Auth.create(properties.getKodo().getAccessKey(), properties.getKodo().getAccessKeySecret());
upToken = auth.uploadToken(properties.getKodo().getBucket());
Configuration cfg = new Configuration(Region.region0());
cfg.resumableUploadAPIVersion = Configuration.ResumableUploadAPIVersion.V2;
uploadManager = new UploadManager(cfg);
bucketManager = new BucketManager(auth, cfg);
log.info("OssClient Init Success...");
}
@Override
public boolean checkFileIsExisted(String fileRelativePath) {
try {
if (null == bucketManager.stat(properties.getKodo().getBucket(), fileRelativePath)) {
return false;
}
} catch (QiniuException e) {
return false;
}
return true;
}
@Override
public void executeUpload(MultipartFile file, String fileRelativePath) throws IOException {
try {
uploadManager.put(file.getInputStream(), fileRelativePath, upToken, null, null);
} catch (IOException e) {
log.error("文件上传失败");
throw new BaseException("文件上传失败");
}
}
@Override
public String getPublicNetworkAccessUrl(String fileRelativePath) {
return properties.getKodo().getDomainUrl() + fileRelativePath;
}
}
2.4.4、本地上传策略具体实现
我们在
LocalUploadStrategyImpl
实现文件上传至本地
/**
* 本地上传策略
*
* @author DarkClouds
* @date 2023/05/13
*/
@Service("localUploadStrategyImpl")
public class LocalUploadStrategyImpl extends AbstractUploadStrategyImpl {
/**
* 本地路径
*/
@Value("${upload.local.path}")
private String localPath;
/**
* 访问url
*/
@Value("${upload.local.url}")
private String localUrl;
@Override
public Boolean exists(String filePath) {
return new File(localPath + filePath).exists();
}
@Override
public void upload(String path, String fileName, InputStream inputStream) throws IOException {
// 判断目录是否存在
File directory = new File(localPath + path);
if (!directory.exists()) {
if (!directory.mkdirs()) {
throw new ServiceException("创建目录失败");
}
}
// 写入文件
File file = new File(localPath + path + fileName);
if (file.createNewFile()) {
try (BufferedInputStream bis = new BufferedInputStream(inputStream);
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file))) {
byte[] bytes = new byte[4096];
int length;
while ((length = bis.read(bytes)) != -1) {
bos.write(bytes, 0, length);
}
}
}
}
@Override
public String getFileAccessUrl(String filePath) {
return localUrl + filePath;
}
}
2.5、策略上下文实现
我们通过策略上下文来选择使用哪种上传方式。注意点:
当Map集合的Value为接口类型时,Spring会自动对Map集合进行注入。
- 其中map集合的key为接口对应实现类的BeanName
- 其中map集合的vlaue为接口对应实现类的实例
其中传入的uploadServiceName就是对应策略类所规定的的BeanName,这里的BeanName就作为选择的条件。
/**
* 上传策略上下文
*
* @author DarkClouds
* @date 2023/05/13
*/
@Service
public class UploadStrategyContext {
/**
* 上传模式
*/
@Value("${upload.strategy}")
private String uploadStrategy;
@Autowired
private Map<String, UploadStrategy> uploadStrategyMap;
/**
* 上传文件
*
* @param file 文件
* @param path 路径
* @return {@link String} 文件地址
*/
public String executeUploadStrategy(MultipartFile file, String path) {
return uploadStrategyMap.get(getStrategy(uploadStrategy)).uploadFile(file, path);
}
}
三、总结
上述只是对于策略模式的简单实践。
我们可以通过网站全局配制结合前端界面来完成选择使用哪个平台来进行文件的上传。
当我们选中哪种上传模式,那么后台则会执行该上传方式