SpringBoot文件上传

215 阅读5分钟

什么是文件上传?

定义

文件上传是指客户端(浏览器)通过 HTTP 协议将本地文件传输到服务器端的過程。

技术本质

  • 使用 multipart/form-data 格式编码(非 application/x-www-form-urlencoded
  • 允许在单个请求中发送二进制数据和文本数据
  • 每个部分(part)都有独立的 Content-Type 和 headers

🌐 第二部分:前后端联系机制

通信流程

前端(表单/JS) → HTTP Multipart Request → 后端(Spring Controller) → 文件处理

联系的关键点

  1. 前端:使用 <form enctype="multipart/form-data"> 或 FormData 对象
  2. 后端:使用 @RequestParam("file") MultipartFile 接收
  3. 协议:基于 HTTP 的 multipart/form-data 格式

📋 第三部分:请求参数详析(核心部分)

1. 请求头信息(Request Headers)

一个典型的多文件上传请求头:

POST /upload HTTP/1.1
Host: localhost:8080
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: 10240
User-Agent: Mozilla/5.0
Accept: */*

关键头部说明

头部字段值示例说明
Content-Typemultipart/form-data; boundary=----xxx必须,定义多部分格式和边界符
Content-Length10240整个请求体的大小(字节)
User-AgentMozilla/5.0客户端浏览器信息
AuthorizationBearer xxx如果需要身份验证

2. 请求体结构(Request Body)

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="example.jpg"
Content-Type: image/jpeg
<文件二进制数据>
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="description"
这是一个文件描述
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="category"
images
------WebKitFormBoundary7MA4YWxkTrZu0gW--

各部分说明

部分说明
boundary分隔不同部分的唯一字符串
Content-Disposition包含字段名和文件名
Content-Type当前部分的 MIME 类型
空行分隔头部和内容体
数据内容文件二进制数据或文本值
结束标记--boundary-- 表示结束

3. 请求参数类型

参数类型示例说明
文件参数file=@example.jpg二进制文件数据
文本参数description=图片描述普通文本字段
元数据参数category=images附加信息(分类、标签等)

🖥️ 第四部分:前端实现方式

方式一:HTML 表单(传统方式)

<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="file" multiple>  <!-- multiple 允许多选 -->
    <input type="text" name="description" placeholder="文件描述">
    <input type="text" name="category" placeholder="分类">
    <button type="submit">上传</button>
</form>

方式二:JavaScript + FormData(现代方式)

// 创建 FormData 对象
const formData = new FormData();
formData.append('file', fileInput.files[0]);  // 文件
formData.append('description', '文件描述');    // 文本参数
formData.append('category', 'images');        // 文本参数
// 发送 AJAX 请求
fetch('/upload', {
    method: 'POST',
    body: formData,
    // headers: {'Authorization': 'Bearer xxx'} // 通常不需要设置 Content-Type!
})
.then(response => response.json())
.then(data => console.log(data));

重要提示:使用 FormData 时不要手动设置 Content-Type,浏览器会自动设置正确的 multipart/form-data 和 boundary。


🛠️ 第五部分:后端接收与处理

1. Spring Boot 配置

首先在 application.properties 中配置:

# 单个文件最大大小
spring.servlet.multipart.max-file-size=10MB
# 单次请求最大大小
spring.servlet.multipart.max-request-size=100MB
# 文件上传临时目录
spring.servlet.multipart.location=/tmp
# 是否启用文件上传
spring.servlet.multipart.enabled=true

2. Controller 接收方式

方式一:基本文件上传

@RestController
public class FileUploadController {
    
    @PostMapping("/upload")
    public ResponseEntity<String> uploadFile(
            @RequestParam("file") MultipartFile file,
            @RequestParam(value = "description", required = false) String description,
            @RequestParam(value = "category", required = false) String category) {
        
        try {
            // 1. 校验文件是否为空
            if (file.isEmpty()) {
                return ResponseEntity.badRequest().body("请选择文件");
            }
            
            // 2. 获取文件信息
            String fileName = file.getOriginalFilename();
            String contentType = file.getContentType();
            long size = file.getSize();
            
            // 3. 保存文件(示例:保存到本地)
            byte[] bytes = file.getBytes();
            Path path = Paths.get("/uploads/" + fileName);
            Files.write(path, bytes);
            
            // 4. 处理其他参数
            System.out.println("描述: " + description);
            System.out.println("分类: " + category);
            
            return ResponseEntity.ok("文件上传成功: " + fileName);
            
        } catch (IOException e) {
            return ResponseEntity.status(500).body("上传失败: " + e.getMessage());
        }
    }
}

方式二:多文件上传

@PostMapping("/upload-multiple")
public ResponseEntity<String> uploadMultipleFiles(
        @RequestParam("files") MultipartFile[] files,
        @RequestParam(value = "description", required = false) String description) {
    
    if (files.length == 0) {
        return ResponseEntity.badRequest().body("请选择文件");
    }
    
    List<String> fileNames = new ArrayList<>();
    for (MultipartFile file : files) {
        if (!file.isEmpty()) {
            try {
                String fileName = file.getOriginalFilename();
                // 保存文件逻辑...
                fileNames.add(fileName);
            } catch (IOException e) {
                return ResponseEntity.status(500)
                    .body("文件 " + file.getOriginalFilename() + " 上传失败");
            }
        }
    }
    
    return ResponseEntity.ok("上传成功: " + String.join(", ", fileNames));
}

方式三:使用 DTO 对象接收(推荐)

public class FileUploadDTO {
    private MultipartFile file;
    private String description;
    private String category;
    
    // getters and setters
}
@PostMapping("/upload-dto")
public ResponseEntity<String> uploadWithDTO(FileUploadDTO dto) {
    MultipartFile file = dto.getFile();
    String description = dto.getDescription();
    String category = dto.getCategory();
    
    // 处理逻辑...
    return ResponseEntity.ok("上传成功");
}

📊 第六部分:MultipartFile 接口详解

Spring 提供的 MultipartFile 接口包含以下重要方法:

方法返回类型说明
getOriginalFilename()String获取原始文件名
getContentType()String获取文件 MIME 类型
isEmpty()boolean判断文件是否为空
getSize()long获取文件大小(字节)
getBytes()byte[]获取文件字节数组
getInputStream()InputStream获取文件输入流
transferTo(File dest)void将文件传输到目标文件

🛡️ 第七部分:安全与校验

1. 文件类型校验

private boolean isValidFileType(MultipartFile file) {
    String contentType = file.getContentType();
    String fileName = file.getOriginalFilename();
    
    // 允许的 MIME 类型
    List<String> allowedTypes = Arrays.asList("image/jpeg", "image/png", "application/pdf");
    
    // 文件扩展名检查
    if (fileName != null && !fileName.toLowerCase().endsWith(".jpg") && 
        !fileName.toLowerCase().endsWith(".png") && !fileName.toLowerCase().endsWith(".pdf")) {
        return false;
    }
    
    return allowedTypes.contains(contentType);
}

2. 文件大小校验

private boolean isValidFileSize(MultipartFile file) {
    long maxSize = 10 * 1024 * 1024; // 10MB
    return file.getSize() <= maxSize;
}

3. 文件名安全处理

private String sanitizeFileName(String originalFileName) {
    // 移除路径信息,防止路径遍历攻击
    String fileName = new File(originalFileName).getName();
    
    // 移除特殊字符
    fileName = fileName.replaceAll("[^a-zA-Z0-9.-]", "_");
    
    // 添加时间戳防止重名
    String timestamp = String.valueOf(System.currentTimeMillis());
    return timestamp + "_" + fileName;
}

🔄 第八部分:完整工作流程

  1. 前端:用户选择文件 → 创建 FormData → 发送 POST 请求
  2. 网络:浏览器构造 multipart/form-data 请求 → 发送到服务器
  3. Spring:DispatcherServlet 接收请求 → MultipartResolver 解析
  4. Controller:@RequestParam 接收参数 → 业务处理 → 返回响应
  5. 响应:返回 JSON 或重定向信息 → 前端更新界面

📝 第九部分:常见问题与解决方案

问题原因解决方案
MaxUploadSizeExceededException文件过大调整 max-file-size 配置
MissingServletRequestPartException参数名不匹配检查 @RequestParam("name")
中文文件名乱码编码问题配置字符过滤器
文件覆盖同名文件使用 UUID 或时间戳重命名
安全漏洞未校验文件类型实现文件类型白名单