Java使用MinIO
Java使用MinIo
MinIo介绍
Minio 是个基于Apache License v2.0开源协议的对象存储服务,虽然轻量,却拥有着不错的性能。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据。
例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几 kb 到最大 5T 不等。
官网地址:minio
何为对象存储?我们来看下阿里云 OSS(Object Storage Service) 的介绍
对象存储服务(Object Storage Service,OSS)是一种海量、安全、低成本、高可靠的云存储服务,适合存放任意类型的文件。容量和处理能力弹性扩展,多种存储类型供选择,全面优化存储成本。
对于中小型企业,如果不选择存储上云,那么 Minio 是个不错的选择,麻雀虽小,五脏俱全。当然 Minio 除了直接作为对象存储使用,还可以作为云上对象存储服务的网关层,无缝对接到 Amazon S3、MicroSoft Azure。
下载MinIO
既然知道了Minio是做什么的,就来看一下如何下载Minio
Docker安装minio
Docker如果想安装软件,必须先到 Docker 镜像仓库下载镜像
寻找Minio镜像


下载Minio镜像
命令 描述 docker pull minio/minio 下载最新版Minio镜像 (其实此命令就等同于 : docker pull minio/minio:latest ) docker pull minio/minio:RELEASE.2024-03-26T22-10-45Z.fips 下载指定版本的Minio镜像(xxx指具体版本号) 检查当前Docker下载的镜像
docker images创建目录
一个用来存放配置,一个用来存储上传文件的目录
启动前需要先创建Minio外部挂载的配置文件(/home/minio/config)和存储上传文件的目录(/home/minio/data)1
2mkdir -p /home/minio/config
mkdir -p /home/minio/data创建Minio容器并运行
1
2
3
4
5
6
7
8
9
10docker run -p 9000:9000 -p 9090:9090 \
--net=host \
--name minio \
-d --restart=always \
-e "MINIO_ACCESS_KEY=minioadmin" \
-e "MINIO_SECRET_KEY=minioadmin" \
-v /home/minio/data:/data \
-v /home/minio/config:/root/.minio \
minio/minio server \
/data --console-address ":9090" -address ":9000"9090端口指的是minio的客户端端口
MINIO_ACCESS_KEY :账号
MINIO_SECRET_KEY :密码(账号长度必须大于等于5,密码长度必须大于等于8位)
通过docker ps查看是否正在运行
- 访问操作
访问: http://你虚拟机的ip:9090/login
用户名和密码填你自己的
创建Bucket

、
点进去设置访问策略为Public

Windows安装minio
官网页面中有服务端、客户端以及不同语言SDK的依赖包

可以点击DOWNLOAD的下载服务端按钮,或者在Windows PowerShell中执行该命令:
1 | Invoke-WebRequest -Uri "https://dl.min.io/server/minio/release/windows-amd64/minio.exe" -OutFile "C:\minio.exe" |
其中C:\minio.exe为下载的路径位置,可以自行更改

创建一个minio存放文件的目录

设置用户名和密码
1 | # 用户环境变量 |
启动
启动需要在cmd里输入命令启动
D:\minio\data 为刚才创建的minio存放文件的目录
1 | minin.exe server D:\minio\data |
如果 9000 端口被别的服务占用了,可以在启动的时候指定服务的端口
例如指定 9100 和 9101,前提是这两个端口没有被占用
1 | minio.exe server D:\minio\data --console-address ":9100" --address ":9101" |
访问
在启动后的窗口的WebUI查看访问地址

访问后输入用户名和密码登录进入控制台页面

创建桶


点进去设置访问策略为Public

