一、核心概念与原理
Spring Boot通过Spring MVC的MultipartFile接口处理文件上传,该接口封装了上传文件的所有信息,包括文件内容、文件名、MIME类型等。文件上传的本质是将HTTP请求中的multipart/form-data格式数据解析为文件流并保存到指定位置,而文件下载则是将文件流写入HTTP响应体并设置相应的响应头信息。
二、环境配置
1. 项目依赖配置
在pom.xml中添加web依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
2. 文件上传配置
在application.properties中配置上传参数:
# 单个文件最大大小
spring.servlet.multipart.max-file-size=10MB
# 单次请求最大大小
spring.servlet.multipart.max-request-size=50MB
# 启用文件上传
spring.servlet.multipart.enabled=true
# 字符编码
spring.servlet.encoding.charset=UTF-8
spring.servlet.encoding.force=true
或者在application.yml中配置:
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 50MB
enabled: true
encoding:
charset: UTF-8
force: true
注意事项:
- max-file-size必须小于等于max-request-size
- 生产环境建议配合前端校验和分片上传
- 如需GB级上传,建议使用OSS对象存储服务
三、文件上传实现
1. 单文件上传
@RestController
@RequestMapping("/file")
public class FileUploadController {
private static final String UPLOAD_DIR = "uploads/";
@PostMapping("/upload")
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
// 校验文件是否为空
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("请选择要上传的文件");
}
try {
// 获取原始文件名
String originalFilename = file.getOriginalFilename();
// 生成唯一文件名防止覆盖
String fileName = UUID.randomUUID().toString() + "_" + originalFilename;
// 创建上传目录
File uploadDir = new File(UPLOAD_DIR);
if (!uploadDir.exists()) {
uploadDir.mkdirs();
}
// 保存文件
File dest = new File(uploadDir.getAbsolutePath() + File.separator + fileName);
file.transferTo(dest);
return ResponseEntity.ok("文件上传成功: " + fileName);
} catch (IOException e) {
return ResponseEntity.internalServerError().body("文件上传失败: " + e.getMessage());
}
}
}
2. 多文件上传
@PostMapping("/multi-upload")
public ResponseEntity<List<String>> uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) {
List<String> results = new ArrayList<>();
for (MultipartFile file : files) {
if (file.isEmpty()) {
results.add("文件为空: " + file.getOriginalFilename());
continue;
}
try {
String fileName = UUID.randomUUID().toString() + "_" + file.getOriginalFilename();
File dest = new File(UPLOAD_DIR + fileName);
file.transferTo(dest);
results.add("上传成功: " + fileName);
} catch (IOException e) {
results.add("上传失败: " + file.getOriginalFilename());
}
}
return ResponseEntity.ok(results);
}
3. MultipartFile核心方法
| 方法 | 说明 |
|---|---|
getOriginalFilename() | 获取原始文件名(含扩展名) |
getName() | 获取表单中的参数名 |
getContentType() | 获取文件MIME类型 |
isEmpty() | 判断文件是否为空 |
getSize() | 获取文件大小(字节数) |
getBytes() | 获取文件字节数组 |
getInputStream() | 获取文件输入流 |
transferTo(File dest) | 将文件保存到指定路径 |
四、文件下载实现
1. 使用HttpServletResponse方式
@GetMapping("/download")
public void downloadFile(@RequestParam String fileName, HttpServletResponse response) {
try {
File file = new File(UPLOAD_DIR + fileName);
// 校验文件是否存在
if (!file.exists()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.getWriter().write("文件不存在");
return;
}
// 设置响应头
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));
response.setContentLength((int) file.length());
// 写入响应流
try (InputStream inputStream = new FileInputStream(file);
OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
} catch (IOException e) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
2. 使用ResponseEntity方式(推荐)
@GetMapping("/download2")
public ResponseEntity<Resource> downloadFile2(@RequestParam String fileName) {
try {
File file = new File(UPLOAD_DIR + fileName);
if (!file.exists()) {
return ResponseEntity.notFound().build();
}
Resource resource = new FileSystemResource(file);
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);
return ResponseEntity.ok()
.headers(headers)
.contentLength(file.length())
.body(resource);
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
3. 使用StreamingResponseBody(大文件优化)
@GetMapping("/download3")
public ResponseEntity<StreamingResponseBody> downloadFile3(@RequestParam String fileName) {
File file = new File(UPLOAD_DIR + fileName);
if (!file.exists()) {
return ResponseEntity.notFound().build();
}
StreamingResponseBody responseBody = outputStream -> {
try (InputStream inputStream = new FileInputStream(file)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
}
};
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));
return ResponseEntity.ok()
.headers(headers)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(responseBody);
}
五、安全与校验
1. 文件类型校验
private static final String[] ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".pdf", ".doc", ".docx"};
private boolean isValidFileType(String fileName) {
if (fileName == null) {
return false;
}
String extension = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();
return Arrays.asList(ALLOWED_EXTENSIONS).contains(extension);
}
// 在Controller中使用
if (!isValidFileType(file.getOriginalFilename())) {
return ResponseEntity.badRequest().body("不支持的文件类型");
}
2. 文件大小校验
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
if (file.getSize() > MAX_FILE_SIZE) {
return ResponseEntity.badRequest().body("文件大小超过限制");
}
3. 文件名安全处理
// 防止路径遍历攻击
String safeFileName = new File(file.getOriginalFilename()).getName();
// 替换特殊字符
safeFileName = safeFileName.replaceAll("[^a-zA-Z0-9._-]", "_");
六、大文件优化方案
1. 分片上传
@PostMapping("/upload-chunk")
public ResponseEntity<String> uploadChunk(
@RequestParam("chunk") MultipartFile chunk,
@RequestParam("fileName") String fileName,
@RequestParam("chunkNumber") int chunkNumber,
@RequestParam("totalChunks") int totalChunks) throws IOException {
// 保存分片到临时目录
File tempFile = new File(TEMP_DIR + fileName + "_" + chunkNumber);
chunk.transferTo(tempFile);
// 如果所有分片都已上传,合并文件
if (chunkNumber == totalChunks) {
mergeFile(fileName, totalChunks);
}
return ResponseEntity.ok("分片上传成功");
}
private void mergeFile(String fileName, int totalChunks) throws IOException {
File mergedFile = new File(UPLOAD_DIR + fileName);
try (RandomAccessFile accessFile = new RandomAccessFile(mergedFile, "rw")) {
for (int i = 1; i <= totalChunks; i++) {
File chunkFile = new File(TEMP_DIR + fileName + "_" + i);
try (RandomAccessFile chunkAccessFile = new RandomAccessFile(chunkFile, "r")) {
byte[] buffer = new byte[1024];
int len;
while ((len = chunkAccessFile.read(buffer)) != -1) {
accessFile.write(buffer, 0, len);
}
}
chunkFile.delete(); // 删除临时分片
}
}
}
2. 断点续传
@PostMapping("/upload-resume")
public ResponseEntity<String> uploadResume(
@RequestParam("file") MultipartFile file,
@RequestParam("index") Integer index,
@RequestParam("md5") String md5,
HttpServletRequest request) throws Exception {
// 检查分片是否已上传
if (chunkService.isAlreadyUpload(md5, index)) {
return ResponseEntity.ok("分片已存在");
}
// 获取已上传字节数
long uploadedBytes = getUploadedBytes(md5, index);
// 续传逻辑
String path = getFilePath(md5, index);
File dest = new File(path);
long fileLength = file.getSize();
try (RandomAccessFile accessFile = new RandomAccessFile(dest, "rw")) {
accessFile.seek(uploadedBytes);
try (InputStream inputStream = file.getInputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
accessFile.write(buffer, 0, len);
uploadedBytes += len;
// 记录上传进度
chunkService.saveResume(md5, index, uploadedBytes);
}
}
}
return ResponseEntity.ok("续传成功");
}
七、异常处理
1. 全局异常处理器
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<String> handleMaxSizeException(MaxUploadSizeExceededException e) {
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
.body("文件大小超过限制");
}
@ExceptionHandler(MultipartException.class)
public ResponseEntity<String> handleMultipartException(MultipartException e) {
return ResponseEntity.badRequest()
.body("文件上传失败: " + e.getMessage());
}
@ExceptionHandler(IOException.class)
public ResponseEntity<String> handleIOException(IOException e) {
return ResponseEntity.internalServerError()
.body("文件操作失败: " + e.getMessage());
}
}
2. Tomcat配置优化
# 设置Tomcat最大吞吐量
server.tomcat.max-swallow-size=100MB
# 或设置为-1不限制
server.tomcat.max-swallow-size=-1
八、性能优化建议
- 异步处理:使用@Async注解异步处理文件上传下载任务,避免主线程阻塞
- 流式处理:使用StreamUtils.copy()替代手动字节数组操作,减少内存占用
- CDN集成:使用云存储服务(如阿里云OSS、AWS S3)存储文件,减轻服务器压力
- 分片上传:对于大文件采用分片上传和断点续传机制
- 内存优化:避免将大文件全部加载到内存中,使用流式读写
- 限流熔断:对上传下载接口进行限流,防止服务器过载
- 日志监控:详细记录文件操作的关键步骤,监控磁盘空间和I/O性能
九、前端调用示例
HTML表单
<form method="POST" action="/file/upload" enctype="multipart/form-data">
<input type="file" name="file" accept=".jpg,.jpeg,.png,.pdf">
<button type="submit">上传</button>
</form>
JavaScript调用
// 单文件上传
const formData = new FormData();
formData.append('file', fileInput.files[0]);
fetch('/file/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('上传失败:', error));
// 文件下载
function downloadFile(fileName) {
window.open(`/file/download?fileName=${encodeURIComponent(fileName)}`);
}
十、完整项目结构
src/main/java/
├── com.example.demo
│ ├── config
│ │ ├── MultipartConfig.java
│ │ └── WebMvcConfig.java
│ ├── controller
│ │ └── FileController.java
│ ├── exception
│ │ └── GlobalExceptionHandler.java
│ ├── service
│ │ └── FileService.java
│ └── DemoApplication.java
src/main/resources/
├── application.properties
└── uploads/
总结
Spring Boot文件上传下载功能虽然看似简单,但在实际开发中需要考虑文件大小限制、类型校验、安全防护、性能优化等多个方面。通过本文的完整实现方案,可以快速构建稳定、安全、高效的文件处理系统。对于生产环境,建议结合云存储服务、CDN加速、分片上传等高级特性,进一步提升系统的可靠性和用户体验。