杰瑞科技汇

文件上传 java tomcat

目录

  1. 核心原理:HTTP 协议如何传输文件
  2. 准备工作:HTML 表单
  3. 使用 Servlet 3.0+ 内置 API (推荐)
    • 代码示例
    • 配置说明 (web.xml)
    • 优点与缺点
  4. 使用 Apache Commons FileUpload (传统方式)
    • 添加依赖
    • 代码示例
    • 优点与缺点
  5. 最佳实践与注意事项
    • 文件存储位置
    • 安全性
    • 性能
    • 中文文件名乱码问题
  6. 完整示例代码

核心原理:HTTP 协议如何传输文件

标准的 HTTP 请求中,表单数据默认是 application/x-www-form-urlencoded 格式,它只能传输文本,当表单包含 <input type="file"> 时,必须使用 multipart/form-data 编码类型。

文件上传 java tomcat-图1
(图片来源网络,侵删)

这种类型的请求会将表单的各个部分(包括文本字段和文件内容)分割成多个“块”(Part),每个块之间用一个特殊的边界字符串(Boundary)分隔,服务器端需要一个特殊的解析器来读取这个混合流,识别出每个块的类型(是文本还是文件),并分别提取出文件名、文件内容等数据。

关键点:

  • <form> 标签必须设置 enctype="multipart/form-data"
  • <form> 标签必须使用 POST 方法,因为文件数据通常很大。

准备工作:HTML 表单

我们需要一个在前端页面上传文件的表单,这是一个简单的 upload.html 示例。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">文件上传</title>
</head>
<body>
    <h1>选择文件上传</h1>
    <form action="upload" method="post" enctype="multipart/form-data">
        <!-- 
          name="file" 这个 name 属性非常重要,后端代码会通过它来获取文件
          multiple 属性允许一次选择多个文件
        -->
        <input type="file" name="file" multiple>
        <input type="submit" value="上传">
    </form>
</body>
</html>

关键点:

文件上传 java tomcat-图2
(图片来源网络,侵删)
  • action="upload":指向服务器端处理上传请求的 Servlet 或 URL。
  • method="post":必须使用 POST。
  • enctype="multipart/form-data":必须设置,这是文件上传的标志。

方案一:使用 Servlet 3.0+ 内置 API (推荐)

从 Servlet 3.0 开始,Java EE 规范内置了对文件上传的支持,使用起来非常简单,无需额外引入第三方库。

代码示例

创建一个 UploadServlet.java 来处理请求。

import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
@WebServlet("/upload")
@MultipartConfig // <-- 关键注解,声明该Servlet可以处理multipart请求
public class UploadServlet extends HttpServlet {
    // 定义上传文件保存的目录(相对于 webapp 目录)
    private static final String UPLOAD_DIR = "uploads";
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        // 1. 获取上传文件的目录
        // getServletContext().getRealPath() 可以获取 webapp 目录下的绝对路径
        String applicationPath = getServletContext().getRealPath("");
        String uploadFilePath = applicationPath + File.separator + UPLOAD_DIR;
        // 如果上传目录不存在,则创建
        File uploadFileDir = new File(uploadFilePath);
        if (!uploadFileDir.exists()) {
            uploadFileDir.mkdirs();
        }
        // 2. 遍历所有上传的 Part
        Collection<Part> parts = request.getParts();
        if (parts.size() > 0) {
            for (Part part : parts) {
                // 3. 获取文件名
                // 注意:对于非文件字段(如文本输入),part.getSubmittedFileName() 返回 null
                String fileName = getSubmittedFileName(part);
                if (fileName != null && !fileName.isEmpty()) {
                    // 4. 写入文件到服务器
                    // part.write() 方法将文件内容写入到指定路径
                    part.write(uploadFilePath + File.separator + fileName);
                    System.out.println("文件 " + fileName + " 上传成功!");
                }
            }
        }
        // 5. 向客户端返回成功信息
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.println("<html><body>");
        out.println("<h3>文件上传成功!</h3>");
        out.println("<a href='upload.html'>返回上传页面</a>");
        out.println("</body></html>");
    }
    /**
     * 辅助方法:从 Part 对象中获取提交的文件名。
     * 这个方法可以兼容不同浏览器提交的文件名格式。
     */
    private String getSubmittedFileName(Part part) {
        String contentDisp = part.getHeader("content-disposition");
        String[] items = contentDisp.split(";");
        for (String s : items) {
            if (s.trim().startsWith("filename")) {
                // 处理可能的中文文件名乱码问题
                String fileName = s.substring(s.indexOf("=") + 2, s.length() - 1);
                return fileName;
            }
        }
        return "";
    }
}

配置说明 (web.xml)

使用 @WebServlet@MultipartConfig 注解后,通常不再需要在 web.xml 中配置 Servlet,但如果想进行更详细的配置(如设置上传文件的最大尺寸),可以在 web.xml 中配置 multipart-config