环境搭建
首先我们需要导入minio的依赖,来使用minio的api
1 | <dependency> |
配置minio的各项属性
1 | # minio的配置 |
1 |
|
这样minio的各项配置就配置完成,我们就可以用MinioClient中的方法来进行操作了
Minio分片上传
minio上传方法
1 | MinioClient minioClient = new MinioClient(); |
首先来分析上传需要的参数:
- bucket
这个我们指定上传文件在哪个桶就行了 - fileName
由于是本地地址,所以接收了前端传过来的文件后存储在临时的文件夹中就可以了 - object
我们用传过来的文件的md5作目录加上/chunk加上/分片的顺序也就是1,2,3,4…这样就不会重复了 - contentType
由于是分片所以类型是空也就是 “”
这样我们就知道上传分片的接口要有什么参数了
正常来说切片应该是由前端来切片文件,然后调用接口来上传分片文件,等所有分片文件上传完后请求合并接口,如果上传到一半暂停检查minio中的分片,如果有从最后的分片开始接着传后面的分片,等所有分片文件上传完后请求合并接口
接口大概就是这样
1 |
|
但是我是后端,在测试的时候需要查看分片的效果,所以接口就变成接受完整文件,分片在后端完成,返回分片数量
1 |
|
实现分片的上传方法
首先创建MinioService
定义上传方法1
2
3
4
5
6
7
8public interface MinioService {
/**
* 上传分片文件
* @param videoFIle 完整视频文件
* @return 上传分片的数量
*/
public int uploadVideo(File videoFIle) ;
}创建MinioServiceImpl,注入MinioClient,定义好每个分片的大小,从yaml里获取存储视频文件的桶名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MinioServiceImpl implements MinioService {
private final MinioClient minioClient;
//存储视频文件
private String bucket_video;
// 设置分片大小
long partSize = 5 * 1024 * 1024; //5MB
public MinioServiceImpl(MinioClient minioClient) {
this.minioClient = minioClient;
}
}创建根据扩展名获取mimeType的方法,也就是我们上传需要的contentType
1
2
3
4
5
6
7
8
9
10
11
12
13//根据扩展名获取mimeType
private String getMimeType(String extension){
if(extension == null){
extension = "";
}
//根据扩展名取出mimeType
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
if(extensionMatch != null){
mimeType = extensionMatch.getMimeType();
}
return mimeType;
}创建获取文件的值的方法
1
2
3
4
5
6
7
8
9
10//获取文件的md5
private String getFileMd5(File file) {
try (FileInputStream fileInputStream = new FileInputStream(file)) {
String fileMd5 = DigestUtils.md5Hex(fileInputStream);
return fileMd5;
} catch (Exception e) {
log.error("获取文件md5失败",e);
return null;
}
}创建根据md5值来获取分块文件在minio里的目录的方法
1
2
3
4//得到分块文件的目录
private String getChunkFileFolderPath(String fileMd5) {
return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
}实现上传视频的方法
1
2
3
public int uploadVideo(File videoFile) {
}首先调用方法获取文件的md5和定义一个目录来存放分片文件
前端切片的话并不需要md5和定义目录存放分片文件1
2
3
4//获取文件的md5值
String fileMd5 = getFileMd5(videoFile);
//存放分片的目录
String chunkPath = "D:/testchunk/";计算文件总大小来计算需要分片的数量
1
2
3
4
5// 计算文件总大小
long fileSize = videoFile.length();
// 计算需要分片的数量
int partCount = (int) Math.ceil((double) fileSize / partSize);利用for循环循环上传每一个分片
1
2for(int i = 0; i < partCount; i++){
}for循环里计算当前分片的范围
1
2
3
4
5
6// 计算分片的范围
// 已经读取的数据
long offset = i * partSize;
// 看5MB和剩下的数据哪个小,哪个小就用哪个
long size = Math.min(partSize, fileSize - offset);读取分片的数据将分片的数据写入文件中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 读取分片的数据
byte[] data = new byte[(int) size];
try (RandomAccessFile raf = new RandomAccessFile(videoFile, "r")) {
//在这个位置添加指针,从这个位置读
raf.seek(offset);
raf.readFully(data);
} catch (IOException e) {
throw new RuntimeException(e);
}
// 将分片的数据写入到文件中
String chunkFileName = chunkPath + fileMd5 + (i + 1);
try (FileOutputStream fos = new FileOutputStream(chunkFileName)) {
fos.write(data);
} catch (IOException e) {
throw new RuntimeException(e);
}将分片文件上传到minio
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//将分片上传到Minio
try {
minioClient.uploadObject(
//构建上传文件的对象
UploadObjectArgs.builder()
.bucket(bucket_video)
.filename(chunkFileName)
.object(getChunkFileFolderPath(fileMd5) + (i + 1))
.contentType(getMimeType(null))
.build()
);
} catch (Exception e) {
log.error("上传分块失败", e);
throw new RuntimeException("上传分块失败");
}在for循环外返回分块数量
1
return partCount;
上传方法完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
public class MinioServiceImpl implements MinioService {
private final MinioClient minioClient;
//存储视频文件
private String bucket_video;
// 设置分片大小
long partSize = 5 * 1024 * 1024; //5MB
public MinioServiceImpl(MinioClient minioClient) {
this.minioClient = minioClient;
}
//根据扩展名获取mimeType
private String getMimeType(String extension){
if(extension == null){
extension = "";
}
//根据扩展名取出mimeType
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
if(extensionMatch != null){
mimeType = extensionMatch.getMimeType();
}
return mimeType;
}
//获取文件的md5
private String getFileMd5(File file) {
try (FileInputStream fileInputStream = new FileInputStream(file)) {
String fileMd5 = DigestUtils.md5Hex(fileInputStream);
return fileMd5;
} catch (Exception e) {
log.error("获取文件md5失败",e);
return null;
}
}
//得到分块文件的目录
private String getChunkFileFolderPath(String fileMd5) {
return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
}
public int uploadVideo(File videoFile) {
//获取文件的md5值
String fileMd5 = getFileMd5(videoFile);
//存放分片的目录
String chunkPath = "D:/testchunk/";
// 计算文件总大小
long fileSize = videoFile.length();
// 计算需要分片的数量
int partCount = (int) Math.ceil((double) fileSize / partSize);
//上传每个分片
for(int i = 0; i < partCount; i++){
// 计算分片的范围
long offset = i * partSize;
long size = Math.min(partSize, fileSize - offset);
// 读取分片的数据
byte[] data = new byte[(int) size];
try (RandomAccessFile raf = new RandomAccessFile(videoFile, "r")) {
//在这个位置添加指针,从这个位置读
raf.seek(offset);
raf.readFully(data);
} catch (IOException e) {
throw new RuntimeException(e);
}
// 将分片的数据写入到文件中
String chunkFileName = chunkPath + fileMd5 + (i + 1);
try (FileOutputStream fos = new FileOutputStream(chunkFileName)) {
fos.write(data);
} catch (IOException e) {
throw new RuntimeException(e);
}
//将分片上传到Minio
try {
minioClient.uploadObject(
//构建上传文件的对象
UploadObjectArgs.builder()
.bucket(bucket_video)
.filename(chunkFileName)
.object(getChunkFileFolderPath(fileMd5) + (i + 1))
.contentType(getMimeType(null))
.build()
);
} catch (Exception e) {
log.error("上传分块失败", e);
throw new RuntimeException("上传分块失败");
}
}
return partCount;
}
}测试结果:


