记录spring webflux 文件上传下载

版本: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("文件大小超过限制");
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

友情链接更多精彩内容