优化:minio大文件web交互流式响应
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -113,4 +113,5 @@ minio:
|
||||
secret-key: minioadmin
|
||||
secure: false
|
||||
secret-business-bucket: secretbusiness # 存放保密业务代码、脚本的桶(仅超级管理员访问)
|
||||
spdm-bucket: spdm # 普通业务数据桶(分配给用户读写权限)
|
||||
spdm-bucket: spdm # 普通业务数据桶(分配给用户读写权限)
|
||||
directMemory: 16384 # 16kb
|
||||
@@ -102,6 +102,7 @@ minio:
|
||||
secure: false
|
||||
secret-business-bucket: secretbusiness # 存放保密业务代码、脚本的桶(仅超级管理员访问)
|
||||
spdm-bucket: spdm # 普通业务数据桶(分配给用户读写权限)
|
||||
directMemory: 16384 # 16kb
|
||||
|
||||
|
||||
management:
|
||||
|
||||
@@ -154,4 +154,5 @@ minio:
|
||||
secret-key: minioadmin
|
||||
secure: false
|
||||
secret-business-bucket: secretbusiness # 存放保密业务代码、脚本的桶(仅超级管理员访问)
|
||||
spdm-bucket: spdm # 普通业务数据桶(分配给用户读写权限)
|
||||
spdm-bucket: spdm # 普通业务数据桶(分配给用户读写权限)
|
||||
directMemory: 16384 # 16kb
|
||||
@@ -152,4 +152,5 @@ minio:
|
||||
secret-key: minioadmin
|
||||
secure: false
|
||||
secret-business-bucket: secretbusiness # 存放保密业务代码、脚本的桶(仅超级管理员访问)
|
||||
spdm-bucket: spdm # 普通业务数据桶(分配给用户读写权限)
|
||||
spdm-bucket: spdm # 普通业务数据桶(分配给用户读写权限)
|
||||
directMemory: 16384 # 16kb
|
||||
Reference in New Issue
Block a user