<!-- web.xml -->
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
         http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>UploadServlet</servlet-name>
        <servlet-class>com.yourpackage.UploadServlet</servlet-class>
        <multipart-config>
            <!-- 设置单个文件的最大尺寸 (e.g., 5MB) -->
            <max-file-size>5242880</max-file-size>
            <!-- 设置整个请求的最大尺寸 (e.g., 10MB) -->
            <max-request-size>10485760</max-request-size>
            <!-- 设置临时文件存放目录,如果未设置,则使用系统默认的临时目录 -->
            <file-temp-directory>/path/to/temp/dir</file-temp-directory>
        </multipart-config>
    </servlet>
    <servlet-mapping>
        <servlet-name>UploadServlet</servlet-name>
        <url-pattern>/upload</url-pattern>
    </servlet-mapping>
</web-app>

优点与缺点

  • 优点:
    • 无需额外依赖:是 Java EE 标准的一部分,开箱即用。
    • 代码简洁:API 设计直观,request.getParts()part.write() 非常简单。
    • 官方支持:由 Java 社区维护,稳定可靠。
  • 缺点:
    • 灵活性稍差:对于复杂的业务逻辑(如获取文件类型、动态生成文件名等),可能需要写更多辅助代码。

方案二:使用 Apache Commons FileUpload (传统方式)

如果你在使用旧版本的 Servlet(低于 3.0),或者更喜欢使用功能更强大的库,Apache Commons FileUpload 是一个非常经典的选择。

文件上传 java tomcat-图3
(图片来源网络,侵删)

添加依赖

如果你使用 Maven,在 pom.xml 中添加以下依赖:

<dependencies>
    <!-- Commons FileUpload -->
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.4</version>
    </dependency>
    <!-- Commons IO (FileUpload 依赖它) -->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.11.0</version>
    </dependency>
</dependencies>

代码示例

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
@WebServlet("/upload-legacy")
public class LegacyUploadServlet extends HttpServlet {
    private static final String UPLOAD_DIR = "uploads";
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        // 1. 检查请求是否为 multipart
        boolean isMultipart = ServletFileUpload.isMultipartContent(request);
        if (!isMultipart) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "请求不是 multipart/form-data 类型");
            return;
        }
        // 2. 配置上传参数
        DiskFileItemFactory factory = new DiskFileItemFactory();
        // 设置内存中存储文件内容的阈值,超过此大小将写入临时文件
        factory.setSizeThreshold(1024 * 1024); // 1MB
        // 设置临时文件存储目录
        String tempPath = getServletContext().getRealPath("") + File.separator + "temp";
        File tempDir = new File(tempPath);
        if (!tempDir.exists()) tempDir.mkdirs();
        factory.setRepository(tempDir);
        ServletFileUpload upload = new ServletFileUpload(factory);
        // 设置单个文件的最大尺寸
        upload.setFileSizeMax(1024 * 1024 * 5); // 5MB
        // 设置整个请求的最大尺寸
        upload.setSizeMax(1024 * 1024 * 10); // 10MB
        // 3. 解析请求,获取 FileItem 列表
        try {
            List<FileItem> items = upload.parseRequest(request);
            String uploadPath = getServletContext().getRealPath("") + File.separator + UPLOAD_DIR;
            File uploadDir = new File(uploadPath);
            if (!uploadDir.exists()) uploadDir.mkdirs();
            for (FileItem item : items) {
                // 4. 判断是普通表单字段还是文件
                if (item.isFormField()) {
                    // 普通字段,例如文本输入
                    String fieldName = item.getFieldName();
                    String fieldValue = item.getString("UTF-8"); // 指定编码防止乱码
                    System.out.println("字段: " + fieldName + ", 值: " + fieldValue);
                } else {
                    // 文件字段
                    String fileName = new File(item.getName()).getName();
                    String filePath = uploadPath + File.separator + fileName;
                    // 将文件写入到指定路径
                    item.write(new File(filePath));
                    System.out.println("文件 " + fileName + " 上传成功!");
                }
            }
            // 5. 返回成功信息
            response.setContentType("text/html;charset=UTF-8");
            PrintWriter out = response.getWriter();
            out.println("<html><body>");
            out.println("<h3>文件上传成功!(Legacy方式)</h3>");
            out.println("</body></html>");
        } catch (Exception e) {
            throw new ServletException("文件解析或上传失败", e);
        }
    }
}

优点与缺点

  • 优点:
    • 功能强大:提供了对上传过程的细粒度控制,如内存阈值、临时目录、进度监听等。
    • 兼容性好:适用于所有 Servlet 版本。
    • 处理普通字段方便:能自动区分文件和普通字段,并提供了 getString() 方法方便获取文本内容。
  • 缺点:
    • 需要引入外部依赖:增加了项目的复杂性和体积。
    • 代码量稍多:需要创建 FileItemFactoryServletFileUpload 对象,配置步骤比内置 API 繁琐。

最佳实践与注意事项

