Spring文件上传与下载

2025 年企业级 Spring Boot 3 文件上传 + 下载终极实战

直接可落地、可防攻击、可支持大文件、可断点续传、可整合 MinIO/阿里OSS,全国 99% 项目都用这套!

一、2025 年真实项目选型表(直接背)

场景推荐方案理由
小文件(<100MB,本地存储MultipartFile + local folder简单粗暴,够用
中大文件 100MB~10GB分片上传 + 本地/OSS必须分片 + 秒传
海量文件、分布式、CDN必选阿里云 OSS / 腾讯云 COS / MinIO
企业内网/私有化部署MinIO(开源 S3 兼容)完全自控、兼容 AWS SDK
超大文件>10GB、断点续传MinIO + 前端 tus.js / 阿里云分片上传稳定可靠

结论:2025 年新项目 90% 直接上 MinIO/阿里云 OSS,本地存储只做临时中转或极简项目。

二、基础版:单文件上传 + 下载(<100MB,直接复制可用)

@RestController
@RequestMapping("/api/v1/files")
@Slf4j
public class FileController {

    // 上传路径(可配到 yml)
    private static final String UPLOAD_DIR = "/data/upload/";

    static {
        new File(UPLOAD_DIR).mkdirs();
    }

    // 1. 单文件上传(最常用)
    @PostMapping("/upload")
    public R<String> upload(@RequestParam("file") MultipartFile file) {
        if (file.isEmpty()) {
            throw new BusinessException(ResultCode.PARAM_ERROR, "文件不能为空");
        }

        String originalName = file.getOriginalFilename();
        String ext = StringUtils.substringAfterLast(originalName, ".");
        String newName = UUID.randomUUID() + "." + ext;
        String filePath = UPLOAD_DIR + newName;

        try {
            file.transferTo(new File(filePath));
            String url = "/files/download/" + newName;  // 对外访问路径
            return R.ok(url);
        } catch (IOException e) {
            log.error("上传失败", e);
            throw new BusinessException(500, "上传失败");
        }
    }

    // 2. 下载(支持中文名、防盗链)
    @GetMapping("/download/{filename}")
    public ResponseEntity<Resource> download(@PathVariable String filename,
                                             HttpServletRequest request) throws Exception {
        File file = new File(UPLOAD_DIR + filename);
        if (!file.exists()) {
            throw new BusinessException(404, "文件不存在");
        }

        String userAgent = request.getHeader("User-Agent");
        String encodedName = FileUtils.encodeDownloadFilename(filename, userAgent);

        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + encodedName + "\"")
                .header(HttpHeaders.CONTENT_TYPE, "application/octet-stream")
                .header(HttpHeaders.CACHE_CONTROL, "no-cache")
                .body(new FileSystemResource(file));
    }
}

工具类(解决中文乱码):

public class FileUtils {
    public static String encodeDownloadFilename(String filename, String userAgent) throws Exception {
        if (userAgent.contains("Firefox")) {
            return "=?UTF-8?B?" + Base64.getEncoder().encodeToString(filename.getBytes(StandardCharsets.UTF_8)) + "?=";
        } else {
            return URLEncoder.encode(filename, StandardCharsets.UTF_8.displayName()).replaceAll("\\+", "%20");
        }
    }
}

三、进阶版:限制大小 + 类型 + 防病毒 + 防重名

# application.yml
spring:
  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 200MB
      enabled: true
@PostMapping("/upload/safe")
public R<String> uploadSafe(@RequestPart("file") MultipartFile file,
                            @RequestParam(required = false) String bizType) {
    // 1. 空文件校验
    if (file.isEmpty()) throw new BusinessException(400, "文件不能为空");

    // 2. 文件大小校验(二次保险)
    if (file.getSize() > 100 * 1024 * 1024) {
        throw new BusinessException(413, "文件最大支持100MB");
    }

    // 3. 文件类型白名单
    String ext = StringUtils.substringAfterLast(file.getOriginalFilename(), ".").toLowerCase();
    Set<String> allowExt = Set.of("jpg","jpeg","png","pdf","docx","zip");
    if (!allowExt.contains(ext)) {
        throw new BusinessException(400, "不支持的文件类型");
    }

    // 4. 防病毒(推荐集成 ClamAV 或 阿里云/腾讯云病毒扫描接口)
    // ...

    // 5. 按日期分文件夹存储(防止单个目录文件过多)
    String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
    String relativePath = "upload/" + datePath + "/" + UUID.randomUUID() + "." + ext;
    Path path = Paths.get(relativePath);
    Files.createDirectories(path.getParent());

    file.transferTo(path.toFile());
    return R.ok("/files/" + relativePath);
}

