java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot MinIO大文件分片上传

SpringBoot集成MinIO实现大文件分片上传的示例代码

作者:Liberty715

本文主要介绍了SpringBoot集成MinIO实现大文件分片上传,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

需求背景:为什么需要分片上传?

1. 传统上传方式的痛点

在文件上传场景中,当用户尝试上传超过 100MB 的大文件时,传统单次上传方式会面临三大核心问题:

(1)网络稳定性挑战

(2)服务器资源瓶颈

(3)用户体验缺陷

2. 分片上传的核心优势

技术价值

特性说明
可靠性单个分片失败不影响整体上传,支持分片级重试
内存控制分片按需加载(如5MB/片),内存占用恒定
并行加速支持多分片并发上传(需配合前端Worker实现)

业务价值

3. 典型应用场景

(1)企业级网盘系统

(2)医疗影像系统

(3)在线教育平台

4. 为什么选择MinIO?

MinIO作为高性能对象存储方案,与分片上传架构完美契合:

(1)分布式架构

(2)高性能合并

// MinIO服务端合并只需一次API调用
minioClient.composeObject(ComposeObjectArgs.builder()...);

相比传统文件IO合并方式,速度提升5-10倍

(3)生命周期管理

一、环境准备与依赖配置

1. 开发环境要求

2. 项目依赖(pom.xml)

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>3.3.4</version>
    </dependency>

    <!-- MinIO Java SDK -->
    <dependency>
        <groupId>io.minio</groupId>
        <artifactId>minio</artifactId>
        <version>8.5.7</version>
    </dependency>

    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.30</version>
        <optional>true</optional>
    </dependency>
</dependencies>

二、核心代码实现解析

1. MinIO服务配置(FileUploadService)

(1) 客户端初始化

private MinioClient createMinioClient() {
    return MinioClient.builder()
            .endpoint(endpoint)
            .credentials(accessKey, secretKey)
            .build();
}

(2) 分片上传实现

public String uploadFilePart(String fileId, String fileName, 
                           MultipartFile filePart, Integer chunkIndex, 
                           Integer totalChunks) throws IOException {
    String objectName = fileId + "/" + fileName + '-' + chunkIndex;
    PutObjectArgs args = PutObjectArgs.builder()
            .bucket(bucketName)
            .object(objectName)
            .stream(filePart.getInputStream(), filePart.getSize(), -1)
            .build();
    minioClient.putObject(args);
    return objectName;
}

(3) 分片合并逻辑

public void mergeFileParts(FileMergeReqVO reqVO) throws IOException {
    String finalObjectName = "merged/" + reqVO.getFileId() + "/" + reqVO.getFileName();
    List<ComposeSource> sources = reqVO.getPartNames().stream()
            .map(name -> ComposeSource.builder()
                    .bucket(bucketName)
                    .object(name)
                    .build())
            .toList();
    
    minioClient.composeObject(ComposeObjectArgs.builder()
            .bucket(bucketName)
            .object(finalObjectName)
            .sources(sources)
            .build());
    
    // 清理临时分片
    reqVO.getPartNames().forEach(partName -> {
        try {
            minioClient.removeObject(
                RemoveObjectArgs.builder()
                    .bucket(bucketName)
                    .object(partName)
                    .build());
        } catch (Exception e) {
            log.error("Delete chunk failed: {}", partName, e);
        }
    });
}

2. 控制层设计(FileUploadController)

@PostMapping("/upload/part/{fileId}")
public CommonResult<String> uploadFilePart(
        @PathVariable String fileId,
        @RequestParam String fileName,
        @RequestParam MultipartFile filePart,
        @RequestParam int chunkIndex,
        @RequestParam int totalChunks) {
    // [逻辑处理...]
}

@PostMapping("/merge")
public CommonResult<String> mergeFileParts(@RequestBody FileMergeReqVO reqVO) {
    // [合并逻辑...]
}

3. 前端分片上传实现

const chunkSize = 5 * 1024 * 1024; // 5MB分片

async function uploadFile() {
    const file = document.getElementById('fileInput').files[0];
    const fileId = generateUUID();
    
    // 分片上传循环
    for (let i = 0; i < totalChunks; i++) {
        const chunk = file.slice(start, end);
        const formData = new FormData();
        formData.append('filePart', chunk);
        formData.append('chunkIndex', i + 1);
        
        await fetch('/upload/part/' + fileId, {
            method: 'POST',
            body: formData
        });
    }
    
    // 触发合并
    await fetch('/merge', {
        method: 'POST',
        body: JSON.stringify({
            fileId: fileId,
            partNames: generatedPartNames
        })
    });
}

三、功能测试验证

测试用例1:上传500MB视频文件

选择测试文件:sample.mp4(512MB)

观察分片上传过程:

合并完成后检查MinIO:

sta-bucket
└── merged
    └── 6ba7b814...
        └── sample.mp4

下载验证文件完整性

