版本:webflux3.4.1
1,工具类
package com.zx.util;
import com.zx.common.pojo.RpcResult;
import com.zx.common.utils.DateUtils;
import com.zx.common.utils.StringUtils;
import com.zx.common.utils.SysUtils;
import com.zx.frame.exception.BizException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.PathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.multipart.FilePart;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
* 文件工具类
* @author xufei
* @since 2024/7/24
*/
@Slf4j
public class FileUtils {
public static final String DEFAULT_FOLDER = "upload";
/**
* 保存文件到指定目录 并返回文件信息
* @author xufei
* @since 2024/7/24
* @param filePart 文件信息
*/
public static Mono<RpcResult<FileInfo>> uploadFile(FilePart filePart) {
String fileName = filePart.filename();
final String fileExt = FileUtils.getFileExt(fileName).toLowerCase();
if (!validateFileExt(fileExt)) {
return Mono.error(new BizException("无法上传后缀为." + fileExt + "的可执行文件!"));
}
String currentDate = LocalDate.now().format(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE2));
String uuid = SysUtils.getUUID();
String rootPath = SysUtils.getPropVal("resource.temp");
String folder = DEFAULT_FOLDER + File.separator +currentDate;
String pathStr = rootPath + folder;
String fullPathStr = pathStr + File.separator + uuid + "." + fileExt;
final Path path = Paths.get(pathStr);
return Mono.fromCallable(()->Files.createDirectories(path))
// 切换到弹性线程池 为了不阻塞主线程
.subscribeOn(Schedulers.boundedElastic())
// 创建文件夹失败
.onErrorResume((e)->{
log.error("上传文件夹创建失败", e);
return Mono.error(new BizException("上传文件夹创建失败"));
})
// 文件流存储到本地,这个是流所以取不到文件大小
.then(filePart.transferTo(Paths.get(fullPathStr)).onErrorResume(e -> {
log.error("上传文件失败", e);
return Mono.error(new BizException("上传文件失败"));
}))
// 上传成功, 返回文件信息
.then(Mono.fromCallable(()->{
// 取得文件大小
File file = new File(fullPathStr);
FileInfo fileInfo = new FileInfo();
fileInfo.setName(uuid);
fileInfo.setFileSize(file.length());
fileInfo.setFilePath(fullPathStr);
fileInfo.setFileExt(fileExt);
fileInfo.setFileName(fileName);
return RpcResult.success(fileInfo);
}));
}
/**
* 文件下载
* @author xufei
* @since 2025/1/25
* @param filePath 文件绝对路径
* @return ResponseEntity<Resource>
* @throws BizException 业务异常
*/
public static ResponseEntity<Resource> downloadFile(String filePath) throws BizException {
File file = new File(filePath);
return downloadFile( file, file.getName());
}
/**
* 文件下载
* @author xufei
* @since 2025/1/25
* @param filePath 文件绝对路径
* @param fileName 下载后文件名
* @return ResponseEntity<Resource>
* @throws BizException 业务异常
*/
public static ResponseEntity<Resource> downloadFile(String filePath, String fileName) throws BizException {
File file = new File(filePath);
return downloadFile( file, fileName);
}
/**
* 文件下载
* 这个是流下载,用a标签直接访问,会直接下载文件,并且有进度展示
* @author xufei
* @since 2025/1/25
* @param file 文件
* @param fileName 下载后文件名
* @return ResponseEntity<Resource>
* @throws BizException 业务异常
*/
public static ResponseEntity<Resource> downloadFile(File file, String fileName) throws BizException {
if (!file.exists()) {
String msg = StringUtils.format("文件[{}]不存在!", file.getPath());
log.error(msg);
throw new BizException(msg);
}
try {
Resource resource = new PathResource(file.toPath());
String filename = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
// 处理空格 文件名里的空格会变成+号
filename = filename.replaceAll("\\+", "%20");
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(file.length())
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename + ";filename*=UTF_8''" + filename)
.body(resource);
} catch (Exception e) {
log.error("文件下载失败", e);
throw new BizException("文件下载失败,请联系管理员或稍后重试");
}
}
/**
* 获取文件扩展名
*/
public static String getFileExt(String fileName) {
String fileExt = "";
if (fileName == null) {
return fileExt;
}
int dotIndex = fileName.lastIndexOf(".");
if (dotIndex == -1) {
return fileExt;
}
if (fileName.length() - 1 > dotIndex) {
fileExt = fileName.substring(dotIndex + 1);
}
return fileExt;
}
/**
* 新建目录
*/
public static void createFolder(String dirPath) throws IOException {
File filePath = new File(dirPath);
Files.createDirectories(filePath.toPath());
}
public static boolean validateFileExt(String fileExt) {
//文件扩展名黑名单
String[] blackList = new String[] {"dll", "jar", "conf", "jsp", "jspx", "jspf", "bat", "exe", "msi", "xml", "config", "properties","lrmx"};
for(String ext : blackList) {
if (ext.equals(fileExt)) {
return false;
}
}
return true;
}
}
2,controller
package com.zx.controller;
import com.zx.common.pojo.RpcResult;
import com.zx.frame.exception.BizException;
import com.zx.util.FileInfo;
import com.zx.util.FileUtils;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* 文件控制器
* @author xufei
* @since 2025/1/25
*/
@RestController
@RequestMapping("/file")
public class FileController {
/**
*
* @author xufei
* @since 2025/2/5
* @param filePart 文件
* @return 文件路径等信息
*/
@PostMapping("/upload")
public Mono<RpcResult<FileInfo>> uploadFile(@RequestPart("file") FilePart filePart) {
return FileUtils.uploadFile(filePart);
}
/**
* 这个不要用,用单个上传的接口,如果有多个文件上传,循环调用,这样一个文件上传失败不会影响其他的上传成功的,可以单独重新上传
* @author xufei
* @since 2025/2/5
* @param fileParts 多个文件
* @return 成功失败信息
*/
@PostMapping("/multi-upload")
public Mono<List<RpcResult<FileInfo>>> uploadMultipleFiles(@RequestPart("files") Flux<FilePart> fileParts) {
return fileParts
.flatMap(FileUtils::uploadFile)
.collectList();
}
/**
* 文件下载
* @author xufei
* @since 2025/1/25
* @param filePath 文件路径
* @param fileName 文件名
* @return 文件流
* @throws BizException 异常
*/
@GetMapping("/download")
public Mono<ResponseEntity<Resource>> downloadFile(@RequestParam("filePath") String filePath, @RequestParam("fileName") String fileName) throws BizException {
return Mono.just(FileUtils.downloadFile(filePath, fileName));
}
}
3,限制单个上传文件大小
spring:
webflux:
multipart:
max-disk-usage-per-part: 5MB
max-parts: 10
max-in-memory-size: 256KB
max-disk-usage-per-part:这个有点问题,我设置成5KB,但是还能上传15KB的文件,但是250KB的就不行了。我猜大概有个默认的最小值。
多个文件上传的接口,如果有一个文件超了就会全部失败。
4,统一异常处理返回错误信息
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
@Order(-2) // 确保优先级高于默认处理器
public class GlobalExceptionHandler {
@ExceptionHandler(DataBufferLimitException.class)
public ResponseEntity<String> handleDataBufferLimitException(DataBufferLimitException ex) {
return ResponseEntity
.status(HttpStatus.PAYLOAD_TOO_LARGE)
.body("文件大小超过限制");
}
}