目录
- 核心原理简介
- 第一步:项目配置
- 依赖引入
- Spring Boot 配置
- 第二步:前端实现
- HTML 表单
- 前端框架(如 Axios)
- 第三步:后端控制器
- 接收单个文件
- 接收多个文件
- 接收附带其他数据的文件
- 第四步:文件存储
- 存储到本地服务器
- 存储到云存储(如 Amazon S3, 阿里云 OSS) - 强烈推荐
- 第五步:高级主题与最佳实践
- 文件大小限制
- 文件类型/内容校验
- 安全性考虑
- 异步上传
- 完整示例代码
核心原理简介
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.properties 或 application.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
第二步:前端实现
前端表单必须满足两个条件:
method="post"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 为例。
步骤:
-
添加依赖:
<dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>s3</artifactId> <version>2.20.0</version> <!-- 使用最新版本 --> </dependency> -
配置 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
-
创建 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(); } } -
修改控制器以使用 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 "只允许上传图片文件!";
}
// ... 安全的上传逻辑 ...
}
安全性考虑
- 文件名: 永远不要直接使用用户提供的文件名,它可能包含路径(
../../../etc/passwd)或恶意字符,使用UUID或其他安全方式生成新文件名。 - : 上传的文件可能是可执行脚本(.jsp, .php, .py),如果用户能访问这些文件的URL,可能会导致服务器被攻击。不要将上传的文件直接放在Web服务器的根目录下,最好存放在一个无法通过Web直接访问的目录(如
WEB-INF/uploads),或者使用云存储。 - 病毒扫描: 对于用户上传的任何文件,都应该进行病毒扫描。
异步上传
对于大文件上传,同步处理会导致请求长时间占用线程,影响服务器性能,可以使用 Spring 的异步支持。
-
启用异步:
@SpringBootApplication @EnableAsync // 启用异步方法 public class YourApplication { public static void main(String[] args) { SpringApplication.run(YourApplication.class, args); } } -
创建异步任务服务:
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()); } } -
修改控制器:
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 响应,其中包括一个下载链接,点击该链接可以下载文件。
