杰瑞科技汇

Spring上传文件如何实现?

目录

  1. 核心原理简介
  2. 第一步:项目配置
    • 依赖引入
    • Spring Boot 配置
  3. 第二步:前端实现
    • HTML 表单
    • 前端框架(如 Axios)
  4. 第三步:后端控制器
    • 接收单个文件
    • 接收多个文件
    • 接收附带其他数据的文件
  5. 第四步:文件存储
    • 存储到本地服务器
    • 存储到云存储(如 Amazon S3, 阿里云 OSS) - 强烈推荐
  6. 第五步:高级主题与最佳实践
    • 文件大小限制
    • 文件类型/内容校验
    • 安全性考虑
    • 异步上传
  7. 完整示例代码

核心原理简介

Spring 文件上传的核心是 MultipartFile 接口,当浏览器通过一个 multipart/form-data 类型的表单提交文件时,Spring 的 DispatcherServlet 会使用 MultipartResolver 将请求中的文件部分解析成一个 MultipartFile 对象,你的控制器方法只需接收这个 MultipartFile 参数,就可以对文件进行处理(如获取内容、获取原始文件名、保存到磁盘等)。

关键接口/类:

  • MultipartFile: 代表上传的文件,提供了获取文件内容、名称、大小等方法。
  • MultipartResolver: 用于解析 multipart 请求的组件,Spring Boot 会自动配置 StandardServletMultipartResolver

第一步:项目配置

依赖引入

如果你使用的是 Spring Boot,通常只需要 spring-boot-starter-web 依赖,它已经包含了所有必需的组件。

<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 如果需要使用云存储,需要额外添加依赖 -->
    <!-- AWS S3 SDK -->
    <!-- <dependency>
        <groupId>software.amazon.awssdk</groupId>
        <artifactId>s3</artifactId>
    </dependency> -->
</dependencies>

如果你使用的是传统的 Spring MVC,需要确保 commons-fileupload 在类路径中。

Spring Boot 配置

application.propertiesapplication.yml 中进行配置。

application.properties 示例:

# 设置单个文件的最大大小 (e.g., 10MB)
spring.servlet.multipart.max-file-size=10MB
# 设置请求中所有文件的总大小 (e.g., 100MB)
spring.servlet.multipart.max-request-size=100MB
# (可选)在上传后是否临时文件
spring.servlet.multipart.location=/tmp

application.yml 示例:

spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 100MB
      location: /tmp

第二步:前端实现

前端表单必须满足两个条件:

  1. method="post"
  2. enctype="multipart/form-data"

HTML 表单示例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">File Upload</title>
</head>
<body>
    <h1>上传单个文件</h1>
    <form action="/upload/single" method="post" enctype="multipart/form-data">
        <input type="file" name="file" /> <!-- name="file" 必须与后端 @RequestParam 的值一致 -->
        <button type="submit">上传</button>
    </form>
    <hr>
    <h1>上传多个文件</h1>
    <form action="/upload/multiple" method="post" enctype="multipart/form-data">
        <input type="file" name="files" multiple /> <!-- name="files" 必须与后端 @RequestParam 的值一致 -->
        <button type="submit">上传</button>
    </form>
</body>
</html>

使用 JavaScript (Axios) 上传

现代 Web 应用通常使用 AJAX 上传文件,这不会导致页面刷新。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">AJAX File Upload</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
    <h1>AJAX 上传文件</h1>
    <input type="file" id="fileInput" />
    <button id="uploadButton">上传</button>
    <div id="progressBar" style="width: 300px; height: 20px; background-color: #f0f0f0; margin-top: 10px;">
        <div id="progress" style="width: 0%; height: 100%; background-color: green;"></div>
    </div>
    <p id="status"></p>
    <script>
        document.getElementById('uploadButton').addEventListener('click', async () => {
            const fileInput = document.getElementById('fileInput');
            const file = fileInput.files[0];
            if (!file) {
                alert('请选择一个文件');
                return;
            }
            const formData = new FormData();
            formData.append('file', file); // 'file' 必须与后端 @RequestParam 的值一致
            const progressBar = document.getElementById('progress');
            const status = document.getElementById('status');
            try {
                status.textContent = '上传中...';
                const response = await axios.post('/upload/single', formData, {
                    headers: {
                        'Content-Type': 'multipart/form-data'
                    },
                    onUploadProgress: progressEvent => {
                        const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
                        progressBar.style.width = percentCompleted + '%';
                        status.textContent = `上传进度: ${percentCompleted}%`;
                    }
                });
                status.textContent = '上传成功: ' + response.data.message;
                console.log(response.data);
            } catch (error) {
                status.textContent = '上传失败: ' + (error.response?.data?.message || error.message);
                console.error('Upload error:', error);
            }
        });
    </script>
