做JSP老项目维护和二次开发的朋友,应该都遇到过这种场景:客户发来一个几十MB甚至上百MB的文件,要求通过网页上传到服务器。传统JSP项目里,Form表单一次性提交,要么因为请求超时直接断掉,要么就是Tomcat默认的2MB上传限制把人卡死。我早期做传统JSP项目时,最怕的就是"上传大文件"这四个字,直到后来在项目里接入了WebUploader分块上传,才算是把这根刺拔掉。WebUploader是百度开源的一个上传组件,它的核心思路就是分块上传,也就是把一个大文件切成多块,逐块发送到服务端,最后再由服务端把这些分块合并成一个完整文件。这篇文章我就从JSP视角,把WebUploader分块上传的源码示例和实现原理掰开揉碎讲一遍。
很多教程只讲了前端怎么调组件,真正落到JSP项目里时,你会发现还有一堆问题要处理:后端接口怎么写、分块怎么接收、合并逻辑放哪里、断点续传怎么判断、文件校验怎么做。这些我都会在这篇源码解析里给出完整示例,并把参数设计背后的理由说清楚。无论你是刚接手老项目的开发,还是正在做基于JSP的毕业设计,这个方案都能直接用。
1. 为什么传统JSP项目里大文件上传总是让人头疼
1.1 一次性上传方式的三座大山
以前的JSP项目做文件上传,最普遍的做法就是Commons FileUpload或者Servlet 3.0的Part接口,配合一个简单的<input type="file">表单,点击提交之后后端一次性接收整个文件。这种方式对小文件没什么问题,一旦文件达到上百MB,问题就全冒出来了。
第一座大山是超时。文件通过HTTP传输时,受网络带宽限制,传输时间本身就是不可控的。很多Servlet容器默认的请求超时设置在几十秒到几分钟,如果一个100MB文件在网络状况一般的情况下传了一半,请求超时被容器强制断开,用户那边只有一个"上传失败"的提示,文件连影子都没见着。第二座大山是内存和临时目录压力。Tomcat接收大文件时,如果配置不当,会把整个请求体读入内存,或者写入磁盘临时目录,内存溢出和磁盘占用率飙升都是常见现象。第三座大山是体验问题。一次性上传期间,用户看不到任何进度,也不知道是成功还是失败,等待过程中有一种"听天由命"的感觉。
这三座大山本质上都源于同一个设计缺陷:把整个文件当作一个不可分割的整体,在一次网络请求里完成传输。这就好比搬家时把所有家具塞进一辆小货车,路况不好就翻车,而分块上传的思路是让一辆小车只搬一部分,多跑几趟。
1.2 WebUploader分块上传是如何绕开这些问题的
WebUploader分块上传的核心机制,是在前端通过HTML5的File API把文件切片,每个分块大小可以配置,比如默认的chunkSize是2MB,然后通过threads参数控制并发上传的线程数,比如一次同时传3个分块。每个分块都是独立的HTTP请求,服务端逐块接收并保存到临时目录。所有分块传完之后,由前端发送一个合并请求,后端再把这些临时文件按顺序拼接成最终文件。
这套机制绕开大文件上传困境的方式很直接:每个请求体变小了,请求超时问题不再出现,某个分块失败只需要重传那一个分块,而不是整个文件;临时文件的内存占用也大幅降低,因为服务端处理的是固定大小的块,而不是整个文件流。同时,WebUploader会为每个文件生成一个MD5值,通过MD5值可以做秒传判断和断点续传,这些是原生input[type=file]完全不具备的能力。
还有一个容易被忽略的优势:分块上传天然支持并发。前端一次发3个分块请求,服务端的处理压力分散到多个请求上,上传的总体吞吐量比串行的一次性请求更高。尤其在内网传输或者上行带宽较充足的环境里,这个优势非常明显。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 项目环境准备与WebUploader资源引入
2.1 这套方案需要的开发环境
在开始写源码前,先把环境说清楚。我这里讲的方案基于传统的JSP项目,使用Servlet 3.0或Spring MVC皆可。如果你用的是IDEA新建的一个JSP项目,或者是一个需要打成War包部署在Tomcat的老项目,这套方案都适用。我在实际项目里用的是Spring MVC,但Servlet方式也可以,后面我会分别给出接收分块和合并分块的接口写法。
前端方面,WebUploader的基础运行环境是HTML5浏览器,IE9及以下浏览器会自动降级为Flash上传模式。如果是内部管理系统,我建议直接要求用户使用现代浏览器,省去处理Flash兼容的麻烦。
需要引入的资源文件有这些:
- WebUploader的CSS样式文件
webuploader.css - WebUploader的JS核心文件
webuploader.min.js - jQuery(WebUploader依赖jQuery,我用的版本是jQuery 1.12.4)
- 上传界面相关的HTML结构和按钮样式
我在项目里是把这些资源放在webapp/statics/uploader/目录下,然后在JSP页面中按顺序引入CSS和JS。如果你用的是Spring Boot集成JSP的项目,资源放到src/main/resources/static/uploader/,JSP放在src/main/webapp/WEB-INF/views/下,引入方式一样。
2.2 JSP页面中引入上传组件资源和前端骨架
新建一个upload.jsp页面,页面的重点不是花哨的UI,而是把上传组件的基础结构搭好。WebUploader的界面由一个选择按钮、一个文件列表容器和一个上传按钮组成。
jsp复制<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>WebUploader分块上传示例</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/statics/uploader/webuploader.css">
<script src="${pageContext.request.contextPath}/statics/js/jquery-1.12.4.min.js"></script>
<script src="${pageContext.request.contextPath}/statics/uploader/webuploader.min.js"></script>
</head>
<body>
<div id="uploader" class="wu-example">
<!-- 存放文件选择按钮 -->
<div class="btns">
<div id="picker">选择文件</div>
<button id="ctlBtn" class="btn btn-default">开始上传</button>
</div>
<!-- 文件列表容器 -->
<div id="thelist" class="uploader-list"></div>
</div>
</body>
</html>
pageContext.request.contextPath是JSP里获取项目根路径的标准方式,这个必须写,否则JS和CSS路径在部署到不同上下文路径时会404。picker是选择按钮的容器,thelist是上传文件列表的展示区域,WebUploader会自动把每个文件的状态信息渲染到这个容器里。
这一步没有特别复杂的地方,但有一个容易踩的坑:CSS或JS路径写错会导致界面样式不加载、组件无法初始化。我见过不少刚接触JSP的初学者,把webuploader.css路径写成了相对路径webuploader/webuploader.css,结果页面一刷新样式全乱。用${pageContext.request.contextPath}拼绝对路径是JSP项目里的标准做法,能在任何目录层级下正确找到资源。
3. 分块上传前端源码逐行拆解
3.1 初始化配置:从uploader实例到分块参数
页面结构搭好之后,接下来就是核心的JS初始化代码。这个代码块负责创建一个WebUploader实例,并配置分块相关的核心参数。我直接贴出我在项目里使用的初始化配置,并逐行解释。
javascript复制var uploader = WebUploader.create({
// 选择按钮的id选择器
pick: '#picker',
// 文件列表容器
dnd: '#uploader',
// 粘贴上传
paste: '#uploader',
// 上传服务端接口地址
server: '${pageContext.request.contextPath}/upload/chunk',
// 接受的文件类型
accept: {
title: 'Files',
extensions: 'zip,rar,pdf,doc,docx,xls,xlsx,mp4,avi,jpg,png,txt',
mimeTypes: '*/*'
},
// 开启分块上传,这是本方案的核心
chunked: true,
// 每个分块的大小,单位是字节,这里设为2MB
chunkSize: 2 * 1024 * 1024,
// 同时上传的分块数量,这里设为3个线程
threads: 3,
// 是否开启压缩,这里关闭,保持原始文件上传
compress: false,
// 上传文件的表单字段名
fileVal: 'file',
// 上传时额外携带的参数
formData: {
module: 'temp'
},
// 失败后重试次数
retries: 3,
// 是否分块,如果是真的分块上传,这个参数会让WebUploader在chunked为true时自动生效
fileSingleSizeLimit: 5 * 1024 * 1024 * 1024
});
chunked: true是开启分块上传的总开关,chunkSize: 2 * 1024 * 1024表示每个分块的大小为2MB,配合threads: 3,一次最多同时发送3个分块请求。这两个参数决定了服务端接收分块的粒度。fileVal: 'file'是后端获取文件时使用的参数名,后端接口里用request.getParameter("file")或Spring MVC的@RequestParam("file")来接收,保持一致即可。
formData里的module: 'temp'是我自定义的一个业务参数,用来区分上传用途,比如模块A的上传文件存到/moduleA目录,模块B的上传存到/moduleB目录。这个参数在分块接收接口里会被拿到,用于动态指定临时目录。
还需要注意accept参数中的extensions和mimeTypes。我设置extensions限定常见文件后缀,但mimeTypes设置为*/*,目的是防止某些浏览器因为MIME类型误判而阻止选择文件。很多教程只设置了extensions,结果在Mac的Chrome里选择某些文件时按钮无法响应,原因就是MIME类型不匹配。
3.2 事件回调:上传进度、成功、失败的处理
初始化完uploader实例后,还需要绑定一些事件回调来处理上传生命周期中的各个环节。下面是我项目里实际使用的事件处理代码。
javascript复制var $list = $('#thelist');
// 文件添加到上传队列后触发
uploader.on('fileQueued', function (file) {
var $li = $('<div id="' + file.id + '" class="file-item">' +
'<span class="name">' + file.name + '</span>' +
'<span class="state">等待上传</span>' +
'</div>');
$list.append($li);
});
// 文件开始上传前触发,携带分块信息
uploader.on('startUpload', function () {
// 可在此处做业务校验,比如判断是否登录、是否被允许上传
console.log('开始上传');
});
// 上传进度回调,file是文件信息,percentage是百分比
uploader.on('progress', function (file, percentage) {
var $li = $('#' + file.id);
$li.find('.state').text('上传中 ' + Math.round(percentage * 100) + '%');
});
// 上传成功回调
uploader.on('uploadSuccess', function (file, response) {
var $li = $('#' + file.id);
$li.find('.state').text('上传成功');
});
// 上传失败回调
uploader.on('uploadError', function (file, reason) {
var $li = $('#' + file.id);
$li.find('.state').text('上传失败,原因:' + reason);
});
// 文件上传结束,无论成功失败都会触发
uploader.on('uploadComplete', function (file) {
// 可以在这里统一处理队列状态
});
// 点击开始上传按钮
$('#ctlBtn').on('click', function () {
uploader.upload();
});
这里需要多解释一下progress事件。在分块上传模式中,percentage表示的是整个文件的上传百分比,而不是单个分块的上传百分比。WebUploader在内部会根据已成功上传的分块数、总块数和当前正在上传的分块来计算整个文件的进度比例,所以你只需要直接使用这个百分比渲染进度条即可,不用自己去算。
uploadSuccess回调里的response参数是后端接口返回的JSON数据。在后端合并完成后,我会在响应中返回一个文件路径或文件ID,前端可以在这里拿到并回显到页面上。如果后端返回了业务错误码,也在这个回调里做判断并弹出提示。
3.3 分块上传与后端约定的数据格式
前端配置完成后,需要明确前端与后端之间的数据约定。这是整个方案能否跑通的关键。每次分块上传请求,WebUploader发送的是一个标准的multipart/form-data请求,除了fileVal指定的分块文件本体外,还会自动携带以下关键参数:
| 参数名 | 说明 | 示例值 |
|---|---|---|
chunk |
当前分块的索引,从0开始 | 0 |
chunks |
文件总分块数 | 48 |
name |
原始文件名 | 设计文档.zip |
size |
原始文件总大小(字节) | 98675200 |
guid |
文件唯一标识,WebUploader自动生成的MD5值 | a8f0... |
status |
状态标记,一般用于判断是否是分块请求 | start 或 chunk |
md5 |
当前分块的MD5(需要在beforeSend或uploadProgress中计算后放入formData) | f2d3... |
在后端接口中,我通常通过判断chunk参数是否为空来确定当前请求是分块上传还是合并请求。WebUploader的合并逻辑是这样:所有分块上传完成后,前端会向同一个server地址发送一个不带chunk参数的请求,此时后端需要执行文件合并操作并返回最终结果。
guid参数特别重要,它是WebUploader在文件被加入队列时计算出的唯一标识,同一文件的各个分块都携带相同的guid。后端用guid作为文件夹名,可以很方便地把分块文件统一存放到一个临时目录下。我下面的后端代码就是基于这个约定设计的。
4. 后端接收分块与合并的Java核心代码
4.1 Servlet/SpringMVC接收分块文件的实现
前端发送的每个分块请求,在后端都要有对应的接口来接收。使用Spring MVC时,接口方法可以这样写。先创建一个ChunkUploadController,核心方法负责接收单个分块。
java复制@Controller
@RequestMapping("/upload")
public class ChunkUploadController {
// 分块上传的临时根目录
private static final String TEMP_DIR = "D:/upload_temp/";
@RequestMapping(value = "/chunk", method = RequestMethod.POST)
@ResponseBody
public Map<String, Object> chunkUpload(
@RequestParam(value = "file", required = false) MultipartFile file,
@RequestParam(value = "chunk", required = false) Integer chunk,
@RequestParam(value = "chunks", required = false) Integer chunks,
@RequestParam(value = "name", required = false) String name,
@RequestParam(value = "guid", required = false) String guid,
@RequestParam(value = "md5", required = false) String md5,
HttpServletRequest request) {
Map<String, Object> result = new HashMap<>();
// 如果没有chunk参数,说明是分块合并请求
if (chunk == null) {
return mergeChunks(guid, name, chunks);
}
// 如果chunk参数存在,说明是普通分块上传
if (file == null || file.isEmpty()) {
result.put("status", "error");
result.put("msg", "分块文件为空");
return result;
}
try {
// 以guid为维度建立临时目录
String chunkDir = TEMP_DIR + guid + "/";
File dir = new File(chunkDir);
if (!dir.exists()) {
dir.mkdirs();
}
// 分块文件名统一使用索引,方便后续合并排序
File chunkFile = new File(chunkDir, chunk + ".part");
file.transferTo(chunkFile);
result.put("status", "success");
result.put("chunk", chunk);
result.put("chunks", chunks);
result.put("guid", guid);
} catch (Exception e) {
e.printStackTrace();
result.put("status", "error");
result.put("msg", "分块保存失败");
}
return result;
}
/**
* 合并所有分块
*/
private Map<String, Object> mergeChunks(String guid, String name, Integer chunks) {
Map<String, Object> result = new HashMap<>();
String chunkDir = TEMP_DIR + guid + "/";
File dir = new File(chunkDir);
if (!dir.exists()) {
result.put("status", "error");
result.put("msg", "临时目录不存在,可能分块已被清理");
return result;
}
// 根据上传时约定的原始文件名,生成最终目标文件
String ext = name.substring(name.lastIndexOf(".") + 1);
String targetName = guid + "." + ext;
File targetFile = new File(TEMP_DIR + "merged/" + targetName);
if (!targetFile.getParentFile().exists()) {
targetFile.getParentFile().mkdirs();
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(targetFile);
// 按分块索引顺序合并
for (int i = 0; i < chunks; i++) {
File partFile = new File(chunkDir, i + ".part");
if (!partFile.exists()) {
result.put("status", "error");
result.put("msg", "缺失分块,索引:" + i);
return result;
}
FileInputStream fis = new FileInputStream(partFile);
byte[] buffer = new byte[4 * 1024];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fis.close();
}
} catch (Exception e) {
e.printStackTrace();
result.put("status", "error");
result.put("msg", "合并失败");
return result;
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 合并完成,删除临时目录
deleteDir(dir);
result.put("status", "success");
result.put("filePath", "/upload/merged/" + targetName);
result.put("fileName", targetName);
return result;
}
private void deleteDir(File dir) {
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) {
file.delete();
}
}
dir.delete();
}
}
这个接口的写法有一个关键点:@RequestParam(value = "chunk", required = false)。因为分块请求和合并请求共用同一个URL,合并请求里没有chunk参数,必须允许它为null,否则Spring MVC会直接抛参数缺失异常。我在代码里用chunk == null作为分块请求和合并请求的区分条件,简洁有效。
4.2 分块合并、断点续传的落地
上面代码中的mergeChunks方法,核心逻辑是把临时目录下所有分块按索引顺序写入目标文件。这里有几个细节必须注意。
第一,分块文件命名统一用数字索引加.part后缀,比如0.part、1.part、2.part。这样合并时按索引从小到大排序,就是原始文件的分块顺序。有些项目里会把分块命名成guid_chunk_index之类,合并时还要再解析字符串,容易出乱序问题,我直接从命名上规避掉。
第二,合并时逐块读取并写入FileOutputStream,缓冲区大小我设置为4KB转存,这个值在性能和内存占用之间比较均衡。分块总数不多时可以一把梭,全读进来再写,但分块数量达到几百个时,一次性读取会占大量内存,所以用流式写入更稳妥。
第三,合并失败时,我在返回错误信息前保留临时分块不删除,这样前端可以根据retries配置联合同步重试。如果合并成功,则删除整个临时目录,避免磁盘空间被垃圾文件占满。
再说断点续传的落地。断点续传要靠前端在启动上传前向后端发起一个查询请求,检查该文件的guid对应的分块已经上传了多少。我通常在uploader.option('formData')里附加guid,然后WebUploader上传分块时自动带上这个参数。后端查询时访问TEMP_DIR + guid目录,数一下目录里有几个.part文件,然后告诉前端哪些分块已经存在。前端拿到这个信息后,把对应的分块标记为已完成,只上传剩余的分块。这个功能不需要额外引入复杂的断点库,一个简单的文件目录统计接口就能做到。
如果你是用纯Servlet而不是Spring MVC,接收分块的核心逻辑其实一样,只是把MultipartFile换成Part接口:
java复制@WebServlet("/upload/chunk")
public class ChunkServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
Part part = req.getPart("file");
String chunk = req.getParameter("chunk");
String guid = req.getParameter("guid");
String name = req.getParameter("name");
// 后续逻辑与Spring版本一致
}
}
5. 上传中断恢复与文件完整性校验
5.1 基于MD5的文件唯一标识
WebUploader在fileQueued事件之后会异步计算整个文件的MD5值,计算完成后存入uploader.options.formData.md5字段。这个MD5有两个重要用途:第一,它是判断秒传的依据;第二,它作为文件唯一标识,可以用于断点续传。
秒传的实现思路是这样:用户可以分块上传,但为了更极致地优化,前端在上传前可以先请求后端,带着文件的MD5值,问后端是否存在相同内容且大小一致的文件。如果存在,前端直接跳过所有分块,后端返回已有的文件路径。这个功能在网盘系统、资源管理平台里非常实用,用户上传同一份文件时几乎瞬间完成。
我这里提供后端秒传判断接口的示例:
java复制@RequestMapping(value = "/checkMd5", method = RequestMethod.POST)
@ResponseBody
public Map<String, Object> checkMd5(@RequestParam("md5") String md5,
@RequestParam("size") Long size) {
Map<String, Object> result = new HashMap<>();
// 如果存在相同md5和size的文件,说明文件已上传过
File target = new File(TEMP_DIR + "merged/" + md5 + "_" + size + ".file");
if (target.exists()) {
result.put("status", "success");
result.put("skipUpload", true);
result.put("filePath", "/upload/merged/" + target.getName());
} else {
result.put("status", "success");
result.put("skipUpload", false);
}
return result;
}
前端在计算完MD5后,可以用uploader.on('md5', function(file, md5) { ... })事件拿到这个值,然后调用checkMd5接口,根据返回的skipUpload值决定直接触发uploader.skipFile(file)还是正常走上传流程。
5.2 缓存分块与合并校验的坑
分块上传过程中,临时文件目录中会出现成百上千个.part文件。如果服务器突然断电或者应用重启,这些临时文件就变成了垃圾文件,占用磁盘空间却无人清理。我建议在合并完成后彻底删除临时目录,同时在应用启动时定时清理某个时间点之前创建的临时文件。
另外,合并后的文件完整性校验也值得认真对待。我在后端合并完成后,会把合并后文件的字节数与前端发送的size参数做对比,不一致就返回错误报告。如果项目对文件正确性要求高,比如涉及时效性文件或合同类文件,还可以用Java的MessageDigest计算合并后文件的MD5,与前端计算出的MD5比对,一致才算成功。
java复制// 合并完成后计算MD5
MessageDigest md = MessageDigest.getInstance("MD5");
try (InputStream in = new FileInputStream(targetFile)) {
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) != -1) {
md.update(buffer, 0, len);
}
}
String targetMd5 = new BigInteger(1, md.digest()).toString(16);
if (!targetMd5.equalsIgnoreCase(md5)) {
result.put("status", "error");
result.put("msg", "文件完整性校验失败");
return result;
}
这个环节看起来不复杂,但在实际业务里非常关键。有一次我在现场排查问题,用户反馈上传一个压缩包后无法解压,就是因为在分块传输过程中有某个分块内容损坏,但合并过程完全忽略了数据校验,最终生成的文件比原始文件少了几个字节。从那以后我不管任何上传项目,都会把完整性校验加上,费不了多少性能,但能避免大量诡异问题。
断点续传里还有一个容易被忽视的坑:分块缓存和MD5缓存的生命周期不一致。WebUploader默认在上传前会计算整个文件的MD5,如果用户选择了一个文件后又重新选择另一个同名文件,MD5值和guid都会变化,临时目录里就会留下旧的分块。我建议在fileQueued事件里根据guid清理旧的临时目录,或者用一个定时任务清理超过24小时未合并的临时目录。
6. 实战中遇到的坑与选型心得
6.1 分块大小、并发数与服务器配置怎么匹配
分块大小和并发线程数的选择,直接影响上传速度和服务器稳定性。chunkSize设得太小,比如512KB,分块请求数量会非常多,Tomcat处理大量请求的线程开销增大,合并且起文件时也需要循环更多次,整体性能并不会提升;chunkSize设得太大,比如10MB以上,单个请求体又回到了大文件上传的问题,超时和内存压力会重新出现。我一般在2MB到5MB之间选择,默认用2MB,适合绝大多数局域网办公系统。
threads并发数也要谨慎。并发数越大,同一时刻发送的HTTP请求越多,服务端需要同时打开的文件句柄也越多。Tomcat默认的最大线程数是200,如果50个人同时用默认的3并发上传,请求数量已经不少了。我在部署环境里一般把threads控制在3到5个,文件数量少但单个文件大的场景用2个并发,追求极限吞吐量再调到5个。并发再高反而容易触发Tomcat的线程池饱和和文件锁冲突。
还有一个配置容易被忽略:server接口所在的Servlet容器的maxPostSize和maxFileSize限制。Tomcat 8.5之后,默认的maxPostSize只有2MB,如果你在web.xml或application.properties里没改,那么分块请求即使每块只有2MB也正好卡在边界上,时不时会出现上传失败的情况。我建议在Tomcat的conf/server.xml的Connector节点上加一条参数:
xml复制<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
maxPostSize="-1" />
maxPostSize="-1"表示不限制POST请求体大小,这是分块上传跑通的前提之一。传统JSP项目里最坑的一点就是只改了前端配置,忘记了服务端容器还有这层限制,导致上传大文件极不稳定。
6.2 内存溢出与临时目录清理
WebUploader的分块上传虽然把单次请求体减小了,但服务端处理并发请求时,如果JVM的堆内存设置不够大,仍然会面临内存溢出风险。尤其是Spring MVC接收MultipartFile时,文件内容默认会先写入Tomcat的临时目录,再转为MultipartFile对象传给Controller,这里涉及频繁的磁盘读写。如果同时有大量分块请求并发进来,堆内存中驻留的对象会明显上升。
我建议在部署生产环境时,给Tomcat的JVM参数增加合理的内存分配,至少保证堆内存在1GB以上。简单来说就是修改TOMCAT_HOME/bin/catalina.bat或catalina.sh里的JAVA_OPTS,加入-Xms512m -Xmx1024m。如果项目本身还有大量图片缩放、报表生成等内存消耗活动,堆内存还要预留更多余量。
临时目录的清理,我上面已经提到过一次,这里再展开说说。一个完整的临时目录清理定时任务可以这样设计:每天凌晨扫描TEMP_DIR下所有guid目录,判断目录的最后修改时间,如果超过24小时没有新的分块写入,就把整个目录删除。理由很简单:一个正常的文件上传一般几分钟内就会完成合并,超过24小时还残留着分块,不是上传中断后的垃圾文件,就是用户早就放放弃的僵尸任务,留着没有任何意义。我用一个简单的Spring @Scheduled定时任务来处理,几行代码的事,但能节省一大块磁盘空间。
6.3 我的一些经验总结
最后说几句经验之谈。WebUploader分块上传在JSP项目里其实是一个"前端组件+后端约定"的完整方案,技术难点不在组件本身,而在于前后端参数的约定、异常分支的处理、以及各类容器的隐藏限制。
从选型角度看,如果你的项目是Spring Boot 2.x集成的JSP项目,这套方案照样能跑,只是后端接口用@RestController代替@Controller加@ResponseBody,目录结构换成Spring Boot的静态资源规范,核心逻辑完全不变。如果项目还要用到Nginx做反向代理,注意Nginx默认的client_max_body_size是1MB,需要改成client_max_body_size 0或更大的值,否则分块请求会被Nginx直接拦截,你在后端根本看不到请求到达。
我早期在做一个基于JSP的毕业设计时,就是因为在分块上传的交互协议上没有提前定好,前端传的字段名和后端接收的参数名不一致,调了两天才发现问题出在一个参数名拼写错误上。所以如果现在有人问我,建议先写一份简单的接口约定文档,哪怕是两个人协作的小项目,也能省下大量联调时间。分块上传这套方案里,前端负责切块、发请求、更新进度,后端负责收块、存储、合并,只要把两边的数据和状态约定清楚,JSP项目里的大文件上传就不再是洪水猛兽了。
