优化:minio大文件web交互流式响应

This commit is contained in:
yangyang01000846
2025-11-27 10:58:07 +08:00
parent 55a2e72da6
commit fdae339b17
8 changed files with 83 additions and 37 deletions

View File

@@ -4,7 +4,6 @@ import io.minio.MinioClient;
import io.minio.admin.MinioAdminClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -26,6 +25,9 @@ public class MinioConfig {
private String spdmBucket; // 普通业务数据桶
// allocateDirect 单次缓冲的数据
private int directMemory;
// 初始化超级管理员客户端(用于 admin 操作:桶、用户、策略)
@Bean
public MinioAdminClient minioAdminClient() {

View File

@@ -1,5 +1,6 @@
package com.sdm.data.service;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
@@ -108,7 +109,13 @@ public interface IMinioService {
* @param objectName 文件名objectKey
*/
byte[] downloadFile(String objectName) throws Exception;
/**
* 从MinIO下载文件到response
* @param objectName 文件名objectKey
*/
void downloadFile(String objectName, HttpServletResponse response,String encodedFileName,Boolean preview,String contentType) throws Exception;
/**
* 从MinIO下载文件
* @param objectName 文件名objectKey

View File

@@ -61,7 +61,10 @@ import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
@@ -1176,22 +1179,9 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
response.sendError(HttpServletResponse.SC_NON_AUTHORITATIVE_INFORMATION, "用户无权限");
return;
}
// 从MinIO下载文件
byte[] fileData = minioService.downloadFile(fileObjectKey);
// 设置响应头
response.reset();
response.setContentType("application/octet-stream;charset=UTF-8");
String encodedFileName = URLEncoder.encode(fileMetadataInfo.getOriginalName(), StandardCharsets.UTF_8);
response.addHeader("Content-Disposition", "attachment;filename=" + encodedFileName);
response.addHeader("Content-Length", String.valueOf(fileData.length));
// 写入响应流
OutputStream outputStream = response.getOutputStream();
outputStream.write(fileData);
outputStream.flush();
outputStream.close();
minioService.downloadFile(fileObjectKey,response,encodedFileName,false,"");
} catch (Exception e) {
log.error("下载文件失败", e);
try {
@@ -1823,32 +1813,17 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 检查文件是否存在且为图片类型
String objectKey = fileMetadataInfo.getObjectKey();
if (objectKey == null || objectKey.isEmpty()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 从minioService获取文件数据
byte[] fileData = minioService.downloadFile(objectKey);
// 设置响应头信息
// 由于FileMetadataInfo中没有fileType字段我们从originalName推断MIME类型
String originalName = fileMetadataInfo.getOriginalName();
// 由于FileMetadataInfo中没有fileType字段我们从originalName推断MIME类型
String contentType = getContentTypeByFileName(originalName);
response.setContentType(contentType);
response.setContentLength(fileData.length);
response.setHeader("Content-Disposition", "inline; filename=\"" +
URLEncoder.encode(originalName, StandardCharsets.UTF_8.toString()) + "\"");
// 将文件数据写入响应输出流
try (OutputStream outputStream = response.getOutputStream()) {
outputStream.write(fileData);
outputStream.flush();
}
minioService.downloadFile(objectKey,response,originalName,true,contentType);
} catch (Exception e) {
log.error("预览图片失败: fileId={}", fileId, e);
try {

View File

@@ -1,6 +1,7 @@
package com.sdm.data.service.minio;
import com.alibaba.fastjson2.JSON;
import com.sdm.common.entity.constants.NumberConstants;
import com.sdm.common.log.CoreLogger;
import com.sdm.data.config.MinioConfig;
import com.sdm.data.service.IMinioService;
@@ -10,9 +11,12 @@ import io.minio.http.Method;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
@@ -21,6 +25,10 @@ import javax.annotation.PostConstruct;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
@@ -36,6 +44,8 @@ public class MinioService implements IMinioService {
private final MinioClient minioClient;
private final MinioConfig minioConfig;
// 可配置缓冲区大小) 16KB
private static final int DEFAULT_BUFFER_SIZE = 16384;
@Autowired
public MinioService(MinioClient minioClient, MinioConfig minioConfig) {
@@ -329,6 +339,54 @@ public class MinioService implements IMinioService {
return downloadFile(objectName, minioConfig.getSpdmBucket());
}
@Override
public void downloadFile(String objectName, HttpServletResponse response,String encodedFileName,Boolean preview,String contentType) throws Exception {
downloadFile(objectName, minioConfig.getSpdmBucket(),response,encodedFileName,preview,contentType);
}
// 流式响应的返回
public void downloadFile(String objectName, String bucketName, HttpServletResponse response,String encodedFileName,Boolean preview, String contentType) throws Exception {
// 1. 获取文件元数据(大小)
StatObjectResponse stat = minioClient.statObject(
StatObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
long fileSize = stat.size();
// 1. 获取MinIO的流式数据使用try-with-resources确保资源释放
try (InputStream stream = minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
ReadableByteChannel inChannel = Channels.newChannel(stream);
WritableByteChannel outChannel = Channels.newChannel(response.getOutputStream())) {
String mediaType = org.apache.commons.lang3.StringUtils.isBlank(contentType) ?
MediaType.APPLICATION_OCTET_STREAM_VALUE: contentType ;
response.setContentType(mediaType);
String contentDisposition = preview ?
"inline; filename=\"" + encodedFileName + "\"" :"attachment; filename=\"" + encodedFileName + "\"";
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,contentDisposition);
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
response.addHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(fileSize));
// 3. 使用NIO通道+ByteBuffer缓冲区可根据性能调整
ByteBuffer buffer = ByteBuffer.allocateDirect(minioConfig.getDirectMemory()<= NumberConstants.ZERO ?
DEFAULT_BUFFER_SIZE:minioConfig.getDirectMemory());
while (inChannel.read(buffer) != -1) {
// 切换为读模式
buffer.flip();
outChannel.write(buffer);
buffer.compact();
}
buffer.flip();
while (buffer.hasRemaining()) {
outChannel.write(buffer);
}
response.flushBuffer();
}
}
@Override
public InputStream getMinioInputStream(String objectName) {
return getMinioInputStream(objectName, minioConfig.getSpdmBucket());

View File

@@ -113,4 +113,5 @@ minio:
secret-key: minioadmin
secure: false
secret-business-bucket: secretbusiness # 存放保密业务代码、脚本的桶(仅超级管理员访问)
spdm-bucket: spdm # 普通业务数据桶(分配给用户读写权限)
spdm-bucket: spdm # 普通业务数据桶(分配给用户读写权限)
directMemory: 16384 # 16kb

View File

@@ -102,6 +102,7 @@ minio:
secure: false
secret-business-bucket: secretbusiness # 存放保密业务代码、脚本的桶(仅超级管理员访问)
spdm-bucket: spdm # 普通业务数据桶(分配给用户读写权限)
directMemory: 16384 # 16kb
management:

View File

@@ -154,4 +154,5 @@ minio:
secret-key: minioadmin
secure: false
secret-business-bucket: secretbusiness # 存放保密业务代码、脚本的桶(仅超级管理员访问)
spdm-bucket: spdm # 普通业务数据桶(分配给用户读写权限)
spdm-bucket: spdm # 普通业务数据桶(分配给用户读写权限)
directMemory: 16384 # 16kb

View File

@@ -152,4 +152,5 @@ minio:
secret-key: minioadmin
secure: false
secret-business-bucket: secretbusiness # 存放保密业务代码、脚本的桶(仅超级管理员访问)
spdm-bucket: spdm # 普通业务数据桶(分配给用户读写权限)
spdm-bucket: spdm # 普通业务数据桶(分配给用户读写权限)
directMemory: 16384 # 16kb