资讯详情

资讯详情

jspSmartUpload组件源代码解析:文件上传原理与老系统维护实战

简介jspSmartUpload组件源代码是一套面向Java Web开发者的文件上传下载解决方案特别适合在JSP项目中快速实现文件传输、并需要解决中文文件名乱码问题的场景作者针对原组件在下载中文文件时出现的编码缺陷进行了修复修改后的逻辑能够正确保留中文文件名使组件更贴合国内项目的实际需求。资源以rar压缩包发布整包仅37KB共包含13个文件其中有6个Java源文件、6个class编译文件以及1个可直接引用的jar包源文件便于研究上传下载流程并做二次开发class文件可在编译阶段直接使用jar包则适合快速集成到Web应用中压缩包内文件对应关系清晰方便按需查看源码与编译产物。目前已有289人浏览学习适合有基本JSP基础、想深入理解轻量级上传组件机制或排查乱码问题的开发者参考通过这份代码读者既能阅读ServletUpload等核心类的完整实现也能直接获得修复版的关键逻辑与打包好的组件产物为实际项目提供一套可运行、可改造的文件传输基础方案。1. jspSmartUpload 组件源代码一个还在用的老牌上传下载库接手一个维护了很久的老系统用户反馈文件上传一直失败后台异常停在某个“类找不到”的报错上。翻到 JSP 页面发现调用的是 jspSmartUpload这个组件在 Servlet/JSP 时代几乎是文件上传下载的事实标准如今很多人只听过名字手里却没有能直接编译的完整源代码。jspSmartUpload 组件源代码的价值就在这里它本身是一个能独立编译、二次改造的 Java 源码包适合给老系统补上传接口、排查历史代码里的兼容问题也适合用来理解 multipart/form-data 的解析过程。维护老项目的人需要它新接触 Servlet 上传的开发者同样能从源码里学到不少协议层面的细节。下面按解析原理、编译打包、上传配置、常见坑位的顺序一步步拆开把这份源码真正用起来。2. jspSmartUpload 的解析原理从 multipart 表单到内存文件2.1 一次上传浏览器到底发来了什么很多开发者第一次接触 jspSmartUpload 时是直接拿 jar 包当黑匣子用的页面把文件丢给 SmartUpload然后调 save 就完事了。一旦出问题不知道中间发生了什么。这里的关键在于理解 HTTP 层的数据格式。普通表单提交时请求体是一段keyvaluekeyvalue的 URL 编码文本。一旦 form 标签里写了enctypemultipart/form-data请求体就变成多段拼接的二进制内容每段之间用一个 boundary 字符串分隔。一个含普通字段和文件字段的请求体大概长这样------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; namenickname 某个昵称 ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; namefile; filenametest.txt Content-Type: text/plain 文件内容…… ------WebKitFormBoundary7MA4YWxkTrZu0gW--浏览器会随机生成 boundary并在 Content-Type 头里带给服务端。jspSmartUpload 的upload()方法做的事情就是从request.getInputStream()读字节流按 boundary 切段再看每一段的 Content-Disposition 里有没有filename有则是文件没有则是普通表单字段。这个判断逻辑在源码里就是几行字符串查找但它是整个组件工作的核心。这里有一个容易忽略的点普通字段和文件字段是混在同一个流里的所以 jspSmartUpload 在解析完成后会把原始 request 的输入流消费掉。因此执行完upload()之后再调用原生request.getParameter()会拿不到任何表单参数必须通过su.getRequest()去取。这个设计影响了后面 Servlet 里的写法也是老项目最容易改错的地方。2.2 SmartUpload 的类分工谁负责解析、谁负责存储源码包打开后com.jspsmart.upload包下的类并不算多但每一个都有明确的职责。把类分工看明白后面做二次改造才有底。类名职责对应方法/属性SmartUpload门面类负责初始化、解析请求、保存文件、下载文件initialize、upload、save、downloadFiles文件集合包装了本次上传的所有文件getCount、getFile、getSizeFile单个上传文件代表一个文件字段getFileName、getFileExt、getSize、saveAsRequest请求参数包装器保存解析出的普通表单字段getParameterResponse响应包装器主要用于下载时设置响应头内部使用实际调用时用户接触最多的是 SmartUpload 和 File。SmartUpload.initialize()负责绑定当前 Servlet 的 ServletConfig、HttpServletRequest 和 HttpServletResponseupload()负责触发解析文件保存则有两个路径要么调SmartUpload.save()直接批量保存所有文件要么遍历getFiles()后逐个调File.saveAs()保存。批量 save 适合所有文件存同一个目录、使用默认文件名的场景逐个 saveAs 适合需要对文件名二次处理的场景比如加时间戳防重名、按子目录分类。源码里还保留了Request类这是它的一个“私货”因为原生的 request 流已经被 upload 消费掉于是组件把解析出的普通字段缓存到自己维护的 Map 里再通过getRequest().getParameter()暴露出来。理解了这个设计就不会把原生 request 和包装 Request 搞混。2.3 和 commons-fileupload 怎么选什么时候该用这份源码但凡做过 Java 文件上传的都会拿 jspSmartUpload 和 commons-fileupload 对比。选择依据其实很简单。commons-fileupload 是 Apache 生态下的标准方案采用流式解析文件超过阈值会先落临时文件内存压力小且提供了 DiskFileItemFactory、ServletFileUpload 等完整 API。但它的缺点是依赖 commons-io项目如果想尽量减少第三方 jar或者是在一个不能随意升级依赖的老环境里引入它要连带处理多个包。jspSmartUpload 的优势是单 jar、无第三方依赖、API 极简。老系统的 JSP 页面里可能直接这么写jsp:useBean idsu scopepage classcom.jspsmart.upload.SmartUpload / % su.initialize(pageContext); su.upload(); %这种写法对从 JSP 维护时代过来的项目来说非常友好几乎不需要额外学习成本。但它的短板也明显默认把请求体整体读入内存再做拆分单个文件稍大就容易出现内存压力对异常文件类型的过滤也比较粗。所以在选型上我的经验是如果是新项目优先把 commons-fileupload 的流式方案作为默认项如果是在维护老系统或者只想用一个 jar 快速完成不超过几十 MB 的简单上传jspSmartUpload 源码包完全够用。3. 编译与接入把源代码从 src 变成可用的 jar3.1 源码目录里该看到的文件拿到 jspSmartUpload 源代码后不要急着丢进 IDE。先看目录结构确认源码是否完整。一个标准的源码包src/com/jspsmart/upload/目录下至少应该有 SmartUpload.java、File.java、Files.java、Request.java、Response.java 这五个核心文件有的版本还会带一个 ServletUpload.java 之类的辅助类以及一个示例用的 JSP 页面和 web.xml 片段。第一步先确认每个类大概做什么再检查是否有缺失。最常见的情况是网上流传的源码包只带了编译好的 jar没有 src 目录这种其实不符合“源代码”的定义。另一些包把源码和示例混在一起src 目录被拆分到多个子目录需要手工整理回标准包结构。src/com/jspsmart/upload/ ├── SmartUpload.java ├── File.java ├── Files.java ├── Request.java └── Response.java在这个结构基础上编译时还需要一个 servlet-api.jar。因为 SmartUpload 的 initialize 方法签名里带 ServletConfig、HttpServletRequest、HttpServletResponse这些类不在 JDK 里必须在 classpath 中提供。如果你找不到合适的 jar可以从项目里已用的 Servlet 容器目录里拷贝一份或者从本机 Maven 仓库中取 javax.servlet-api 的对应版本。3.2 用 javac 手动编译源码包因为这是一个源码包最稳妥的复现方式是手动编译而不是直接用现成 jar。整个过程就两条命令mkdir -p out javac -encoding UTF-8 -cp servlet-api.jar -d out src/com/jspsmart/upload/*.java jar cf jspsmartupload-custom.jar -C out .-encoding UTF-8是防止源码注释里的中文乱码有的老源码文件是 GBK 编码需要根据实际文件编码调整-cp servlet-api.jar指定编译期依赖-d out把生成的 .class 写到 out 目录最后的jar cf把 out 目录下的内容打成 jar-C out .表示进入 out 目录再打包这样 jar 根目录就是 com 包路径不会多套一层目录。编译过程中如果报错说“程序包 javax.servlet 不存在”说明 -cp 路径写错了或 servlet-api.jar 版本不对。如果报错提示某个类缺少方法多半是源码版本和 servlet-api 版本相差太大比如老版本的 SmartUpload 用了javax.servlet.jsp.PageContext作为 initialize 参数这时还需要把 jsp-api.jar 也加进 classpathjavac -encoding UTF-8 -cp servlet-api.jar:jsp-api.jar -d out src/com/jspsmart/upload/*.javajar 打好之后可以用jar tf jspsmartupload-custom.jar | head快速验证包内结构确认 com/jspsmart/upload/SmartUpload.class 存在。这一步通过说明源码本身没有编译问题。3.3 把 jar 丢进 WEB-INF/lib 的最小运行环境编译好的 jar 接进项目最常见做法是放到WEB-INF/lib/下容器启动时会自动加载。如果不走 Maven直接把 jspsmartupload-custom.jar 复制到该目录即可如果走 Maven可以在项目根目录执行 install 把它装进本地仓库mvn install:install-file -Dfilejspsmartupload-custom.jar -DgroupIdcom.legacy \ -DartifactIdjspsmartupload -Dversion1.0 -Dpackagingjar装进本地仓库后pom.xml 里就能用坐标引用了。这里有一个容易踩的坑老版本源码编译时可能依赖旧版 servlet-api如果当前容器已经升级到新版 Servlet 规范jar 打包时不会报错但运行时会在类加载阶段出现 ClassNotFoundError。所以接入现有项目前最好先确认项目的 Servlet 容器版本与编译时使用的 servlet-api 版本在同一个大版本下。老项目通常用的是 3.1 或更早版本这个源码包在这些环境里基本是即插即用的。最小运行环境其实不需要额外的配置文件只要保证 jar 在类路径下、页面或 Servlet 能正常 new 出 SmartUpload 对象就算接入成功。下一步要做的才是真正的功能实现写一个能接收文件的 Servlet 或 JSP。4. 文件上传实操表单、Servlet 与四个必要设置4.1 表单enctype 和 multipart 的写法文件上传的起点是页面表单。很多新手只记得加input typefile却漏写 enctype结果后端拿到的还是普通表单格式。这里给一个最标准的模板form action/upload methodpost enctypemultipart/form-data input typetext namenickname / input typefile namefile / button typesubmit提交/button /form三个关键点form 的 method 必须是 post因为文件内容要通过请求体传递enctype 必须是multipart/form-data否则文件内容不会以多段报文传输文件域的 name 需要和后端遍历时用到的名称一致不过 jspSmartUpload 是按文件集合遍历的对 name 的依赖不强。在 JSP 时代的写法里也常见直接在 JSP 页面里内嵌 Java 脚本调用组件比如su.initialize(pageContext)。这种方式在维护老页面时能少写一个 Servlet但它把业务逻辑写进了视图层后续如果要扩展上传目录或加校验会非常别扭。如果是新写接口我建议走 Servlet 路线把解析和保存逻辑收敛在一起。4.2 Servlet 调用upload() 与 getRequest() 的关系先看一段完整的 Servlet doPost 实现protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { SmartUpload su new SmartUpload(); su.initialize(getServletConfig(), request, response); su.upload(); String nickname su.getRequest().getParameter(nickname); for (int i 0; i su.getFiles().getCount(); i) { File f su.getFiles().getFile(i); if (!f.isMissing()) { String saveDir /data/upload/; File dir new File(saveDir); if (!dir.exists()) { dir.mkdirs(); } String finalName System.currentTimeMillis() _ f.getFileName(); f.saveAs(saveDir finalName); } } response.getWriter().write(upload ok: nickname); }这段代码里有几个值得说明的细节。su.initialize(getServletConfig(), request, response)的 getServletConfig 不能漏因为组件内部需要用它读取 Servlet 初始参数su.upload()一旦执行原生 request 的输入流就被读完了所以普通字段只能通过su.getRequest().getParameter(nickname)获取不能再调request.getParameter(nickname)后者永远返回 null。遍历文件时用su.getFiles().getCount()拿文件总数再用getFile(i)拿单文件。f.isMissing()用来跳过未选文件的空输入框如果不判断空文件域也会被当成一个缺失文件处理。保存时先判断目录是否存在并 mkdirs这一步看似多余但能避免目录不存在时 saveAs 直接抛异常或静默失败。文件名前面加时间戳是为了防止不同用户上传同名文件互相覆盖。4.3 必设参数setMaxFileSize、setDeniedFilesList、setAllowedFilesList直接裸用 SmartUpload默认情况下对文件大小和类型没有任何限制这在生产环境里等于不设防。在调用 upload() 之前建议把下面几个参数全部设置好方法作用示例值setMaxFileSize(long)限制单个文件大小单位字节5 * 1024 * 1024 表示 5MBsetTotalMaxFileSize(long)限制一次请求所有文件总大小20 * 1024 * 1024setDeniedFilesList(String)设置禁止上传的扩展名列表逗号分隔jsp,jspx,exe,bat,shsetAllowedFilesList(String)设置允许上传的扩展名列表逗号分隔jpg,gif,png,pdf一段带限制的完整初始化SmartUpload su new SmartUpload(); su.initialize(getServletConfig(), request, response); su.setMaxFileSize(5 * 1024 * 1024); su.setTotalMaxFileSize(20 * 1024 * 1024); su.setDeniedFilesList(jsp,jspx,exe,bat,sh); su.upload();这里要着重提醒setDeniedFilesList 和 setAllowedFilesList 是两套互斥的过滤逻辑实际项目中只启用一套即可不要两个都设置。比如既设置了 allowList 为 jpg又设置 denyList 为 exe一旦后续维护的人把规则改乱很容易出现“用户传了个合法图片却被拒绝”的诡异问题。设置的顺序也重要这些参数必须在 upload() 之前调用因为 upload() 时组件已经按当时的配置执行过滤了之后再设置不会对本次请求生效。过滤逻辑是以扩展名黑名单/白名单方式实现的它能在一定程度上挡住直接上传 JSP 的恶意行为但不能当作唯一防线。文件上传到服务器后真正起决定性作用的是 Web 容器对 upload 目录的执行权限设置这个问题下面一章会专门展开。5. 避坑清单jspSmartUpload 移植时最常踩的五个坑5.1 中文文件名与中文参数乱码现象上传一个“测试报告.pdf”服务端保存后变成“????.pdf”表单里的昵称也变成乱码。原因jspSmartUpload 在解析 multipart 字节流时默认按 ISO-8859-1 做字节到字符串的转换而浏览器端提交的是 UTF-8 编码的中文。解决取到文件名后手工转码一次。String rawName f.getFileName(); String fileName new String(rawName.getBytes(ISO-8859-1), UTF-8);如果源码里是 GBK 编码的 Servlet要相应调整第二个参数。更彻底的做法是修改源码把解析处在拿到字节后直接用 UTF-8 构建字符串然后重新编译 jar这样就不用每次都在业务代码里转码。注意 IE 老版本上传时会把文件路径带上来getFileName()可能返回C:\fakepath\测试报告.pdf需要先截取最后一个\或/后面的部分。5.2 大文件上传把 JVM 内存撑爆现象上传一个几 GB 的压缩包后台一直抛出 OutOfMemoryError轻则上传失败重则整个应用卡死。原因jspSmartUpload 的 upload() 会把整个请求体读入内存然后才做拆分和过滤单文件大小限制只在校验时生效但数据已经先进内存了。解决大文件场景必须换用基于流式解析的 commons-fileupload或者对上传接口做前置限制在 Servlet 容器层配置 maxPostSize阻止超大请求体进入业务代码。如果一定要用 jspSmartUpload就把最大文件限制调低同时注意 JVM 堆内存设置给上传留出足够余量。有一个取巧的补救方式在 Nginx 或网关层限制客户端请求体大小比如client_max_body_size 20m这样到达 Java 应用层的请求已经被拦了一道内存压力可控。但这只是把问题外置源码层面的内存模型没有变。5.3 新版 Servlet 容器下的 ClassNotFoundException现象项目从老容器迁移到新容器后启动或上传时抛出ClassNotFoundException: javax.servlet.ServletConfig。原因新版 Servlet 规范把 API 包名从 javax.servlet 迁移到了 jakarta.servlet而 jspSmartUpload 源码里写的还是 javax.servlet 包名。解决两个办法一是让项目继续运行在兼容旧规范的 Servlet 容器版本上二是手动修改源码里的 import 和代码中的所有 javax.servlet统一替换为 jakarta.servlet再重新编译 jar。替换时要注意除了 import 语句initialize 方法签名里的类型引用也要同步替换只改 import 不够。这个坑在老项目升级容器时最隐蔽因为在 IDE 里编源码不会报错运行时才炸。5.4 saveAs 目录不存在的“假成功”现象接口返回“上传成功”但去指定目录看文件不存在或者偶尔存在、偶尔不存在。原因saveAs 传的是相对路径时最终落盘位置取决于 Servlet 容器的当前工作目录用 getRealPath 生成的路径在 war 包部署方式下又可能拿到一个临时解压目录容器重启后目录被清空或变化造成文件丢失。解决不要用相对路径也不要依赖 getRealPath直接在配置文件中定义一个绝对路径上传前用 File 创建目录String saveDir /data/app/upload/; File dir new File(saveDir); if (!dir.exists()) { dir.mkdirs(); }这个绝对路径要保证 Java 进程有写权限。常见做法是放在应用配置文件里运维部署时按实际磁盘路径调整而不是写死在代码里。5.5 危险后缀没过滤站点被上传了 JSP 后门现象线上站点被人传了一个shell.jsp服务器马上被拿下日志里能看到来自外部的上传请求。原因jspSmartUpload 默认不做任何扩展名限制攻击者只要找到一个可以上传的接口就能直接把可执行的 JSP 脚本传上来。解决上传前设置拒绝列表把 jsp、jspx、war 等容器可执行的扩展名都禁掉同时不要把上传目录放在 Web 容器的可执行目录内。正确做法是把上传目录配置在应用根目录之外例如/data/uploads/然后通过独立的下载接口读取文件而不是让容器直接静态映射。这样即使传上来一个叫 jsp 的文件它也没有执行入口。还要留意路径穿越如果直接使用原始文件名拼接保存路径攻击者可以通过../../../../tmp/shell.jsp这类文件名把文件写到其他目录。所以保存时要么把文件名改成随机字符串要么把原始文件名里的..、/、\全部替换掉。推荐统一使用服务端生成的文件名原始名称只保留在数据库记录里不参与文件系统路径。6. 下载功能与源码改造把一个老库用出新花样jspSmartUpload 不只做上传它的 SmartUpload 类里也封装了下载方法。老代码里常见的是直接调su.download(/data/files/xxx.pdf)但这种方法在文件名包含中文时有一个典型缺陷Content-Disposition 头里直接拼了原始文件名老浏览器会乱码某些浏览器还会直接忽略文件名。我的做法是在业务代码里覆盖掉组件生成的响应头su.download(savePath); String fileName 测试文件.pdf; String encodedName URLEncoder.encode(fileName, UTF-8).replaceAll(\\, %20); response.setHeader(Content-Disposition, attachment; filename\ encodedName \);先用su.download()完成流写出再设置兼容现代浏览器的 Content-Disposition这样既保留了组件对文件流的处理能力又解决了中文文件名下载乱码。URLEncoder 默认把空格编码成所以要用 replaceAll 替换成%20否则部分浏览器会把加号当成字面量。如果想彻底一点可以直接改源码在 SmartUpload 的 download 方法里把 Content-Disposition 的生成逻辑替换成上面这段编码方式重新编译 jar之后所有调用点都不用再补这一刀。这个改造只涉及一个方法风险很低适合作为第一次读源码、改源码的练手项目。从那以后我每次接入老组件都不是直接拿 jar 用了事而是先编译一遍源码确认它能通过当前的 JDK 和 Servlet API 编译再顺手把中文文件名、绝对路径这类老坑补上。这份 jspSmartUpload 组件源代码值得在动手前认真读一遍也希望这篇文章能帮你在复现和改造的路上少走几步弯路。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →