当前位置:首页 > 技术 > 正文内容

Java Web中Servlet文件操作:上传与下载功能详解

访客 技术 2026年10月29日 1

在Java Web开发中,基于Servlet实现文件上传与下载功能,相比于使用如Struts2这类更高级的框架,可能需要处理更多底层细节。然而,理解并掌握这些基础操作对于深入Web应用开发至关重要。

文件上传

实现文件上传功能时,通常会借助于Apache Commons FileUpload和Apache Commons IO这两个库。它们极大地简化了Servlet API中处理multipart/form-data类型请求的复杂性。

所需依赖

  • commons-fileupload-x.x.jar (例如:commons-fileupload-1.4.jar)
  • commons-io-x.x.jar (例如:commons-io-2.11.0.jar)

前端页面示例

上传文件的前端表单需要使用POST方法,并且enctype属性必须设置为multipart/form-data,这是处理文件上传请求的必要条件。

<form action="/upload" method="POST" enctype="multipart/form-data">
    <label for="fileInput">选择文件:</label>
    <input type="file" id="fileInput" name="uploadFile"/><br/>
    <input type="submit" value="上传"/>
</form>

Servlet文件上传核心步骤

  1. 创建磁盘文件项工厂(DiskFileItemFactory):该工厂用于设置上传文件时的临时存储目录和内存阈值。当文件大小超过内存阈值时,文件会先写入临时目录。
  2. 创建文件上传处理器(ServletFileUpload):基于工厂创建文件处理器,负责解析HTTP请求中的文件数据。
  3. 解析请求获取文件项(FileItem):使用处理器解析HttpServletRequest对象,获取一个包含所有表单字段(包括普通文本字段和文件字段)的FileItem列表。
  4. 遍历处理文件项:迭代FileItem列表。对于文件字段,从中提取文件名、保存文件内容;对于普通表单字段,则获取其对应的文本值。

服务器端Servlet代码示例

为了提高文件管理的效率和安全性,在实际应用中,通常会将上传文件保存到Web应用目录下的/WEB-INF中(该目录下的资源无法通过浏览器直接访问),并采取如"目录打散"、"生成唯一文件名"等策略。

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
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 org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

@WebServlet("/upload")
public class FileUploadServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 检查请求是否为multipart类型
        if (!ServletFileUpload.isMultipartContent(request)) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "请求必须是multipart/form-data类型");
            return;
        }

        // 2. 配置工厂和文件上传处理器
        // 设置临时文件存储目录(系统默认临时目录)和内存阈值(10MB)
        File tempRepository = new File(System.getProperty("java.io.tmpdir"));
        DiskFileItemFactory factory = new DiskFileItemFactory(10 * 1024 * 1024, tempRepository);
        
        ServletFileUpload uploadHandler = new ServletFileUpload(factory);
        uploadHandler.setHeaderEncoding("UTF-8"); // 解决上传文件名中文乱码问题

        try {
            // 3. 解析请求,获取所有文件项
            List<FileItem> formItems = uploadHandler.parseRequest(request);

            if (formItems == null || formItems.isEmpty()) {
                response.getWriter().println("没有发现上传文件。");
                return;
            }

            // 获取Web应用的实际保存路径:/WEB-INF/uploads
            String uploadBaseDirPath = getServletContext().getRealPath("/WEB-INF/uploads");
            File baseDirectory = new File(uploadBaseDirPath);
            if (!baseDirectory.exists()) {
                baseDirectory.mkdirs(); // 确保上传根目录存在
            }

            // 4. 遍历处理每个文件项
            for (FileItem item : formItems) {
                if (!item.isFormField()) { // 如果是文件字段
                    String originalClientFileName = item.getName(); // 客户端上传的原始文件名

                    // 兼容不同操作系统和浏览器,处理文件名中可能包含的路径信息
                    if (originalClientFileName != null) {
                        int pathSeparatorIndex = Math.max(originalClientFileName.lastIndexOf('/'), originalClientFileName.lastIndexOf('\\'));
                        if (pathSeparatorIndex != -1) {
                            originalClientFileName = originalClientFileName.substring(pathSeparatorIndex + 1);
                        }
                    }
                    
                    if (originalClientFileName == null || originalClientFileName.trim().isEmpty()) {
                        continue; // 空文件名,跳过
                    }

                    // 生成唯一文件名,避免文件名冲突,例如:UUID_原始文件名.扩展名
                    String uniqueStoredFileName = UUID.randomUUID().toString() + "_" + originalClientFileName;

                    // 使用原始文件名的哈希值进行目录打散,提高查找效率和目录性能
                    // 例如:/WEB-INF/uploads/h/a/uniqueStoredFileName.ext
                    int fileNameHash = originalClientFileName.hashCode();
                    String hashHex = Integer.toHexString(fileNameHash);
                    
                    File subDirectory = new File(baseDirectory, hashHex.charAt(0) + File.separator + hashHex.charAt(1));
                    if (!subDirectory.exists()) {
                        subDirectory.mkdirs(); // 创建打散后的子目录
                    }

                    // 5. 将文件写入目标位置
                    File targetFile = new File(subDirectory, uniqueStoredFileName);
                    item.write(targetFile);
                    
                    response.getWriter().println("文件 "" + originalClientFileName + "" 上传成功,保存为: " + targetFile.getName());
                } else {
                    // 处理普通表单字段
                    String fieldName = item.getFieldName();
                    String fieldValue = item.getString("UTF-8"); // 获取字段值,注意编码
                    System.out.println("普通表单字段: " + fieldName + "=" + fieldValue);
                }
            }
        } catch (FileUploadException e) {
            throw new ServletException("文件上传处理失败", e);
        } catch (Exception e) { // 捕获 item.write() 抛出的其他异常
            throw new ServletException("文件保存失败", e);
        }
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request, response);
    }
}

