一、引入依赖
<dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.3.3</version></dependency>
二、自定义Minio客户端
package com.gstanzer.video.controller;import com.google.common.collect.Multimap;
import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.Part;import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;public class CustomMinioClient extends MinioClient {/*** 继承父类*/public CustomMinioClient(MinioClient client) {super(client);}/*** 初始化分片上传即获取uploadId*/public String initMultiPartUpload(String bucket, String region, String object, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException {CreateMultipartUploadResponse response = this.createMultipartUpload(bucket, region, object, headers, extraQueryParams);return response.result().uploadId();}/*** 上传单个分片*/public UploadPartResponse uploadMultiPart(String bucket, String region, String object, Object data,long length,String uploadId,int partNumber,Multimap<String, String> headers,Multimap<String, String> extraQueryParams) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException {return this.uploadPart(bucket, region, object, data, length, uploadId, partNumber, headers, extraQueryParams);}/*** 合并分片*/public ObjectWriteResponse mergeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws IOException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException, ServerException, InvalidKeyException {return this.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);}public void cancelMultipartUpload(String bucketName, String region, String objectName, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, IOException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {this.abortMultipartUpload(bucketName, region, objectName, uploadId, extraHeaders, extraQueryParams);}/*** 查询当前上传后的分片信息*/public ListPartsResponse listMultipart(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {return this.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);}
}
三、分片上传核心完整代码
1.实体类
package com.gstanzer.video.form;import lombok.Data;/*** @author: tangbingbing* @date: 2025/6/4 08:27*/
@Data
public class UploadPartForm {// 分片文件路径拼接字符串(比如:E:\\scdx\\test\\test.doc.part1,E:\\scdx\\test\\test.doc.part2)private String partFilePaths;// 分片文件urlprivate String partFileUrl;// 上传idprivate String uploadId;// 文件类型(word类型:application/msword,安卓安装包类型:application/vnd.android.package-archive)private String contentType;
}
2.控制层
package com.gstanzer.video.controller;import com.alibaba.fastjson.JSON;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.gstanzer.video.form.UploadPartForm;
import com.gstanzer.video.swagger.ApiForBackEndInVideo;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Part;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.FileEntity;
import org.apache.http.impl.client.HttpClients;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;import javax.annotation.PostConstruct;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;@Slf4j
@RestController
@RequestMapping("/minio")
public class MinioController {private CustomMinioClient minioClient;@Value("${minio.endpoint}")private String endpoint;@Value("${minio.bucketName}")private String bucketName;@Value("${minio.access-key}")private String accessKey;@Value("${minio.secret-key}")private String secretKey;@PostConstructpublic void init() throws Exception {minioClient = new CustomMinioClient(MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build());boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());if (!found) {log.info("Not found minio bucket: {}, auto create it now", bucketName);minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());log.info("Auto create minio bucket: {} {}", bucketName, found);} else {log.info("Found minio bucket: {}", bucketName);}}/*** 第一步,简单用一个10MB出头的文件,按5MB分片大小进行分片*/public static void main(String[] args) throws Exception {long CHUNK_SIZE = 15 * 1024 * 1024;// 将文件分片存储String filePath = "E:\\scdx\\app\\xfgj_v3.0.88.apk";File file = new File(filePath);long fileSize = file.length();int chunkCount = (int) Math.ceil((double) fileSize / CHUNK_SIZE);log.info("Created: " + chunkCount + " chunks");try (FileInputStream fis = new FileInputStream(file)) {byte[] buffer = new byte[(int) CHUNK_SIZE];for (int i = 0; i < chunkCount; i++) {String chunkFileName = filePath + ".part" + (i + 1);try (FileOutputStream fos = new FileOutputStream(chunkFileName)) {int bytesRead = fis.read(buffer);fos.write(buffer, 0, bytesRead);log.info("Created: " + chunkFileName + " (" + bytesRead + " bytes)");}}} catch (IOException e) {e.printStackTrace();}}/*** 第二步,申请一个大文件上传*/@ApiForBackEndInVideo@PostMapping("/uploadLargeFile/apply")public ResponseEntity<Map> applyUploadPsiResult2Minio(@RequestParam("largeFileName") String largeFileName,@RequestParam("chunkCount") Integer chunkCount) {String uploadId = getUploadId(largeFileName, null);Map<String, Object> map = new HashMap<>();map.put("uploadId", uploadId);Map<String, String> reqParams = new HashMap<>();reqParams.put("uploadId", uploadId);List<String> uploadUrlList = new ArrayList<>();for (int i = 1; i <= chunkCount; i++) {reqParams.put("partNumber", String.valueOf(i));String uploadUrl = getPresignedObjectUrl(largeFileName, reqParams);uploadUrlList.add(uploadUrl);}map.put("chunkUploadUrls", uploadUrlList);return ResponseEntity.ok(map);}/*** 准备分片上传时,在此先获取上传任务id*/private String getUploadId(String objectName, String contentType){log.info("tip message: 通过 <{}-{}-{}> 开始初始化<分片上传>数据", objectName, contentType, bucketName);if (StringUtils.isBlank(contentType)) {contentType = "application/octet-stream";}HashMultimap<String, String> headers = HashMultimap.create();headers.put("Content-Type", contentType);try {return minioClient.initMultiPartUpload(bucketName, null, objectName, headers, null);} catch (Exception e) {throw new RuntimeException("获取uploadId失败", e);}}/*** 请求分片上传的url*/private String getPresignedObjectUrl(String fileName, Map<String, String> reqParams) {try {String presignedObjectUrl = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().method(Method.PUT).bucket(bucketName).object(fileName).expiry(1, TimeUnit.DAYS).extraQueryParams(reqParams).build());return presignedObjectUrl;} catch (Exception e) {throw new RuntimeException("getPresignedObjectUrl failed", e);}}/*** 第三步,每个分片逐个通过签名后的分片上传url,进行上传,未合并之前都可以重复覆盖上传* 在video服务的控制台通过如下命令将分片下载到video服务中* wget http://100.86.2.1:80/xfaq/xfgj_v3.0.88.apk.part1*/@ApiForBackEndInVideo@PostMapping("/uploadLargeFile/uploadPart")public ResponseEntity<String> applyUploadPsiResult2Minio(@RequestBody UploadPartForm form) {String partFilePaths = form.getPartFilePaths();String partFileUrl = form.getPartFileUrl();try {// 第一步得到的分片文件List<String> chunkFilePaths = Lists.newArrayList(partFilePaths.split(","));// 第二步得到的上传url信息Map<String, Object> uploadIdMap = new HashMap<>();uploadIdMap.put("uploadId", form.getUploadId());List<String> chunkUploadUrls = Lists.newArrayList(partFileUrl.split(","));uploadIdMap.put("chunkUploadIds", chunkUploadUrls);// 基于此,分片直接上传到minio,不走服务端,省去一次网络IO开销HttpClient httpClient = HttpClients.createDefault();for (int i = 0; i < chunkUploadUrls.size(); i++) {// PUT直接上传到minioString chunkUploadId = chunkUploadUrls.get(i);HttpPut httpPut = new HttpPut(chunkUploadId);httpPut.setHeader("Content-Type",form.getContentType());File chunkFile = new File(chunkFilePaths.get(i));FileEntity chunkFileEntity = new FileEntity(chunkFile);httpPut.setEntity(chunkFileEntity);HttpResponse chunkUploadResp = httpClient.execute(httpPut);log.info("[分片" + (i + 1) + "]上传响应:" + JSON.toJSONString(chunkUploadResp));httpPut.releaseConnection();}return ResponseEntity.ok("上传成功!");} catch (IOException e) {e.printStackTrace();throw new RuntimeException(e);}}public static void main3(String[] args) throws Exception {// 第一步得到的分片文件List<String> chunkFilePaths = Lists.newArrayList("E:\\scdx\\test\\test.doc.part1","E:\\scdx\\test\\test.doc.part2");// 第二步得到的上传url信息Map<String, Object> uploadIdMap = new HashMap<>();uploadIdMap.put("uploadId", "YTJlOWZhMmEtM2I3My00MmIyLWE0YjgtMDFkYjQzMzIyNmVhLjFmYzQ2M2ViLTI2YmYtNDZjMi04M2ZlLWJjMThjOTk0MWU4OHgxNzQ4OTM3NDE3MjI4ODA1MTAw");List<String> chunkUploadUrls = Lists.newArrayList("http://127.0.0.1:9000/xfaq/test.doc?uploadId=YTJlOWZhMmEtM2I3My00MmIyLWE0YjgtMDFkYjQzMzIyNmVhLjFmYzQ2M2ViLTI2YmYtNDZjMi04M2ZlLWJjMThjOTk0MWU4OHgxNzQ4OTM3NDE3MjI4ODA1MTAw&partNumber=1&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20250603%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250603T075657Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=649af2001e096f422479e139efc4aab51301061c026f40c0685cd20edbb28211","http://127.0.0.1:9000/xfaq/test.doc?uploadId=YTJlOWZhMmEtM2I3My00MmIyLWE0YjgtMDFkYjQzMzIyNmVhLjFmYzQ2M2ViLTI2YmYtNDZjMi04M2ZlLWJjMThjOTk0MWU4OHgxNzQ4OTM3NDE3MjI4ODA1MTAw&partNumber=2&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20250603%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250603T075657Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=eedd5431a6e4ba0ebda1c2b227f7543eed77d245b3d86c6d5d505d27f61cd823");uploadIdMap.put("chunkUploadIds", chunkUploadUrls);// 基于此,分片直接上传到minio,不走服务端,省去一次网络IO开销HttpClient httpClient = HttpClients.createDefault();for (int i = 0; i < chunkUploadUrls.size(); i++) {// PUT直接上传到minioString chunkUploadId = chunkUploadUrls.get(i);HttpPut httpPut = new HttpPut(chunkUploadId);httpPut.setHeader("Content-Type","application/msword");File chunkFile = new File(chunkFilePaths.get(i));FileEntity chunkFileEntity = new FileEntity(chunkFile);httpPut.setEntity(chunkFileEntity);HttpResponse chunkUploadResp = httpClient.execute(httpPut);log.info("[分片" + (i + 1) + "]上传响应:" + JSON.toJSONString(chunkUploadResp));httpPut.releaseConnection();}}/*** 第四步,合并分片*/@ApiForBackEndInVideo@PostMapping("/uploadLargeFile/merge")public ResponseEntity<Boolean> applyUploadPsiResult2Minio(@RequestParam("largeFileName") String largeFileName,@RequestParam("uploadId") String uploadId) {boolean mergeResult = mergeMultipartUpload(largeFileName, uploadId);return ResponseEntity.ok(mergeResult);}/*** 分片上传完后合并* @param objectName 文件名称* @param uploadId* @return*/public boolean mergeMultipartUpload(String objectName, String uploadId) {try {log.info("ready to merge <" + objectName + " - " + uploadId + " - " + bucketName + ">");// 查询上传后的分片数据ListPartsResponse partResult = minioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null);int chunkCount = partResult.result().partList().size();Part[] parts = new Part[chunkCount];int partNumber = 1;for (Part part : partResult.result().partList()) {parts[partNumber - 1] = new Part(partNumber, part.etag());partNumber++;}// 合并分片ObjectWriteResponse objectWriteResponse = minioClient.mergeMultipartUpload(bucketName, null, objectName, uploadId, parts, null, null);log.info("mergeMultipartUpload resp: {}", objectWriteResponse);} catch (Exception e) {log.error("合并失败:", e);return false;}return true;}
}