JavaWeb 文件上传的四种实现方案

第一部分:方式一 ------ 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 页面设计

核心强调:表单必须满足两个条件:

  1. enctype="multipart/form-data"(否则文件内容不传输)。
  2. <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() 处理,防止路径穿越(如 ../)
通用 没有大小限制 前后端都要做限制,防止超大文件撑爆内存或磁盘
相关推荐
长欢喜一场1 小时前
豆包:字节跳动AI助手的全方位透视
人工智能·搜索引擎·1024程序员节
4SAPI1 小时前
搭建 AI 数据分析工具大模型 API 选型指南
数据库·人工智能·架构·php·1024程序员节
KingOTC1 小时前
国产模块测评:基于国产芯片的实际表现
rfid·1024程序员节
小嘉丶学长1 小时前
Day04 · 程序入门
1024程序员节
Alsn861 小时前
Windows 安装 Ollama 及本地模型部署
学习·1024程序员节
霂鳯1 小时前
新手勇闯网络安全 | MySQL基础(二)
1024程序员节
俊哥V2 小时前
每日 AI 研究简报 · 2026-10-24
人工智能·ai·1024程序员节
burg_xun2 小时前
【生命科学 Agent】推理平台(三)
人工智能·系统架构·1024程序员节
幼儿园蛋小黑2 小时前
STM32F103 入门指南:从芯片架构到第一个工程
1024程序员节