文件存储位置

  • 不要存放在 WEB-INF 之外:直接存放在 webapp 目录下的文件(如 uploads)可以通过 URL 直接访问,存在安全隐患,建议将上传的文件存放在 WEB-INF 目录下,这样它们就不能被直接通过浏览器访问了。
    // 推荐:存放在 WEB-INF 下
    String uploadPath = getServletContext().getRealPath("") + File.separator + "WEB-INF" + File.separator + UPLOAD_DIR;
  • 使用绝对路径:始终使用 getServletContext().getRealPath() 来获取服务器的绝对路径,而不是使用相对路径,因为后者在不同部署环境下可能出错。

安全性

  • 文件名过滤:恶意用户可能上传包含 的文件名,试图将文件写入到服务器上的任意位置(路径遍历攻击),在保存文件前,务必对文件名进行清理和验证。
    // 简单的文件名清理示例
    String fileName = new File(item.getName()).getName();
    // 防止路径遍历攻击
    fileName = fileName.replaceAll("\\.\\./", "");
    // 也可以限制文件名只包含字母、数字、下划线和点
    fileName = fileName.replaceAll("[^a-zA-Z0-9._-]", "");
  • 文件类型验证:不要仅依赖文件扩展名,应该读取文件的“魔数”(Magic Number)或文件头来判断真实的文件类型,防止用户将 .exe 文件伪装成 .jpg 文件上传。
  • 病毒扫描:对于生产环境,上传的文件应经过杀毒软件的扫描。

性能

  • 限制文件大小:始终在 web.xml 或代码中设置 max-file-sizemax-request-size,防止用户上传超大文件耗尽服务器磁盘空间或导致内存溢出。
  • 使用临时文件:对于大文件,像 Commons FileUpload 那样使用临时文件机制,可以避免在内存中占用过多空间。

中文文件名乱码问题

  • Servlet 3.0+part.getSubmittedFileName() 方法通常能正确处理文件名,如果仍有问题,可以尝试获取 Content-Disposition 头并手动解析,指定字符集。
  • Commons FileUpload:在调用 item.getString() 获取普通字段内容时,可以指定编码,如 item.getString("UTF-8"),但对于文件名,item.getName() 返回的通常是原始字符串,需要确保客户端(HTML <meta charset="UTF-8">)和服务器端都使用 UTF-8 编码。

完整示例代码

这里提供一个完整的、包含安全性的 Servlet 3.0+ 示例。

UploadServlet.java

import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.UUID;
@WebServlet("/safe-upload")
@MultipartConfig(
    fileSizeThreshold = 1024 * 1024,      // 1 MB
    maxFileSize = 1024 * 1024 * 5,        // 5 MB
    maxRequestSize = 1024 * 1024 * 10     // 10 MB
)
public class SafeUploadServlet extends HttpServlet {
    private static final String UPLOAD_DIR = "WEB-INF/uploads";
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        // 1. 确保上传目录存在
        String uploadPath = getServletContext().getRealPath("") + File.separator + UPLOAD_DIR;
        File uploadDir = new File(uploadPath);
        if (!uploadDir.exists()) {
            uploadDir.mkdirs();
        }
        // 2. 获取上传的 Part
        Part filePart = request.getPart("file"); // 通过 name="file" 获取
        if (filePart == null || filePart.getSize() == 0) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "没有选择文件");
            return;
        }
        // 3. 安全地处理文件名
        String fileName = getSubmittedFileName(filePart);
        if (fileName == null || fileName.isEmpty()) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "文件名无效");
            return;
        }
        // 防止路径遍历攻击
        fileName = new File(fileName).getName();
        // 使用 UUID 生成新的文件名,防止文件名冲突和恶意文件名
        String fileExtension = fileName.substring(fileName.lastIndexOf("."));
        String newFileName = UUID.randomUUID().toString() + fileExtension;
        // 4. 构建安全的文件路径
        String filePath = uploadPath + File.separator + newFileName;
        // 5. 写入文件
        filePart.write(filePath);
        System.out.println("文件 " + fileName + " 已安全保存为 " + newFileName + " 在 " + filePath);
        // 6. 返回成功信息
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.println("<html><body>");
        out.println("<h3>文件上传成功!</h3>");
        out.println("<p>原始文件名: " + fileName + "</p>");
        out.println("<p>新文件名: " + newFileName + "</p>");
        out.println("<a href='upload.html'>返回上传页面</a>");
        out.println("</body></html>");
    }
    private String getSubmittedFileName(Part part) {
        String contentDisp = part.getHeader("content-disposition");
        String[] items = contentDisp.split(";");
        for (String s : items) {
            if (s.trim().startsWith("filename")) {
                String fileName = s.substring(s.indexOf("=") + 2, s.length() - 1);
                return fileName;
            }
        }
        return "";
    }
}

对于新的项目,强烈推荐使用 Servlet 3.0+ 的内置 API,因为它更简单、更标准,只有在维护旧项目或需要非常精细的控制时,才考虑使用 Apache Commons FileUpload,无论选择哪种方案,安全性、性能和健壮性都是开发时必须考虑的重点。

分享:
扫描分享到社交APP
上一篇
下一篇