前面完成了minio的技术预研,今天基于文件存储框架,集成minio,实现平台使用对象存储组件来存储文件的功能。
相比于直接磁盘存储,只需要指定存储类和根路径,minio还需要指定服务地址、账号、密码、桶名,因此在配置文件的oss节点下新增minio属性,存放这几个配置信息。
oss:
# 使用minio做文件存储
storeClass: tech.abc.platform.oss.service.impl.MinioStoreServiceImpl
# minio因使用桶作为逻辑存储,无根路径,留空即可
basePath:
minio:
server: http://127.0.0.1:9000
accessKey: admin
secretKey: 12345678
bucketName: abc
新增配置类,加载application.yml中minio属性的值。
/**
* miniio配置属性
*
* @author wqliu
* @date 2023-11-21
*/
@Data
public class MinioConfig {
/**
* 服务
*/
private String server = "http://127.0.0.1:9000";
/**
* 账号
*/
private String accessKey = "admin";
/**
* 密钥
*/
private String secretKey = "12345678";
/**
* 桶名
*/
private String bucketName = "abc";
}
修改OssConfig配置类,增加MinioConfig配置。
/**
* 对象存储配置文件
*
* @author wqliu
* @date 2023-05-20
*/
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "platform-config.oss")
public class OssConfig {
/**
* 对象存储类名
*/
private String storeClass = "";
/**
* 存储根路径
*/
private String basePath = "";
/**
* minio配置
*/
private MinioConfig minioConfig=new MinioConfig();
}
同本地磁盘存储的服务类似,新建minio存储服务类,继承抽象基类BaseObjectStoreService,并实现ObjectStoreService接口就行了,部分方法,需要使用minio的API来替换掉磁盘的IO操作,完整源码如下:
/**
* 使用minio 对象存储服务
*
* @author wqliu
* @date 2023-11-21
*/
@Slf4j
public class MinioStoreServiceImpl extends BaseObjectStoreService {
@Autowired
private MinioClient minioClient;
@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 {
storeFile(fileChunk.getFile(),path + filePrefix + fileChunk.getFilename());
} catch (Exception e) {
throw new CustomException(FileExceptionEnum.FILE_CHUNK_STORE_ERROR);
}
}
@Override
public InputStream getFile(String relativePath) {
String fullPath = getFullPath(relativePath);
try {
GetObjectArgs args = GetObjectArgs.builder()
.bucket(ossConfig.getMinioConfig().getBucketName())
.object(fullPath)
.build();
GetObjectResponse response = minioClient.getObject(args);
return response;
}catch (Exception ex){
log.error(ex.getMessage());
return null;
}
}
@Override
public void deleteFile(String relativePath) {
try {
RemoveObjectArgs args = RemoveObjectArgs.builder()
.bucket(ossConfig.getMinioConfig().getBucketName())
.object(relativePath)
.build();
minioClient.removeObject(args);
}catch (Exception e){
log.error("删除文件出错", e);
}
}
@Override
public void mergeChunks(FileInfo fileInfo) {
TreeSet<String> objectList = new TreeSet<>();
// 获取临时文件全路径
String relativePath = generateRelativePath(fileInfo.getModuleCode(),fileInfo.getEntityType());
String tempPath = relativePath+ FileConstant.TEMP_PATH;
String tempFullPath = getFullPath(tempPath);
try {
// 获取该路径下以id开始的文件
Iterable<Result<Item>> fileList = minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(ossConfig.getMinioConfig().getBucketName())
.prefix(tempFullPath+fileInfo.getIdentifier())
.build());
//定义合并数据源
List<ComposeSource> sourceObjectList = new ArrayList<ComposeSource>();
Iterator<Result<Item>> it = fileList.iterator();
while (it.hasNext()) {
Item item = it.next().get();
objectList.add(item.objectName());
}
//合并文件
String fileStoreName = fileInfo.getIdentifier() + fileInfo.getFilename();
String fullPath = getFullPath(relativePath);
// 添加合并数据源
for (String object : objectList) {
sourceObjectList.add(
ComposeSource.builder().bucket(ossConfig.getMinioConfig().getBucketName()).object(object).build());
}
minioClient.composeObject(
ComposeObjectArgs.builder()
.bucket(ossConfig.getMinioConfig().getBucketName())
.object(fullPath + fileStoreName)
.sources(sourceObjectList)
.build());
} catch (Exception e) {
log.error("合并文件块出错", e);
throw new CustomException(FileExceptionEnum.FILE_CHUNK_MERGE_ERROR);
} finally {
// 删除临时文件
for (String object : objectList) {
deleteFile(object);
}
}
}
@Override
public void uploadImage(MultipartFile image, String id) {
try {
PutObjectArgs args = PutObjectArgs.builder()
.bucket(ossConfig.getMinioConfig().getBucketName())
.contentType(image.getContentType())
.object(FileConstant.IMAGE_PATH+id + image.getOriginalFilename())
.stream(image.getInputStream(),image.getSize(), -1)
.build();
minioClient.putObject(args);
}catch (Exception e){
log.error("存储文件块出错", e);
throw new CustomException(FileExceptionEnum.FILE_CHUNK_STORE_ERROR);
}
}
/**
* 存储文件
* @param file 文件
* @param filePath 文件路径
*/
private void storeFile(MultipartFile file, String filePath) {
try {
PutObjectArgs args = PutObjectArgs.builder()
.bucket(ossConfig.getMinioConfig().getBucketName())
.contentType(file.getContentType())
.object(filePath)
.stream(file.getInputStream(),file.getSize(),-1)
.build();
minioClient.putObject(args);
} catch (Exception e) {
log.error("文件存储出错", e);
throw new CustomException(FileExceptionEnum.FILE_STORE_ERROR);
}
}
}
在技术验证环节,使用uploadObject来上传文件,此时文件实际来源于本地磁盘:
/**
* 上传文件
*/
@GetMapping("/upload")
public void upload() {
try {
MinioClient client = getClient();
UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
.bucket("abc")
.object("image/123/1.png")
.filename("e:/1.png")
.build();
client.uploadObject(uploadObjectArgs);
}catch (Exception ex){
log.error(ex.getMessage());
}
}
这种方式适用于C/S架构下的客户端应用程序。
对于大多数B/S结构,实际后端服务收到的是MultipartFile类型的文件,这时候需要更换API,使用的是putObject方法。
/**
* 存储文件
* @param file 文件
* @param filePath 文件路径
*/
private void storeFile(MultipartFile file, String filePath) {
try {
PutObjectArgs args = PutObjectArgs.builder()
.bucket(ossConfig.getMinioConfig().getBucketName())
.contentType(file.getContentType())
.object(filePath)
.stream(file.getInputStream(),file.getSize(),-1)
.build();
minioClient.putObject(args);
} catch (Exception e) {
log.error("文件存储出错", e);
throw new CustomException(FileExceptionEnum.FILE_STORE_ERROR);
}
}
该方法有几个关键参数:
bucket:桶名
contentType:文件类型
object:对象名
stream:内含了三个参数,第一个是文件流,第二个是文件大小,第三个是块大小,后两个参数,如不确定大小可以传-1,minio会自动检测大小,但同时只能一个传-1,不能两个同时都是-1。
其中object可以携带路径,例如image/2023/1.png,若目录不存在minio内部处理会自动创建目录。
获取文件流比较简单,调用的是GetObjectArgs方法,传入文件的路径,拿到文件流。
注意API返回的类型GetObjectResponse,实际是InputStream的子类,因此可以用InputStream来接收。
public InputStream getFile(String relativePath) {
String fullPath = getFullPath(relativePath);
try {
GetObjectArgs args = GetObjectArgs.builder()
.bucket(ossConfig.getMinioConfig().getBucketName())
.object(fullPath)
.build();
GetObjectResponse response = minioClient.getObject(args);
return response;
}catch (Exception ex){
log.error(ex.getMessage());
return null;
}
}
这是技术验证未涉及的一个方法,在文件块合并的逻辑处理中需要用到,调用的是listObjects方法,通过prefix方法来指定前缀。
// 获取该路径下以id开始的文件
Iterable<Result<Item>> fileList = minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(ossConfig.getMinioConfig().getBucketName())
.prefix(tempFullPath+fileInfo.getIdentifier())
.build());
合并文件块,需要使用composeObject方法,入参是文件块的集合。
//定义合并数据源
List<ComposeSource> sourceObjectList = new ArrayList<ComposeSource>();
……
// 添加合并数据源
for (String object : objectList) {
sourceObjectList.add(
ComposeSource.builder().bucket(ossConfig.getMinioConfig().getBucketName()).object(object).build());
}
minioClient.composeObject(
ComposeObjectArgs.builder()
.bucket(ossConfig.getMinioConfig().getBucketName())
.object(fullPath + fileStoreName)
.sources(sourceObjectList)
.build());
因为minio会自动创建目录,上面的服务实现类实际没有用到单独创建目录的操作,作为探索项,也列在这里供参考吧。
如果需要单独创建目录,例如在系统初始化阶段,将默认图片存放目录先创建起来,使用的也是putObject方法,只是用法比较特殊,stream方法中,构建一个空的字节数组,转换为字节流,文件大小赋值0,文件块大小赋值为0(官方实例是赋值了-1,代表自动检测,也可以),同时注意,一定要以“/"结尾,否则minio会将其作为文件处理,如下所示:
/**
* 创建目录
*/
@GetMapping("/createFolder")
public void createFolder() {
try {
MinioClient client = getClient();
PutObjectArgs args = PutObjectArgs.builder()
.bucket("abc")
.object("image/123/")
.stream(new ByteArrayInputStream(new byte[] {}),0,0)
.build();
client.putObject(args);
}catch (Exception ex){
log.error(ex.getMessage());
}
}
平台名称:一二三开发平台
简介: 企业级通用开发平台
设计资料:csdn专栏
开源地址:Gitee
开源协议:MIT
欢迎收藏、点赞、评论,你的支持是我前行的动力。