</body>
</html>

第三步:后端控制器

创建一个 @RestController 来处理文件上传请求。

接收单个文件

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
@RestController
public class FileUploadController {
    // 定义一个文件存储的目录
    private final String UPLOAD_DIR = "uploads/";
    @PostMapping("/upload/single")
    public String handleFileUpload(@RequestParam("file") MultipartFile file) {
        if (file.isEmpty()) {
            return "请选择一个文件上传";
        }
        try {
            // 获取原始文件名
            String originalFilename = file.getOriginalFilename();
            // 生成一个唯一的新文件名,防止覆盖
            String uniqueFilename = UUID.randomUUID().toString() + "_" + originalFilename;
            // 创建上传目录(如果不存在)
            Path uploadPath = Paths.get(UPLOAD_DIR);
            if (!Files.exists(uploadPath)) {
                Files.createDirectories(uploadPath);
            }
            // 构建文件的完整保存路径
            Path destination = uploadPath.resolve(uniqueFilename);
            // 将文件内容写入到指定路径
            Files.copy(file.getInputStream(), destination);
            return "文件上传成功: " + destination.toString();
        } catch (IOException e) {
            e.printStackTrace();
            return "文件上传失败: " + e.getMessage();
        }
    }
}

接收多个文件

接收多个文件非常简单,只需要将 @RequestParam 的类型改为 MultipartFile[]List<MultipartFile>

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
@RestController
public class FileUploadController {
    private final String UPLOAD_DIR = "uploads/";
    @PostMapping("/upload/multiple")
    public String handleMultipleFileUpload(@RequestParam("files") MultipartFile[] files) {
        if (files == null || files.length == 0) {
            return "请选择至少一个文件上传";
        }
        StringBuilder result = new StringBuilder();
        for (MultipartFile file : files) {
            if (file.isEmpty()) {
                continue;
            }
            try {
                String originalFilename = file.getOriginalFilename();
                String uniqueFilename = UUID.randomUUID().toString() + "_" + originalFilename;
                Path uploadPath = Paths.get(UPLOAD_DIR);
                if (!Files.exists(uploadPath)) {
                    Files.createDirectories(uploadPath);
                }
                Path destination = uploadPath.resolve(uniqueFilename);
                Files.copy(file.getInputStream(), destination);
                result.append("文件 '").append(originalFilename).append("' 上传成功至: ").append(destination).append("\n");
            } catch (IOException e) {
                result.append("文件 '").append(file.getOriginalFilename()).append("' 上传失败: ").append(e.getMessage()).append("\n");
                e.printStackTrace();
            }
        }
        return result.toString();
    }
}

接收附带其他数据的文件

当表单中除了文件还有其他字段时,可以这样处理:

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
public class FileUploadController {
    @PostMapping("/upload/with-data")
    public String handleFileWithData(
            @RequestParam("description") String description,
            @RequestParam("file") MultipartFile file) {
        // 处理 description
        System.out.println("描述: " + description);
        // 处理 file
        if (file.isEmpty()) {
            return "请选择一个文件上传";
        }
        // ... 保存文件的逻辑 ...
        return "文件和描述已接收,文件名: " + file.getOriginalFilename() + ", 描述: " + description;
    }
}

第四步:文件存储

存储到本地服务器

如上例所示,直接使用 java.nio.file 将文件写入服务器本地磁盘。这在生产环境中通常不推荐,因为:

  • 可扩展性差:服务器磁盘空间有限,无法水平扩展。
  • 可用性风险:如果服务器宕机,文件可能会丢失。
  • 管理困难:备份、迁移等操作复杂。

存储到云存储 (强烈推荐)

生产环境应使用云存储服务,如 Amazon S3, Google Cloud Storage, Azure Blob Storage, 阿里云 OSS 等,这里以 Amazon S3 为例。

