第一部分:方式一 ------ Apache Commons FileUpload(原生 Servlet 第三方方案)
1.1 方案概述与特点
老师强调 :这是面向 Servlet 2.5 及更早时代 的方案。Java 标准库未内置 multipart 解析,必须引入第三方依赖手动解析
HttpServletRequest。
- 优势:兼容老容器老项目,控制粒度极细(临时目录、阈值、大小限制、进度监听均可手动控制)。
- 缺点:依赖多、代码模板化严重(解析、校验、写盘、清理都需手写);易漏掉临时文件清理和安全校验;新项目基本不再采用。
1.2 导入依赖
需要两个依赖,缺一不可:
-
commons-fileupload:负责解析 multipart 请求。 -
commons-io:提供底层的流复制与文件操作支持。commons-fileupload commons-fileupload 1.5 commons-io commons-io 2.11.0
1.3 页面设计
核心强调:表单必须满足两个条件:
enctype="multipart/form-data"(否则文件内容不传输)。<input type="file" name="...">(name对应后端字段名)。
<form action="uploadServlet" method="post" enctype="multipart/form-data">
选择文件:<input type="file" name="uploadFile" />
<input type="submit" value="上传" />
</form>
1.4 接口逻辑实现
核心组件:
-
DiskFileItemFactory:配置缓存区大小和临时目录。 -
ServletFileUpload:核心解析器,设置大小限制和编码。@WebServlet("/uploadServlet")
public class FileUploadServlet extends HttpServlet {
private static final String UPLOAD_DIRECTORY = "upload";
private static final int MEMORY_THRESHOLD = 1024 * 1024 * 3; // 3MB
private static final long MAX_FILE_SIZE = 1024 * 1024 * 40; // 40MB
private static final long MAX_REQUEST_SIZE = 1024 * 1024 * 50; // 50MB@Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); // 1. 检查是否为 multipart 请求 if (!ServletFileUpload.isMultipartContent(request)) { response.getWriter().println("错误:表单必须包含 enctype=multipart/form-data"); return; } // 2. 配置 DiskFileItemFactory DiskFileItemFactory factory = new DiskFileItemFactory(); factory.setSizeThreshold(MEMORY_THRESHOLD); factory.setRepository(new File(System.getProperty("java.io.tmpdir"))); // 3. 创建解析器并设置限制 ServletFileUpload upload = new ServletFileUpload(factory); upload.setFileSizeMax(MAX_FILE_SIZE); upload.setSizeMax(MAX_REQUEST_SIZE); upload.setHeaderEncoding("UTF-8"); // 4. 构造上传目录 String uploadPath = getServletContext().getRealPath("") + File.separator + UPLOAD_DIRECTORY; File uploadDir = new File(uploadPath); if (!uploadDir.exists()) { uploadDir.mkdirs(); } try { // 5. 解析请求,得到 FileItem 列表 List<FileItem> formItems = upload.parseRequest(request); if (formItems != null && !formItems.isEmpty()) { for (FileItem item : formItems) { // 6. 只处理文件字段,跳过普通表单字段 if (item.isFormField()) { continue; } // 7. 安全提取文件名,防止路径穿越 String fileName = new File(item.getName()).getName(); if (fileName == null || fileName.isEmpty()) { continue; } // 8. 写盘 File storeFile = new File(uploadPath, fileName); item.write(storeFile); response.getWriter().println("上传成功:" + storeFile.getAbsolutePath()); } } } catch (Exception ex) { response.getWriter().println("上传失败:" + ex.getMessage()); } }}
第二部分:方式二 ------ Servlet 3.0 原生 @MultipartConfig + Part
2.1 方案概述与特点
老师强调 :规范内置 multipart 支持,零第三方依赖 。在 Servlet 上标注
@MultipartConfig后由容器自动解析请求。
- 优势:标准原生、零依赖、部署简单;容器负责解析省去大量模板代码。
- 缺点 :需 Servlet 3.0+ 容器;API 偏底层(无
isFormField()),多文件需手动遍历;Part.write()路径语义有坑;安全校验仍需业务补充。
2.2 核心变化
-
删掉 Commons 依赖:只需 Servlet API(由容器提供)。
-
不需要手动解析 :但必须加
@MultipartConfig注解 ,否则request.getPart()会抛IllegalStateException。javax.servlet javax.servlet-api 4.0.1 provided
2.3 接口逻辑实现
核心组件:
-
Part接口:代表上传的某个部分(文件或普通字段)。 -
getParts():获取所有 Part 的集合。@WebServlet("/partUploadServlet")
@MultipartConfig(
fileSizeThreshold = 1024 * 1024 * 3, // 3MB
maxFileSize = 1024 * 1024 * 40, // 40MB
maxRequestSize = 1024 * 1024 * 50 // 50MB
)
public class PartUploadServlet extends HttpServlet {
private static final String UPLOAD_DIRECTORY = "upload";@Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); // 1. 构造上传目录 String uploadPath = getServletContext().getRealPath("") + File.separator + UPLOAD_DIRECTORY; File uploadDir = new File(uploadPath); if (!uploadDir.exists()) { uploadDir.mkdirs(); } try { // 2. 一次性取出所有 Part(文件字段 + 普通字段混在一起) Collection<Part> parts = request.getParts(); for (Part part : parts) { // 3. 用 getSubmittedFileName() 是否为 null 区分普通字段 String submittedName = part.getSubmittedFileName(); if (submittedName == null) { continue; // 普通表单字段 } if (submittedName.isEmpty()) { continue; // 用户未选择文件的空控件 } // 4. 安全提取文件名 String fileName = new File(submittedName).getName(); // 5. 用 UUID 重命名,避免同名覆盖 String ext = ""; int dot = fileName.lastIndexOf('.'); if (dot > 0) { ext = fileName.substring(dot); } String savedName = UUID.randomUUID().toString().replace("-", "") + ext; // 6. 流式写盘(推荐,比 Part.write 路径语义更明确) File target = new File(uploadPath, savedName); try (InputStream in = part.getInputStream()) { Files.copy(in, target.toPath(), StandardCopyOption.REPLACE_EXISTING); } // 7. 清理临时文件 part.delete(); response.getWriter().println("上传成功:" + target.getAbsolutePath() + "(原始文件名:" + fileName + ",大小:" + part.getSize() + "字节)"); } } catch (IllegalStateException e) { response.getWriter().println("上传失败:文件超过大小限制- " + e.getMessage()); } catch (ServletException e) { response.getWriter().println("上传失败:请求解析异常- " + e.getMessage()); } }}
2.4 方式一 vs 方式二 核心差异对比
| 维度 | 方式一:Commons FileUpload | 方式二:Servlet 3.0 Part |
|---|---|---|
| 依赖 | 需 commons-fileupload + commons-io |
零第三方依赖,仅 Servlet API |
| 声明 | 无需注解,代码手动解析 | 必须 @MultipartConfig,容器负责解析 |
| 解析入口 | ServletFileUpload.parseRequest(request) |
request.getParts() |
| 字段区分 | FileItem.isFormField() |
Part.getSubmittedFileName() == null |
| 写盘 | item.write(File) |
推荐 getInputStream() + Files.copy() |
| 大小限制 | 代码设置 setFileSizeMax |
@MultipartConfig 注解属性(声明式) |
| 异常 | SizeException / FileUploadException |
IllegalStateException / ServletException |
| 临时文件 | DiskFileItemFactory + item.delete() |
@MultipartConfig.location + part.delete() |
| 容器要求 | 兼容 Servlet 2.5 | 必须 Servlet 3.0+ |
第三部分:方式三 ------ Spring MVC MultipartResolver + MultipartFile
3.1 方案概述与特点
老师强调 :这是 Spring MVC 对 Servlet multipart 的声明式封装 。
MultipartResolver将HttpServletRequest包装为MultipartHttpServletRequest,控制器方法直接声明MultipartFile参数即可。
- 优势:声明式参数绑定,代码极简;与 Spring MVC 校验、异常、拦截器、AOP 完美融合;多文件接收方便。
- 缺点 :非 Spring Boot 项目需显式注册
MultipartResolver,配置不当会静默失效。
3.2 导入依赖
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
3.3 配置多媒体解析器(springmvc.xml)
核心强调 :
id的名称必须是multipartResolver,因为前端控制器DispatcherServlet是按这个名称去 IOC 容器中找解析器的。
<!-- 配置文件上传的解析器组件。id的名称固定,不能乱写 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 设置上传文件总大小 8M = 8 * 1024 * 1024 -->
<property name="maxUploadSize" value="8388608" />
</bean>
3.4 接口逻辑实现
核心强调 :
MultipartFile的变量名必须和表单file标签的name属性一致。
@Controller
public class UploadController {
@RequestMapping("/fileupload.do")
public String upload(MultipartFile upload, HttpServletRequest request) throws IOException {
// 1. 获取服务器物理路径
String realPath = request.getSession().getServletContext().getRealPath("/uploads");
// 2. 创建文件夹
File file = new File(realPath);
if(!file.exists()){
file.mkdirs();
}
// 3. 获取原始文件名并生成唯一UUID前缀
String filename = upload.getOriginalFilename();
String uuid = UUID.randomUUID().toString().replace("-", "").toUpperCase();
filename = uuid + "_" + filename;
// 4. 将文件写入磁盘
upload.transferTo(new File(file, filename));
return "suc";
}
}
第四部分:方式四 ------ Spring Boot + MultipartFile
4.1 方案概述与特点
老师强调 :与方式三核心代码几乎一样,真正的区别在于**"零配置"**。Spring Boot 自动注册了
StandardServletMultipartResolver,无需手写 Bean。
4.2 导入依赖
只需 spring-boot-starter-web,它已内置 Spring MVC、Tomcat 和 Jackson,无需额外引包。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
4.3 配置上传限制(关键步骤)
核心强调 :Spring Boot 默认限制极小(单文件 1MB,总请求 10MB)。必须根据需求在
application.yml中调整,否则上传稍大文件直接报MaxUploadSizeExceededException。
spring:
servlet:
multipart:
max-file-size: 10MB # 单个文件上限
max-request-size: 50MB # 整个请求上限
file-size-threshold: 2MB # 超过2MB才写临时磁盘,默认0
4.4 接口逻辑实现
@RestController
public class FileUploadController {
@Value("${file.upload-dir:uploads}")
private String uploadDir;
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) throws IOException {
// 1. 空文件校验
if (file == null || file.isEmpty()) {
return "请选择要上传的文件";
}
// 2. 构造上传目录(绝对路径)
File dir = new File(uploadDir).getAbsoluteFile();
if (!dir.exists()) {
dir.mkdirs();
}
// 3. 安全提取文件名
String originalName = file.getOriginalFilename();
if (originalName == null || originalName.isEmpty()) {
return "文件名无效";
}
originalName = new File(originalName).getName();
// 4. UUID 重命名
String ext = "";
int dot = originalName.lastIndexOf('.');
if (dot > 0) {
ext = originalName.substring(dot);
}
String savedName = UUID.randomUUID().toString().replace("-", "") + ext;
// 5. transferTo 写盘(必须传绝对路径)
File target = new File(dir, savedName);
file.transferTo(target);
return "上传成功:" + target.getAbsolutePath()
+ "(原始文件名:" + originalName + ",大小:" + file.getSize() + "字节)";
}
// 6. 统一处理超限异常
@ExceptionHandler(MaxUploadSizeExceededException.class)
public String handleMaxSize(MaxUploadSizeExceededException e) {
return "上传失败:文件超过大小限制,请检查 spring.servlet.multipart 配置";
}
}
附录:核心易错点速查表
| 类别 | 易错点 | 正确理解 |
|---|---|---|
| 表单 | 漏写 enctype |
必须是 multipart/form-data,否则传的是普通文本 |
| 请求方式 | 用 GET 请求上传 | 文件上传必须用 POST 请求 |
| 参数名 | 表单 name 与形参名不一致 | MultipartFile 变量名必须和前端 name 保持一致 |
| 方式一 | 忘记 commons-io |
FileItem.write() 底层依赖它,二者缺一不可 |
| 方式二 | 忘记加 @MultipartConfig |
不加会直接抛 IllegalStateException |
| 方式二 | 直接调用 Part.write(path) |
路径语义容易出错,推荐 getInputStream + Files.copy |
| 方式三 | 解析器 id 乱起 | Spring MVC 配置中 id="multipartResolver" 固定不可改 |
| 方式三 | 忘记配置解析器 | 会导致 MultipartFile 接收不到数据,不报错但为 null |
| 方式四 | 使用默认大小限制 | Spring Boot 默认单文件仅 1MB,稍大文件会报错 |
| 通用 | 用原文件名保存 | 必须用 UUID 或时间戳重命名,防止同名覆盖 |
| 通用 | 信任前端传的文件名 | 必须用 new File(name).getName() 处理,防止路径穿越(如 ../) |
| 通用 | 没有大小限制 | 前后端都要做限制,防止超大文件撑爆内存或磁盘 |