摘要: 在前面的章节中,我们已经构建了符合RESTful规范、具备标准输入输出的API,但它们处理的都是结构化的JSON数据。然而,现代Web应用远不止于此,我们经常需要处理各种非结构化数据,例如用户上传的头像、文章中附加的PDF文档,或是系统导出的Excel报表。文件处理是Web开发中一个极其常见且重要的场景。本章,我们将专门攻克这一主题,学习如何在Spring Boot中轻松实现文件的上传和下载功能。我们将深入了解
MultipartFile
接口的核心用法,并学会如何通过ResponseEntity<Resource>
构建安全、高效的文件下载服务。
引言:超越JSON的界限
到目前为止,我们的API一直在JSON的世界里遨游。但如果一个用户想要更新他的个人资料,上传一张新的头像图片,我们的API该如何接收呢?如果系统需要提供一个功能,让用户可以下载一份PDF格式的月度报告,API又该如何响应呢?
这些场景都涉及到了文件的传输。文件上传和下载在技术实现上与处理JSON有所不同,它需要我们理解HTTP协议中的multipart/form-data
编码以及如何通过设置特定的响应头来控制浏览器的行为。幸运的是,Spring Boot对这一切都提供了强大的支持,使得文件处理变得异常简单。
第一部分:文件上传
1. 文件上传的背后:multipart/form-data
当一个HTML表单包含<input type="file">
时,为了能将文件数据和其它表单数据一起发送到服务端,浏览器会使用multipart/form-data
格式来编码请求体。它会将请求体分割成多个部分(part),每个部分都有自己的Content-Disposition
头,用于描述该部分的信息(如字段名、原始文件名等)。
Spring Boot的自动配置会启用对这种编码格式的解析,我们无需手动处理这些复杂的底层细节。
2. 核心组件:MultipartFile
接口
在Spring Boot中,上传的文件被封装成一个org.springframework.web.multipart.MultipartFile
对象。这是一个非常有用的接口,它提供了操作上传文件所需的一切方法:
String getOriginalFilename()
: 获取文件在客户端的原始名称。String getContentType()
: 获取文件的MIME类型,如image/jpeg
。long getSize()
: 获取文件的大小(以字节为单位)。byte[] getBytes()
: 将文件内容读取为字节数组(小心内存溢出)。InputStream getInputStream()
: 获取文件的输入流,用于读取内容。void transferTo(File dest)
: 最常用的方法,将上传的文件快速保存到目标文件系统。
3. 配置上传限制
在application.properties
或application.yml
中,我们可以轻松配置与文件上传相关的限制,以防止恶意的大文件攻击。
application.yml示例:
spring:
servlet:
multipart:
enabled: true
max-file-size: 2MB # 单个文件的最大大小
max-request-size: 10MB # 整个请求的最大大小(可包含多个文件)
4. 实战:编写文件上传接口
让我们创建一个Controller来处理文件上传。我们将把上传的文件保存在项目根目录下的uploads
文件夹中。
package com.example.myfirstapp.controller;
import com.example.myfirstapp.model.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
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.util.UUID;
@RestController
@RequestMapping("/api/v1/files")
public class FileController {
private static final Logger logger = LoggerFactory.getLogger(FileController.class);
// 通过@Value注解从配置文件读取上传目录
@Value("${file.upload-dir}")
private String uploadDir;
@PostMapping("/upload")
public Result<String> uploadFile(@RequestParam("file") MultipartFile file) {
// 1. 检查文件是否为空
if (file.isEmpty()) {
return Result.error("上传失败,请选择文件");
}
// 2. 获取原始文件名并进行安全处理
String originalFilename = file.getOriginalFilename();
String fileExtension = "";
if (originalFilename != null) {
fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
}
// 3. 生成唯一文件名,防止覆盖和安全问题
String newFilename = UUID.randomUUID().toString() + fileExtension;
// 4. 创建目标文件
File dest = new File(uploadDir + File.separator + newFilename);
// 5. 确保目录存在
if (!dest.getParentFile().exists()) {
dest.getParentFile().mkdirs();
}
try {
// 6. 保存文件
file.transferTo(dest);
logger.info("文件上传成功,保存路径: {}", dest.getAbsolutePath());
// 7. 返回文件的可访问路径或标识
return Result.success("/api/v1/files/download/" + newFilename);
} catch (IOException e) {
logger.error("文件上传失败", e);
return Result.error("文件上传失败: " + e.getMessage());
}
}
}
代码解读与安全提示:
@RequestParam("file")
: 将请求中名为file
的部分绑定到MultipartFile
参数。- 安全警告: 绝对不要直接使用
originalFilename
作为服务器上的文件名。这会带来严重的安全风险,例如路径遍历攻击(文件名如../../evil.sh
)。我们使用UUID
生成一个随机且唯一的文件名,这是一种非常好的实践。 - 配置
uploadDir
: 我们在application.yml
中添加file.upload-dir: uploads
,然后通过@Value
注入,使上传路径可配置。
第二部分:文件下载
1. 文件下载的原理
文件下载本质上是一个GET
请求。服务器需要做两件事:
- 将文件的二进制内容写入HTTP响应体。
- 设置正确的HTTP响应头,告诉浏览器这是一个需要下载的文件,而不是直接在页面上显示。
关键的HTTP响应头是:
Content-Type
: 文件的MIME类型,如application/pdf
。Content-Disposition
:attachment; filename="report.pdf"
。attachment
指示浏览器附件形式(即下载),filename
指定默认保存的文件名。
2. 核心组件:Resource
与ResponseEntity
Spring框架提供了一个org.springframework.core.io.Resource
接口,用于统一访问各种来源的资源(文件系统、Classpath、URL等)。ResponseEntity
则可以让我们精细地控制HTTP响应,包括状态码、头部和响应体。
3. 实战:编写文件下载接口
我们继续在FileController
中添加下载功能。
// 在FileController中添加以下方法
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.net.MalformedURLException;
import java.nio.file.Path;
import java.nio.file.Paths;
// ...
@GetMapping("/download/{filename:.+}")
public ResponseEntity<Resource> downloadFile(@PathVariable String filename) {
try {
// 1. 构建文件路径
Path filePath = Paths.get(uploadDir).resolve(filename).normalize();
Resource resource = new UrlResource(filePath.toUri());
// 2. 检查文件是否存在且可读
if (!resource.exists() || !resource.isReadable()) {
// 可以返回一个自定义的404响应
return ResponseEntity.notFound().build();
}
// 3. 设置响应头
String contentType = "application/octet-stream"; // 默认MIME类型
// 尝试自动确定文件类型
try {
contentType = Files.probeContentType(filePath);
} catch (IOException ex) {
logger.warn("无法确定文件类型: {}", filename);
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
} catch (MalformedURLException e) {
logger.error("文件路径格式错误", e);
return ResponseEntity.badRequest().build();
}
}
代码解读:
{filename:.+}
:@PathVariable
中的正则表达式:+.
至关重要。它能确保Spring MVC不会因为文件名中包含.
而截断它。UrlResource
: 我们使用它从文件系统的路径加载资源。ResponseEntity.ok()
: 这是构建一个HTTP 200 OK响应的起点。.contentType()
: 设置Content-Type
头。.header(HttpHeaders.CONTENT_DISPOSITION, ...)
: 设置Content-Disposition
头,触发浏览器下载。.body(resource)
: 将Resource
对象作为响应体。Spring Boot会负责高效地将文件流写入响应。
总结
文件处理是Web开发不可或缺的一环。通过本章的学习,我们掌握了在Spring Boot中实现文件上传和下载的完整流程和最佳实践。
- 文件上传: 核心是使用
MultipartFile
接收文件,并通过transferTo
方法保存。关键是要生成唯一的文件名以保证安全。 - 文件下载: 核心是使用
ResponseEntity<Resource>
来构建响应。关键是设置正确的Content-Type
和Content-Disposition
响应头。
现在,我们的API服务能力又上了一个新台阶,不再局限于纯文本的JSON数据。
预告: 我们已经构建了各种功能的Controller。但在很多场景下,我们需要在请求到达Controller之前或之后执行一些通用逻辑,比如记录每个请求的耗时、对所有请求进行身份验证等。直接在每个Controller方法里写这些代码显然是不可取的。如何优雅地实现这些横切关注点呢?下一章,我们将学习第一个Web组件:掌握Web组件(一):使用过滤器(Filter)实现请求预处理。