资讯动态

大文件分块上传技术实现与JSP解决方案

发布时间:2026/10/14 9:28:01 来源:尧图企业网站定制
1. 大文件上传的技术挑战与解决方案作为一名经历过无数奇葩需求的老程序员我深知大文件上传功能在实际开发中的痛点。特别是当需求方提出20G文件上传IE8兼容这种近乎不可能完成的任务时那种绝望感简直比东北零下30度的寒风还刺骨。1.1 传统文件上传的局限性传统的表单文件上传方式input typefile存在几个致命缺陷内存占用高浏览器会尝试将整个文件加载到内存大文件直接导致浏览器崩溃无断点续传网络中断后必须重新上传无进度显示用户无法知晓上传进度兼容性问题现代API在老旧浏览器上无法使用1.2 分块上传的核心原理分块上传Chunked Upload是将大文件分割成多个小块如5MB/块然后分批上传的技术方案。其核心优势在于内存友好每次只处理一个小块支持断点续传记录已上传的块网络恢复后继续上传剩余部分进度可控可以精确计算上传百分比并行上传多个块可以同时上传提高速度2. JSP环境下的分块上传实现2.1 前端实现方案2.1.1 现代浏览器方案对于现代浏览器Chrome/Firefox/Edge等我们可以使用HTML5的File API// 文件分片处理 function uploadFile(file) { const chunkSize 5 * 1024 * 1024; // 5MB const totalChunks Math.ceil(file.size / chunkSize); const fileMd5 await calculateMD5(file); // 计算文件唯一标识 for (let i 0; i totalChunks; i) { const start i * chunkSize; const end Math.min(start chunkSize, file.size); const chunk file.slice(start, end); const formData new FormData(); formData.append(file, chunk); formData.append(chunkNumber, i); formData.append(totalChunks, totalChunks); formData.append(fileMd5, fileMd5); await axios.post(/upload, formData, { onUploadProgress: progress { const percent Math.round( ((i * chunkSize) progress.loaded) / file.size * 100 ); updateProgress(percent); } }); } }2.1.2 IE8兼容方案对于IE8这种古董浏览器我们不得不使用Flash或ActiveX方案function initIEFileUpload() { if (typeof window.ActiveXObject ! undefined) { // 使用ActiveX组件 try { var uploader new ActiveXObject(MSXML2.XMLHTTP); // ...IE特有的上传逻辑 } catch (e) { alert(请启用ActiveX控件); } } else if (navigator.mimeTypes[application/x-shockwave-flash] ! undefined) { // 使用Flash上传 var swf object typeapplication/x-shockwave-flash dataupload.swf; document.getElementById(uploader).innerHTML swf; } else { alert(请升级浏览器或安装Flash插件); } }注意Flash方案需要额外开发Flash上传组件且需要用户安装Flash插件这在今天已经不太现实。对于必须支持IE8的项目建议与客户沟通升级浏览器。2.2 后端实现方案JSP/Servlet2.2.1 分片接收与存储WebServlet(/upload) public class UploadServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 获取分片信息 int chunkNumber Integer.parseInt(request.getParameter(chunkNumber)); int totalChunks Integer.parseInt(request.getParameter(totalChunks)); String fileMd5 request.getParameter(fileMd5); // 创建临时目录 String tempDir getServletContext().getRealPath(/upload/temp/ fileMd5); new File(tempDir).mkdirs(); // 保存分片 Part filePart request.getPart(file); String chunkPath tempDir / chunkNumber; filePart.write(chunkPath); // 检查是否全部上传完成 File[] chunks new File(tempDir).listFiles(); if (chunks.length totalChunks) { // 合并文件 mergeFiles(tempDir, getServletContext().getRealPath(/upload/ fileMd5)); deleteDirectory(tempDir); response.getWriter().write({\status\:\complete\}); } else { response.getWriter().write({\status\:\partial\,\uploaded\:chunks.length}); } } private void mergeFiles(String tempDir, String destPath) throws IOException { FileOutputStream fos new FileOutputStream(destPath); File[] chunks new File(tempDir).listFiles(); // 按分片编号排序 Arrays.sort(chunks, Comparator.comparingInt(f - Integer.parseInt(f.getName()))); for (File chunk : chunks) { Files.copy(chunk.toPath(), fos); chunk.delete(); } fos.close(); } }2.2.2 断点续传实现要实现断点续传后端需要记录每个文件的上传进度// 在UploadServlet中添加 private MapString, SetInteger uploadProgress new ConcurrentHashMap(); protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String fileMd5 request.getParameter(fileMd5); String tempDir getServletContext().getRealPath(/upload/temp/ fileMd5); File dir new File(tempDir); if (dir.exists()) { SetInteger uploaded Arrays.stream(dir.listFiles()) .map(f - Integer.parseInt(f.getName())) .collect(Collectors.toSet()); response.setContentType(application/json); response.getWriter().write(new Gson().toJson(uploaded)); } else { response.getWriter().write([]); } }前端在开始上传前可以先查询已上传的分片async function checkUploadProgress(fileMd5) { const response await fetch(/upload?fileMd5${fileMd5}); const uploadedChunks await response.json(); return new Set(uploadedChunks); }3. 高级功能实现3.1 秒传功能秒传是通过文件内容哈希值判断文件是否已存在服务器避免重复上传// 计算文件MD5前端 function calculateMD5(file) { return new Promise((resolve) { const chunkSize 2 * 1024 * 1024; // 2MB const chunks Math.ceil(file.size / chunkSize); const spark new SparkMD5.ArrayBuffer(); const fileReader new FileReader(); let currentChunk 0; fileReader.onload function(e) { spark.append(e.target.result); currentChunk; if (currentChunk chunks) { loadNext(); } else { resolve(spark.end()); } }; function loadNext() { const start currentChunk * chunkSize; const end Math.min(start chunkSize, file.size); fileReader.readAsArrayBuffer(file.slice(start, end)); } loadNext(); }); } // 后端检查接口 WebServlet(/checkFile) public class CheckFileServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String fileMd5 request.getParameter(md5); File file new File(getServletContext().getRealPath(/upload/ fileMd5)); response.setContentType(application/json); if (file.exists()) { response.getWriter().write({\exists\:true,\path\:\file.getName()\}); } else { response.getWriter().write({\exists\:false}); } } }3.2 文件夹上传HTML5提供了webkitRelativePath属性获取文件夹结构document.getElementById(folderInput).addEventListener(change, function(e) { const files Array.from(e.target.files); const folderStructure {}; files.forEach(file { const path file.webkitRelativePath; const parts path.split(/); let current folderStructure; // 构建文件夹树 for (let i 0; i parts.length - 1; i) { const part parts[i]; if (!current[part]) { current[part] {}; } current current[part]; } // 添加文件 current[parts[parts.length - 1]] file; }); // 上传文件夹结构 uploadFolder(folderStructure); });3.3 加密传输对于敏感文件可以在前端进行加密后再上传// 使用CryptoJS进行AES加密 function encryptChunk(chunk, key) { const wordArray CryptoJS.lib.WordArray.create(chunk); const encrypted CryptoJS.AES.encrypt(wordArray, key); return encrypted.toString(); } // 解密示例 function decryptData(encrypted, key) { const decrypted CryptoJS.AES.decrypt(encrypted, key); return decrypted.toString(CryptoJS.enc.Utf8); }4. 性能优化与问题排查4.1 上传速度优化并行上传同时上传多个分片// 使用Promise.all并行上传多个分片 const parallel 3; // 并行数 for (let i 0; i totalChunks; i parallel) { const chunkPromises []; for (let j 0; j parallel (i j) totalChunks; j) { chunkPromises.push(uploadChunk(i j)); } await Promise.all(chunkPromises); }动态分片大小根据网络状况调整分片大小let chunkSize 1 * 1024 * 1024; // 初始1MB function adjustChunkSize(uploadSpeed) { if (uploadSpeed 1024 * 1024) { // 1MB/s以上 chunkSize 5 * 1024 * 1024; } else if (uploadSpeed 512 * 1024) { // 512KB/s以上 chunkSize 2 * 1024 * 1024; } else { chunkSize 512 * 1024; } }4.2 常见问题排查分片合并失败检查临时目录权限确保分片按顺序合并验证合并后的文件大小是否与原始文件一致内存泄漏及时释放已上传分片的内存避免在前端同时保存过多分片数据IE8特殊问题ActiveX安全设置可能导致上传组件无法加载Flash版本兼容性问题最大上传文件大小限制通常2GB5. 实际部署建议5.1 服务器配置调整对于Tomcat服务器需要调整以下参数maxPostSize在server.xml中设置Connector port8080 protocolHTTP/1.1 maxPostSize0 !-- 0表示不限制 -- connectionTimeout20000 /文件保存位置建议使用独立存储卷String storagePath /data/uploads/; // 而不是放在webapp目录下5.2 数据库设计如果需要记录上传历史可以设计如下表结构CREATE TABLE file_uploads ( id VARCHAR(32) PRIMARY KEY, file_name VARCHAR(255), file_path VARCHAR(512), file_size BIGINT, file_md5 VARCHAR(32), upload_status ENUM(uploading,completed,failed), created_at DATETIME, completed_at DATETIME, user_id INT ); CREATE TABLE file_chunks ( id INT AUTO_INCREMENT PRIMARY KEY, file_id VARCHAR(32), chunk_number INT, chunk_size INT, chunk_md5 VARCHAR(32), upload_time DATETIME, FOREIGN KEY (file_id) REFERENCES file_uploads(id) );5.3 安全注意事项文件类型检查不要仅依赖前端验证boolean isSafeFile(String filename) { String[] allowedExtensions {.jpg, .png, .pdf, .docx}; String lowerName filename.toLowerCase(); return Arrays.stream(allowedExtensions).anyMatch(lowerName::endsWith); }病毒扫描集成ClamAV等杀毒软件ProcessBuilder pb new ProcessBuilder(clamscan, uploadedFilePath); Process p pb.start(); int exitCode p.waitFor(); if (exitCode 1) { // 发现病毒 new File(uploadedFilePath).delete(); }权限控制确保用户只能访问自己的文件WebServlet(/download) public class DownloadServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String fileId request.getParameter(id); // 验证用户权限 if (!checkPermission(request.getSession(), fileId)) { response.sendError(403); return; } // 允许下载 } }6. 替代方案与工具推荐如果项目允许使用第三方解决方案以下工具值得考虑Resumable.js强大的分块上传库Uppy现代文件上传组件支持多种后端Tus协议基于HTTP的可恢复文件上传协议WebSocket上传适合不稳定网络环境对于Java后端可以考虑Apache Commons FileUpload处理multipart请求Spring ContentSpring生态的文件管理模块MinIO兼容S3协议的对象存储服务7. 项目实战经验分享在实现大文件上传功能时我总结出以下几点经验分片大小选择通常2-5MB比较合适太小会增加请求次数太大会增加失败重试成本进度显示技巧前端计算总体进度时应考虑已确认的分片和正在上传的分片使用Web Worker处理大文件哈希计算避免界面卡顿IE兼容的妥协方案对于必须支持IE8的项目可以提供单独的简单上传页面限制IE8用户的最大上传文件大小如500MB测试要点模拟网络中断测试断点续传测试不同文件类型特别是大视频文件测试并发上传场景性能监控// 记录上传耗时 long startTime System.currentTimeMillis(); // ...上传逻辑... long duration System.currentTimeMillis() - startTime; logger.info(文件{}上传耗时{}ms平均速度{}KB/s, fileMd5, duration, (fileSize/1024)/(duration/1000));最后对于100元预算实现20G上传这种需求我的建议是与客户明确技术限制和成本关系提供分阶段实施方案先实现基本功能再逐步优化考虑使用现成的云存储解决方案如七牛云、阿里云OSS对于实在不合理的需求学会礼貌地说不

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

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

免费获取报价 →
↑