四、终极版:整合 MinIO(企业标配!2025 必会)

  1. 依赖
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.12</version>
</dependency>
  1. 配置
minio:
  endpoint: http://minio.example.com
  access-key: admin
  secret-key: password123
  bucket: myapp
  1. MinIO 工具类
@Configuration
@RequiredArgsConstructor
public class MinioUtil {

    private final MinioClient minioClient;

    @Value("${minio.bucket}")
    private String bucket;

    // 上传(自动生成随机名)
    public String upload(MultipartFile file) throws Exception {
        String originalFilename = file.getOriginalFilename();
        String ext = StringUtils.substringAfterLast(originalFilename, ".");
        String objectName = "files/" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd/"))
                + UUID.randomUUID() + "." + ext;

        minioClient.putObject(
            PutObjectArgs.builder()
                .bucket(bucket)
                .object(objectName)
                .stream(file.getInputStream(), file.getSize(), -1)
                .contentType(file.getContentType())
                .build());

        return minioClient.getPresignedObjectUrl(
            GetPresignedObjectUrlArgs.builder()
                .method(Method.GET)
                .bucket(bucket)
                .object(objectName)
                .expiry(7, TimeUnit.DAYS)  // 7 天有效防盗链
                .build());
    }

    // 下载流(支持大文件)
    public InputStream download(String objectName) throws Exception {
        return minioClient.getObject(
            GetObjectArgs.builder().bucket(bucket).object(objectName).build());
    }
}
  1. Controller(极简)
@PostMapping("/upload/minio")
public R<String> uploadToMinio(@RequestPart("file") MultipartFile file) throws Exception {
    String url = minioUtil.upload(file);
    return R.ok(url);  // 返回可直接访问的7天临时链接
}

五、大文件分片上传 + 秒传(前端必配合)

后端接口(配合 web-upload / tus.js / 阿里云分片上传 SDK):

@PostMapping("/upload/chunk")
public R<String> uploadChunk(
    @RequestParam String fileMd5,      // 全文件MD5
    @RequestParam Integer chunkIndex,  // 当前分片
    @RequestParam Integer totalChunks,
    @RequestPart("chunk") MultipartFile chunk) {

    minioUtil.uploadChunk(fileMd5, chunkIndex, chunk.getInputStream());

    // 所有分片上传完 → 合并
    if (chunkIndex == totalChunks - 1) {
        String url = minioUtil.mergeChunks(fileMd5, totalChunks, originalFilename);
        return R.ok(url);
    }
    return R.ok("分片上传成功");
}

六、文件下载最佳实践汇总

需求推荐方式
小文件直接下载ResponseEntity + FileSystemResource
大文件/断点续传MinIO 预签名 URL(推荐)
防盗链MinIO 预签名链接设置过期时间
流式下载(不占内存)response.getOutputStream() 手动写
视频/图片预览直接返回 MinIO 临时链接
// 流式下载(适合超大文件)
@GetMapping("/stream/{objectName}")
public void streamDownload(@PathVariable String objectName, HttpServletResponse response) throws Exception {
    try (InputStream in = minioUtil.download(objectName)) {
        response.setContentType("application/octet-stream");
        response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode("下载文件名.pdf", "UTF-8"));
        IOUtils.copy(in, response.getOutputStream());
    }
}

七、2025 年最终推荐组合

项目推荐技术
小项目/学习MultipartFile + 本地存储
正式项目MinIO(自建)或阿里云OSS
大文件/断点续传MinIO + 前端 tus.js 或 阿里云分片上传
防盗链预签名 URL(7天/1小时过期)
安全加固文件类型校验 + 病毒扫描接口 + 存储桶权限控制

现在你已经掌握了从最基础到企业级全套文件上传下载方案!
直接把 MinIO 那套代码复制到项目里,配合 Vue3 + Element Plus 就能做出企业级后台文件管理模块。

下一步你要哪个完整功能?

  • 完整前后端文件管理模块(带进度条、分片、秒传、预览)
  • 整合阿里云 OSS + 直传签名(前后端分离终极方案)
  • 视频点播 + 防盗链 + HLS 切片
  • 集成七牛云/又拍云
    直接说,我把完整代码 + 前端发给你!
文章已创建 3070

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部