在这里插入图片描述

测试用例2:中断恢复测试

四、关键配置项说明

配置项示例值说明
minio.endpointhttp://localhost:9991MinIO服务器地址
minio.access-keyroot访问密钥
minio.secret-keyxxxxx秘密密钥
minio.bucket-nameminio-xxxx默认存储桶名称
server.servlet.port8080Spring Boot服务端口

附录:完整源代码

1. 后端核心类

FileUploadService.java

@Service
public class FileUploadService {

    @Value("${minio.endpoint:http://localhost:9991}")
    private String endpoint; // MinIO服务器地址

    @Value("${minio.access-key:root}")
    private String accessKey; // MinIO访问密钥

    @Value("${minio.secret-key:xxxx}")
    private String secretKey; // MinIO秘密密钥

    @Value("${minio.bucket-name:minio-xxxx}")
    private String bucketName; // 存储桶名称

    /**
     * 创建 MinIO 客户端
     *
     * @return MinioClient 实例
     */
    private MinioClient createMinioClient() {
        return MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();
    }

    /**
     * 如果存储桶不存在,则创建存储桶
     */
    public void createBucketIfNotExists() throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        MinioClient minioClient = createMinioClient();
        try {
            boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
            if (!found) {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            }
        } catch (MinioException e) {
            throw new IOException("Error checking or creating bucket: " + e.getMessage(), e);
        }
    }

    /**
     * 上传文件分片到MinIO
     *
     * @param fileId   文件标识符
     * @param filePart 文件分片
     * @return 分片对象名称
     */
    public String uploadFilePart(String fileId, String fileName, MultipartFile filePart, Integer chunkIndex, Integer totalChunks) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        MinioClient minioClient = createMinioClient();
        try {
            // 构建分片对象名称
            String objectName = fileId + "/" + fileName + '-' + chunkIndex;
            // 设置上传参数
            PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .stream(filePart.getInputStream(), filePart.getSize(), -1)
                    .contentType(filePart.getContentType())
                    .build();
            // 上传文件分片
            minioClient.putObject(putObjectArgs);
            return objectName;
        } catch (MinioException e) {
            throw new IOException("Error uploading file part: " + e.getMessage(), e);
        }
    }

    /**
     * 合并多个文件分片为一个完整文件
     */
    public void mergeFileParts(FileMergeReqVO reqVO) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        MinioClient minioClient = createMinioClient();
        try {
            // 构建最终文件对象名称
            String finalObjectName = "merged/" + reqVO.getFileId() + "/" + reqVO.getFileName();
            // 构建ComposeSource数组
            List<ComposeSource> sources = reqVO.getPartNames().stream().map(name ->
                    ComposeSource.builder().bucket(bucketName).object(name).build()).toList();
            // 设置合并参数
            ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
                    .bucket(bucketName)
                    .object(finalObjectName)
                    .sources(sources)
                    .build();
            // 合并文件分片
            minioClient.composeObject(composeObjectArgs);

            // 删除合并后的分片
            for (String partName : reqVO.getPartNames()) {
                minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(partName).build());
            }
        } catch (MinioException e) {
            throw new IOException("Error merging file parts: " + e.getMessage(), e);
        }
    }

    /**
     * 删除指定文件
     *
     * @param fileName 文件名
     */
    public void deleteFile(String fileName) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        MinioClient minioClient = createMinioClient();
        try {
            // 删除文件
            minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(fileName).build());
        } catch (MinioException e) {
            throw new IOException("Error deleting file: " + e.getMessage(), e);
        }
    }
}

FileUploadController.java

@AllArgsConstructor
@RestController
@RequestMapping("/files")
public class FileUploadController {

    private final FileUploadService fileUploadService;

    /**
     * 创建存储桶
     *
     * @return 响应状态
     */
    @PostMapping("/bucket")
    @PermitAll
    public CommonResult<String> createBucket() throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        fileUploadService.createBucketIfNotExists();
        return CommonResult.success("创建成功");
    }

    /**
     * 上传文件分片
     *
     * @param fileId      文件标识符
     * @param filePart    文件分片
     * @param chunkIndex  当前分片索引
     * @param totalChunks 总分片数
     * @return 响应状态
     */
    @PostMapping("/upload/part/{fileId}")
    @PermitAll
    public CommonResult<String> uploadFilePart(
            @PathVariable String fileId,
            @RequestParam String fileName,
            @RequestParam MultipartFile filePart,
            @RequestParam int chunkIndex,
            @RequestParam int totalChunks) {

        try {
            // 上传文件分片
            String objectName = fileUploadService.uploadFilePart(fileId,fileName, filePart, chunkIndex, totalChunks);
            return CommonResult.success("Uploaded file part: " + objectName);
        } catch (IOException | NoSuchAlgorithmException | InvalidKeyException e) {
            return CommonResult.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Error uploading file part: " + e.getMessage());
        }
    }

    /**
     * 合并文件分片
     *
     * @param reqVO 参数
     * @return 响应状态
     */
    @PostMapping("/merge")
    @PermitAll
    public CommonResult<String> mergeFileParts(@RequestBody FileMergeReqVO reqVO) {
        try {
            fileUploadService.mergeFileParts(reqVO);
            return CommonResult.success("File parts merged successfully.");
        } catch (IOException | NoSuchAlgorithmException | InvalidKeyException e) {
            return CommonResult.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Error merging file parts: " + e.getMessage());
        }
    }

    /**
     * 删除指定文件
     *
     * @param fileId 文件ID
     * @return 响应状态
     */
    @DeleteMapping("/delete/{fileId}")
    @PermitAll
    public CommonResult<String> deleteFile(@PathVariable String fileId) {
        try {
            fileUploadService.deleteFile(fileId);
            return CommonResult.success("File deleted successfully.");
        } catch (IOException | NoSuchAlgorithmException | InvalidKeyException e) {
            return CommonResult.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Error deleting file: " + e.getMessage());
        }
    }
}

