|
|
@@ -17,7 +17,6 @@ import shop.alien.util.file.FileUtil;
|
|
|
|
|
|
import java.io.File;
|
|
|
import java.io.FileOutputStream;
|
|
|
-import java.io.IOException;
|
|
|
import java.io.InputStream;
|
|
|
import java.nio.channels.Channels;
|
|
|
import java.nio.channels.FileChannel;
|
|
|
@@ -215,28 +214,16 @@ public class FileUploadUtil {
|
|
|
log.info("FileUpload.processSingleFile 获取到视频文件准备上传 {} {}", prefix, multipartFile.getOriginalFilename());
|
|
|
String videoFileName = fileNameAndType.get("name").replaceAll(",", "") + RandomCreateUtil.getRandomNum(6);
|
|
|
|
|
|
- // 重要:必须先保存文件到本地,因为 MultipartFile 的临时文件可能被清理
|
|
|
- // 在并行处理之前,先确保文件已经保存到本地
|
|
|
- String cacheVideoPath = copyFileOptimized(uploadDir, multipartFile);
|
|
|
- File videoFile = new File(cacheVideoPath);
|
|
|
-
|
|
|
- if (!videoFile.exists() || videoFile.length() == 0) {
|
|
|
- throw new RuntimeException("视频文件复制失败,文件不存在或为空: " + cacheVideoPath);
|
|
|
- }
|
|
|
-
|
|
|
- // 使用本地文件进行上传(避免 MultipartFile 临时文件被清理的问题)
|
|
|
- File localVideoFile = videoFile;
|
|
|
+ // 优化:并行执行视频上传和文件复制(这两个操作互不依赖,可以并行)
|
|
|
CompletableFuture<String> uploadFuture = CompletableFuture.supplyAsync(() -> {
|
|
|
- try {
|
|
|
- // 使用本地文件上传,而不是 MultipartFile
|
|
|
- return aliOSSUtil.uploadFile(localVideoFile, prefix + videoFileName + "." + fileNameAndType.get("type"));
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("视频上传失败", e);
|
|
|
- throw new RuntimeException("视频上传失败: " + e.getMessage(), e);
|
|
|
- }
|
|
|
+ return aliOSSUtil.uploadFile(multipartFile, prefix + videoFileName + "." + fileNameAndType.get("type"));
|
|
|
});
|
|
|
|
|
|
- // 等待上传完成
|
|
|
+ // 复制文件用于截图(使用优化的流式复制)
|
|
|
+ String cacheVideoPath = copyFileOptimized(uploadDir, multipartFile);
|
|
|
+ File videoFile = new File(cacheVideoPath);
|
|
|
+
|
|
|
+ // 等待上传完成(如果复制已完成,这里不会阻塞太久)
|
|
|
String videoUrl = uploadFuture.get();
|
|
|
filePathList.add(videoUrl);
|
|
|
|
|
|
@@ -375,8 +362,6 @@ public class FileUploadUtil {
|
|
|
/**
|
|
|
* 优化的文件复制方法 - 使用流式复制,避免一次性读取全部字节
|
|
|
* 对于大文件(如视频),性能提升显著
|
|
|
- *
|
|
|
- * 注意:MultipartFile 的临时文件可能被清理,需要立即读取
|
|
|
*
|
|
|
* @param localFilePath 本地路径
|
|
|
* @param file 文件
|
|
|
@@ -385,6 +370,8 @@ public class FileUploadUtil {
|
|
|
private String copyFileOptimized(String localFilePath, MultipartFile file) {
|
|
|
InputStream inputStream = null;
|
|
|
FileOutputStream outputStream = null;
|
|
|
+ ReadableByteChannel inputChannel = null;
|
|
|
+ FileChannel outputChannel = null;
|
|
|
|
|
|
try {
|
|
|
File cacheFilePath = new File(localFilePath);
|
|
|
@@ -396,47 +383,53 @@ public class FileUploadUtil {
|
|
|
Path path = Paths.get(localFilePath, fileName);
|
|
|
Files.createDirectories(path.getParent());
|
|
|
|
|
|
+ // 使用NIO的Channel进行高效复制(零拷贝技术)
|
|
|
File targetFile = path.toFile();
|
|
|
+ inputStream = file.getInputStream();
|
|
|
+ outputStream = new FileOutputStream(targetFile);
|
|
|
+
|
|
|
+ // 将InputStream转换为ReadableByteChannel
|
|
|
+ inputChannel = Channels.newChannel(inputStream);
|
|
|
+ outputChannel = outputStream.getChannel();
|
|
|
|
|
|
- // 重要:立即获取 InputStream,因为临时文件可能很快被清理
|
|
|
- // 使用 transferTo 方法,这是 MultipartFile 推荐的方式
|
|
|
+ // 使用transferFrom进行高效复制,对于大文件性能更好
|
|
|
+ // 对于大文件,分块传输以避免内存问题
|
|
|
+ long transferred = 0;
|
|
|
+ long fileSize = file.getSize();
|
|
|
+ long chunkSize = 8 * 1024 * 1024; // 8MB chunks
|
|
|
+
|
|
|
+ while (transferred < fileSize) {
|
|
|
+ long remaining = fileSize - transferred;
|
|
|
+ long toTransfer = Math.min(chunkSize, remaining);
|
|
|
+ transferred += outputChannel.transferFrom(inputChannel, transferred, toTransfer);
|
|
|
+ }
|
|
|
+
|
|
|
+ return localFilePath + fileName;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("FileUpload.copyFileOptimized ERROR Msg={}", e.getMessage(), e);
|
|
|
+ // 降级到传统方式
|
|
|
try {
|
|
|
- // 优先使用 transferTo 方法(Spring 5.1+ 支持,性能最好)
|
|
|
- file.transferTo(targetFile);
|
|
|
- return localFilePath + fileName;
|
|
|
- } catch (IllegalStateException | IOException e) {
|
|
|
- // 如果 transferTo 失败(可能是临时文件已被清理),使用流式复制
|
|
|
- log.warn("transferTo 失败,使用流式复制: {}", e.getMessage());
|
|
|
-
|
|
|
- // 重新获取 InputStream(如果可能)
|
|
|
- inputStream = file.getInputStream();
|
|
|
- outputStream = new FileOutputStream(targetFile);
|
|
|
-
|
|
|
- // 使用缓冲流进行复制(8KB 缓冲区)
|
|
|
- byte[] buffer = new byte[8192];
|
|
|
- int bytesRead;
|
|
|
- long totalBytes = 0;
|
|
|
- long fileSize = file.getSize();
|
|
|
-
|
|
|
- while ((bytesRead = inputStream.read(buffer)) != -1) {
|
|
|
- outputStream.write(buffer, 0, bytesRead);
|
|
|
- totalBytes += bytesRead;
|
|
|
-
|
|
|
- // 如果文件大小已知,可以显示进度
|
|
|
- if (fileSize > 0 && totalBytes % (10 * 1024 * 1024) == 0) {
|
|
|
- log.debug("文件复制进度: {}/{} bytes", totalBytes, fileSize);
|
|
|
+ Path path = Paths.get(localFilePath, file.getOriginalFilename());
|
|
|
+ Files.createDirectories(path.getParent());
|
|
|
+ // 使用缓冲流进行复制
|
|
|
+ try (InputStream is = file.getInputStream();
|
|
|
+ FileOutputStream fos = new FileOutputStream(path.toFile())) {
|
|
|
+ byte[] buffer = new byte[8192];
|
|
|
+ int bytesRead;
|
|
|
+ while ((bytesRead = is.read(buffer)) != -1) {
|
|
|
+ fos.write(buffer, 0, bytesRead);
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- outputStream.flush();
|
|
|
- return localFilePath + fileName;
|
|
|
+ return localFilePath + file.getOriginalFilename();
|
|
|
+ } catch (Exception ex) {
|
|
|
+ log.error("FileUpload.copyFileOptimized 降级方案也失败 Msg={}", ex.getMessage(), ex);
|
|
|
+ throw new RuntimeException("文件复制失败: " + ex.getMessage(), ex);
|
|
|
}
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("FileUpload.copyFileOptimized ERROR Msg={}", e.getMessage(), e);
|
|
|
- throw new RuntimeException("文件复制失败: " + e.getMessage(), e);
|
|
|
} finally {
|
|
|
// 关闭资源
|
|
|
try {
|
|
|
+ if (inputChannel != null) inputChannel.close();
|
|
|
+ if (outputChannel != null) outputChannel.close();
|
|
|
if (inputStream != null) inputStream.close();
|
|
|
if (outputStream != null) outputStream.close();
|
|
|
} catch (Exception e) {
|