
做Web文件上传开发的朋友应该都经历过这种场景产品一句话“支持大文件上传”然后你面对的是一个几百MB甚至几个GB的文件如果用普通表单一次性POST出去不仅要忍受漫长的等待一旦网络抖动就直接从头再来后端内存也被撑得够呛。这时候分块上传就成了绕不开的方案。今天我就结合项目里实际在用的方案完整拆解一下如何用WebUploader做前端分块配合Java后端实现整个流程。这篇文章适合正在做文件上传功能、被大文件折磨过、或者想把断点续传做明白的同学参考我会把核心接口设计、合并逻辑、参数调优和踩过的坑都交代清楚。1. 整体方案设计与核心思路拆解1.1 为什么选择WebUploader加Java这套组合先说结论这套方案成熟、稳定、可落地。WebUploader是百度团队开源的一个上传组件虽然现在看起来不像很多新框架那么花哨但它的分块上传机制非常扎实尤其适合做企业内部系统或者通用型CMS后台。Java这边不用多说生态丰富做文件处理、校验、合并都非常顺手很多业务系统本身就是Java技术栈后端集成成本最低。分块上传的核心思路其实很简单就是“化整为零先拆后并”。前端把大文件切成若干小块每块单独上传后端每收一块就记录一块全部传完后再按顺序合并还原成完整文件。这带来的好处非常直接避免单次请求体过大Nginx、Tomcat的连接超时和内存压力都大幅缓解网络中断只需要重传失败的分块不用整个文件从头来过后端可以实时拿到上传进度做到进度条是真实计算出来的而不是假进度结合MD5校验能实现秒传和断点续传从WebUploader的角度看它内部封装了分块策略前端开发者不需要自己写File.slice的循环逻辑组件直接给你搞定。你只需要配置块大小监听回调把分块元数据传给后端就行。但要注意WebUploader只负责前端分块和发送请求后端接收分块、保存临时文件、合并文件这些都得我们自己动手实现。这也是这篇文章要重点讲清楚的地方。1.2 三个核心接口的设计思路整个分块上传后端归根结底就是三个核心接口初始化上传、上传分块、合并文件。第一次做分块上传的人最容易犯的毛病就是只盯着“上传分块”这一个接口写忽略了另外两个结果前端回调里报错都不知道去哪里排查。我建议一开始就按三条主链路来设计。第一个是初始化接口。前端在开始上传前先请求一次后端告诉后端“我要传一个文件了文件名是什么、大小多少、总共有多少块”后端根据这些信息返回一个uploadId作为整个上传流程的唯一标识。有的系统甚至在这个环节就把已上传的分块编号列表返回给前端前端直接跳过这些分块这就是断点续传的基础。第二个是分块上传接口。前端按固定大小切块后逐块POST到后端。每块请求携带的公共参数包括文件唯一标识、uploadId、当前块索引、总块数、当前块大小。后端收到后将分块数据写入临时目录。这里要特别强调一点每个分块在磁盘上必须是独立的小文件不能一边接收一边往最终文件里写因为分块请求是并发到达的顺序是乱的如果直接写最终文件数据顺序就全乱了。第三个是合并接口。前端所有分块都上传完毕后调用合并接口后端按分块索引顺序把临时分块文件逐个读出来追加写入最终文件。合并完成后再校验文件大小如果跟初始化时记录的期望大小一致就算成功然后清掉临时分块文件和对应的记录数据。这三个接口的逻辑没有高深的东西真正的复杂度在异常处理上。后面我会把每个接口的代码实现和关键细节都捋一遍。2. 核心细节解析与实操要点2.1 前端参数与后端接收的对应关系在很多分块上传的项目里排坑的第一步就是先把前端参数名和后端接收的字段名对齐。WebUploader在分块模式下默认通过自定义formData来传递分块元数据后端用HttpServletRequest的getParameter就能拿到。常见的关键字段就这几个字段含义示例值chunk当前是第几块从0开始0chunks总块数10size文件总大小字节10485760name原始文件名example.zipuploadId本次上传会话ID8f9a2c1d3e4b5fmd5文件整体MD5可选秒传用5d41402abc4b2a76b9719d911017c592有个我在项目里被坑过一次的点必须提醒你WebUploader在每个分块的请求体里默认除了分块二进制数据还会带一个字段叫file这个字段名可以通过fileVal配置修改。后端接收分块时用MultipartFile file来接然后用刚才表格里的参数做业务逻辑。如果你后端用RequestParam(chunk) Integer chunk来接注意参数名大小写一致有的框架对大小写敏感对不上就直接报错“MissingServletRequestParameterException”前端就一直在重试你还在后端翻日志找半天。另外还有一个性能相关的细节WebUploader发分块请求时默认是并行发送的。这意味着后端接口必须支持并发写入多个分块文件而且不能依赖全局对象保存状态。我见过一个新手把已接收的分块编号存在一个HashMap里结果并发请求一来数据被互相覆盖进度永远对不上。正确的做法是每个分块落盘独立文件元数据记录放在数据库或者本地文件里用uploadId做维度来隔离。2.2 分块大小的选择逻辑块大小不是随便填一个100就行的它直接影响上传的成功率和整体效率。WebUploader的chunkSize参数默认是2MB但如果你的业务普遍要传视频素材或者大安装包块大小建议调到5MB到10MB。核心考量有三点。第一分块太小比如512KB总请求数会爆炸。一个1GB的文件分块2MB就有512个请求分块512KB就有2048个请求请求本身的网络往返时间、HTTP头开销都会被放大后端处理请求的线程开销也大整体速度反而不如大块。第二分块太大比如50MB单块上传时间长一旦网络波动重传成本很高而且后端接收时内存暂存压力也大。第三要考虑后端所在服务器的Nginx配置和网关限制有的网关对POST请求体大小有上限比如10MB那前端块大小就得配合这个上限。以我实际项目的经验内网系统块大小设置为5MB比较均衡外网场景建议2MB到4MB。块大小不一定要写死可以根据文件大小动态调整——小文件就直接单块上传大文件才切块。WebUploader支持文件大小判断后动态设置chunkSize这个灵活度可以给你后续优化留出空间。2.3 断点续传与秒传的实现逻辑很多业务方其实不关心分块细节他们要的是体验传到一半断了重新打开页面能不能接着传传过的文件再次选择能不能直接秒传这两个功能本质上都依赖分块编号的查询能力。断点续传的链路是前端初始化时把文件MD5发给后端后端在数据库表里查这个MD5是否已经存在分块记录如果存在返回已上传分块编号列表给前端。前端拿到这个列表后就可以只上传缺失的分块。这里关键是MD5怎么算文件整体MD5在WebUploader里可以通过组件的md5属性开启计算但大文件计算整体MD5需要时间所以很多系统会退一步用文件特征标识文件名加大小加修改时间来替代。对于内部系统这完全够用秒传的准确率也不差。合并接口同样要支持重复调用时的幂等处理如果最终文件已经存在直接返回成功不要再重新合并。这个在“文件上传完成后前端重复提交合并请求”的场景下非常实用。3. 实操过程与核心环节实现3.1 开发环境与基础依赖我这个示例用的是Spring Boot 2.7.xJDK8Maven管理依赖。需要引入的依赖不多核心就是spring-boot-starter-web。如果你的项目要记录分块MD5做校验可以额外引入commons-codec但如果只是做基本的分块上传Spring Boot自带的就够了。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcommons-io/groupId artifactIdcommons-io/artifactId version2.11.0/version /dependency工程结构上我建议按职责分层controller包放接收HTTP请求的接口service包放分块核心逻辑entity或者dto包放参数对象utils包放文件合并等工具方法。虽然业务简单可以不拆这么细但分块上传后面很可能还要扩展秒传、任务列表查询、过期文件清理提前把结构搭好后面改动会舒服很多。3.2 分块元数据实体设计无论你用不用数据库分块上传过程中都需要记录每个上传会话的状态。我这里用一个简单的实体类来代表分块文件的元数据如果你项目里已经有MyBatis或JPA直接映射成表即可字段设计可以参考下面这个结构字段名类型说明uploadIdString唯一上传会话IDfileNameString原始文件名fileSizeLong文件总大小chunkSizeLong分块大小totalChunksInteger总分块数uploadedChunksList已上传分块编号列表targetPathString最终文件保存路径statusInteger上传状态0初始化1上传中2已完成这里要提醒一个容易被忽略的点uploadedChunks这个列表不能存在内存Map里因为应用重启就丢了断点续传也就失效了。内部系统可以用Redis存生产环境建议落库。如果没有Redis也没有关系直接查临时分块文件目录把文件名的编号提取出来也能得到已上传分块列表这个方案最简单也最可靠。3.3 初始化接口实现初始化接口做的事情非常朴素接收前端传来的fileName、fileSize、chunkSize、totalChunks生成uploadId保存初始化记录然后返回给前端。重点在于生成uploadId的规则我是用UUID去掉中划线再拼接时间戳保证唯一性。PostMapping(/upload/init) public Result initUpload(RequestBody UploadInitRequest request) { String uploadId UUID.randomUUID().toString().replace(-, ) System.currentTimeMillis(); ChunkMeta meta new ChunkMeta(); meta.setUploadId(uploadId); meta.setFileName(request.getFileName()); meta.setFileSize(request.getFileSize()); meta.setChunkSize(request.getChunkSize()); meta.setTotalChunks(request.getTotalChunks()); meta.setStatus(0); chunkMetaService.save(meta); return Result.ok().data(uploadId, uploadId); }这个接口还有一个扩展点值得说如果前端在初始化时上传了文件的整体MD5后端可以在这里先查一遍有没有相同MD5的已完成文件。如果存在直接返回一个特殊状态比如“uploaded”并附上文件访问路径前端收到后就不再走分块流程了这就是秒传的简化实现。不过在初始化阶段算MD5前端需要等待一段时间体验上要看具体场景取舍。3.4 分块上传接口实现分块上传接口是整个流程的核心。前端每个分块请求都带着二进制内容后端要做的事情包括按uploadId和chunk编号构造临时分块文件路径把二进制内容写入该文件。这里分块文件命名格式建议用“{uploadId}_{chunkIndex}.part”后续合并时按chunkIndex排序即可。PostMapping(/upload/chunk) public Result uploadChunk(RequestParam(file) MultipartFile file, RequestParam(uploadId) String uploadId, RequestParam(value chunk, required false) Integer chunk, RequestParam(value chunks, required false) Integer chunks) { if (chunk null || chunks null) { // WebUploader对于小文件可能不分块此时直接保存整个文件 return handleWholeFile(file, uploadId); } String chunkDir uploadConfig.getChunkDir() uploadId; File dir new File(chunkDir); if (!dir.exists()) { dir.mkdirs(); } File chunkFile new File(chunkDir, chunk .part); try (InputStream in file.getInputStream(); FileOutputStream out new FileOutputStream(chunkFile)) { IOUtils.copy(in, out); } catch (IOException e) { return Result.error(分块保存失败); } return Result.ok(); }这段代码里有个很重要的隐藏逻辑chunk和chunks参数用requiredfalse来处理。为什么因为WebUploader在上传小文件时如果文件大小小于分块阈值它默认是不走分块逻辑的而是直接作为单个文件上传。这时候请求里就没有chunk和chunks参数如果你强制要求这两个参数前端会报参数缺失。这是一个非常容易踩的兼容性坑特别是文件体积跨在阈值附近的时候。另外要注意分块文件不能直接命名为“uploadId_chunk.part”然后放在同一个目录下要按uploadId建独立子目录。否则多用户同时上传时文件会互相覆盖或者混在一起清理的时候也分不清哪些分块属于哪个上传任务。3.5 合并接口实现全部块传完之后前端会调用合并接口。合并的逻辑就是按索引顺序读取所有分块文件然后依次写入最终文件。这里我会带上一个文件大小校验读出来所有分块文件的总字节数如果等于初始化时记录的fileSize才认为合并成功否则说明有分块缺失或者损坏。PostMapping(/upload/merge) public Result mergeUpload(RequestParam(uploadId) String uploadId) { String chunkDir uploadConfig.getChunkDir() uploadId; String targetPath uploadConfig.getTargetDir() uploadId _ System.currentTimeMillis() _ originalName; File dest new File(targetPath); ListFile partFiles Arrays.asList(Objects.requireNonNull(new File(chunkDir).listFiles())); partFiles.sort(Comparator.comparingInt(f - Integer.parseInt(f.getName().split(\\.)[0]))); try (FileOutputStream fos new FileOutputStream(dest)) { for (File part : partFiles) { try (FileInputStream fis new FileInputStream(part)) { IOUtils.copy(fis, fos); } } } catch (IOException e) { return Result.error(合并失败); } // 清理临时分块目录 FileUtils.deleteDirectory(new File(chunkDir)); return Result.ok().data(path, targetPath); }合并的时候有一个我特别想强调的细节分块文件排序不能只按自然排序String.compareTo因为文件名中的索引会出现1、10、11、2这种顺序问题。必须把文件名中的数字提取出来转成int后再比较。我用的是split(\.)[0]因为文件名格式是“0.part”“1.part”“2.part”拆出数字部分来排序。如果你的分块号是从1开始自己心里要有数别在索引边界上搞混。合并完成后我建议顺手把临时目录删掉。否则时间一长临时分块文件会堆积成百上千个小文件磁盘占用和inode压力都会出问题。这块后面我会专门讲清理策略。3.6 前端WebUploader配置要点前端配置直接决定后端能不能正常工作我把核心配置贴出来并标注每个参数的作用var uploader WebUploader.create({ swf: /static/Uploader.swf, server: http://localhost:8080/upload/chunk, pick: #picker, auto: true, chunked: true, chunkSize: 5 * 1024 * 1024, threads: 3, formData: { uploadId: uploadId }, fileVal: file, accept: { title: All Files, extensions: zip,rar,7z,jpg,png,pdf,mp4 } });这里的threads是并发上传的线程数不建议设置太高。并发数 分块大小 × 线程数 / 网络带宽 × 时间冗余。如果设置成105MB块10并发同时打过来就是50MB流量后端带宽不够直接排队超时反而比串行还慢。我经验值给3到5比较合适。还有一点WebUploader默认会对分块做失败重试。组件配置里的retry可以设置次数我在生产环境设置为3次。重试机制本身是好的但要注意如果后端分块接口处理慢前端重试会堆积请求。所以后端接口性能越高前端重试越少整体越稳定。这两个是相互关联的。 做Web文件上传开发的朋友应该都经历过这种场景产品一句话“支持大文件上传”你面对的是一个几百MB甚至几个GB的文件如果用普通表单一次性POST出去不仅要忍受漫长的等待一旦网络抖动就直接从头再来后端内存也被撑得够呛。这时候分块上传就成了绕不开的方案。今天我就结合项目里实际在用的方案完整拆解一下如何用WebUploader做前端分块配合Java后端实现整个流程。这篇文章适合正在做文件上传功能、被大文件折磨过、或者想把断点续传做明白的同学参考我会把核心接口设计、合并逻辑、参数调优和踩过的坑都交代清楚。1. 整体方案设计与核心思路拆解1.1 为什么选择WebUploader加Java这套组合先说结论这套方案成熟、稳定、可落地。WebUploader是百度团队开源的一个上传组件虽然现在看起来不像很多新框架那么花哨但它的分块上传机制非常扎实尤其适合做企业内部系统或者通用型CMS后台。Java这边不用多说生态丰富做文件处理、校验、合并都非常顺手很多业务系统本身就是Java技术栈后端集成成本最低。分块上传的核心思路其实很简单就是“化整为零先拆后并”。前端把大文件切成若干小块每块单独上传后端每收一块就记录一块全部传完后再按顺序合并还原成完整文件。这带来的好处非常直接避免单次请求体过大Nginx、Tomcat的连接超时和内存压力都大幅缓解网络中断只需要重传失败的分块不用整个文件从头来过后端可以实时拿到上传进度做到进度条是真实计算出来的而不是假进度结合MD5校验能实现秒传和断点续传从WebUploader的角度看它内部封装了分块策略前端开发者不需要自己写File.slice的循环逻辑组件直接给你搞定。你只需要配置块大小监听回调把分块元数据传给后端就行。但要注意WebUploader只负责前端分块和发送请求后端接收分块、保存临时文件、合并文件这些都得我们自己动手实现。这也是这篇文章要重点讲清楚的地方。1.2 三个核心接口的设计思路整个分块上传后端归根结底就是三个核心接口初始化上传、上传分块、合并文件。第一次做分块上传的人最容易犯的毛病就是只盯着“上传分块”这一个接口写忽略了另外两个结果前端回调里报错都不知道去哪里排查。我建议一开始就按三条主链路来设计。第一个是初始化接口。前端在开始上传前先请求一次后端告诉后端“我要传一个文件了文件名是什么、大小多少、总共有多少块”后端根据这些信息返回一个uploadId作为整个上传流程的唯一标识。有的系统甚至在这个环节就把已上传的分块编号列表返回给前端前端直接跳过这些分块这就是断点续传的基础。第二个是分块上传接口。前端按固定大小切块后逐块POST到后端。每块请求携带的公共参数包括文件唯一标识、uploadId、当前块索引、总块数、当前块大小。后端收到后将分块数据写入临时目录。这里要特别强调一点每个分块在磁盘上必须是独立的小文件不能一边接收一边往最终文件里写因为分块请求是并发到达的顺序是乱的如果直接写最终文件数据顺序就全乱了。第三个是合并接口。前端所有分块都上传完毕后调用合并接口后端按分块索引顺序把临时分块文件逐个读出来追加写入最终文件。合并完成后再校验文件大小如果跟初始化时记录的期望大小一致就算成功然后清掉临时分块文件和对应的记录数据。这三个接口的逻辑没有高深的东西真正的复杂度在异常处理上。后面我会把每个接口的代码实现和关键细节都捋一遍。2. 核心细节解析与实操要点2.1 前端参数与后端接收的对应关系在很多分块上传的项目里排坑的第一步就是先把前端参数名和后端接收的字段名对齐。WebUploader在分块模式下默认通过自定义formData来传递分块元数据后端用HttpServletRequest的getParameter就能拿到。常见的关键字段就这几个字段含义示例值chunk当前是第几块从0开始0chunks总块数10size文件总大小字节10485760name原始文件名example.zipuploadId本次上传会话ID8f9a2c1d3e4b5fmd5文件整体MD5可选秒传用5d41402abc4b2a76b9719d911017c592有个我在项目里被坑过一次的点必须提醒你WebUploader在每个分块的请求体里默认除了分块二进制数据还会带一个字段叫file这个字段名可以通过fileVal配置修改。后端接收分块时用MultipartFile file来接然后用刚才表格里的参数做业务逻辑。如果你后端用RequestParam(chunk) Integer chunk来接注意参数名大小写一致有的框架对大小写敏感对不上就直接报错“MissingServletRequestParameterException”前端就一直在重试你还在后端翻日志找半天。另外还有一个性能相关的细节WebUploader发分块请求时默认是并行发送的。这意味着后端接口必须支持并发写入多个分块文件而且不能依赖全局对象保存状态。我见过一个新手把已接收的分块编号存在一个HashMap里结果并发请求一来数据被互相覆盖进度永远对不上。正确的做法是每个分块落盘独立文件元数据记录放在数据库或者本地文件里用uploadId做维度来隔离。2.2 分块大小的选择逻辑块大小不是随便填一个100就行的它直接影响上传的成功率和整体效率。WebUploader的chunkSize参数默认是2MB但如果你的业务普遍要传视频素材或者大安装包块大小建议调到5MB到10MB。核心考量有三点。第一分块太小比如512KB总请求数会爆炸。一个1GB的文件分块2MB就有512个请求分块512KB就有2048个请求请求本身的网络往返时间、HTTP头开销都会被放大后端处理请求的线程开销也大整体速度反而不如大块。第二分块太大比如50MB单块上传时间长一旦网络波动重传成本很高而且后端接收时内存暂存压力也大。第三要考虑后端所在服务器的Nginx配置和网关限制有的网关对POST请求体大小有上限比如10MB那前端块大小就得配合这个上限。以我实际项目的经验内网系统块大小设置为5MB比较均衡外网场景建议2MB到4MB。块大小不一定要写死可以根据文件大小动态调整——小文件就直接单块上传大文件才切块。WebUploader支持文件大小判断后动态设置chunkSize这个灵活度可以给你后续优化留出空间。2.3 断点续传与秒传的实现逻辑很多业务方其实不关心分块细节他们要的是体验传到一半断了重新打开页面能不能接着传传过的文件再次选择能不能直接秒传这两个功能本质上都依赖分块编号的查询能力。断点续传的链路是前端初始化时把文件MD5发给后端后端在数据库表里查这个MD5是否已经存在分块记录如果存在返回已上传分块编号列表给前端。前端拿到这个列表后就可以只上传缺失的分块。这里关键是MD5怎么算文件整体MD5在WebUploader里可以通过组件的md5属性开启计算但大文件计算整体MD5需要时间所以很多系统会退一步用文件特征标识文件名加大小加修改时间来替代。对于内部系统这完全够用秒传的准确率也不差。合并接口同样要支持重复调用时的幂等处理如果最终文件已经存在直接返回成功不要再重新合并。这个在“文件上传完成后前端重复提交合并请求”的场景下非常实用。3. 实操过程与核心环节实现3.1 开发环境与基础依赖我这个示例用的是Spring Boot 2.7.xJDK8Maven管理依赖。需要引入的依赖不多核心就是spring-boot-starter-web。如果你的项目要记录分块MD5做校验可以额外引入commons-codec但如果只是做基本的分块上传Spring Boot自带的就够了。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcommons-io/groupId artifactIdcommons-io/artifactId version2.11.0/version /dependency工程结构上我建议按职责分层controller包放接收HTTP请求的接口service包放分块核心逻辑entity或者dto包放参数对象utils包放文件合并等工具方法。虽然业务简单可以不拆这么细但分块上传后面很可能还要扩展秒传、任务列表查询、过期文件清理提前把结构搭好后面改动会舒服很多。3.2 分块元数据实体设计无论你用不用数据库分块上传过程中都需要记录每个上传会话的状态。我这里用一个简单的实体类来代表分块文件的元数据如果你项目里已经有MyBatis或JPA直接映射成表即可字段设计可以参考下面这个结构字段名类型说明uploadIdString唯一上传会话IDfileNameString原始文件名fileSizeLong文件总大小chunkSizeLong分块大小totalChunksInteger总分块数uploadedChunksList已上传分块编号列表targetPathString最终文件保存路径statusInteger上传状态0初始化1上传中2已完成这里要提醒一个容易被忽略的点uploadedChunks这个列表不能存在内存Map里因为应用重启就丢了断点续传也就失效了。内部系统可以用Redis存生产环境建议落库。如果没有Redis也没有关系直接查临时分块文件目录把文件名的编号提取出来也能得到已上传分块列表这个方案最简单也最可靠。3.3 初始化接口实现初始化接口做的事情非常朴素接收前端传来的fileName、fileSize、chunkSize、totalChunks生成uploadId保存初始化记录然后返回给前端。重点在于生成uploadId的规则我是用UUID去掉中划线再拼接时间戳保证唯一性。PostMapping(/upload/init) public Result initUpload(RequestBody UploadInitRequest request) { String uploadId UUID.randomUUID().toString().replace(-, ) System.currentTimeMillis(); ChunkMeta meta new ChunkMeta(); meta.setUploadId(uploadId); meta.setFileName(request.getFileName()); meta.setFileSize(request.getFileSize()); meta.setChunkSize(request.getChunkSize()); meta.setTotalChunks(request.getTotalChunks()); meta.setStatus(0); chunkMetaService.save(meta); return Result.ok().data(uploadId, uploadId); }这个接口还有一个扩展点值得说如果前端在初始化时上传了文件的整体MD5后端可以在这里先查一遍有没有相同MD5的已完成文件。如果存在直接返回一个特殊状态比如“uploaded”并附上文件访问路径前端收到后就不再走分块流程了这就是秒传的简化实现。不过在初始化阶段算MD5前端需要等待一段时间体验上要看具体场景取舍。3.4 分块上传接口实现分块上传接口是整个流程的核心。前端每个分块请求都带着二进制内容后端要做的事情包括按uploadId和chunk编号构造临时分块文件路径把二进制内容写入该文件。这里分块文件命名格式建议用“{uploadId}_{chunkIndex}.part”后续合并时按chunkIndex排序即可。PostMapping(/upload/chunk) public Result uploadChunk(RequestParam(file) MultipartFile file, RequestParam(uploadId) String uploadId, RequestParam(value chunk, required false) Integer chunk, RequestParam(value chunks, required false) Integer chunks) { if (chunk null || chunks null) { // WebUploader对于小文件可能不分块此时直接保存整个文件 return handleWholeFile(file, uploadId); } String chunkDir uploadConfig.getChunkDir() uploadId; File dir new File(chunkDir); if (!dir.exists()) { dir.mkdirs(); } File chunkFile new File(chunkDir, chunk .part); try (InputStream in file.getInputStream(); FileOutputStream out new FileOutputStream(chunkFile)) { IOUtils.copy(in, out); } catch (IOException e) { return Result.error(分块保存失败); } return Result.ok(); }这段代码里有个很重要的隐藏逻辑chunk和chunks参数用requiredfalse来处理。为什么因为WebUploader在上传小文件时如果文件大小小于分块阈值它默认是不走分块逻辑的而是直接作为单个文件上传。这时候请求里就没有chunk和chunks参数如果你强制要求这两个参数前端会报参数缺失。这是一个非常容易踩的兼容性坑特别是文件体积跨在阈值附近的时候。另外要注意分块文件不能直接命名为“uploadId_chunk.part”然后放在同一个目录下要按uploadId建独立子目录。否则多用户同时上传时文件会互相覆盖或者混在一起清理的时候也分不清哪些分块属于哪个上传任务。3.5 合并接口实现全部块传完之后前端会调用合并接口。合并的逻辑就是按索引顺序读取所有分块文件然后依次写入最终文件。这里我会带上一个文件大小校验读出来所有分块文件的总字节数如果等于初始化时记录的fileSize才认为合并成功否则说明有分块缺失或者损坏。PostMapping(/upload/merge) public Result mergeUpload(RequestParam(uploadId) String uploadId) { String chunkDir uploadConfig.getChunkDir() uploadId; String targetPath uploadConfig.getTargetDir() uploadId _ System.currentTimeMillis() _ originalName; File dest new File(targetPath); ListFile partFiles Arrays.asList(Objects.requireNonNull(new File(chunkDir).listFiles())); partFiles.sort(Comparator.comparingInt(f - Integer.parseInt(f.getName().split(\\.)[0]))); try (FileOutputStream fos new FileOutputStream(dest)) { for (File part : partFiles) { try (FileInputStream fis new FileInputStream(part)) { IOUtils.copy(fis, fos); } } } catch (IOException e) { return Result.error(合并失败); } // 清理临时分块目录 FileUtils.deleteDirectory(new File(chunkDir)); return Result.ok().data(path, targetPath); }合并的时候有一个我特别想强调的细节分块文件排序不能只按自然排序String.compareTo因为文件名中的索引会出现1、10、11、2这种顺序问题。必须把文件名中的数字提取出来转成int后再比较。我用的是split(\.)[0]因为文件名格式是“0.part”“1.part”“2.part”拆出数字部分来排序。如果你的分块号是从1开始自己心里要有数别在索引边界上搞混。合并完成后我建议顺手把临时目录删掉。否则时间一长临时分块文件会堆积成百上千个小文件磁盘占用和inode压力都会出问题。这块后面我会专门讲清理策略。3.6 前端WebUploader配置要点前端配置直接决定后端能否正常工作我把核心配置贴出来并标注每个参数的作用var uploader WebUploader.create({ swf: /static/Uploader.swf, server: http://localhost:8080/upload/chunk, pick: #picker, auto: true, chunked: true, chunkSize: 5 * 1024 * 1024, threads: 3, formData: { uploadId: uploadId }, fileVal: file, accept: { title: All Files, extensions: zip,rar,7z,jpg,png,pdf,mp4 } });这里的threads是并发上传的线程数不建议设置太高。并发数 分块大小 × 线程数 / 网络带宽 × 时间冗余。如果设置成105MB块10并发同时打过来就是50MB流量后端带宽不够直接排队超时反而比串行还慢。我经验值给3到5比较合适。还有一点WebUploader默认会对分块做失败重试。组件配置里的retry可以设置次数我在生产环境设置为3次。重试机制本身是好的但要注意如果后端分块接口处理慢前端重试会堆积请求。所以后端接口性能越高前端重试越少整体越稳定。这两个是相互关联的。