1. 需求拆解与可行方案设计
先说说这个需求最真实的场景。机械制造企业的设备手册,往往不是一本简单的PDF,而是一整个文件夹,里头套着子文件夹,放着操作说明、电气原理图、保养记录、备件清单、甚至供应商提供的DWG图纸。这些文件加在一起,动辄几个GB,文件夹层级四五层深是常态。
真正上手做的时候,你会发现最头疼的不是“上传”,而是“续传”。车间现场网络环境不像写字楼,内网传输看着是千兆,但中间可能隔着交换机、防火墙审计策略,甚至车间里老旧的无线AP,传一个512MB的大文件传到一半闪断是常有的事,让人心态炸裂。更麻烦的是,设备服务器里的手册文件往往还要分发给多个分厂或班组终端,每一次全量拷贝都是一种折磨。
既然标题圈定了Java,说明技术栈已经定了。我在设计时选定了“分卷压缩+目录结构清单+断点续传+文件重组”这条路线,而不是直接做单个文件的断点续传。为什么?因为设备手册文件夹里头有大量几KB的小文件(比如CAD图里的BOM表、excel备件单),如果每一个文件都单独走一遍断点续传协议,光是握手和校验的时间就够喝一壶茶了。把文件夹先打成一个大分卷包,再对分卷包做续传,能够把“文件数”的复杂度降到“分卷数”,效率完全不在一个量级。
有人可能会问,为什么不直接升级成rsync或者用文件同步工具?这个我也考虑过。但在很多制造企业内网里,Windows Server + 自建Web应用是主流,网络策略也不允许开放SSH等额外端口。基于已有的Java Web应用平铺一套分卷续传能力,不用动基础设施,也不用在车间终端上装额外客户端,是最省事的路径。
整个方案我拆成了四个核心环节:扫描目录结构、生成清单文件、分卷续传、重组还原。这四个环节环环相扣,前一个环节产出的东西,后一个环节直接消费,没有多余的数据中转。
2. 核心流程一:扫描目录结构与生成清单
动手写第一行Java代码之前,先把目录结构的持久化设计想清楚。我采用的是JSON清单文件,Java的生态里其实并没有强制要求,但用workbook记录meta信息的做法确实值得一试。用Java的NIO的Files.walkFileTree去遍历设备手册根目录,每遇到一个文件或文件夹,就把相对路径记录到JSON数组里,同时把CRC32校验值放到数组项中。
生产环境的真实数据大概是这样:根目录下有“整机说明书”“电气图纸”“液压原理图”三个一级目录,电气图纸下面又分了“2024年更新版”和“原厂版”两个子目录。文件名有些是中文的,有些还带空格和括号,路径层级也深浅不一,所以清单必须支持特殊字符,否则重组时一碰到文件名里有空格就废了。
这里有个关键点:不要把扫描结果只放在内存里,一定要实时写盘。比如每扫描500个文件就把当前JSON快照更新一次,这样即使中途有人改了文件夹内容,也能对比“当前扫描快照”和“上次扫描快照”找出差异。我做过一个版本,因为忘了这一步,结果扫描完7万个文件后进程OOM了,白干一场。
清单文件的格式我也直接给出,后面组装的时候要用这个内容:
{ "version": "1.0", "rootName": "CNC-850L手册汇总", "fileCount": 6238, "totalSize": 7123456789, "entries": [ { "path": "整机说明书/电气原理图/2024版/主回路图.pdf", "size": 2457600, "crc32": 890123456 }, { "path": "备件清单/BOM表_更新.xlsx", "size": 102400, "crc32": 123456789 } ] }在shop floor(车间现场)使用场景里,这个JSON的totalSize非常关键,它直接决定下面分卷尺寸怎么算。还有一点容易被忽略:JSON里一定要记录rootName,因为不同设备的文件夹命名习惯差异很大,有了rootName,后面重组时才能创建一个精确对应的顶级目录,不会把东西散一地。
3. 核心流程二:分卷策略与文件生成细节
分卷大小不能拍脑袋定,要结合内网实际传输速度来定。我这边车间里实测过,千兆局域网理论速率是125MB/s,但经过三层交换机跑到业务服务器,实际稳定速率大概在30-50MB/s。如果分卷设得太大,比如单个卷1GB,一旦中间闪断,恢复续传后要从头传这个1GB,代价太高;如果分卷设得太小,比如2MB一个,又会导致并发上传时文件句柄频繁开关,Java的FileOutputStream开开关关产生的IO开销反而拖慢整体速度。
我最终把分卷大小设成200MB一个。这个值有两个好处:一是一般网络波动在几十秒内就能完成一个分卷的传输,闪断造成的重传范围可控;二是200MB正好可以被JVM的堆外内存缓冲很好地处理,不需要搞复杂的多级内存映射。
分卷实现思路其实很朴素,但有一个细节要注意。我读源文件时用BufferedInputStream,写分卷时用BufferedOutputStream,每写完一个分卷就关闭输出流,然后new一个新的流继续写。这里最容易踩坑的是:最后一个分卷往往不满200MB,如果你不做特殊处理,接收端重组时就会因为卷的末尾填充不一致导致文件校验失败。我通常的做法是,最后一个分卷不填充,在清单文件的元信息里记录每卷的实际字节数,接收端重组时严格按记录长度读取,而不是固定按200MB去切。
生成分卷文件时,输出文件的命名也有讲究。我建议用原文件名.part.001这种三段式命名,不要用part1、part2这么简陋的命名。为什么?因为一旦分卷数超过999个,简洁命名会直接导致排序错乱,重组时候就抓瞎了。用001、002这种数字位数固定的格式,后续无论用Java的Collections.sort还是直接用数据库排序,都不会乱。
同时,为了不让这个流程白跑一遍,每次分卷切割完成后,程序需要对当前分卷文件再做一次MD5校验,并把MD5值记录到数据库表manual_part_info里。这里的校验不要复用传输时的CRC32,因为CRC32在几GB分卷场景下碰撞概率虽小,但我不想赌。传输完成后用MD5做完整性确认,心里踏实。
分卷的状态机设计也值得一说:
| 状态码 | 状态含义 | 下一步动作 |
|---|---|---|
| 0 | 初始已生成 | 等待分配上传节点 |
| 1 | 传输中 | 记录当前偏移量 |
| 2 | 已传完 | 服务端做MD5校验 |
| 3 | 校验通过 | 标记为待重组 |
| 4 | 重组完成 | 清理分卷与临时文件 |
这个状态机不算复杂,但它把整个“传输中崩溃”的场景兜住了。手里有状态,心里就不慌。
4. 核心流程三:目录结构的续传传输闭环实现
真正实现续传的核心逻辑,依赖的是一个偏移量记录机制。我在数据库表manual_upload_offset里记录每个分卷文件的已传输字节数,每次客户端上传数据时,都带着分卷文件的ID和当前分卷内的起始偏移量来请求。
Java后端接收上传的接口,我用的Spring Boot的MultipartFile方式,但这里有个使用细节:MultipartFile默认会先把整个文件加载到内存,对于200MB的分卷来说,这个默认配置是致命的。一定要在配置文件里设置spring.servlet.multipart.max-file-size=256MB和spring.servlet.multipart.max-request-size=300MB,同时把spring.servlet.multipart.file-size-threshold设置为10MB,这样小于10MB的临时文件走内存,大于10MB的走磁盘临时文件,兼顾速度和稳定性。
接口内部真正的逻辑其实就三步:第一步,校验请求参数里的分卷ID和偏移量,与数据库里的记录对比,如果偏移量对不上,直接返回409提示客户端需要重新对齐。这样做的目的是防止并发情况下重复上传同一段数据,造成数据错位。第二步,读取MultipartFile的InputStream,把数据追加到服务器端分卷文件的RandomAccessFile的指定位置,注意这里用RandomAccessFile的seek(offset),不是直接写文件末尾。换成线下能听懂的比喻,就是在给一本300页的书续写内容,你得把书翻到第200页再开始写,而不是傻乎乎地在第300页后面接着写。第三步,写入成功后更新数据库的偏移量和更新时间,同时给客户端返回服务端当前确认的偏移量,这个返回偏移量的动作很重要,客户端可以用它做双保险校验。
客户端分卷续传的上传策略,我也总结成了一段可以照抄的伪代码流程:
- 加载分卷文件列表,过滤掉状态为4和3的分卷。
- 对每一个分卷文件,先调用
/getUploadedOffset接口,获取该分卷的服务端已确认偏移量。 - 如果服务端偏移量等于分卷文件总大小,直接标记完成,进入下一个分卷。
- 如果服务端偏移量小于分卷文件总大小,用
RandomAccessFile打开本地分卷文件,seek到服务端偏移量位置,开始边读边传。 - 每传完一个块(我设的是4MB,对齐网络的MTU,能减少小包数量),更新本地进度条,并记录到本地临时进度文件。
这套流程不依赖任何第三方断点续传库,就是纯Java的IO流和HTTP请求,跟老技术栈的兼容性也很不错。Sping MVC的@RequestParam("file") MultipartFile file直接接收分块数据就好。
还有一个车间实际的痛点,就是有些分厂终端的网络可能更差,200MB的分卷依然有极低概率出现“传了199MB后闪断”的情况。针对这个场景,我又在客户端单独做了一个补救开关:当连续三次上传失败且失败偏移量相同,则启动“微重传”模式,把分卷粒度内部再细分32MB的小段重传。这个小功能在生产中救过我好几次,虽然不是常规路径,但备着心里踏实。
5. 核心流程四:服务端重组还原设备手册目录结构
全部文件状态都变成3(校验通过)之后,就可以触发重组逻辑。重组逻辑我建议做成一个独立的定时任务,不要跟上传接口混在一起,避免长事务占用数据库连接。我用Spring的@Scheduled每30分钟扫描一次数据库,发现某个根目录下的所有分卷状态都为3时,就自动开始重组。
组装代码的核心思路是:根据分卷文件号从小到大读取每个分卷的字节内容,写入一个临时完整压缩包文件。这里强调的是“按顺序”,因为分卷文件切割时就是一个字节一个字节切开的,组装时必须完全按顺序拼接,不能倒序,否则文件直接就废了。
完整压缩包拼好后,再根据清单JSON里的entries数组做解压,并逐个写入对应目录。因为清单里已经记录了每个文件的路径和CRC32,解压后把文件写到rootName对应的目录下,然后逐文件做校验,对不上号的记录下来,并回退状态机到分卷状态。
实际写代码的时候,重组解压环节我用的是Apache Commons Compress库,它对各种压缩格式的支持比JDK自带的ZipInputStream要全面得多,特别是在处理中文文件名和超大文件上,坑少很多。
重组完成后,还有一个容易被忽略的事:把零散分卷文件移动到归档目录,保留至少30天再清理。我遇到过一种情况:一个车间说手册看不了,排查到最后是办公网里的杀毒软件把某些分卷文件当病毒给隔离了。因为分卷文件的后缀名是.part.001这类非标准后缀,杀毒软件确实可能误判。归档保留30天,就是为了这种意外留恢复余地。
重组过程的关键参数,我这里放一张表,方便你们参考:
| 参数项 | 推荐值 | 说明 |
|---|---|---|
| 分卷大小 | 200MB | 根据网络质量可调 |
| 临时目录 | 与正式库不同磁盘 | 避免IO竞争 |
| 校验算法 | MD5 | 兼顾速度与可靠性 |
| 重试次数 | 3次 | 超过直接报警 |
| 数据库连接 | 独立事务边界 | 避免长事务锁表 |
6. 常见问题排查与人肉避坑实录
开发这套系统时,我踩了不少坑,有些东西是文档里不会写的,这里整理成清单,能给后来人省点时间。
第一个经典问题,中文文件名在解压时乱码。这个问题十有八九会碰到,因为设备手册里大量由供应商提供的文件,中文命名比例极高。解决办法是在解压时明确指定字符集:ZipFile构造时传入GBK或UTF-8,具体依据压缩工具而定。我建议你们在做分卷压缩时,先统一用Java的ZipOutputStream,并且创建条目时强制使用UTF-8,同时设置ZipOutputStream的编码方式,这样能够从源头规避大量乱码问题。
第二个问题,断点续传时服务端RandomAccessFile文件句柄泄漏。别笑,这个真的很常见。因为每上传一个分块,程序就会打开一次RandomAccessFile,如果忘记在finally块中关闭,Windows上文件就会一直被占用,后续重组时想读取该文件就会报“文件正在被另一进程使用”。排查方式也简单,上传完一个分卷后,尝试手动重命名该分卷文件,如果重命名失败,基本就是句柄没释放。
第三个问题,也是最有“制造业特色”的:车间的网络设备重启后,分卷文件在服务器上只有一部分。这其实是正常的,因为服务端是RandomAccessFile,只有写到数据库偏移量才算数。但有些同事习惯用网盘上传,遇到一半失败就以为服务器上有了半份文件,所以我要在代码里给每个分卷文件加一个隐藏的标记文件(比如.part.001.meta),里面记录服务端确认的偏移量。这样即使数据库丢失了,也能通过这个标记文件恢复出偏移量记录,不至于满脸问号。
还有一个问题是性能调优相关的:如果车间内网很繁忙,大分卷的IO容易造成CPU占高。我在生产环境调优时,把RandomAccessFile的写入缓冲设成1MB,并且关闭了OS的文件系统缓存策略(不开启fileChannel.force()),这样在传输场景下能把吞吐量提上去不少。不过代价是极端断电场景下这部分数据可能丢失,但因为分卷状态是可以通过客户端重新上传补齐的,所以这个取舍是划算的。
最后再给个安全相关的提示:内网上传接口务必加一个简单的Token鉴权,不要裸奔。车间里偶尔会有维修人员或者外部厂商工程师接入内网,扫到没有鉴权的上传端口,可能会狂传数据把你的磁盘塞满。我在服务端加了基于HTTP Header的X-Access-Token校验,Token存在配置中心,定期更换,成本极低但很管用。
7. 性能监控与后续扩展思路
这套方案上线运行之后,监控是必不可少的。我在服务端加了一个简单的业务监控指标,每完成一个分卷就向后端数据库表写入一条记录,然后通过Spring Boot Actuator暴露给轻量级监控面板展示。主要看三个数据:平均每个分卷传输耗时、失败重试次数、以及正在传输中的分卷数与分卷总量比值。这个比值可以直观反映车间网络健康度,如果持续高,那就建议车间网络管理员去排查交换机端口或防火墙规则了。
后续如果要扩展,有两个我很推荐的方向。一是把清单JSON和分卷文件整体加密,因为设备手册里包含的电气图纸和机械参数,在很多企业里算是不公开的技术资料,走HTTPS加解密既能防旁路窃听,又不会对性能造成太大影响。二是把传输协议从HTTP改造为基于WebSocket的二进制流传输。WebSocket对长连接和重连的支持比HTTP的短连接更顺滑,特别适合车间这种物理链路经常抖动的场景。不过这个改造量稍大,得动整体网络框架,我是建议在当前方案跑通一个月、数据积累稳定之后再考虑。
8. 开发与运维曲线的一点建议
从技术框架搭建到车间试点,我建议把它当一个小型交付项目来排期,别太乐观地以为两三天就能搞定。我在实际推进时,排的是三周的节奏,分三个阶段来跑。第一周是做分卷切割和清单生成,这个阶段相对单纯,只需要在测试环境里把目录结构的数据做准确。第二周是突破点,重点做续传接口和客户端上传逻辑,这部分的联调要在模拟车间网络抖动的情况下测试,不能只在稳定局域网里测。第三周是重组和校验联调,同时给车间的设备管理员做一个简单的使用培训。
运维层面的建议是,一定要在正式环境备一台映射了内网地址的测试机,当车间反馈“手册看不到”的时候,你能直接登上去排查,不用来回拷贝文件。我之前的经验是,很多“手册看不到”的问题,90%都不是传输问题,而是他们终端电脑的盘符映射变了或者杀毒软件拦截了访问权限。所以问题定位顺序也很重要,先查终端环境,再查服务端文件,最后才是查数据完整性。
这个方案本身不花哨,但说实话,在制造业内网环境里,"稳定、看得见、能续传"比什么都重要。用Java写这套东西,最大的好处就是好维护、好交底,车间IT团队哪怕是初级工程师,看几遍代码也能接得住,这对长期维护来说是最大的隐形成本节约。