概述
最近在想做个cloud项目,gitee上找了个模板项目,后端使用到 Nacos、Gateway、Security等技术,看到 文件上传 模板模式+策略模式 用得很好,再次分享一下。
什么是模板模式
一个抽象类中,有一个主方法,再定义1…n个方法,可以是抽象的,也可以是实际的方法,定义一个类,继承该抽象类,重写抽象方法,通过调用抽象类,实现对子类的调用。
优点
- 封装不变的部分,将不变的部分抽取出来;
- 扩展可变部分,将可变的设置抽象方法,让具体子类来实现。
- 抽取的公共代码,便于后期维护。
- 行为有基类来控制,具体操作有子类实现。
缺点
- 每一个不同的实现都需要有一个子类来实现,这样就会导致类的数量大大的增加,使得系统更加庞大。
模式实现及实践
需求
比如我们有一个文件上传功能,后端保存文件的方式很多,比如本地上传、阿里云OSS、华为、腾讯等等,根据模板模式 + 策略模式实现文件上传。
Spring Boot 实现
yml
storage:
enabled: true
config:
# 存储类型:local、aliyun、tencent、qiniu、huawei、minio
type: local
domain: http://localhost:8080
local:
# 存储地址
path: /package/web_project/fast_shaking_cloud_package/
StorageProperties(yml映射)
**
* 存储配置项
*
* @author lanys
*/
@Data
@ConfigurationProperties(prefix = "storage")
public class StorageProperties {
/**
* 是否开启存储
*/
private boolean enabled;
/**
* 通用配置项
*/
private StorageConfig config;
/**
* 本地配置项
*/
private LocalStorageProperties local;
/**
* 阿里云配置项
*/
private AliyunStorageProperties aliyun;
@Data
public static class StorageConfig {
/**
* 访问域名
*/
private String domain;
/**
* 配置路径前缀
*/
private String prefix;
/**
* 存储类型
*/
private StorageTypeEnum type;
}
@Bean
@ConfigurationProperties(prefix = "storage.local")
public LocalStorageProperties localStorageProperties() {
return new LocalStorageProperties();
}
@Bean
@ConfigurationProperties(prefix = "storage.aliyun")
public AliyunStorageProperties aliyunStorageProperties() {
return new AliyunStorageProperties();
}
}
LocalStorageProperties(本地映射实体)
/**
* 本地存储配置项
*
* @author lanys
*/
@Data
public class LocalStorageProperties {
/**
* 本地存储路径
*/
private String path;
/**
* 资源起始路径
*/
private String url = "upload";
}
AliyunStorageProperties(阿里云映射实体)
/**
* 阿里云存储配置项
*
* @author 阿沐 [email protected]
*/
@Data
public class AliyunStorageProperties {
private String endPoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
StorageConfiguration (映射配置,策略模式)
@Configuration
@EnableConfigurationProperties(StorageProperties.class)
@ConditionalOnProperty(prefix = "storage", value = "enabled")
public class StorageConfiguration {
@Bean
public StorageService storageService(StorageProperties properties) {
if (properties.getConfig().getType() == StorageTypeEnum.LOCAL) {
return new LocalStorageService(properties);
} else if (properties.getConfig().getType() == StorageTypeEnum.ALIYUN) {
return new AliyunStorageService(properties);
}
return null;
}
}
StorageService (抽象模板)
/**
* 存储服务
*
* @author lanys
*/
public abstract class StorageService {
public StorageProperties properties;
/**
* 根据文件名,生成带时间戳的新文件名
*
* @param fileName 文件名
* @return 返回带时间戳的文件名
*/
public String getNewFileName(String fileName) {
// 主文件名,不包含扩展名
String prefix = FileNameUtil.getPrefix(fileName);
// 文件扩展名
String suffix = FileNameUtil.getSuffix(fileName);
// 把当天HH:mm:ss,转换成秒
long time = DateUtil.timeToSecond(DateUtil.formatTime(new Date()));
// 新文件名
return prefix + "_" + time + "." + suffix;
}
/**
* 生成路径,不包含文件名
*
* @return 返回生成的路径
*/
public String getPath() {
// 文件路径
String path = DateUtil.format(new Date(), "yyyyMMdd");
// 如果有前缀,则也带上
if (StringUtils.hasText(properties.getConfig().getPrefix())) {
path = properties.getConfig().getPrefix() + "/" + path;
}
return path;
}
/**
* 根据文件名,生成路径
*
* @param fileName 文件名
* @return 生成文件路径
*/
public String getPath(String fileName) {
return getPath() + "/" + getNewFileName(fileName);
}
/**
* 文件上传(抽象方法)
*
* @param data 文件字节数组
* @param path 文件路径,包含文件名
* @return 返回http地址
*/
public abstract String upload(byte[] data, String path);
/**
* 文件上传(抽象方法)
*
* @param inputStream 字节流
* @param path 文件路径,包含文件名
* @return 返回http地址
*/
public abstract String upload(InputStream inputStream, String path);
}
LocalStorageService(本地具体实现)
public class LocalStorageService extends StorageService {
public LocalStorageService(StorageProperties properties) {
this.properties = properties;
}
@Override
public String upload(byte[] data, String path) {
return upload(new ByteArrayInputStream(data), path);
}
@Override
public String upload(InputStream inputStream, String path) {
try {
File file = new File(properties.getLocal().getPath() + File.separator + path);
// 没有目录,则自动创建目录
File parent = file.getParentFile();
if (parent != null && !parent.mkdirs() && !parent.isDirectory()) {
throw new IOException("目录 '" + parent + "' 创建失败");
}
FileCopyUtils.copy(inputStream, Files.newOutputStream(file.toPath()));
} catch (Exception e) {
throw new ServerException("上传文件失败:", e);
}
return properties.getConfig().getDomain() + "/" + properties.getLocal().getUrl() + "/" + path;
}
}
AliyunStorageService(OSS具体实现)
public class AliyunStorageService extends StorageService {
public AliyunStorageService(StorageProperties properties) {
this.properties = properties;
}
@Override
public String upload(byte[] data, String path) {
return upload(new ByteArrayInputStream(data), path);
}
@Override
public String upload(InputStream inputStream, String path) {
OSS client = new OSSClientBuilder().build(properties.getAliyun().getEndPoint(),
properties.getAliyun().getAccessKeyId(), properties.getAliyun().getAccessKeySecret());
try {
client.putObject(properties.getAliyun().getBucketName(), path, inputStream);
} catch (Exception e) {
throw new ServerException("上传文件失败:", e);
} finally {
if (client != null) {
client.shutdown();
}
}
return properties.getConfig().getDomain() + "/" + path;
}
}
测试
@RestController
@RequestMapping("file")
@Tag(name = "文件上传")
@AllArgsConstructor
public class SysFileUploadController {
private final StorageService storageService;
@PostMapping("upload")
@Operation(summary = "上传")
public Result<SysFileUploadVO> upload(@RequestParam("file") MultipartFile file) throws Exception {
if (file.isEmpty()) {
return Result.error("请选择需要上传的文件");
}
// 上传路径
String path = storageService.getPath(file.getOriginalFilename());
// 上传文件
String url = storageService.upload(file.getBytes(), path);
// 封装vo
SysFileUploadVO vo = new SysFileUploadVO();
vo.setUrl(url);
vo.setSize(file.getSize());
vo.setName(file.getOriginalFilename());
vo.setPlatform(storageService.properties.getConfig().getType().name());
return Result.ok(vo);
}
}