步骤:

  1. 添加依赖:

    <dependency>
        <groupId>software.amazon.awssdk</groupId>
        <artifactId>s3</artifactId>
        <version>2.20.0</version> <!-- 使用最新版本 -->
    </dependency>
  2. 配置 AWS 凭证:

    • 最佳实践: 使用 IAM 角色。
    • 开发环境: 在 ~/.aws/credentials 文件中配置。
    • Spring Boot 配置:
      # application.properties
      aws.accessKeyId=YOUR_ACCESS_KEY
      aws.secretKey=YOUR_SECRET_KEY
      aws.region=us-east-1
      aws.s3.bucket.name=your-s3-bucket-name
  3. 创建 S3 客户端和配置类:

    import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
    import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
    import software.amazon.awssdk.regions.Region;
    import software.amazon.awssdk.services.s3.S3Client;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    @Configuration
    public class AwsConfig {
        @Value("${aws.accessKeyId}")
        private String accessKeyId;
        @Value("${aws.secretKey}")
        private String secretKey;
        @Value("${aws.region}")
        private String region;
        @Bean
        public S3Client s3Client() {
            AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKeyId, secretKey);
            return S3Client.builder()
                    .region(Region.of(region))
                    .credentialsProvider(StaticCredentialsProvider.create(credentials))
                    .build();
        }
    }
  4. 修改控制器以使用 S3:

    import software.amazon.awssdk.services.s3.S3Client;
    import software.amazon.awssdk.services.s3.model.PutObjectRequest;
    import software.amazon.awssdk.services.s3.model.S3Exception;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.multipart.MultipartFile;
    import java.io.IOException;
    import java.nio.file.Paths;
    import java.util.UUID;
    @RestController
    public class CloudFileUploadController {
        @Autowired
        private S3Client s3Client;
        @Value("${aws.s3.bucket.name}")
        private String bucketName;
        @PostMapping("/upload/s3")
        public String uploadToS3(@RequestParam("file") MultipartFile file) {
            if (file.isEmpty()) {
                return "请选择一个文件上传";
            }
            try {
                String originalFilename = file.getOriginalFilename();
                String key = "uploads/" + UUID.randomUUID() + "_" + originalFilename; // S3中的对象键
                // 创建 PutObjectRequest
                PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                        .bucket(bucketName)
                        .key(key)
                        .build();
                // 上传文件
                s3Client.putObject(putObjectRequest, file.getInputStream());
                // 返回文件的公开访问URL (假设桶是公开的)
                // 注意:实际生产中应使用预签名URL
                String fileUrl = "https://" + bucketName + ".s3." + s3Client.serviceClient().region().toString() + ".amazonaws.com/" + key;
                return "文件上传成功到 S3! URL: " + fileUrl;
            } catch (S3Exception e) {
                System.err.println("S3 错误: " + e.awsErrorDetails().errorMessage());
                return "上传到 S3 失败: " + e.awsErrorDetails().errorMessage();
            } catch (IOException e) {
                e.printStackTrace();
                return "文件处理失败: " + e.getMessage();
            }
        }
    }

第五步:高级主题与最佳实践

文件大小限制

已经在 application.properties 中配置过。

  • spring.servlet.multipart.max-file-size: 单个文件大小。
  • spring.servlet.multipart.max-request-size: 整个请求大小。

如果超过限制,Spring 会抛出 MaxUploadSizeExceededException,你可以通过全局异常处理器来捕获它并返回友好的错误信息。

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.http.ResponseEntity;
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ResponseEntity<String> handleMaxSizeException(MaxUploadSizeExceededException exc) {
        return ResponseEntity.status(413).body("文件大小超过限制!");
    }
}

文件类型/内容校验

不要仅仅依赖文件扩展名,因为它是可以被伪造的,最好的方法是检查文件的 “魔术数字” (Magic Number),即文件头部的字节序列。

可以使用 Apache Tika 等库来检测文件的真正类型。

示例:检查是否为图片

import org.apache.tika.Tika;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
public boolean isImage(MultipartFile file) throws IOException {
    Tika tika = new Tika();
    String mimeType = tika.detect(file.getInputStream());
    return mimeType.startsWith("image/");
}
// 在控制器中使用
@PostMapping("/upload/safe")
public String safeUpload(@RequestParam("file") MultipartFile file) throws IOException {
    if (!isImage(file)) {
        return "只允许上传图片文件!";
    }
    // ... 安全的上传逻辑 ...
}

安全性考虑

  1. 文件名: 永远不要直接使用用户提供的文件名,它可能包含路径(../../../etc/passwd)或恶意字符,使用 UUID 或其他安全方式生成新文件名。
  2. : 上传的文件可能是可执行脚本(.jsp, .php, .py),如果用户能访问这些文件的URL,可能会导致服务器被攻击。不要将上传的文件直接放在Web服务器的根目录下,最好存放在一个无法通过Web直接访问的目录(如 WEB-INF/uploads),或者使用云存储。
  3. 病毒扫描: 对于用户上传的任何文件,都应该进行病毒扫描。

异步上传