FileMergeReqVO.java

@Data
public class FileMergeReqVO {

    /**
     * 文件标识ID
     */
    private String fileId;

    /**
     * 文件名
     */
    private String fileName;

    /**
     * 合并文件列表
     */
    @NotEmpty(message = "合并文件列表不允许为空")
    private List<String> partNames;
}

2. 前端HTML页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>File Upload</title>
    <style>
        #progressBar {
            width: 100%;
            background-color: #f3f3f3;
            border: 1px solid #ccc;
        }
        #progress {
            height: 30px;
            width: 0%;
            background-color: #4caf50;
            text-align: center;
            line-height: 30px;
            color: white;
        }
    </style>
</head>
<body>
    <input type="file" id="fileInput" />
    <button id="uploadButton">Upload</button>
    <div id="progressBar">
        <div id="progress">0%</div>
    </div>
    <script>
        const chunkSize = 5 * 1024 * 1024; // 每个分片大小为1MB

        // 生成 UUID 的函数
        function generateUUID() {
            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
                return v.toString(16);
            });
        }
        
        document.getElementById('uploadButton').addEventListener('click', async () => {
            const file = document.getElementById('fileInput').files[0];
            if (!file) {
                alert("Please select a file to upload.");
                return;
            }

            // 生成唯一的 fileId
            const fileId = generateUUID();

            // 获取文件名
            const fileName = file.name; // 可以直接使用文件名

            const totalChunks = Math.ceil(file.size / chunkSize);
            let uploadedChunks = 0;

            // 上传每个分片
            for (let i = 0; i < totalChunks; i++) {
                const start = i * chunkSize;
                const end = Math.min(start + chunkSize, file.size);
                const chunk = file.slice(start, end);

                const formData = new FormData();
                formData.append('filePart', chunk);
                formData.append('fileName', fileName); // 传递 文件名
                formData.append('fileId', fileId); // 传递 fileId
                formData.append('chunkIndex', i + 1); // 从1开始
                formData.append('totalChunks', totalChunks);

                // 发送分片上传请求
                const response = await fetch('http://localhost:8080/files/upload/part/' + encodeURIComponent(fileId), {
                    method: 'POST',
                    headers: {
                        'tenant-id': '1',
                    },
                    body: formData,
                });

                if (response.ok) {
                    uploadedChunks++;
                    const progressPercentage = Math.round((uploadedChunks / totalChunks) * 100);
                    updateProgressBar(progressPercentage);
                } else {
                    console.error('Error uploading chunk:', await response.text());
                    alert('Error uploading chunk: ' + await response.text());
                    break; // 如果上传失败,退出循环
                }
            }

            // 合并分片
            const mergeResponse = await fetch('http://localhost:8080/files/merge', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'tenant-id': '1',
                },
                body: JSON.stringify({
                    fileId: fileId,
                    fileName: fileName,
                    partNames: Array.from({ length: totalChunks }, (_, i) => `${fileId}/${fileName}-${i + 1}`),
                }),
            });

            if (mergeResponse.ok) {
                const mergeResult = await mergeResponse.text();
                console.log(mergeResult);
            } else {
                console.error('Error merging chunks:', await mergeResponse.text());
                alert('Error merging chunks: ' + await mergeResponse.text());
            }
            // 最后更新进度条为100%
            updateProgressBar(100);
        });

        function updateProgressBar(percent) {
            const progress = document.getElementById('progress');
            progress.style.width = percent + '%';
            progress.textContent = percent + '%';
        }
    </script>
</body>
</html>

注意事项:

通过本方案可实现稳定的大文件上传功能,经测试可支持10GB以上文件传输,实际应用时可根据业务需求调整分片大小和并发策略。

到此这篇关于SpringBoot集成MinIO实现大文件分片上传的文章就介绍到这了,更多相关SpringBoot MinIO大文件分片上传内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文