SpringBoot实现文件上传下载的完整指南
作者:爱码少年 00fly.online
在现代Web应用中,文件上传下载是几乎每个系统都需要的基础功能,无论是用户头像上传、文档管理、还是大数据文件处理,文件操作都扮演着重要角色,本文将全面讲解Spring Boot中文件上传下载的实现方式,需要的朋友可以参考下
1. 引言
在现代Web应用中,文件上传下载是几乎每个系统都需要的基础功能。无论是用户头像上传、文档管理、还是大数据文件处理,文件操作都扮演着重要角色。Spring Boot作为Java领域最流行的微服务框架,提供了强大而灵活的文件处理能力。
本文将全面讲解Spring Boot中文件上传下载的实现方式,涵盖从基础的单文件上传到高级的分片上传、断点续传等场景,并提供完整的代码示例和最佳实践建议。
2. 环境准备
2.1 项目创建
使用Spring Initializr创建项目,选择以下依赖:
- Spring Web
- Spring Boot DevTools
- Lombok(可选,简化代码)
2.2 配置文件
在application.properties中添加文件上传配置:
# 单个文件最大大小 spring.servlet.multipart.max-file-size=10MB # 单次请求最大大小 spring.servlet.multipart.max-request-size=100MB # 文件存储路径 file.upload-dir=./uploads # 启用文件上传 spring.servlet.multipart.enabled=true
3. 基础文件上传
3.1 单文件上传实现
@RestController
@RequestMapping("/api/files")
public class FileUploadController {
@Value("${file.upload-dir}")
private String uploadDir;
@PostMapping("/upload")
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
try {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("请选择要上传的文件");
}
// 生成唯一文件名
String fileName = UUID.randomUUID().toString() +
"_" + file.getOriginalFilename();
// 创建存储目录
Path uploadPath = Paths.get(uploadDir);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
// 保存文件
Path filePath = uploadPath.resolve(fileName);
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
return ResponseEntity.ok("文件上传成功: " + fileName);
} catch (IOException e) {
return ResponseEntity.status(500).body("文件上传失败: " + e.getMessage());
}
}
}
3.2 多文件上传
@PostMapping("/upload-multiple")
public ResponseEntity<List<String>> uploadMultipleFiles(
@RequestParam("files") MultipartFile[] files) {
List<String> fileNames = new ArrayList<>();
for (MultipartFile file : files) {
if (!file.isEmpty()) {
try {
String fileName = saveFile(file);
fileNames.add(fileName);
} catch (IOException e) {
return ResponseEntity.status(500)
.body(Collections.singletonList("部分文件上传失败"));
}
}
}
return ResponseEntity.ok(fileNames);
}
private String saveFile(MultipartFile file) throws IOException {
String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
Path filePath = Paths.get(uploadDir).resolve(fileName);
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
return fileName;
}
4. 文件下载实现
4.1 基础文件下载
@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() && resource.isReadable()) {
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + resource.getFilename() + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
} else {
return ResponseEntity.notFound().build();
}
} catch (MalformedURLException e) {
return ResponseEntity.badRequest().build();
}
}
4.2 带进度显示的文件下载
@GetMapping("/download-with-progress/{fileName}")
public StreamingResponseBody downloadWithProgress(
@PathVariable String fileName,
HttpServletResponse response) {
Path filePath = Paths.get(uploadDir).resolve(fileName);
return outputStream -> {
try (InputStream inputStream = Files.newInputStream(filePath)) {
byte[] buffer = new byte[1024];
int bytesRead;
long totalBytes = Files.size(filePath);
long bytesCopied = 0;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
bytesCopied += bytesRead;
// 计算并记录进度(实际项目中可推送到前端)
int progress = (int) ((bytesCopied * 100) / totalBytes);
System.out.println("下载进度: " + progress + "%");
}
}
};
}
5. 高级功能实现
5.1 大文件分片上传
@PostMapping("/chunk-upload")
public ResponseEntity<Map<String, Object>> chunkUpload(
@RequestParam("file") MultipartFile file,
@RequestParam("chunkNumber") int chunkNumber,
@RequestParam("totalChunks") int totalChunks,
@RequestParam("identifier") String identifier) {
try {
// 创建临时目录存储分片
Path tempDir = Paths.get(uploadDir, "temp", identifier);
if (!Files.exists(tempDir)) {
Files.createDirectories(tempDir);
}
// 保存分片文件
Path chunkPath = tempDir.resolve(chunkNumber + ".part");
Files.copy(file.getInputStream(), chunkPath, StandardCopyOption.REPLACE_EXISTING);
Map<String, Object> response = new HashMap<>();
response.put("chunkNumber", chunkNumber);
response.put("totalChunks", totalChunks);
// 检查是否所有分片都已上传
if (chunkNumber == totalChunks) {
response.put("mergeRequired", true);
}
return ResponseEntity.ok(response);
} catch (IOException e) {
return ResponseEntity.status(500).body(
Collections.singletonMap("error", "分片上传失败"));
}
}
5.2 文件合并逻辑
private void mergeChunks(String identifier, String originalFileName) throws IOException {
Path tempDir = Paths.get(uploadDir, "temp", identifier);
Path outputFile = Paths.get(uploadDir).resolve(originalFileName);
try (OutputStream outputStream = Files.newOutputStream(outputFile,
StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
// 按顺序合并所有分片
Files.list(tempDir)
.sorted((p1, p2) -> {
int n1 = Integer.parseInt(p1.getFileName().toString().replace(".part", ""));
int n2 = Integer.parseInt(p2.getFileName().toString().replace(".part", ""));
return Integer.compare(n1, n2);
})
.forEach(chunkPath -> {
try {
Files.copy(chunkPath, outputStream);
} catch (IOException e) {
throw new RuntimeException("合并分片失败", e);
}
});
// 清理临时文件
Files.walk(tempDir)
.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
}
}
6. 安全性考虑
6.1 文件类型验证
private boolean isValidFileType(MultipartFile file) {
String fileName = file.getOriginalFilename();
if (fileName == null) return false;
String extension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
Set<String> allowedExtensions = Set.of("jpg", "jpeg", "png", "pdf", "doc", "docx");
return allowedExtensions.contains(extension);
}
private boolean isValidContentType(MultipartFile file) {
String contentType = file.getContentType();
return contentType != null &&
(contentType.startsWith("image/") ||
contentType.equals("application/pdf") ||
contentType.equals("application/msword"));
}
6.2 文件大小限制与病毒扫描
@Component
public class FileSecurityService {
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
public void validateFile(MultipartFile file) {
// 大小检查
if (file.getSize() > MAX_FILE_SIZE) {
throw new FileSizeExceededException("文件大小超过限制");
}
// 文件名安全检查
String fileName = file.getOriginalFilename();
if (fileName != null && fileName.contains("..")) {
throw new SecurityException("文件名包含非法字符");
}
// 实际项目中可集成病毒扫描服务
// scanForViruses(file);
}
}
7. 前端集成示例
7.1 HTML 表单
<!-- 基础文件上传表单 -->
<form id="uploadForm" enctype="multipart/form-data">
<input type="file" name="file" id="fileInput" multiple>
<button type="submit">上传文件</button>
</form>
<!-- 进度显示 -->
<div id="progressContainer" style="display: none;">
<progress id="uploadProgress" value="0" max="100"></progress>
<span id="progressText">0%</span>
</div>7.2 JavaScript 上传逻辑
// 使用 Fetch API 上传文件
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/files/upload', {
method: 'POST',
body: formData
});
if (response.ok) {
const result = await response.text();
console.log('上传成功:', result);
return result;
} else {
throw new Error('上传失败');
}
} catch (error) {
console.error('上传错误:', error);
throw error;
}
}
// 带进度显示的上传
function uploadWithProgress(file) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
updateProgress(percentComplete);
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
resolve(xhr.responseText);
} else {
reject(new Error('上传失败'));
}
});
xhr.addEventListener('error', () => reject(new Error('网络错误')));
const formData = new FormData();
formData.append('file', file);
xhr.open('POST', '/api/files/upload');
xhr.send(formData);
});
}
8. 最佳实践与性能优化
8.1 存储策略选择
- 本地存储:适合小型应用,部署简单
- 对象存储(OSS):推荐生产环境使用(如阿里云OSS、AWS S3)
- 分布式文件系统:适合大规模文件存储
8.2 性能优化建议
- 启用GZIP压缩:减少传输数据量
- 使用CDN加速:静态文件通过CDN分发
- 实现断点续传:大文件上传更可靠
- 异步处理:耗时操作放入消息队列
- 缓存策略:频繁访问的文件添加缓存
8.3 监控与日志
@Slf4j
@RestControllerAdvice
public class FileUploadExceptionHandler {
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<String> handleSizeExceeded(MaxUploadSizeExceededException e) {
log.warn("文件大小超过限制", e);
return ResponseEntity.status(413).body("文件大小超过限制");
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGeneralException(Exception e) {
log.error("文件上传处理异常", e);
return ResponseEntity.status(500).body("服务器内部错误");
}
}
9. 常见问题与解决方案
Q1: 文件上传大小限制如何调整?
A: 在application.properties中调整以下配置:
spring.servlet.multipart.max-file-size=50MB spring.servlet.multipart.max-request-size=200MB
Q2: 如何防止文件名冲突?
A: 使用UUID或时间戳重命名文件:
String newFileName = UUID.randomUUID() +
"_" + System.currentTimeMillis() +
getFileExtension(originalFileName);
Q3: 上传文件后如何提供访问链接?
A: 根据存储方式生成访问URL:
// 本地存储 String fileUrl = "/api/files/download/" + fileName; // 对象存储 String fileUrl = "https://bucket.region.aliyuncs.com/" + fileName;
Q4: 如何实现图片缩略图?
A: 使用Thumbnailator等库处理:
Thumbnails.of(originalFile)
.size(200, 200)
.outputFormat("jpg")
.toFile(thumbnailFile);
10. 总结
本文详细介绍了Spring Boot中文件上传下载的完整实现方案,从基础的单文件操作到高级的分片上传、安全性考虑和性能优化。关键要点包括:
- 基础实现:掌握
MultipartFile的基本用法 - 高级功能:实现大文件分片上传和断点续传
- 安全性:严格验证文件类型和内容
- 性能优化:合理配置和存储策略选择
- 错误处理:完善的异常处理和用户反馈
在实际项目中,建议根据具体需求选择合适的存储方案,并充分考虑安全性和性能因素。随着业务发展,可以考虑迁移到专业的对象存储服务,以获得更好的可扩展性和可靠性。
以上就是SpringBoot实现文件上传下载的完整指南的详细内容,更多关于SpringBoot文件上传下载的资料请关注脚本之家其它相关文章!
