资讯动态

JSP文件夹上传实战:从webkitdirectory到Servlet目录重建

发布时间:2026/9/14 9:22:29 来源:尧图企业网站定制
做 JSP 项目的朋友应该都有过这种经历系统里要处理资料、图片、作业但input typefile一次只能选几个文件面对一个几十个文件的目录只能一个 Ctrl 一个 Ctrl 地加点得手都酸了。后来我研究了一下发现 JSP 网页里其实可以完整实现“文件夹上传”——选中的不只是单个文件而是整个目录连同里面的子目录结构和文件一次提交给后端自动落盘。这篇文章就是把我整理的详细步骤和可以直接抄作业的示例代码分享出来涉及前端如何拿到文件夹里的文件列表、如何把文件的相对路径一起传给后端、后端 Servlet 怎么用 commons-fileupload 接收并重建目录以及我在实际项目里踩过的一些坑和处理办法。无论你是在做 JSP 学生信息管理系统、个人展示页还是内部资料库这个方案基本都能直接套用。1. 需求与方案为什么 JSP 里文件夹上传不是“开箱即用”1.1 从单文件到文件夹HTML 原生能力的边界很多人一开始以为文件夹上传就是把input typefile加一个multiple属性这么简单。加了multiple之后确实能一次选多个文件但选的时候仍然是一个一个点选体验并不好而且即便一次选了多个文件它们之间的目录层级关系也会丢失——后端拿到的只是一堆平铺文件无法知道它们原本属于哪个子目录。真正让网页能“选文件夹”的开关是一个叫webkitdirectory的 HTML 属性。这个属性最早是 Chrome 提出并实现的后来被 Firefox、Edge 也兼容了。你只要把它写到input标签上浏览器弹出的文件选择窗口就会多出一个“上传文件夹”的选项选中一个目录后input.files里会包含该目录下所有子文件并且每个文件对象上会带一个webkitRelativePath属性记录这个文件在目录里的相对路径。比如你选了project文件夹里面有src/main/java/Hello.java那么这条记录的webkitRelativePath就是project/src/main/java/Hello.java。所以前端的能力其实已经给了我们一个“平铺列表 相对路径”的数据形态。接下来的问题是后端怎么把这些信息接住并按相对路径把文件逐个写到磁盘上。有人会问为什么 GitHub 网页版可以直接拖文件夹上传而自己的 JSP 页面不行其实 GitHub 也是读取了文件夹文件列表然后逐个或分块上传底层同样需要拿到 relativePath。原理并不神秘关键就是“文件列表 相对路径”这两样信息能安全地传给后端。1.2 技术组合怎么选前端标识 后端 Multipart 解析后端这块老 JSP 项目里有两条路线比较主流。一条是 Servlet 3.x 自带的上传 API也就是request.getPart()配MultipartConfig另一条是 Apache Commons FileUpload也就是常见的ServletFileUpload.parseRequest()这种方式。我的建议是如果你项目里 Servlet 版本比较老或者需要更细粒度地控制内存阈值、文件大小、临时目录直接用 commons-fileupload 会稳得多。它虽然是个“老库”了但对付绝大多数上传场景都够用社区里出问题的概率也小。服务端接受的请求类型必须是multipart/form-data。前端把每个文件作为file字段、把对应的相对路径作为path字段依次 append 到FormData里提交给 Servlet。Servlet 解析出所有FileItem后把文件类型的条目和字段类型的条目分别收集起来然后后按索引一一对应用相对路径在保存根目录下创建子目录、写入文件即可。整体思路一句话概括就是前端负责告诉后端“这个文件放哪”后端负责“按路径建目录落盘”。2. 开发准备依赖与项目结构调整2.1 准备 commons-fileupload 依赖如果你用的是 Maven 项目直接在你的pom.xml里加两段依赖即可dependency groupIdcommons-fileupload/groupId artifactIdcommons-fileupload/artifactId version1.5/version /dependency dependency groupIdcommons-io/groupId artifactIdcommons-io/artifactId version2.15.1/version /dependency如果项目不是 Maven 管理的那就手动下载commons-fileupload-1.5.jar和commons-io-2.15.1.jar并放到WebContent/WEB-INF/lib目录下。放到这里的原因是 Tomcat 启动后会把这个目录里的 jar 自动加载到 classpathServlet 运行时才能找到对应的类。一个容易忽略的细节commons-fileupload 底层的某些文件工具方法会依赖 commons-io所以这两个 jar 最好一起引入别只放 fileupload 一个。另外上面代码用的是javax.servlet.*包名适用于 Tomcat 9 及之前如果你用的容器是 Tomcat 10 及以上包名变成了jakarta.servlet.*注意把 import 同时改掉。2.2 项目目录规划与上传目录约定一个典型的 Eclipse 动态 Web 项目大概是这样的结构FolderUploadDemo/ ├── src/ │ └── com/ │ └── example/ │ └── UploadServlet.java ├── WebContent/ │ ├── index.jsp │ ├── upload.js │ └── WEB-INF/ │ ├── lib/ │ │ ├── commons-fileupload-1.5.jar │ │ └── commons-io-2.15.1.jar │ └── web.xml上传保存的根目录我建议不要直接写死在代码里也不要默认放在部署目录的 uploads 下面。虽然getServletContext().getRealPath(/)拿到的部署目录在本地开发时很方便但如果以后把应用部署到服务器上重启或重新部署时部署目录可能被清理你辛苦传的文件就没了。比较稳妥的做法是在项目外面指定一个固定的上传根目录比如System.getProperty(user.home) /uploads或者从配置文件里读取这样文件和服务工程解耦备份和迁移也都方便。当然如果你只是做本地 demo那直接用getServletContext().getRealPath(/)加子目录也完全可以后面第 4 章会单独讲这个问题。3. 完整实现前端取文件、后端落盘全流程3.1 前端页面接入 webkitdirectory前端页面非常简单一个文件选择的输入框加一个上传按钮就够了。关键点是input上同时加webkitdirectory和multiple两个属性。有些资料里还会加一个directory属性这个是非标准写法Chrome 下其实是通过webkitdirectory识别的directory写上也不碍事但最稳妥的还是webkitdirectory。% page contentTypetext/html;charsetUTF-8 languagejava % !DOCTYPE html html head meta charsetUTF-8 titleJSP 文件夹上传示例/title /head body h2请选择要上传的文件夹/h2 input typefile idfolderInput webkitdirectory multiple / button iduploadBtn开始上传/button div idtip尚未选择文件夹/div script srcupload.js/script /body /htmlmultiple的作用是允许一个input持有多个文件配合webkitdirectory之后用户选中整个目录时文件列表里会自动包含目录下全部文件。3.2 前端脚本遍历文件并组装 FormData接下来是核心的前端 JavaScript。逻辑分三步第一从fileInput.files拿到所有文件第二遍历每个文件把文件本身和它的webkitRelativePath一起 append 到FormData第三发送 POST 请求到后端 Servlet。document.getElementById(uploadBtn).addEventListener(click, function () { var fileInput document.getElementById(folderInput); var files fileInput.files; if (!files || files.length 0) { document.getElementById(tip).innerText 请先选择一个文件夹; return; } var formData new FormData(); for (var i 0; i files.length; i) { var file files[i]; // 兼容旧浏览器没有 webkitRelativePath 时直接使用文件名 var relativePath file.webkitRelativePath || file.name; formData.append(file, file); formData.append(path, relativePath); } document.getElementById(tip).innerText 开始上传共 files.length 个文件...; var xhr new XMLHttpRequest(); xhr.open(POST, uploadServlet, true); xhr.onreadystatechange function () { if (xhr.readyState 4) { if (xhr.status 200) { document.getElementById(tip).innerText 上传完成; } else { document.getElementById(tip).innerText 上传失败HTTP 状态码 xhr.status; } } }; xhr.send(formData); });这里有几个容易踩的小地方我特意处理了。一是file.webkitRelativePath在个别旧浏览器或者某些特殊场景下会返回空字符串所以我在后面用|| file.name兜底避免路径为空导致后端无法判断目录结构。二是前端什么都没选就点按钮时要主动拦截避免发一个空请求给后端。用XMLHttpRequest而不是fetch主要是考虑到老 JSP 项目里可能还要兼容 IE 之外的旧版内核浏览器而且XMLHttpRequest的事件回调更容易扩展进度显示。如果你项目全部是现代浏览器换成fetch也完全没问题。3.3 后端 ServletMultipart 解析与目录重建后端是整个方案的核心。我写了一个WebServlet(/uploadServlet)注解注册的 Servlet省去在web.xml里新增映射的麻烦。你在 Eclipse 里直接创建类然后复制全部代码就能跑通。package com.example; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; 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.disk.DiskFileItemFactory; import org.apache.commons.fileupload.servlet.ServletFileUpload; WebServlet(/uploadServlet) public class UploadServlet extends HttpServlet { private static final long serialVersionUID 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); PrintWriter out response.getWriter(); if (!ServletFileUpload.isMultipartContent(request)) { out.println(请求必须是 multipart/form-data 类型); return; } // 保存根目录可改成从配置文件读取 String saveRootPath System.getProperty(user.home) File.separator uploads; File saveRootDir new File(saveRootPath); if (!saveRootDir.exists()) { saveRootDir.mkdirs(); } DiskFileItemFactory factory new DiskFileItemFactory(); factory.setSizeThreshold(2 * 1024 * 1024); // 2MB factory.setRepository(new File(System.getProperty(java.io.tmpdir))); ServletFileUpload upload new ServletFileUpload(factory); upload.setFileSizeMax(100 * 1024 * 1024); // 单个文件最大 100MB upload.setSizeMax(500 * 1024 * 1024); // 整个请求最大 500MB try { ListFileItem items upload.parseRequest(request); ListFileItem fileItems new ArrayList(); ListString pathItems new ArrayList(); for (FileItem item : items) { if (item.isFormField()) { if (path.equals(item.getFieldName())) { pathItems.add(item.getString(UTF-8)); } } else { fileItems.add(item); } } if (fileItems.size() ! pathItems.size()) { out.println(文件数量与路径数量不一致上传中止); return; } for (int i 0; i fileItems.size(); i) { FileItem item fileItems.get(i); String relativePath pathItems.get(i).replace(\\, /); relativePath trimLeadingSlash(relativePath); File destFile new File(saveRootDir, relativePath); // 安全校验防止路径穿越 String canonicalParent destFile.getParentFile().getCanonicalPath(); String canonicalRoot saveRootDir.getCanonicalPath(); if (!canonicalParent.startsWith(canonicalRoot)) { out.println(非法路径 relativePath); return; } if (!destFile.getParentFile().exists()) { destFile.getParentFile().mkdirs(); } item.write(destFile); out.println(上传成功 relativePath); } out.println(全部文件处理完成共 fileItems.size() 个文件); } catch (Exception e) { e.printStackTrace(); out.println(上传异常 e.getMessage()); } } private String trimLeadingSlash(String path) { while (path.startsWith(/)) { path path.substring(1); } return path; } }3.4 关键代码解读相对路径传递与安全校验为什么我前面建议前端把path作为一个独立表单字段而不是直接拼到文件名里因为FileItem.getName()在不同浏览器里差异很大Chrome 可能返回完整路径Firefox 可能只返回文件名你不能指望这个名字自带目录结构。单独传webkitRelativePath语义最清晰后端只要把它和file按顺序对应起来就行。按顺序对应我是怎么保证的parseRequest返回的ListFileItem会保持请求体里字段的原始顺序。前端在 FormData 里先 appendfile再 appendpath循环了很多轮所以后端拿到的列表也是按同样顺序排列的。只要数量一致索引就能对上。关于安全校验这个不能省。用户恶意构造一个包含../的路径比如../../etc/passwd如果不处理文件就可能被写到保存目录之外造成很严重的安全问题。我这里先统一把反斜杠转成正斜杠去掉开头的斜杠再用getCanonicalPath()拿到真实绝对路径做前缀判断只要目标父目录不在保存根目录下就直接拒绝。这一套看起来啰嗦但上线项目必须要写。4. 上线前必须处理的坑常见问题与排查技巧4.1 相对路径丢失或反斜杠问题有朋友在本地测的时候发现文件倒是传上去了但子目录全没了所有文件都平铺在保存根目录下。排查时先看浏览器抓包看请求里的path字段是不是空的。如果path为空多半是浏览器版本或者换浏览器导致webkitRelativePath不可用如果path里有\说明传的是 Windows 风格路径后端记得统一把\替换成/再交给new File(saveRootDir, relativePath)使用否则在 Windows 上有时会解析异常。别小看这个很多人改到一半发现目录结构不对其实就差这一行替换。4.2 中文文件名和路径乱码JSP 文件夹上传最典型的问题就是中文文件名或者中文目录名上传后变成乱码。这里有两个地方必须同时设置前端页面本身的meta charsetUTF-8以及后端request.setCharacterEncoding(UTF-8)。特别是后端那句一定要放在读取任何参数之前。另外path是普通表单字段我在代码里是用item.getString(UTF-8)来读取的而不是item.getString()如果你用后者默认编码可能是 ISO-8859-1中文必然乱码。这一条是我被问过最多的问题没有之一。4.3 大目录上传卡顿与超时几百个文件一起上传时前端遍历没问题但一次性把大量文件 append 到 FormData然后发一个超大请求很容易出现两个情况后端解析时间过长容器或浏览器把连接断掉或者后端内存被撑爆。处理思路有几条。后端这边DiskFileItemFactory的setSizeThreshold控制文件在内存中的临界值超过这个值会自动写入临时目录setFileSizeMax和setSizeMax分别控制单个文件和整个请求的上限。前端这边如果文件总量很大建议分批次发送例如每 10 个文件一组一组一组地 POST既能降低单次请求压力也方便做上传进度和失败重试。不要小看批量上传的“批量”二字真正到了生产环境永远要把数据量往最坏情况估计。4.4 不同浏览器的兼容差异webkitdirectory虽然名字里带 webkit但现代 Chrome、Edge、Firefox 都已经支持了。真正头疼的是 SafarimacOS 和 iOS 上的 Safari 对文件夹选择的支持比较弱iOS 下基本无法直接选文件夹。如果项目用户里有 Safari 使用者建议加一个“拖拽上传”作为替代方案用户把一个文件夹拖到网页里前端通过DataTransferItem.webkitGetAsEntry()递归读取目录结构然后再走同一套上传逻辑。另外IE 从 11 开始也不再支持这类新特性如果你是给老系统做升级最好让用户简洁地把文件夹打成压缩包上传后端再用解压工具处理。这个方案虽然没有文件夹上传那么直观但兼容性最稳。4.5 重启后上传文件丢失问题如果你一开始图省事把保存路径写成了getServletContext().getRealPath(/) uploads会发现本地跑得好好的可一旦杀掉 Tomcat 重启或者用 Eclipse 重新部署之前上传的文件就全没了。原因是getRealPath指向的是 Tomcat 解压工程后的目录重新部署或者 clean 时很可能被清掉。这个坑我在最开始也踩过。后来我统一改成了“外部目录 配置文件”的方案。具体做法是在web.xml或一个 properties 配置文件里定义上传根目录Servlet 启动时读取一次没有就创建。这样无论项目怎么重启文件都安安稳稳躺在外部磁盘上备份也方便。我把这个方案常见问题整理成一张速查表方便你排查问题现象可能原因处理方式文件上传后全部平铺目录消失前端没有传 path 或 path 为空检查 webkitRelativePath 是否兼容增加|| file.name兜底中文文件名变成乱码后端没有用 UTF-8 读取 path 字段设置request.setCharacterEncoding(UTF-8)并用item.getString(UTF-8)上传几百个文件后请求中断单次请求体过大或超出容器超时分批上传合理设置setSizeMax增加超时时间部分浏览器无法选择文件夹Safari/iOS/IE 不支持 webkitdirectory增加拖拽上传或压缩包上传方案Tomcat 重启后文件消失保存路径在部署目录内改为外部目录或配置文件指定5. 实操心得与后续扩展5.1 我在真实项目里的使用感受这个文件夹上传方案我最早是在一个档案管理相关的 JSP 系统里落地的。当时的业务场景是需要批量上传一批项目资料每个项目下面有合同、方案、图片、附件等多个子目录文件几十个很正常上百个也不少。没做文件夹上传之前用户是一个目录一个目录打开多选文件后再传最后在后端手动建目录搬文件非常痛苦。换成本文的方案之后前端选根目录就能把所有子目录和文件一次传完后端自动还原目录结构用户的满意度提升非常明显。但我也要老实说前端webkitdirectory的体验虽然好不代表这个功能就“做完”了。真正上线前我做了几件加分的事一是加了上传前文件数量、总大小的提示和校验二是加了单文件进度和总数进度显示避免用户看不到进度而反复点击三是在 Servlet 里打印了完整的日志包括每个文件保存的位置和大小排查问题的时候能少走很多弯路。这些看起来都是小事但在真实使用里非常重要。5.2 还可以怎样扩展这个能力如果你想让这个功能更好用有两条路可以走。第一条是在上传完成后把文件的相对路径、大小、类型等元数据记录到数据库里这样页面展示时不用再扫磁盘直接查表就行这也是很多 JSP 学生信息管理系统里附件模块的常见做法。第二条是给文件生成一个唯一标识比如用时间戳加随机数重命名然后把原始相对路径存在数据库里这样能避免文件名冲突也方便后续文件下载时做权限控制。另外如果你遇到超大目录的极端情况可以考虑前端先把整个文件夹压缩成 zip后端再解压这种思路对于网络不稳定或者浏览器兼容要求较高的场景反而会比逐个文件上传更可靠。关于离开页面的提示也顺带说一句如果你发现 JSP 页面在文件上传过程中弹出了“确定要离开此页面吗”的提示先别急着屏蔽检查一下是不是上传逻辑里不小心触发了beforeunload事件或者项目引用的某个第三方 JS 库主动注册了离开监听。如果上传是异步的页面本身不需要跳转这个提示确实没必要但如果你确实要防止用户在中途关闭页面那保留提示反而是一种保护。很多人在 JSP 里莫名其妙看到这个提示第一反应是屏蔽它却忘了去检查是不是第三方脚本在捣乱。最后再分享一个小技巧调试 JSP 后端时如果你发现代码改了却没生效多半是 Tomcat 里编译后的 class 没有更新。可以在项目的 work 目录里找到对应的UploadServlet.class确认 Tomcat 到底加载了哪个版本的代码。这个排查点虽然听起来基础但能帮你省下不少无谓的困惑。希望上面这套步骤和代码能直接解决你的问题也欢迎你在实际项目里把细节调成最适合自己的样子。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价