Servlet 文件上传

multipart 表单、@MultipartConfig、Part 读取与落盘的安全限制。

Intermediate
Key Points表单要 enctype=multipart/form-data,Servlet 要标 @MultipartConfig,用 Part 读字节并重命名落盘。

表单

HTML
<form action="upload" method="post" enctype="multipart/form-data">
  选择文件:<input type="file" name="file" multiple><br>
  说明:<input type="text" name="note"><br>
  <button type="submit">上传</button>
</form>

Without enctype="multipart/form-data" the browser sends only the file name, not the bytes, and Part is empty — the most common upload failure.

Servlet 端

Java
@WebServlet("/upload")
@MultipartConfig(
        fileSizeThreshold = 1024 * 1024,       // 超过 1MB 写临时文件
        maxFileSize = 5 * 1024 * 1024,         // 单文件上限 5MB
        maxRequestSize = 20 * 1024 * 1024      // 整个请求上限 20MB
)
public class UploadServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        req.setCharacterEncoding("UTF-8");
        Path dir = Path.of(getServletContext().getRealPath("/uploads"));
        Files.createDirectories(dir);

        resp.setContentType("text/plain;charset=UTF-8");
        try (PrintWriter out = resp.getWriter()) {
            for (Part part : req.getParts()) {
                String name = part.getSubmittedFileName();
                if (name == null || name.isBlank()) {
                    out.println("字段 " + part.getName() + " = " + readText(part));
                    continue;
                }
                String ext = name.contains(".")
                        ? name.substring(name.lastIndexOf('.')) : "";
                String stored = UUID.randomUUID() + ext;     // 不信任原文件名
                part.write(dir.resolve(stored).toString());
                out.println("已保存 " + name + " -> " + stored
                        + " (" + part.getSize() + " 字节)");
            }
        }
    }

    private static String readText(Part part) throws IOException {
        try (InputStream in = part.getInputStream()) {
            return new String(in.readAllBytes(), StandardCharsets.UTF_8);
        }
    }
}
Output
已保存 report.pdf -> 6f1c2a9e-3b7d-4a10-9c55-2e8d4f0a1b23.pdf (184320 字节)
字段 note = 月度报告

三条安全规则

  1. 重命名落盘 —— 原始文件名可能是 ../../web.xml,直接用会覆盖任意文件
  2. 校验扩展名与 MIME —— 白名单,不要黑名单;image/jpeg 才允许
  3. 落在 Web 根之外或禁止执行 —— 上传目录能跑 JSP 就等于给了 webshell
Java
private static final Set<String> ALLOWED = Set.of(".jpg", ".jpeg", ".png", ".pdf");

private static String safeExt(String filename) {
    String ext = filename.substring(filename.lastIndexOf('.')).toLowerCase();
    if (!ALLOWED.contains(ext)) {
        throw new IllegalArgumentException("不支持的类型:" + ext);
    }
    return ext;
}
Exceeding maxFileSize makes the container throw IllegalStateException (or ServletException on some versions) with no friendly message. Catch it and sendError(413) to tell the user why.

大文件与进度

Part.write() stages the whole file in a temp directory, doubling disk use for GB-sized uploads. For very large files switch to a streaming parser (Commons FileUpload Streaming API) that reads and writes in chunks.

Friends & Partners