可以看到分片文件已上传成功
Minio合并分片并清除分片
minio合并方法
这个方法可以组合来自不同源对象的数据来创建对象,我们用来合并分片为一个文件
1 | public ObjectWriteResponse composeObject(ComposeObjectArgs args) |
示例:
1 | //构建不同文件的对象放入集合中 |
我们来思考合并接口需要的参数
- 首先需要fileMd5获取分块文件的目录
- 其次需要fileExt来决定合成后文件的类型
- 最后需要分片的数量来决定迭代构建多少次对象
合并完后我们返回文件的地址
接口
1 |
|
实现合并分片的方法
首先在MinioService创建合并分片的方法
1
2
3
4
5
6
7
8
9
10public interface MinioService {
/**
* 合并视频分片
* @param fileMd5 文件的md5来决定合并那个视频
* @param fileExt 合并后的文件名称
* @param chunkTotal 分片数量
* @return 合并后的视频地址
*/
public String mergeChunksAndUpload(String fileMd5, String fileExt, int chunkTotal);
}在MinioServiceImpl实现合并分片的方法
1
2
3
public String mergeChunksAndUpload(String fileMd5, String fileExt, int chunkTotal) {
}首先我们定义objectName作为最后返回的文件地址
1
String objectName;
创建根据md5和fileExt获取合并文件的目录的方法
1
2
3
4
5
6
7
8
9/**
* 得到合并后的文件的地址
* @param fileMd5 文件id即md5值
* @param fileExt 文件扩展名
* @return
*/
private String getFilePathByMd5(String fileMd5,String fileExt){
return fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/" +fileMd5 +fileExt;
}然后获取分片文件目录使用迭代构建分块文件的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14//分块文件所在目录
String chunkFolderPath = getChunkFileFolderPath(fileMd5);
//找到分块文件
List<ComposeSource> sources = Stream.iterate(1, i -> ++i)
//总数为5 最大返回5个流
.limit(chunkTotal)
//映射i 构建每个分块文件的对象
.map(i -> ComposeSource.builder()
.bucket(bucket_video)
.object(chunkFolderPath + i)
.build()
)
//转为List
.toList();获取合并后的文件的目录
1
2//合并后的文件的objectName
objectName = getFilePathByMd5(fileMd5,fileExt);将分片合并为一个文件
1
2
3
4
5
6
7//将分片合并为一个文件
minioClient.composeObject(ComposeObjectArgs
.builder()
.bucket(bucket_video)
.object(objectName)
.sources(sources)
.build());
如此合并的方法就已经完成,但是合并后还需要一个操作,那就是将分片全部清除
想要实现清除分片的方法,我们先来看minio的批量删除方法
minio批量删除方法
1 | public Iterable<Result<DeleteError>> removeObjects(RemoveObjectsArgs args) |
示例:
1 | // 创建一个列表来存储要删除的对象 |
我们来思考删除分片方法需要的参数
分块文件的目录
目录加上分片的顺序1,2,3,4,5…作为object
分片总数
作为需要构建多少个对象的参数
实现清除分块的方法
在MinioServiceImpl中创建清除分块的方法
1
2
3
4
5
6
7/**
* 清除分块文件
* @param chunkFileFolderPath 分块文件路径
* @param chunkTotal 分块文件总数
*/
private void clearChunkFiles(String chunkFileFolderPath, int chunkTotal){
}迭代构建删除文件对象
1
2
3
4
5
6
7//迭代构建文件对象放到集合中
List<DeleteObject> deleteObjects = Stream.iterate(1, i -> ++i)
//最多多少
.limit(chunkTotal)
//object是分片目录加上i
.map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))
.toList();删除分片文件
1
2
3
4
5
6//删除分块文件
Iterable<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs
.builder()
.bucket(bucket_video)
.objects(deleteObjects)
.build());遍历结果以处理可能的删除错误
1
2
3
4
5
6
7
8
9//遍历结果以处理可能的删除错误
results.forEach(r -> {
DeleteError deleteError = null;
try {
deleteError = r.get();
} catch (Exception e) {
log.error("清除分块文件失败,objectName:{}",deleteError.objectName(),e);
}
});完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31/**
* 清除分块文件
* @param chunkFileFolderPath 分块文件路径
* @param chunkTotal 分块文件总数
*/
private void clearChunkFiles(String chunkFileFolderPath, int chunkTotal){
try {
//迭代构建文件对象放到集合中
List<DeleteObject> deleteObjects = Stream.iterate(1, i -> ++i)
.limit(chunkTotal)
.map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))
.toList();
//删除分块文件
Iterable<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs
.builder()
.bucket(bucket_video)
.objects(deleteObjects)
.build());
//遍历结果以处理可能的删除错误
results.forEach(r -> {
DeleteError deleteError = null;
try {
deleteError = r.get();
} catch (Exception e) {
log.error("清除分块文件失败,objectName:{}",deleteError.objectName(),e);
}
});
} catch (Exception e) {
log.error("清楚分块文件失败,chunkFileFolderPath:{}",chunkFileFolderPath,e);
}
}
这样清除分块的方法也完成了,在合并完文件后调用清除分块的方法合并分片的接口就完成了
合并分片方法完整代码
1 | /** |
测试:


可以看到分块已经被合并成一个视频文件,分块也已经被删除。请求后也返回了文件的目录
接下来我们来实现根据文件的目录来下载文件的接口
Minio下载文件
minio下载方法
以流的方式下载一个对象
1 | public InputStream getObject(GetObjectArgs args) |
示例:
1 | // 获取对象的InputStream,并保存为文件 |
分析下载接口需要的参数
首先桶名我们指定了对应的桶名所以不需要
文件目录,这个就是合并后返回的,我们需要这个
接口
1 | /** |
实现下载文件的方法
在MinioService创建下载的方法
1
2
3
4
5
6/**
* 下载指定文件
* @param objectName 文件目录
* @return 文件
*/
public ResponseEntity<byte[]> downloadFileFromMinIO(String objectName);在MioioServiceImpl实现下载的方法
1
2
3
public ResponseEntity<byte[]> downloadFileFromMinIO(String objectName) {
}首先获取指定对象的数据
1
2
3
4
5
6//获取指定对象的数据
InputStream inputStream = minioClient.getObject(GetObjectArgs
.builder()
.bucket(bucket_video)
.object(objectName)
.build());将文件转为byte字节数组
1
2//将文件转为byte字节数组
byte[] byteArray = IOUtils.toByteArray(inputStream);设置响应头信息
1
2
3
4
5
6//设置响应头信息
HttpHeaders headers = new HttpHeaders();
//设置二进制流数据格式
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
//通知浏览器以attachment(下载的方式)打开图片
headers.setContentDispositionFormData("attachment",objectName);返回文件
1
2//返回文件
return new ResponseEntity<>(byteArray,headers, HttpStatus.OK);
完整代码
1 | /** |
测试:


可以看到下载了指定的文件,文件也并没有损坏可以打开播放