Java技术栈 —— 使用MinIO进行大文件分片上传与下载
Java技术栈 —— 使用MinIO进行大文件分片上传与下载
- 一、搭建MinIO服务器
- 1.1 安装MinIO
- 1.2 启动MinIO
- 二、普通上传
- 三、分片上传
- Q&A
大文档和大视频的上传下载,一般都采用分片的方式,就像修长城那样,再长再高的建筑,也是一块砖一块砖垒起来的,伟大的中国人民 —— 也就是你,其实早已深谙分片上传精髓。
一、搭建MinIO服务器
搭建MinIO的过程,涉及到的第一个问题,MinIO是一个对象存储服务(Object Storage Service),对象存储服务和数据库的区别是什么?MinIO是用来存数据的,Database也是用来存数据的,二者有什么区别?这些问题可以看参考文章[1]和[2]。
参考文章或视频链接 |
---|
[1] 对象存储与传统数据库比较 |
[2] 对象存储 OSS - 阿里云 |
1.1 安装MinIO
下列的安装和启动过程废弃,推荐使用docker
进行安装
# 使用国内镜像下载MinIO
wget http://dl.minio.org.cn/server/minio/release/linux-amd64/minio
chmod +x minio
参考文章或视频链接 |
---|
[1] 基于MinIO搭建高性能文件服务器 |
[2] MinIO下载和MinIO中国镜像地址 - CSDN |
[3] 官方文档重点参考 Deploy MinIO: Single-Node Single-Drive - MinIO |
[4] How to install MinIO on Linux – Step by Step Practical Guide |
1.2 启动MinIO
# /data/xxfolder/minio是数据存储路径, 你可以自己修改, MinIO 将数据存储在 /data/xxfolder/minio 目录下
MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=password ./minio server /data/xxfolder/minio --address ":9000" --console-address ":9001"MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=password ./minio server /home/programmer/DevelopEnvironment/MinIO/data --address ":9000" --console-address ":9001"MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=password ./minio server /home/lyp/data/minio --address ":9000" --console-address ":9001"
参考文章或视频链接 |
---|
[1] 基于MinIO搭建高性能文件服务器 |
[2] MinIO Server - MinIO Documentation |
二、普通上传
这个阶段,我们的主要目标还是测试接口,不会特意开发一个前端页面,那如何测试呢?Python的FastAPI框架,可以使用/docs
这种地址进行测试,那么Java中,就可以用swagger或其它api的doc框架,但swagger不支持springboot3.0以上的版本,springboot 3.x要使用下面这个库,具体使用方法见参考文章[5]和[6]
<dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>2.1.0</version>
</dependency>
下面是具体代码,我使用的JDK17,字节码版本也是17
server:port: 8080servlet:context-path: /spring-demospring:application:name: business-fileuploadminio:endpoint: http://192.168.206.131:19000accessKey: adminsecretKey: passwordbucket: temp-bucket
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
@Data
public class MinioConfig {@Value("${minio.endpoint}")private String minioEndpoint;@Value("${minio.accessKey}")private String minioAccessKey;@Value("${minio.secretKey}")private String minioSecretKey;@Value("${minio.bucket}")private String minioBucket;@Beanpublic MinioClient minioClient() {// 创建 MinioClient 客户端return MinioClient.builder().endpoint(minioEndpoint).credentials(minioAccessKey, minioSecretKey).build();}
}
import dev.polaris.service.MinioFileUploadService;
import io.minio.RemoveObjectArgs;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;@RestController
@RequestMapping("/file")
public class FileUploadController {private static final Logger log = LoggerFactory.getLogger(FileUploadController.class);@Resourceprivate MinioFileUploadService minioFileUploadService;/*** 上传文件* multipart/form-data 是一种用于 HTTP 协议的媒体类型,它允许将表单数据(包括文件)打包成多个部分(parts)进行传输* @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) 表明该方法只接受 multipart/form-data 类型的请求,其他类型的请求会被拒绝(返回 415 Unsupported Media Type 状态码)。* @PostMapping(value = "/upload") 表示该方法处理所有类型的 POST 请求,而不限制 Content-Type。*/@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)public String upload(@RequestParam("file") MultipartFile file) throws Exception {return minioFileUploadService.upload(file);}/*** 删除文件*/@DeleteMapping("/delete")public void delete(@RequestParam("path") String path) throws Exception {minioFileUploadService.delete(path);}
}
import dev.polaris.config.MinioConfig;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import io.minio.errors.*;
import jakarta.annotation.Resource;
import org.apache.commons.io.FilenameUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.UUID;@Service
public class MinioFileUploadService {@Resourceprivate MinioConfig minioConfig;@Resourceprivate MinioClient minioClient;public String upload(MultipartFile file) throws Exception {// 优化文件命名:结合UUID和原始文件名(如果适用)String originalFileName = FilenameUtils.getName(file.getName());// 文件名,保留原始文件名后缀String path = originalFileName + "-" + UUID.randomUUID().toString();// 使用 try-with-resources 确保资源释放try (InputStream inputStream = file.getInputStream()) {// 考虑文件类型检查和安全性验证(示例未实现,实际应用中需要根据具体要求实现)if (!isValidFile(file)) {throw new IllegalArgumentException("Invalid file type or content.");}// 调用 MinIO 客户端上传文件,添加异常处理try {minioClient.putObject(PutObjectArgs.builder()// 存储桶.bucket(minioConfig.getMinioBucket())// 文件名.object(path)// 文件内容,使用文件长度而不是 -1.stream(inputStream, file.getSize(), -1)// 文件类型.contentType(file.getContentType()).build());System.out.println("File uploaded successfully.");} catch (MinioException e) {System.err.println("Error uploading file: " + e.getMessage());// 可以进一步处理异常,如记录日志、重试等}} catch (IOException e) {System.err.println("Error opening file: " + e.getMessage());// 处理文件读取失败的情况}return String.format("%s/%s/%s", minioConfig.getMinioEndpoint(), minioConfig.getMinioBucket(), path);}// 示例性文件验证方法,需根据实际需求实现private boolean isValidFile(MultipartFile file) {// 实现文件类型检查和内容安全验证return true;}public void delete(String path) {try {minioClient.removeObject(RemoveObjectArgs.builder()// 存储桶.bucket(minioConfig.getMinioBucket())// 文件名.object(path).build());} catch (ErrorResponseException | NoSuchAlgorithmException | XmlParserException | InsufficientDataException |InternalException | InvalidKeyException | InvalidResponseException | IOException | ServerException e) {throw new RuntimeException(e);}}
}
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class BusinessFileuploadApplication {public static void main(String[] args) {SpringApplication.run(BusinessFileuploadApplication.class, args);}}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>dev.polaris</groupId><artifactId>SkillLib</artifactId><version>1.0-SNAPSHOT</version></parent><groupId>dev.polaris</groupId><artifactId>SkillFragment-fileupload</artifactId><version>0.0.1-SNAPSHOT</version><name>business-fileupload</name><description>business-fileupload</description><url/><licenses><license/></licenses><developers><developer/></developers><scm><connection/><developerConnection/><tag/><url/></scm><properties><java.version>17</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>2.1.0</version></dependency><!-- MinIO --><dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.5.11</version></dependency><!--lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
启动后,具体访问地址为
http://localhost:{your_port}/spring-demo/swagger-ui/index.html
参考文章或视频链接 |
---|
[1] SpringBoot 如何生成接口文档,老鸟们都这么玩的! |
[2] SpringBoot 整合 swagger2 实现快速测试和快速生成 API 文档 |
[3] Swagger与SpringBoot版本不兼容 |
[4] Spring Boot 3.0.0 support #4041 |
[5] 使用 Spring Doc 为 Spring REST API 生成 OpenAPI 3.0 文档 |
[6] Swagger UI文件上传 |
三、分片上传
FastDFS应被放弃,它维护难,使用难,性能比不过Minio,见参考文章[2]。如果想用直接的实现,看参考文章[6],只是没有实现分片合并下载与视频定位,但也可以快速开发,非常方便。
下面是具体的分片上传业务逻辑。
(1)未超过分片阈值的文件,直接上传。
(2)超过分片阈值的文件,分片上传,跳转步骤(3)
(3)前端计算整个文件的size
与MD5
值,后端返回chunk
数与文件序号fileNumber
(4)前端逐chunk
向后端发送数据,若遭遇中断,如关闭浏览器页面,后端服务被停,跳转步骤(5)
(5)继续上传。前端向后端发送继续上传请求,后端返回待继续上传数据的chunk
,
public class uploadPartDemo {public static void main(String[] args) throws InsufficientDataException, IOException, NoSuchAlgorithmException, InvalidKeyException, XmlParserException, InternalException {MinioAsyncClient minioAsyncClient = MinioAsyncClient.builder().endpoint("http://localhost:9000") // MinIO服务地址.credentials("", "") // 默认的管理员凭证.build();String bucketName = "{yourbucket}";String region = null;String objectName = "{yourObjectName}";// 1.初始化分片上传任务CompletableFuture<CreateMultipartUploadResponse> uploadIdFuture = minioAsyncClient.createMultipartUploadAsync(bucketName, region, objectName, null, null);// 获取 uploadIdString uploadId = uploadIdFuture.join().result().uploadId();// 2.读取文件并拆分为多个分片(Part)List<CompletableFuture<Part>> futures = new ArrayList<>();List<Part> uploadedParts = new ArrayList<>();String imgPath = "yourfile";try (InputStream is = Files.newInputStream(Paths.get(imgPath))) {int partNumber = 1;long partSize = 5 * 1024 * 1024; // 5MB 每片byte[] buffer = new byte[(int) partSize];while (true) {int read = is.read(buffer);if (read <= 0) break;final int partLen = read;final int partNum = partNumber++;CompletableFuture<UploadPartResponse> future = minioAsyncClient.uploadPartAsync(bucketName,null,objectName,Arrays.copyOf(buffer, partLen), // 当前分片数据partLen,uploadId,partNum,null,null);// 如果要由前端发起合并操作,应该有一个全局的hashMap, 来记录哪些分片已上传, 不应该让前端记录// 转换为 Part 对象(含 ETag)CompletableFuture<Part> partFuture = future.thenApply(resp ->new Part(partNum, resp.etag()));futures.add(partFuture);}}// 3.等待所有分片上传完成// 合并所有 CompletableFutureCompletableFuture<Void> allDone = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));allDone.join(); // 等待全部完成// 收集结果for (CompletableFuture<Part> f : futures) {uploadedParts.add(f.join());}// 4.完成分片上传(合并)//目前仅做了最大1000分片Part[] parts = new Part[1000];// 查询上传后的分片数据CompletableFuture<ListPartsResponse> partResult = minioAsyncClient.listPartsAsync(bucketName, null, objectName, 1000, 0, uploadId, null, null);ListPartsResponse listPartsResponse = partResult.join();int partNumber = 0;for (Part part : listPartsResponse.result().partList()) {parts[partNumber] = new Part(partNumber, part.etag());partNumber++;}
// CompletableFuture<ObjectWriteResponse> result = minioAsyncClient.completeMultipartUploadAsync(bucketName, region, objectName, uploadId, parts, null, null);
// ObjectWriteResponse response = result.join();// System.out.println("上传完成,ETag: " + response.etag());// 5.异常处理与清理(可选)}
}
以下是流程图
Q&A
MinioAsyncClient和MinioClient的联系和区别
参考文章或视频链接 |
---|
[1] 大厂的中小型文件存储技术方案选型 |
[2] MinIO很强-让我放弃FastDFS拥抱MinIO的8个理由 |
[3] Java大文件分片上传(minio版),超详细 |
[4] 使用Minio实现大文件的分片上传 |
[5] SpringBoot通过Minio实现大文件分片上传 |
[6] 直接可用实现 SpringBoot整合minio实现断点续传、分片上传(附源码) |
[7] Minio进阶 |
[8] 图片分片上传功能的实践 |
[9] (分片上传),需求图片视频文件太大,需要前端做分片上传 |
[10] 陈天技术摘抄 |
[11] Spring Boot与MinIO融合:高效实现文件分片上传技术 |
[12] 客户端直连S3实现分片续传思路与实践 |
[13] 基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件 |