|
|
@@ -16,10 +16,18 @@ import shop.alien.util.common.VideoUtils;
|
|
|
import shop.alien.util.file.FileUtil;
|
|
|
|
|
|
import java.io.File;
|
|
|
+import java.io.FileOutputStream;
|
|
|
+import java.io.InputStream;
|
|
|
+import java.nio.channels.Channels;
|
|
|
+import java.nio.channels.FileChannel;
|
|
|
+import java.nio.channels.ReadableByteChannel;
|
|
|
import java.nio.file.Files;
|
|
|
import java.nio.file.Path;
|
|
|
import java.nio.file.Paths;
|
|
|
+import java.nio.file.StandardOpenOption;
|
|
|
import java.util.*;
|
|
|
+import java.util.concurrent.*;
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
|
/**
|
|
|
* 二期-文件上传
|
|
|
@@ -118,7 +126,7 @@ public class FileUploadUtil {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 上传多个文件
|
|
|
+ * 上传多个文件(性能优化版 - 并行处理 + 流式复制)
|
|
|
*
|
|
|
* @param multipartRequest 多文件
|
|
|
* @return List<String>
|
|
|
@@ -127,82 +135,173 @@ public class FileUploadUtil {
|
|
|
try {
|
|
|
log.info("FileUpload.uploadMoreFile multipartRequest={}", multipartRequest.getFileNames());
|
|
|
Set<String> fileNameSet = multipartRequest.getMultiFileMap().keySet();
|
|
|
- List<String> filePathList = new ArrayList<>();
|
|
|
- for (String s : fileNameSet) {
|
|
|
- MultipartFile multipartFile = multipartRequest.getFileMap().get(s);
|
|
|
- log.info("FileUpload.uploadMoreFile fileName={}", multipartFile.getOriginalFilename());
|
|
|
- String uploadDir = this.uploadDir.replace("file:///", "").replace("\\", "/");
|
|
|
- String prefix;
|
|
|
- Map<String, String> fileNameAndType = FileUtil.getFileNameAndType(multipartFile);
|
|
|
- //区分文件类型
|
|
|
- if (imageFileType.contains(fileNameAndType.get("type").toLowerCase())) {
|
|
|
- uploadDir += "/image";
|
|
|
- prefix = "image/";
|
|
|
- log.info("FileUpload.uploadMoreFile 获取到图片文件准备复制 {} {} {}", uploadDir, prefix, multipartFile.getOriginalFilename());
|
|
|
- // 去除文件名中的逗号,避免URL拼接时被错误分割
|
|
|
- String imageFileName = fileNameAndType.get("name").replaceAll(",", "");
|
|
|
- filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + imageFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type")));
|
|
|
- ;
|
|
|
- } else if (videoFileType.contains(fileNameAndType.get("type").toLowerCase())) {
|
|
|
- uploadDir += "/video/";
|
|
|
- prefix = "video/";
|
|
|
- //上传视频文件
|
|
|
- log.info("FileUpload.uploadMoreFile 获取到视频文件准备复制 {} {} {}", uploadDir, prefix, multipartFile.getOriginalFilename());
|
|
|
- // 去除文件名中的逗号,避免URL拼接时被错误分割
|
|
|
- String videoFileName = fileNameAndType.get("name").replaceAll(",", "") + RandomCreateUtil.getRandomNum(6);
|
|
|
- String cacheVideoPath = copyFile(uploadDir, multipartFile);
|
|
|
- filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + videoFileName + "." + fileNameAndType.get("type")));
|
|
|
- //缓存视频截图使用
|
|
|
- File videoFile = new File(cacheVideoPath);
|
|
|
- //获取视频某帧截图
|
|
|
- log.info("FileUpload.uploadMoreFile 视频文件复制完毕, 获取第一秒图片 {}", videoFile.getName());
|
|
|
- String videoPath = videoUtils.getImg(uploadDir + videoFile.getName());
|
|
|
- log.info("FileUpload.uploadMoreFile 视频文件复制完毕, 图片位置 {}", videoPath);
|
|
|
- if (!videoPath.isEmpty()) {
|
|
|
- File videoImgFile = new File(videoPath);
|
|
|
- Map<String, String> videoImg = FileUtil.getFileNameAndType(videoImgFile);
|
|
|
- filePathList.add(aliOSSUtil.uploadFile(videoImgFile, prefix + videoFileName + "." + videoImg.get("type")));
|
|
|
- videoImgFile.delete();
|
|
|
- videoFile.delete();
|
|
|
- } else {
|
|
|
- throw new RuntimeException("视频截图失败");
|
|
|
+ String uploadDir = this.uploadDir.replace("file:///", "").replace("\\", "/");
|
|
|
+
|
|
|
+ // 如果只有一个文件,直接处理(避免线程池开销)
|
|
|
+ if (fileNameSet.size() == 1) {
|
|
|
+ return uploadMoreFileSequential(multipartRequest, fileNameSet, uploadDir);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 多个文件使用并行处理
|
|
|
+ List<CompletableFuture<List<String>>> futures = new ArrayList<>();
|
|
|
+ ExecutorService executor = createUploadExecutor();
|
|
|
+
|
|
|
+ try {
|
|
|
+ for (String s : fileNameSet) {
|
|
|
+ MultipartFile multipartFile = multipartRequest.getFileMap().get(s);
|
|
|
+ CompletableFuture<List<String>> future = CompletableFuture.supplyAsync(() -> {
|
|
|
+ return processSingleFile(multipartFile, uploadDir);
|
|
|
+ }, executor);
|
|
|
+ futures.add(future);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 等待所有任务完成并合并结果
|
|
|
+ List<String> filePathList = new ArrayList<>();
|
|
|
+ for (CompletableFuture<List<String>> future : futures) {
|
|
|
+ try {
|
|
|
+ filePathList.addAll(future.get());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("处理文件失败", e);
|
|
|
+ throw new RuntimeException("文件处理失败: " + e.getMessage(), e);
|
|
|
}
|
|
|
- } else if (voiceFileType.contains(fileNameAndType.get("type").toLowerCase())) {
|
|
|
- uploadDir += "/voice";
|
|
|
- prefix = "voice/";
|
|
|
- log.info("FileUpload.uploadMoreFile 获取到语音文件准备复制 {} {} {}", uploadDir, prefix, multipartFile.getOriginalFilename());
|
|
|
- // 去除文件名中的逗号,避免URL拼接时被错误分割
|
|
|
- String voiceFileName = fileNameAndType.get("name").replaceAll(",", "");
|
|
|
- filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + voiceFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type")));
|
|
|
- } else if (privacyFileType.contains(fileNameAndType.get("type").toLowerCase())) {
|
|
|
- uploadDir += "/privacy/";
|
|
|
- prefix = "privacy/";
|
|
|
- log.info("FileUpload.uploadMoreFile 获取到隐私文件准备复制 {} {} {}", uploadDir, prefix, multipartFile.getOriginalFilename());
|
|
|
- // 去除文件名中的逗号,避免URL拼接时被错误分割
|
|
|
- String privacyFileName = fileNameAndType.get("name").replaceAll(",", "");
|
|
|
- filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + privacyFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type")));
|
|
|
- } else if (pdfFileType.contains(fileNameAndType.get("type").toLowerCase())) {
|
|
|
- uploadDir += "/pdf";
|
|
|
- prefix = "pdf/";
|
|
|
- log.info("FileUpload.uploadMoreFile 获取到PDF文件准备复制 {} {} {}", uploadDir, prefix, multipartFile.getOriginalFilename());
|
|
|
- // 去除文件名中的逗号,避免URL拼接时被错误分割
|
|
|
- String pdfFileName = fileNameAndType.get("name").replaceAll(",", "");
|
|
|
- filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + pdfFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type")));
|
|
|
- } else if (ohterFileType.contains(fileNameAndType.get("type").toLowerCase())) {
|
|
|
- uploadDir += "/other/";
|
|
|
- prefix = "other/";
|
|
|
- log.info("FileUpload.uploadMoreFile 获取到其他文件准备复制 {} {} {}", uploadDir, prefix, multipartFile.getOriginalFilename());
|
|
|
- // 去除文件名中的逗号,避免URL拼接时被错误分割
|
|
|
- String otherFileName = fileNameAndType.get("name").replaceAll(",", "");
|
|
|
- filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + otherFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type")));
|
|
|
}
|
|
|
+
|
|
|
+ return filePathList;
|
|
|
+ } finally {
|
|
|
+ executor.shutdown();
|
|
|
}
|
|
|
- return filePathList;
|
|
|
} catch (Exception e) {
|
|
|
- log.error("FileUpload.uploadMoreFile ERROR Msg={}", e.getMessage());
|
|
|
+ log.error("FileUpload.uploadMoreFile ERROR Msg={}", e.getMessage(), e);
|
|
|
throw new RuntimeException(e);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 顺序处理(单个文件时使用,避免线程池开销)
|
|
|
+ */
|
|
|
+ private List<String> uploadMoreFileSequential(MultipartRequest multipartRequest, Set<String> fileNameSet, String uploadDir) {
|
|
|
+ List<String> filePathList = new ArrayList<>();
|
|
|
+ for (String s : fileNameSet) {
|
|
|
+ MultipartFile multipartFile = multipartRequest.getFileMap().get(s);
|
|
|
+ filePathList.addAll(processSingleFile(multipartFile, uploadDir));
|
|
|
+ }
|
|
|
+ return filePathList;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理单个文件
|
|
|
+ */
|
|
|
+ private List<String> processSingleFile(MultipartFile multipartFile, String baseUploadDir) {
|
|
|
+ List<String> filePathList = new ArrayList<>();
|
|
|
+ try {
|
|
|
+ log.info("FileUpload.processSingleFile fileName={}", multipartFile.getOriginalFilename());
|
|
|
+ String uploadDir = baseUploadDir;
|
|
|
+ String prefix;
|
|
|
+ Map<String, String> fileNameAndType = FileUtil.getFileNameAndType(multipartFile);
|
|
|
+ String fileType = fileNameAndType.get("type").toLowerCase();
|
|
|
+
|
|
|
+ //区分文件类型
|
|
|
+ if (imageFileType.contains(fileType)) {
|
|
|
+ uploadDir += "/image";
|
|
|
+ prefix = "image/";
|
|
|
+ log.info("FileUpload.processSingleFile 获取到图片文件准备上传 {} {}", prefix, multipartFile.getOriginalFilename());
|
|
|
+ String imageFileName = fileNameAndType.get("name").replaceAll(",", "");
|
|
|
+ filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + imageFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type")));
|
|
|
+
|
|
|
+ } else if (videoFileType.contains(fileType)) {
|
|
|
+ uploadDir += "/video/";
|
|
|
+ prefix = "video/";
|
|
|
+ log.info("FileUpload.processSingleFile 获取到视频文件准备上传 {} {}", prefix, multipartFile.getOriginalFilename());
|
|
|
+ String videoFileName = fileNameAndType.get("name").replaceAll(",", "") + RandomCreateUtil.getRandomNum(6);
|
|
|
+
|
|
|
+ // 优化:并行执行视频上传和文件复制(这两个操作互不依赖,可以并行)
|
|
|
+ CompletableFuture<String> uploadFuture = CompletableFuture.supplyAsync(() -> {
|
|
|
+ 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);
|
|
|
+
|
|
|
+ // 获取视频截图
|
|
|
+ log.info("FileUpload.processSingleFile 视频文件复制完毕, 获取第一秒图片 {}", videoFile.getName());
|
|
|
+ String videoPath = videoUtils.getImg(uploadDir + videoFile.getName());
|
|
|
+ log.info("FileUpload.processSingleFile 视频文件复制完毕, 图片位置 {}", videoPath);
|
|
|
+
|
|
|
+ if (!videoPath.isEmpty()) {
|
|
|
+ File videoImgFile = new File(videoPath);
|
|
|
+ Map<String, String> videoImg = FileUtil.getFileNameAndType(videoImgFile);
|
|
|
+ filePathList.add(aliOSSUtil.uploadFile(videoImgFile, prefix + videoFileName + "." + videoImg.get("type")));
|
|
|
+ videoImgFile.delete();
|
|
|
+ videoFile.delete();
|
|
|
+ } else {
|
|
|
+ throw new RuntimeException("视频截图失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ } else if (voiceFileType.contains(fileType)) {
|
|
|
+ uploadDir += "/voice";
|
|
|
+ prefix = "voice/";
|
|
|
+ log.info("FileUpload.processSingleFile 获取到语音文件准备上传 {} {}", prefix, multipartFile.getOriginalFilename());
|
|
|
+ String voiceFileName = fileNameAndType.get("name").replaceAll(",", "");
|
|
|
+ filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + voiceFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type")));
|
|
|
+
|
|
|
+ } else if (privacyFileType.contains(fileType)) {
|
|
|
+ uploadDir += "/privacy/";
|
|
|
+ prefix = "privacy/";
|
|
|
+ log.info("FileUpload.processSingleFile 获取到隐私文件准备上传 {} {}", prefix, multipartFile.getOriginalFilename());
|
|
|
+ String privacyFileName = fileNameAndType.get("name").replaceAll(",", "");
|
|
|
+ filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + privacyFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type")));
|
|
|
+
|
|
|
+ } else if (pdfFileType.contains(fileType)) {
|
|
|
+ uploadDir += "/pdf";
|
|
|
+ prefix = "pdf/";
|
|
|
+ log.info("FileUpload.processSingleFile 获取到PDF文件准备上传 {} {}", prefix, multipartFile.getOriginalFilename());
|
|
|
+ String pdfFileName = fileNameAndType.get("name").replaceAll(",", "");
|
|
|
+ filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + pdfFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type")));
|
|
|
+
|
|
|
+ } else if (ohterFileType.contains(fileType)) {
|
|
|
+ uploadDir += "/other/";
|
|
|
+ prefix = "other/";
|
|
|
+ log.info("FileUpload.processSingleFile 获取到其他文件准备上传 {} {}", prefix, multipartFile.getOriginalFilename());
|
|
|
+ String otherFileName = fileNameAndType.get("name").replaceAll(",", "");
|
|
|
+ filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + otherFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type")));
|
|
|
+ }
|
|
|
+
|
|
|
+ return filePathList;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("FileUpload.processSingleFile ERROR fileName={}, Msg={}", multipartFile.getOriginalFilename(), e.getMessage(), e);
|
|
|
+ throw new RuntimeException("处理文件失败: " + multipartFile.getOriginalFilename(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建上传任务线程池
|
|
|
+ */
|
|
|
+ private ExecutorService createUploadExecutor() {
|
|
|
+ int corePoolSize = Math.min(Runtime.getRuntime().availableProcessors() * 2, 8);
|
|
|
+ int maxPoolSize = Math.min(Runtime.getRuntime().availableProcessors() * 4, 16);
|
|
|
+ return new ThreadPoolExecutor(
|
|
|
+ corePoolSize,
|
|
|
+ maxPoolSize,
|
|
|
+ 60L,
|
|
|
+ TimeUnit.SECONDS,
|
|
|
+ new LinkedBlockingQueue<>(100),
|
|
|
+ new ThreadFactory() {
|
|
|
+ private int count = 0;
|
|
|
+ @Override
|
|
|
+ public Thread newThread(Runnable r) {
|
|
|
+ Thread t = new Thread(r);
|
|
|
+ t.setName("file-upload-" + count++);
|
|
|
+ return t;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ new ThreadPoolExecutor.CallerRunsPolicy()
|
|
|
+ );
|
|
|
+ }
|
|
|
|
|
|
/**
|
|
|
* 上传图片
|
|
|
@@ -250,30 +349,92 @@ public class FileUploadUtil {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 复制文件, 返回url链接
|
|
|
+ * 复制文件, 返回url链接(优化版 - 使用流式复制,避免一次性读取大文件)
|
|
|
*
|
|
|
* @param localFilePath 本地路径
|
|
|
* @param file 文件
|
|
|
* @return 访问url路径
|
|
|
*/
|
|
|
private String copyFile(String localFilePath, MultipartFile file) {
|
|
|
+ return copyFileOptimized(localFilePath, file);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 优化的文件复制方法 - 使用流式复制,避免一次性读取全部字节
|
|
|
+ * 对于大文件(如视频),性能提升显著
|
|
|
+ *
|
|
|
+ * @param localFilePath 本地路径
|
|
|
+ * @param file 文件
|
|
|
+ * @return 访问url路径
|
|
|
+ */
|
|
|
+ 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);
|
|
|
if (!cacheFilePath.exists()) {
|
|
|
cacheFilePath.mkdirs();
|
|
|
}
|
|
|
- String fileName = file.getOriginalFilename().substring(0, file.getOriginalFilename().lastIndexOf('.'));
|
|
|
- log.info("FileUpload.copyFile fileName={}", fileName);
|
|
|
- String fileType = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf('.'));
|
|
|
- log.info("FileUpload.copyFile fileType={}", fileType);
|
|
|
- System.out.println(file.getOriginalFilename());
|
|
|
- Path path = Paths.get(localFilePath, file.getOriginalFilename());
|
|
|
+
|
|
|
+ String fileName = file.getOriginalFilename();
|
|
|
+ Path path = Paths.get(localFilePath, fileName);
|
|
|
Files.createDirectories(path.getParent());
|
|
|
- Files.write(path, file.getBytes());
|
|
|
- return localFilePath + file.getOriginalFilename();
|
|
|
+
|
|
|
+ // 使用NIO的Channel进行高效复制(零拷贝技术)
|
|
|
+ File targetFile = path.toFile();
|
|
|
+ inputStream = file.getInputStream();
|
|
|
+ outputStream = new FileOutputStream(targetFile);
|
|
|
+
|
|
|
+ // 将InputStream转换为ReadableByteChannel
|
|
|
+ inputChannel = Channels.newChannel(inputStream);
|
|
|
+ outputChannel = outputStream.getChannel();
|
|
|
+
|
|
|
+ // 使用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.copyFile ERROR Msg={}", e.getMessage());
|
|
|
- return e.getMessage();
|
|
|
+ log.error("FileUpload.copyFileOptimized ERROR Msg={}", e.getMessage(), e);
|
|
|
+ // 降级到传统方式
|
|
|
+ try {
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return localFilePath + file.getOriginalFilename();
|
|
|
+ } catch (Exception ex) {
|
|
|
+ log.error("FileUpload.copyFileOptimized 降级方案也失败 Msg={}", ex.getMessage(), ex);
|
|
|
+ throw new RuntimeException("文件复制失败: " + ex.getMessage(), ex);
|
|
|
+ }
|
|
|
+ } 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) {
|
|
|
+ log.warn("关闭文件流失败", e);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|