文件下载

文件下载功能相对简单,主要涉及到设置HTTP响应头和通过输入输出流传输文件内容。

所需依赖

  • commons-io-x.x.jar (例如:commons-io-2.11.0.jar)

Servlet文件下载核心要素

文件下载通常被称为"一个流,两个头":

  • 响应头1:Content-Type:告知浏览器文件的MIME类型,例如image/jpeg、application/pdf或application/octet-stream(通用二进制文件)。
  • 响应头2:Content-Disposition:指示浏览器如何处理文件。设置为attachment;filename="文件名"表示作为附件下载,并指定下载时显示的文件名。
  • 流:文件数据流:通过FileInputStream读取服务器上的文件,并通过ServletOutputStream将文件内容写入HTTP响应体。

服务器端Servlet代码示例

在处理下载文件名时,需要特别注意不同浏览器对中文或特殊字符的编码处理方式,以避免乱码。

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
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 org.apache.commons.io.IOUtils;

@WebServlet("/download")
public class FileDownloadServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 从请求参数中获取要下载的唯一文件名(例如:UUID_原始文件名.ext)
        String uniqueFileName = request.getParameter("filename");
        if (uniqueFileName == null || uniqueFileName.trim().isEmpty()) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "缺少文件名参数。");
            return;
        }

        // 从唯一文件名中提取原始文件名,以便计算哈希值和作为下载时的显示名称
        String originalFileNameForDisplay = uniqueFileName;
        int underscoreIndex = uniqueFileName.indexOf('_');
        if (underscoreIndex != -1 && underscoreIndex < uniqueFileName.length() - 1) {
            originalFileNameForDisplay = uniqueFileName.substring(underscoreIndex + 1);
        }

        // 重新构建文件在服务器上的实际存储路径
        String uploadBaseDirPath = getServletContext().getRealPath("/WEB-INF/uploads");
        File baseDirectory = new File(uploadBaseDirPath);

        // 使用原始文件名的哈希值计算打散目录
        int fileNameHash = originalFileNameForDisplay.hashCode();
        String hashHex = Integer.toHexString(fileNameHash);
        
        File actualFile = null;
        if (hashHex.length() >= 2) {
             File subDirectory = new File(baseDirectory, hashHex.charAt(0) + File.separator + hashHex.charAt(1));
             actualFile = new File(subDirectory, uniqueFileName); // 最终文件路径
        } else {
             // 极少数情况哈希值字符串长度不足,直接放在根目录或采取其他策略
             actualFile = new File(baseDirectory, uniqueFileName);
        }
        
        if (!actualFile.exists() || !actualFile.isFile()) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件未找到或无法访问。");
            return;
        }

        // 1. 获取文件的MIME类型
        String mimeType = getServletContext().getMimeType(actualFile.getName());
        if (mimeType == null) {
            mimeType = "application/octet-stream"; // 默认MIME类型,表示是二进制流
        }

        // 2. 设置响应头:Content-Type
        response.setContentType(mimeType);

        // 3. 设置响应头:Content-Disposition,指示浏览器以附件形式下载
        String encodedDisplayFileName = getEncodedFilenameForDownload(originalFileNameForDisplay, request);
        String contentDispositionHeader = "attachment; filename=\"" + encodedDisplayFileName + "\"";
        response.setHeader("Content-Disposition", contentDispositionHeader);

        // 4. 通过流将文件内容写入响应
        try (InputStream inputStream = new FileInputStream(actualFile);
             OutputStream outputStream = response.getOutputStream()) {
            IOUtils.copy(inputStream, outputStream); // 使用Apache Commons IO简化流复制
        } catch (IOException e) {
            throw new ServletException("文件下载失败。", e);
        }
    }
    
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

    /**
     * 根据不同的浏览器Agent对文件名进行编码,以解决下载时的中文乱码问题。
     * @param filename 要进行编码的原始文件名
     * @param request HTTP请求对象
     * @return 编码后的文件名字符串,适用于Content-Disposition头
     * @throws IOException 编码失败时抛出
     */
    private String getEncodedFilenameForDownload(String filename, HttpServletRequest request) throws IOException {
        String userAgent = request.getHeader("User-Agent");
        String encodedFilename;

        if (userAgent.contains("Firefox")) {
            // Firefox浏览器使用Base64编码
            encodedFilename = "=?UTF-8?B?" + Base64.getEncoder().encodeToString(filename.getBytes(StandardCharsets.UTF_8)) + "?=";
        } else if (userAgent.contains("MSIE") || userAgent.contains("Trident")) { // IE及Edge兼容模式
            // IE/Edge浏览器使用URL编码,且需要将空格编码为%20
            encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8.toString()).replace("+", "%20");
        } else if (userAgent.contains("Chrome")) {
            // Chrome浏览器也使用URL编码,同样需要将空格编码为%20
            encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8.toString()).replace("+", "%20");
        } else {
            // 对于其他未知浏览器,默认使用URL编码
            encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8.toString());
        }
        return encodedFilename;
    }
}
标签: Servlet

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。