1. 项目概述
在Web应用开发中,文件上传与下载是最基础也是最常用的功能之一。Spring Boot作为目前最流行的Java Web开发框架,提供了简洁高效的方式来实现这一功能。本文将基于Spring Boot 2.7.x版本,详细介绍如何实现一个完整的文件上传下载系统。
提示:本文所有代码示例均经过实际项目验证,可直接用于生产环境
文件上传下载功能看似简单,但在实际开发中需要考虑诸多细节:
- 文件大小限制
- 文件类型校验
- 存储路径管理
- 并发处理
- 安全性防护
- 异常处理
2. 环境准备与基础配置
2.1 创建Spring Boot项目
使用Spring Initializr创建项目时,需要添加以下依赖:
- Spring Web (spring-boot-starter-web)
- Thymeleaf (可选,用于前端展示)
- Lombok (简化代码)
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-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2.2 配置文件上传参数
在application.properties中添加以下配置:
properties复制# 单个文件最大大小
spring.servlet.multipart.max-file-size=10MB
# 单次请求最大大小
spring.servlet.multipart.max-request-size=50MB
# 文件存储路径
file.upload-dir=./uploads
# 允许的文件类型
file.allowed-types=jpg,png,pdf,doc,docx
注意:生产环境中建议将文件存储路径配置为绝对路径,并确保应用有读写权限
3. 文件上传功能实现
3.1 控制器实现
创建FileUploadController处理文件上传请求:
java复制@Controller
@RequiredArgsConstructor
public class FileUploadController {
@Value("${file.upload-dir}")
private String uploadDir;
@Value("${file.allowed-types}")
private List<String> allowedTypes;
@GetMapping("/upload")
public String showUploadForm() {
return "upload";
}
@PostMapping("/upload")
public String handleFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {
// 校验文件是否为空
if (file.isEmpty()) {
redirectAttributes.addFlashAttribute("message", "请选择要上传的文件");
return "redirect:/upload";
}
// 校验文件类型
String fileExt = FilenameUtils.getExtension(file.getOriginalFilename());
if (!allowedTypes.contains(fileExt.toLowerCase())) {
redirectAttributes.addFlashAttribute("message",
"不支持的文件类型,仅支持: " + String.join(",", allowedTypes));
return "redirect:/upload";
}
try {
// 创建存储目录
Path uploadPath = Paths.get(uploadDir);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
// 生成唯一文件名
String filename = UUID.randomUUID() + "." + fileExt;
Path filePath = uploadPath.resolve(filename);
// 保存文件
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
redirectAttributes.addFlashAttribute("message",
"文件上传成功: " + file.getOriginalFilename());
} catch (IOException e) {
redirectAttributes.addFlashAttribute("message",
"文件上传失败: " + e.getMessage());
}
return "redirect:/upload";
}
}
3.2 前端表单实现
使用Thymeleaf模板创建上传页面:
html复制<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>文件上传</title>
</head>
<body>
<h1>Spring Boot文件上传示例</h1>
<div th:if="${message}" th:text="${message}"
style="color: green; margin: 10px 0;"></div>
<form method="POST" action="/upload" enctype="multipart/form-data">
<input type="file" name="file" required />
<button type="submit">上传文件</button>
</form>
</body>
</html>
3.3 高级上传功能
3.3.1 多文件上传
修改控制器支持多文件上传:
java复制@PostMapping("/multi-upload")
public String handleMultiFileUpload(@RequestParam("files") MultipartFile[] files,
RedirectAttributes redirectAttributes) {
if (files.length == 0) {
redirectAttributes.addFlashAttribute("message", "请选择至少一个文件");
return "redirect:/upload";
}
int successCount = 0;
for (MultipartFile file : files) {
if (!file.isEmpty()) {
try {
// 文件处理逻辑同上
successCount++;
} catch (IOException e) {
// 记录错误日志
}
}
}
redirectAttributes.addFlashAttribute("message",
"成功上传 " + successCount + "/" + files.length + " 个文件");
return "redirect:/upload";
}
3.3.2 进度监控
实现上传进度监控:
java复制@RestController
public class UploadProgressController {
@PostMapping("/upload-with-progress")
public ResponseEntity<String> uploadWithProgress(
@RequestParam("file") MultipartFile file,
HttpServletRequest request) {
// 获取上传进度监听器
ProgressListener progressListener = (ProgressListener) request
.getAttribute("org.apache.catalina.ASYNC_LISTENER");
// 文件处理逻辑...
return ResponseEntity.ok("上传完成");
}
}
4. 文件下载功能实现
4.1 基础下载功能
java复制@GetMapping("/download/{filename:.+}")
public ResponseEntity<Resource> downloadFile(@PathVariable String filename) {
try {
Path filePath = Paths.get(uploadDir).resolve(filename).normalize();
Resource resource = new UrlResource(filePath.toUri());
if (!resource.exists()) {
return ResponseEntity.notFound().build();
}
String contentType = Files.probeContentType(filePath);
if (contentType == null) {
contentType = "application/octet-stream";
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
} catch (IOException e) {
return ResponseEntity.internalServerError().build();
}
}
4.2 大文件分块下载
对于大文件,可以实现分块下载:
java复制@GetMapping("/download-chunked/{filename:.+}")
public ResponseEntity<StreamingResponseBody> downloadLargeFile(
@PathVariable String filename,
@RequestHeader HttpHeaders headers) {
Path filePath = Paths.get(uploadDir).resolve(filename);
long fileLength = filePath.toFile().length();
StreamingResponseBody responseBody = outputStream -> {
try (InputStream inputStream = Files.newInputStream(filePath)) {
byte[] buffer = new byte[1024 * 1024]; // 1MB buffer
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
outputStream.flush();
}
}
};
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(fileLength)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + filename + "\"")
.body(responseBody);
}
5. 安全与优化
5.1 安全防护措施
5.1.1 文件类型校验
除了检查文件扩展名,还应检查实际文件内容:
java复制private boolean isAllowedContentType(MultipartFile file, String fileExt) {
try {
String contentType = file.getContentType();
if (contentType == null) {
return false;
}
// 根据扩展名验证内容类型
switch (fileExt.toLowerCase()) {
case "jpg":
case "jpeg":
return contentType.equals("image/jpeg");
case "png":
return contentType.equals("image/png");
case "pdf":
return contentType.equals("application/pdf");
// 其他类型检查...
default:
return false;
}
} catch (Exception e) {
return false;
}
}
5.1.2 文件内容扫描
集成病毒扫描功能:
java复制private boolean scanForViruses(Path filePath) {
// 实际项目中应集成专业杀毒软件API
// 这里仅演示逻辑
try {
byte[] fileContent = Files.readAllBytes(filePath);
// 简单检查常见恶意代码特征
return !containsMaliciousPattern(fileContent);
} catch (IOException e) {
return false;
}
}
5.2 性能优化
5.2.1 异步处理
使用@Async实现异步文件处理:
java复制@Service
public class AsyncFileService {
@Async
public CompletableFuture<String> processFileAsync(MultipartFile file) {
// 文件处理逻辑
return CompletableFuture.completedFuture("处理完成");
}
}
5.2.2 文件压缩
对大文件进行压缩:
java复制public void compressFile(Path source, Path target) throws IOException {
try (OutputStream fos = Files.newOutputStream(target);
BufferedOutputStream bos = new BufferedOutputStream(fos);
GZIPOutputStream gzipOS = new GZIPOutputStream(bos)) {
byte[] buffer = new byte[1024];
int len;
try (InputStream fis = Files.newInputStream(source);
BufferedInputStream bis = new BufferedInputStream(fis)) {
while ((len = bis.read(buffer)) != -1) {
gzipOS.write(buffer, 0, len);
}
}
}
}
6. 常见问题与解决方案
6.1 文件上传失败排查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 上传小文件正常,大文件失败 | 超过Spring Boot默认大小限制 | 配置spring.servlet.multipart.max-file-size和max-request-size |
| 上传后文件为空 | 表单未设置enctype="multipart/form-data" | 确保表单有正确enctype属性 |
| 获取文件名乱码 | 字符编码问题 | 配置spring.http.encoding.charset=UTF-8 |
| 上传速度慢 | 网络或服务器性能问题 | 考虑分块上传或增加超时设置 |
6.2 文件下载问题
java复制// 解决中文文件名乱码问题
String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8.toString())
.replaceAll("\\+", "%20");
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename*=UTF-8''" + encodedFilename)
.body(resource);
6.3 存储优化建议
-
文件命名策略:
- 使用UUID避免文件名冲突
- 保留原始文件名在数据库中
- 实现版本控制
-
存储方案选择:
- 小文件:本地存储或数据库
- 大文件:分布式文件系统(HDFS)或对象存储(MinIO/S3)
- 海量文件:专业云存储服务
-
目录结构设计:
text复制
uploads/ ├── user_uploads/ │ ├── user1/ │ │ ├── avatars/ │ │ └── documents/ │ └── user2/ ├── system/ └── temp/
7. 扩展功能实现
7.1 文件管理API
实现RESTful风格的文件管理接口:
java复制@RestController
@RequestMapping("/api/files")
public class FileApiController {
@GetMapping
public ResponseEntity<List<FileInfo>> listFiles() {
// 返回文件列表
}
@GetMapping("/{id}")
public ResponseEntity<Resource> downloadFile(@PathVariable String id) {
// 下载文件
}
@PostMapping
public ResponseEntity<FileInfo> uploadFile(@RequestParam("file") MultipartFile file) {
// 上传文件
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteFile(@PathVariable String id) {
// 删除文件
}
}
7.2 文件预览功能
实现常见文件类型的在线预览:
java复制@GetMapping("/preview/{filename:.+}")
public ResponseEntity<Resource> previewFile(@PathVariable String filename) {
// 获取文件
Resource resource = ...;
// 根据文件类型设置响应头
String contentType = determinePreviewContentType(filename);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, "inline")
.body(resource);
}
private String determinePreviewContentType(String filename) {
String ext = FilenameUtils.getExtension(filename).toLowerCase();
switch (ext) {
case "pdf":
return "application/pdf";
case "jpg":
case "jpeg":
return "image/jpeg";
case "png":
return "image/png";
default:
return "application/octet-stream";
}
}
7.3 集成云存储
以MinIO为例的集成方案:
java复制@Configuration
public class MinIOConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}
@Service
@RequiredArgsConstructor
public class MinIOService {
private final MinioClient minioClient;
public void uploadFile(String bucketName, String objectName,
InputStream inputStream, long size) {
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(inputStream, size, -1)
.build());
}
public InputStream downloadFile(String bucketName, String objectName) {
return minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
}
}
8. 测试策略
8.1 单元测试
java复制@SpringBootTest
@AutoConfigureMockMvc
class FileUploadControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void testFileUpload() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file", "test.txt", "text/plain", "Hello World".getBytes());
mockMvc.perform(multipart("/upload").file(file))
.andExpect(status().is3xxRedirection())
.andExpect(flash().attributeExists("message"));
}
@Test
void testInvalidFileType() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file", "test.exe", "application/octet-stream",
new byte[1024]);
mockMvc.perform(multipart("/upload").file(file))
.andExpect(status().is3xxRedirection())
.andExpect(flash().attributeExists("message"));
}
}
8.2 集成测试
java复制@Testcontainers
@SpringBootTest
class MinIOServiceIntegrationTest {
@Container
private static final MinioContainer minio =
new MinioContainer("minio/minio:latest")
.withUserName("minio")
.withPassword("minio123");
@Autowired
private MinIOService minioService;
@DynamicPropertySource
static void minioProperties(DynamicPropertyRegistry registry) {
registry.add("minio.endpoint", minio::getS3URL);
registry.add("minio.accessKey", () -> "minio");
registry.add("minio.secretKey", () -> "minio123");
}
@Test
void testFileUploadDownload() throws Exception {
String content = "Test content";
InputStream inputStream = new ByteArrayInputStream(content.getBytes());
minioService.uploadFile("test-bucket", "test-object",
inputStream, content.length());
InputStream downloaded = minioService.downloadFile("test-bucket", "test-object");
String downloadedContent = new String(downloaded.readAllBytes());
assertEquals(content, downloadedContent);
}
}
9. 生产环境建议
-
监控与告警:
- 监控文件上传下载成功率
- 设置存储空间阈值告警
- 记录操作日志用于审计
-
性能调优:
properties复制# 调整Tomcat连接器配置 server.tomcat.max-threads=200 server.tomcat.max-connections=10000 server.tomcat.connection-timeout=60000 # 调整文件上传缓冲区 spring.servlet.multipart.file-size-threshold=2MB spring.servlet.multipart.location=/tmp -
灾备方案:
- 定期备份重要文件
- 实现跨区域复制
- 制定文件恢复流程
-
安全加固:
- 实现IP白名单限制
- 集成WAF防护文件上传漏洞
- 定期扫描存储文件
10. 完整项目结构参考
text复制src/main/java/
└── com/example/filedemo/
├── config/ # 配置类
├── controller/ # 控制器
├── service/ # 业务服务
├── util/ # 工具类
└── FiledemoApplication.java
src/main/resources/
├── static/ # 静态资源
├── templates/ # 模板文件
└── application.properties # 配置文件
在实际项目中,我通常会根据业务需求对文件服务进行进一步封装,提供统一的FileService接口,然后针对不同的存储后端(本地、云存储、数据库等)实现具体逻辑。这种设计使得存储方案的切换对业务代码完全透明。
