上篇介绍了平台统一使用附件实体来封装文件,今天来说下存储层面的封装与设计。
作为平台,需考虑支持多种文件存储方案,对于中小型系统,可以使用直接存储到磁盘这种简单实用的方式;对一中大型系统,能支持使用对象存储组件。
基于上述考虑,建立抽象层,通过接口定义对于文件的上传、下载、查看等功能,对于对象存储或文件服务器存储,本质上都是具体的存储实现方式。
通过功能类的具体实现,来支持多种存储模式,通过更改配置,可以灵活选择具体的存储方式,业务应用无感知。
平台内置磁盘存储和主流对象存储组件minio两种实现方案,如对接其他对象存储系统,如阿里OSS,通过系统集成的方式,实现预置的抽象接口,适配到阿里OSS的API即可。
对于多种存储模式,定义统一的服务接口,如下所示:
/**
* 对象存储服务接口
*
* @author wqliu
* @date 2023-05-20
*/
public interface ObjectStoreService {
/**
* 上传文件块
*
* @param fileChunk 文件块
*/
void uploadChunk(FileChunk fileChunk);
/**
* 合并文件块
*
* @param fileInfo 文件信息
*/
void mergeChunks(FileInfo fileInfo);
/**
* 删除文件
*
* @param relativePath 文件相对路径
* @return
*/
void deleteFile(String relativePath);
/**
* 获取文件流
*
* @param relativePath 文件相对路径
* @return 文件流
*/
InputStream getFile(String relativePath);
/**
* 上传图片
*
* @param image 图像
* @param id id
*/
void uploadImage(MultipartFile image, String id);
/**
* 获取文件全路径
*
* @param relativePath 相对路径
* @return 完整路径
*/
String getFullPath(String relativePath);
/**
* 生成相对存储路径
*
* @param moduleCode 模板编码
* @param entityType 实体类型
* @return 相对路径
*/
String generateRelativePath(String moduleCode, String entityType);
}
上传文件块、合并文件块、删除文件、获取文件流、上传图片这5个服务接口是跟统一封装的附件服务对应的。在此基础上,增加了2个辅助接口,传入相对路径,获取文件全路径,以及根据模块编码和实体类型生成相对存储路径。
抽象基类是服务接口ObjectStoreService的默认实现,一方面,可以将公用方法提取到抽象基类中实现,提升代码的复用;另一方面,也能简化具体服务实现类的功能实现工作。
/**
* 对象存储服务接口抽象实现类
* @author wqliu
* @date 2023-11-23
*/
public abstract class BaseObjectStoreService implements ObjectStoreService{
@Autowired
protected OssConfig ossConfig;
@Override
public String generateRelativePath(String moduleCode, String entityType) {
// 生成附件上传路径 根路径/模块名/实体类型名/年份/月份
Calendar calendar = Calendar.getInstance();
StringBuilder sbRelativePath = new StringBuilder()
.append(moduleCode).append("/")
.append(entityType).append("/")
.append(calendar.get(Calendar.YEAR)).append("/")
// 月份左边补零到2位
.append(StringUtils.leftPad(String.valueOf(calendar.get(Calendar.MONTH) + 1), 2, "0"))
.append("/");
return sbRelativePath.toString();
}
/**
* 获取文件全路径
*
* @param relativePath
* @return
*/
@Override
public String getFullPath(String relativePath) {
String basePath = ossConfig.getBasePath();
return basePath+relativePath;
}
}
在平台配置文件application.yml中,添加oss的配置节点,来指定具体的存储类和设置基存储根路径。
#平台配置
platform-config:
system:
enablePermission: false
userInitPassword: 12345678
tokenSecret: wqliu
exportDataPageSize: 2
notification:
serverPort: 9997
oss:
# 存储类
storeClass: tech.abc.platform.oss.service.impl.LocalStoreServiceImpl
# 存储根路径
basePath: c:/attachment/
mail:
senderAddress: sealy321@126.com
通过配置类,加载application.yml中oss属性的值。
/**
* 对象存储配置文件
*
* @author wqliu
* @date 2023-05-20
*/
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "platform-config.oss")
public class OssConfig {
/**
* 对象存储类名
*/
private String storeClass = "";
/**
* 存储根路径
*/
private String basePath = "";
}
/**
* 对象存储服务配置
*
* @author wqliu
* @date 2023-05-20
*/
@Configuration
@Slf4j
public class OssServiceConfig {
@Autowired
private OssConfig ossConfig;
@Bean
public ObjectStoreService instance() {
String className = ossConfig.getStoreClass();
try {
return (ObjectStoreService) ClassUtils.getClass(className).newInstance();
} catch (Exception e) {
log.error("加载对象存储服务类出错", e);
throw new CustomException(CommonException.CLASS_NOT_FOUND);
}
}
}
文件存储框架搭建好了,接下来就是具体的实现了。按照目标,我们需要实现本地磁盘存储和集成minio对象存储组件两种模式。
先来说一下磁盘直接存储这种相对简单的模式实现。
新建本地存储服务类,继承抽象基类BaseObjectStoreService,并实现ObjectStoreService接口就行了。
处理逻辑在前面文章的设计中提到过,具体如下:
上传文件块,如文件体积较小,没有触发分块,则该文件块就是一个完整的文件,将该文件直接存储到磁盘。
若文件体积较大,触发了分块,则只将分块存到磁盘临时目录下;待前端检测到所有文件块均已上传完成,调用合并文件块操作,依据全局唯一的文件标识,去临时目录下找到所有的文件块,进行文件合并操作。
对于富文本编辑器中上传的图片,同样使用附件功能来进行统一封装,与普通文件不同的是,图片上传不分块,存放到预置的统一目录(image/)下,生成一个虚拟的实体标识,不对应具体的实体,该实体标识来存储图片及读取图片用来展示。
完整源码如下:
/**
* 本地磁盘模式 对象存储服务
*
* @author wqliu
* @date 2023-05-20
*/
@Slf4j
public class LocalStoreServiceImpl extends BaseObjectStoreService {
@Autowired
private OssConfig ossConfig;
@Override
public void uploadChunk(FileChunk fileChunk) {
// 默认前缀使用唯一性编号id
String filePrefix = fileChunk.getIdentifier();
// 默认是正式目录
String relativePath = generateRelativePath(fileChunk.getModuleCode(),fileChunk.getEntityType());
String path = getFullPath(relativePath);
// 如进行了分块
if (fileChunk.getTotalChunks() > 1) {
// 路径附加临时目录
path = path + FileConstant.TEMP_PATH;
// 前缀附加块编号
filePrefix = filePrefix + "-" + StringUtils.leftPad(fileChunk.getChunkNumber().toString(), 3, "0");
}
try {
File file = new File(path + filePrefix + fileChunk.getFilename());
FileUtils.copyInputStreamToFile(fileChunk.getFile().getInputStream(), file);
} catch (IOException e) {
log.error("存储文件块出错", e);
throw new CustomException(FileExceptionEnum.FILE_CHUNK_STORE_ERROR);
}
}
@Override
public void mergeChunks(FileInfo fileInfo) {
// 生成目标文件
String relativePath = generateRelativePath(fileInfo.getModuleCode(),fileInfo.getEntityType());
String fullPath = getFullPath(relativePath);
File targetFile = new File(fullPath + fileInfo.getIdentifier() + fileInfo.getFilename());
// 获取临时文件全路径
String tempFullPath = fullPath + FileConstant.TEMP_PATH;
// 获取该路径下以id开始的文件
File dir = FileUtils.getFile(tempFullPath);
FilenameFilter filenameFilter = new PrefixFileFilter(fileInfo.getIdentifier());
String[] fileList = dir.list(filenameFilter);
Arrays.sort(fileList);
try {
// 合并文件
for (String file : fileList) {
Path chunkPath = Paths.get(tempFullPath, file);
FileUtils.writeByteArrayToFile(targetFile, Files.readAllBytes(chunkPath), true);
}
} catch (IOException e) {
log.error("合并文件块出错", e);
throw new CustomException(FileExceptionEnum.FILE_CHUNK_MERGE_ERROR);
} finally {
// 删除临时文件
for (String file : fileList) {
FileUtils.deleteQuietly(new File(tempFullPath + file));
}
}
}
@Override
public InputStream getFile(String relativePath) {
String fullPath = getFullPath(relativePath);
try {
return FileUtils.openInputStream(new File(fullPath));
} catch (IOException e) {
log.error("读取附件出错", e);
throw new CustomException(FileExceptionEnum.FILE_READ_ERROR);
}
}
@Override
public void deleteFile(String relativePath) {
String fullPath = getFullPath(relativePath);
FileUtils.deleteQuietly(new File(fullPath));
}
@Override
public void uploadImage(MultipartFile image, String id) {
// 默认是正式目录
String basePath = ossConfig.getBasePath();
String path = FilenameUtils.concat(basePath, FileConstant.IMAGE_PATH);
try {
File file = new File(path + id + image.getOriginalFilename());
FileUtils.copyInputStreamToFile(image.getInputStream(), file);
} catch (IOException e) {
log.error("存储文件块出错", e);
throw new CustomException(FileExceptionEnum.FILE_CHUNK_STORE_ERROR);
}
}
}
平台名称:一二三开发平台
简介: 企业级通用开发平台
设计资料:csdn专栏
开源地址:Gitee
开源协议:MIT
欢迎收藏、点赞、评论,你的支持是我前行的动力。