JavaWeb实现文件上传的四种方案
作者:CarIise
第一部分:方式一 —— Apache Commons FileUpload(原生 Servlet 第三方方案)
1.1 方案概述与特点
老师强调:这是面向 Servlet 2.5 及更早时代的方案。Java 标准库未内置 multipart 解析,必须引入第三方依赖手动解析 HttpServletRequest。
- 优势:兼容老容器老项目,控制粒度极细(临时目录、阈值、大小限制、进度监听均可手动控制)。
- 缺点:依赖多、代码模板化严重(解析、校验、写盘、清理都需手写);易漏掉临时文件清理和安全校验;新项目基本不再采用。
1.2 导入依赖
需要两个依赖,缺一不可:
commons-fileupload:负责解析 multipart 请求。commons-io:提供底层的流复制与文件操作支持。
<!-- 文件上传解析核心 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>
<!-- IO工具,FileItem.write()内部依赖它 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>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。
<!-- Servlet API,由容器提供 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>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才写临时磁盘,默认04.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() 处理,防止路径穿越(如 ../) |
| 通用 | 没有大小限制 | 前后端都要做限制,防止超大文件撑爆内存或磁盘 |
以上就是JavaWeb实现文件上传的四种方案的详细内容,更多关于JavaWeb文件上传方案的资料请关注脚本之家其它相关文章!