对于大文件上传,同步处理会导致请求长时间占用线程,影响服务器性能,可以使用 Spring 的异步支持

  1. 启用异步:

    @SpringBootApplication
    @EnableAsync // 启用异步方法
    public class YourApplication {
        public static void main(String[] args) {
            SpringApplication.run(YourApplication.class, args);
        }
    }
  2. 创建异步任务服务:

    import org.springframework.scheduling.annotation.Async;
    import org.springframework.stereotype.Service;
    import org.springframework.web.multipart.MultipartFile;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    @Service
    public class FileUploadService {
        @Async // 标记此方法为异步执行
        public void uploadFile(MultipartFile file, String destinationPath) throws IOException {
            // 模拟一个耗时的操作
            System.out.println("开始异步上传文件: " + file.getOriginalFilename());
            Thread.sleep(5000); // 暂停5秒
            Files.copy(file.getInputStream(), Paths.get(destinationPath));
            System.out.println("文件上传完成: " + file.getOriginalFilename());
        }
    }
  3. 修改控制器:

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.multipart.MultipartFile;
    import java.util.concurrent.CompletableFuture;
    @RestController
    public class AsyncFileUploadController {
        @Autowired
        private FileUploadService fileUploadService;
        @PostMapping("/upload/async")
        public CompletableFuture<String> handleAsyncUpload(@RequestParam("file") MultipartFile file) {
            String destination = "uploads/async_" + file.getOriginalFilename();
            // 调用异步方法,并立即返回一个 Future 对象
            return CompletableFuture.supplyAsync(() -> {
                try {
                    fileUploadService.uploadFile(file, destination);
                    return "文件上传任务已提交,后台正在处理...";
                } catch (Exception e) {
                    return "提交失败: " + e.getMessage();
                }
            });
        }
    }

完整示例代码 (本地存储版)

这是一个整合了上述要点的完整、可运行的 Spring Boot 示例。

pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
</dependencies>

application.properties

server.port=8080
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB

全局异常处理器 GlobalExceptionHandler.java

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.http.ResponseEntity;
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ResponseEntity<String> handleMaxSizeException(MaxUploadSizeExceededException exc) {
        return ResponseEntity.status(413).body("文件大小超过 10MB 限制!");
    }
}

文件上传控制器 FileUploadController.java

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
@RestController
@RequestMapping("/api/files")
public class FileUploadController {
    // @Value 注解可以从 application.properties 中读取配置
    // 如果没有配置,则使用默认值
    @Value("${file.upload-dir:uploads}")
    private String uploadDir;
    @PostMapping("/upload")
    public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
        // 检查文件是否为空
        if (file.isEmpty()) {
            return ResponseEntity.badRequest().body("请选择一个文件上传");
        }
        try {
            // 创建上传目录
            Path uploadPath = Paths.get(uploadDir);
            if (!Files.exists(uploadPath)) {
                Files.createDirectories(uploadPath);
            }
            // 生成唯一文件名
            String originalFilename = file.getOriginalFilename();
            String uniqueFilename = UUID.randomUUID() + "_" + originalFilename;
            Path destination = uploadPath.resolve(uniqueFilename);
            // 保存文件
            Files.copy(file.getInputStream(), destination);
            // 构建文件的访问URL
            String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
                    .path("/api/files/download/")
                    .path(uniqueFilename)
                    .toUriString();
            return ResponseEntity.ok("文件上传成功!\n" +
                    "原始文件名: " + originalFilename + "\n" +
                    "存储路径: " + destination + "\n" +
                    "访问URL: " + fileDownloadUri);
        } catch (IOException e) {
            e.printStackTrace();
            return ResponseEntity.internalServerError().body("文件上传失败: " + e.getMessage());
        }
    }
    // 提供一个下载接口
    @GetMapping("/download/{filename:.+}")
    public ResponseEntity<byte[]> downloadFile(@PathVariable String filename) {
        try {
            Path filePath = Paths.get(uploadDir).resolve(filename).normalize();
            if (!Files.exists(filePath)) {
                return ResponseEntity.notFound().build();
            }
            byte[] fileContent = Files.readAllBytes(filePath);
            return ResponseEntity.ok()
                    .header("Content-Disposition", "attachment; filename=\"" + filename + "\"")
                    .body(fileContent);
        } catch (IOException e) {
            e.printStackTrace();
            return ResponseEntity.internalServerError().build();
        }
    }
}

主应用类 FileUploadApplication.java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class FileUploadApplication {
    public static void main(String[] args) {
        SpringApplication.run(FileUploadApplication.class, args);
    }
}

启动应用,测试

  • 启动 FileUploadApplication
  • 使用 Postman 或 curl 发送 POST 请求:
    curl -X POST -F "file=@/path/to/your/localfile.txt" http://localhost:8080/api/files/upload
  • 你会收到一个包含文件信息的 JSON 响应,其中包括一个下载链接,点击该链接可以下载文件。
分享:
扫描分享到社交APP
上一篇
下一篇