接手这类文档转换需求,我一度以为就是把文件后缀名改一改。真正做下来才发现,Word 里的文字根本不是“整齐地摆在那里”,TXT 也不是换个名字就能读懂的。如果你正打算在 Java 项目中写 Word 和 TXT 互转,或者接了个批量文档清洗的需求,这篇文章应该能帮你少踩几个坑。我会围绕 Apache POI 这套常用的开源方案,把从零开始接 Word、掌住编码、处理旧版 .doc 文件、写回带格式 Word 的完整过程拆开讲,顺便把那些测试环境没问题、一到生产环境就翻车的细节也交代清楚。
1. 整体思路与方案选择
1.1 先搞清楚 Word 和 TXT 的本质差异
很多人拿着一个a.doc就想通过改后缀变成a.txt,拿a.docx改名为a.zip后确实能看到一堆 XML,但直接把这些 XML 拼起来当文本用,得到的全是标签和乱码。这里面的关键点在于:TXT 是纯文本文件,文字就是文字;而 Word 文件是“容器”,文字、样式、图片、页眉页脚、修订记录都被打包在一个结构化容器里。
.docx本质是一个 zip 压缩包,里面有word/document.xml这样的 XML 文件,文字内容是嵌在<w:p>、<w:r>、<w:t>这类节点里的。.doc更旧,采用的是 OLE 复合文档格式,一种二进制结构,需要专门的解析器才能读出文本。所以想用 Java 实现互转,不能走“字符串读入再写出去”的野路子,必须借助解析 Office 文档结构的专用工具。
我最早接手一个需求,是给某公司做历史合同归档,几千份合同全是.doc老格式。如果直接按字节读,取出来全是乱码,甚至卡死。后面用了专门解析二进制文档的库,才把正文干净地提出来。这个项目也让我养成了看到转换需求先确认文件版本的习惯:.doc、.docx、.txt这三者的处理策略完全不同,一上来就写统一代码,注定要返工。
1.2 常见实现路线对比
做 Word 和 TXT 互转,圈子里能选的路线就那么几条,各有利弊。我列个表给你看:
| 实现路线 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| Apache POI(HWPF + XWPF) | 社区成熟、API 稳定、支持读写 | 对.doc支持不如.docx完善 | 绝大多数后端批量处理 |
| 纯 Java 文件流 | 没有额外依赖、速度快 | 只能处理纯文本,Word 结构解析不了 | TXT 到 TXT、简单拼接 |
| 调用外部命令行工具(如第三方文档转换器) | 兼容性最强 | 依赖系统环境、性能损耗大、部署复杂 | 容器环境、格式很复杂的文档 |
| 在线转换 API | 使用简单 | 有数据安全风险、不适合离线环境 | 临时个人处理,生产项目慎用 |
我后续的示例会以 Apache POI 为主,原因很直接:它在 Java 生态里对 Word 文件的支持最完整,而且不需要额外部署独立服务,打成 jar 就能跑,运维成本低。
选择 Apache POI 不是因为它没有缺点。.doc老格式的富文本处理、复杂样式还原,POI 的能力是有边界的。但如果你只是做“把 Word 正文提取成纯文本”和“把纯文本写进 Word 文档”,这套组件完全够用,上手难度也最低。对于那种还要保留图片、表格、复杂排版的转换需求,POI 力不从心,得另想办法。我遇到的老合同案例里,就需要弄清楚“哪些要素必须保留”,这决定了选型。
1.3 方案选型背后的考量
我之所以强调先读懂需求再选方案,是因为 Word 转 TXT 的“转”字有很多层含义。有人只要正文内容,有人要保留段落顺序,有人要连表格里的格子内容都按顺序导出,还有人希望把页眉页脚一起抓出来。
- 只提取正文段落:用 XWPFParagraph 遍历即可;
- 连表格一起提取:需要额外遍历 XWPFTable;
- 提取文本并保留层级:需要在段落中识别样式、编号;
- 提取后交给下游搜索/分析:建议输出成带分隔符的结构化文件,而不是纯 TXT。
这些需求直接影响代码实现。我在下面几节会按“从最简到稍复杂”的层次来写,你可以按需取用。
2. 核心细节原理解析
2.1 .docx 的文本到底藏在哪
很多时候你打开一个.docx,眼睛看到的是排版整齐的文章,但用文本编辑器打开文件,看到的是乱码。原因就是上面提到的 zip 压缩结构。.docx的内部结构大致是这样:
mimetype _rels/.rels word/document.xml word/_rels/document.xml.rels word/styles.xml真正承载正文的word/document.xml是一个 XML 文件。文本流藏在<w:body>里,每一段是<w:p>,段落中的不同样式片段是<w:r>,真正的内容在<w:t>标签里。举例,一句“今天天气不错”可能是这样存的:
<w:p> <w:r> <w:t>今天天气不错</w:t> </w:r> </w:p>如果中间某个词被加粗,就会多出一个<w:rPr>节点。复杂文档里还会有文本框、表格、批注,它们嵌在不同的节点结构中。所以直接从 XML 里做正则匹配,理论可行,但你会被各种边角情况折磨。Apache POI 的作用就是帮你把这些 XML 节点解析成 Java 对象,然后通过 API 取文本,省去手工解析的麻烦。
2.2 编码问题:UTF-8 与 GBK 的恩怨
TXT 转 Word 时,最常见的问题是中文乱码。这不是 Java 的锅,而是编码不统一导致的。文本文件本身没有元信息显示编码,Windows 记事本保存的 TXT 默认是 GBK(或者说系统 ANSI 编码),而 Linux 环境和 Java 代码里普遍默认 UTF-8。
你写Files.readAllLines(path)时,如果不指定字符集,Java 会用平台默认值。在 Windows 上可能还有救,在 Linux 服务器上就会读取 GBK 文件变成乱码。正确做法是读文件时显式传入编码,比如StandardCharsets.UTF_8或者Charset.forName("GBK")。
写 TXT 时同理。你导出的文件如果拿到 Windows 上打开,可能因为少了 UTF-8 BOM 头而显示乱码。Java 默认不写 BOM,这时需要在输出流最前面手动写入EF BB BF三个字节。很多“为什么我导出的文件客户打开是乱码”的问题,就是 BOM 的问题。
2.3 区分.doc和.docx的 API
Apache POI 对两种 Word 格式提供不同的 API:
| 文件格式 | POI 包 | 核心类 |
|---|---|---|
| .doc(二进制) | poi | HWPFDocument、WordExtractor |
| .docx(XML) | poi-ooxml | XWPFDocument、XWPFParagraph |
.docx的 API 设计得比较现代,操作对象像标准 Java 集合,遍历段落、创建段落都很直观。.doc的老 API 则更像一个“只读提取器”,读写支持和.docx差了一截,尤其对复杂样式的写入,踩坑概率高。
如果你要处理大量.doc老文件,我建议升级到较新的 POI 版本,老版本对某些新版 WPS 生成的.doc文件解析会直接抛异常或者丢内容。后面“常见问题”一节,我会专门聊几个真实遇到的报错。
3. 实操:Word 转 TXT 的完整过程
3.1 项目初始化和依赖引入
我用 Maven 做依赖管理。如果你用 Gradle 或手动引 jar,思路一致。下面是pom.xml里的关键部分:
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>5.2.5</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.5</version> </dependency> </dependencies>这里要留意:poi和poi-ooxml的版本号一定要对齐。我以前曾把poi用 4.1.2、poi-ooxml用 5.0.0,启动时直接报NoSuchMethodError,排查了半天才发现是版本不一致导致的。POI 5.x 要求 Java 8 以上,建议 Java 11 起跳。
3.2 解析 .docx 并输出为 TXT
读.docx的核心代码非常短:
import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.*; public class DocxToTxt { public static void convert(String docxPath, String txtPath) throws IOException { // 读取原始 Word 文档 try (InputStream is = Files.newInputStream(Paths.get(docxPath)); XWPFDocument document = new XWPFDocument(is); BufferedWriter writer = Files.newBufferedWriter( Paths.get(txtPath), StandardCharsets.UTF_8)) { for (XWPFParagraph paragraph : document.getParagraphs()) { String text = paragraph.getText(); if (text == null || text.isBlank()) { // 遇到空段落也输出空行,保持原有文档的大致行结构 writer.newLine(); } else { writer.write(text); writer.newLine(); } } } } public static void main(String[] args) throws IOException { convert("input.docx", "output.txt"); System.out.println("转换完成"); } }这里XWPFDocument会负责解析整个 zip 结构和 XML,document.getParagraphs()返回正文中的所有段落。注意,getText()拿到的只是段落里的文本,不含图片内容。如果一个段落只有一张图片,getText()会返回空字符串。
我还习惯在遍历时打印一下段落总数和总字符数,用于快速判断文件是否解析成功:
long totalChars = 0; for (XWPFParagraph paragraph : document.getParagraphs()) { totalChars += paragraph.getText().length(); } System.out.println("段落数: " + document.getParagraphs().size()); System.out.println("总字符数: " + totalChars);这个“总字符数”是后面做批量校验的重要指标。如果源文件有 5000 字,你转出来只剩 300,那一定有问题。
3.3 解析老式 .doc 文件
.doc老格式用WordExtractor更方便:
import org.apache.poi.hwpf.HWPFDocument; import org.apache.poi.hwpf.extractor.WordExtractor; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.*; public class DocToTxt { public static void convert(String docPath, String txtPath) throws IOException { try (InputStream is = Files.newInputStream(Paths.get(docPath)); WordExtractor extractor = new WordExtractor(is)) { String content = extractor.getText(); // 这里可以按业务需求做进一步清洗 Files.writeString(Paths.get(txtPath), content, StandardCharsets.UTF_8); } } public static void main(String[] args) throws IOException { convert("input.doc", "output_from_doc.txt"); System.out.println("转换完成"); } }WordExtractor.getText()会把正文、页眉、页脚、文本框等多个来源的文本拼接在一起,中间可能有额外的换行符或分隔符。如果你只想要正文,需要在提取后做过滤。一个简单的做法是按行拆分,然后丢弃明显属于页眉页脚重复内容的行,但这属于“脏活”,得结合具体文档来写规则。
另一个重要点是.doc文件可能在末尾包含版本历史、批注等元信息,getText()有时会把它们带出来。生产环境里,我通常会在提取后跑一遍正则清洗和关键词过滤,确保导出文件里只有真正要交付的内容。
3.4 关于表格、文本框和页眉页脚
上面两段代码只处理了段落。如果 Word 里有表格,document.getParagraphs()是拿不到表格里文字的。这就涉及一个经常被忽略的问题:Word 转 TXT 到底要不要转表格?
我的建议是:如果业务上表格内容必须保留,那么不能只遍历段落,要另写逻辑遍历document.getTables()。示例代码如下:
for (XWPFTable table : document.getTables()) { for (XWPFTableRow row : table.getRows()) { for (XWPFTableCell cell : row.getTableCells()) { StringBuilder cellText = new StringBuilder(); for (XWPFParagraph paragraph : cell.getParagraphs()) { cellText.append(paragraph.getText()); } // 用制表符或竖线分隔单元格 writer.write(cellText.toString()); writer.write("\t"); } writer.newLine(); } }这样导出的 TXT 依然不是“漂亮”的表格,但保留了行列顺序,至少比直接把表格丢掉强得多。同理,文本框里的文字在不同版本的 POI 里支持程度不一样,如果发现漏内容,可以试试直接解析 XML 节点,但那种方案只适合特别较真的场景,日常批量处理先接受“部分丢失”。
4. 实操:TXT 转 Word 的实现方法
4.1 最简单但能用的方案
TXT 转 Word 相对简单,因为 Word 文档是“可创建”的。你新建一个空白XWPFDocument,每读一行 TXT 就新建一个段落。基础代码如下:
import org.apache.poi.xwpf.usermodel.*; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.*; public class TxtToDocx { public static void convert(String txtPath, String docxPath) throws IOException { try (BufferedReader reader = Files.newBufferedReader(Paths.get(txtPath), StandardCharsets.UTF_8); XWPFDocument document = new XWPFDocument(); OutputStream out = Files.newOutputStream(Paths.get(docxPath))) { String line; while ((line = reader.readLine()) != null) { // 每个非空行作为一个段落 XWPFParagraph paragraph = document.createParagraph(); XWPFRun run = paragraph.createRun(); run.setText(line); } document.write(out); } } public static void main(String[] args) throws IOException { convert("notes.txt", "notes.docx"); System.out.println("转换完成"); } }这个方案的问题是:TXT 里的空行会直接丢失,因为readLine()读取空行时返回空字符串,我上面的代码还会创建空段落,但如果你在循环里加了continue跳过空串,就应该后续补document.createParagraph()。哪种更好?看你对“保留原文行结构”的要求,个人建议保留空段落,转出来的 Word 会更接近原 TXT 的观感。
4.2 让 Word 更“像样”:标题、加粗和居中
只有文字的 Word 文档在交付时往往会被嫌弃,尤其是给业务方看的时候。既然用 POI,你可以轻松地为 TXT 中的特定行加格式。比如约定规则:以“# ”开头的行变成标题,以“## ”开头的变成二级标题,其余是正文。
public static void convertWithFormat(String txtPath, String docxPath) throws IOException { try (BufferedReader reader = Files.newBufferedReader(Paths.get(txtPath), StandardCharsets.UTF_8); XWPFDocument document = new XWPFDocument(); OutputStream out = Files.newOutputStream(Paths.get(docxPath))) { String line; while ((line = reader.readLine()) != null) { XWPFParagraph paragraph = document.createParagraph(); if (line.startsWith("# ")) { paragraph.setAlignment(ParagraphAlignment.CENTER); XWPFRun run = paragraph.createRun(); run.setText(line.substring(2)); run.setBold(true); run.setFontSize(22); run.setFontFamily("宋体"); } else if (line.startsWith("## ")) { XWPFRun run = paragraph.createRun(); run.setText(line.substring(3)); run.setBold(true); run.setFontSize(16); } else { XWPFRun run = paragraph.createRun(); run.setText(line); run.setFontSize(12); } } document.write(out); } }有几个细节值得注意。setFontFamily("宋体")在 Linux 服务器上并不会真正“嵌入”字体,它只是写入字体名称,最终打开文档的电脑如果没有宋体,会自行替换。这个行为是正常的,不用慌。如果业务上要求字体必须是某种,可以预先在服务器上安装对应字体,但这属于系统层面的事,Java 代码改变不了。
4.3 把 TXT 转换结果存成 .doc 的注意事项
如果你遇到的业务非要.doc后缀,我的建议是:原则上报错或生成.docx后改名,而不是用 HWPF 硬生成.doc。原因很现实:POI 对.doc的写入支持远不如.docx。HWPF 能创建文档、写入基本段落,但对中文样式、页眉、复杂分节的支持都有各种限制,容易踩到内存或未实现功能的坑。
我做过一个项目,客户明确要求导出.doc,因为内部老系统只认这个格式。我的折中方案是:先用XWPFDocument生成.docx,再用第三方转换服务统一转为.doc。如果不想引入重量级方案,也可以把.docx文件名直接改成.doc,很多 Office 软件能识别并打开,但这本质上是不规范的,不推荐用于正式交付。
5. 进阶:批处理转换与结果校验
5.1 批量转换目录下的所有文件
实际业务中很少只有一个文件要转,更多是一整个目录下的 Word 文件批量变 TXT,或者反过来。批量处理最怕的就是单文件异常导致整个任务中断。我推荐的做法是:每个文件单独捕获异常,并把失败信息记录下来。
Path sourceDir = Paths.get("/data/docs"); Path targetDir = Paths.get("/data/txts"); Files.createDirectories(targetDir); try (Stream<Path> paths = Files.walk(sourceDir)) { paths.filter(Files::isRegularFile) .filter(p -> p.toString().endsWith(".docx") || p.toString().endsWith(".doc")) .forEach(p -> { String fileName = p.getFileName().toString(); int dotIndex = fileName.lastIndexOf('.'); String baseName = dotIndex > 0 ? fileName.substring(0, dotIndex) : fileName; Path target = targetDir.resolve(baseName + ".txt"); try { if (p.toString().endsWith(".docx")) { DocxToTxt.convert(p.toString(), target.toString()); } else { DocToTxt.convert(p.toString(), target.toString()); } System.out.println("OK: " + p); } catch (Exception e) { System.err.println("FAIL: " + p + " -> " + e.getMessage()); } }); }这里有两个性能相关的细节。Files.walk遍历大目录时是惰性加载,配合try-with-resources能避免文件句柄泄漏。如果想要更快,可以改造成并行流或者线程池。但要注意,POI 解析文档属于内存密集型操作,并行度不宜太高,我一般控制在 CPU 核心数以内,否则反而因为 GC 频繁导致整体变慢。
5.2 转换后的内容质量如何校验
转换软件和手工交付不一样,自动化批量转换后必须有一套校验机制。我常用的轮子有三层:
- 字符数对比:统计原文档字符总数(可以通过 POI 的
paragraph.getText()累加),和输出 TXT 的字符数对比,允许一定误差,但不能差太多; - 内容抽样:随机抽几个段落,确认含有关键业务词汇;
- 行数稳定性:转换后的 TXT 行数应在合理区间内,如果比段落数还少,说明有空段落被跳过。
public static long countCharsInTxt(String txtPath) throws IOException { long count = 0; try (BufferedReader reader = Files.newBufferedReader(Paths.get(txtPath), StandardCharsets.UTF_8)) { String line; while ((line = reader.readLine()) != null) { count += line.length(); } } return count; }这种“自动化校验”在别人看来可能多余,但真正经历过一次漏转换事故后就会明白它多重要。那次事故是某个.doc文件用旧版 POI 解析时,内容提取完全失败但没抛异常,返回的是空字符串。如果没有字符数校验,这批空文件就静默交付出去了。
5.3 大文件转换的内存建议
POI 解析.docx时会把文档结构加载进内存,一个 10MB 的 Word 文件,解析时可能占用 150MB 以上堆内存。如果内存紧张,有几个方向:
- 使用 SXSSF 的“流式写”思维在这里不适用,XWPF 没有真正意义上的流式读;
- 尽量使用
XWPFDocument(InputStream),不要用XWPFDocument(File),因为传入 File 时 POI 会再做一次内存映射; - 转换完立即关闭资源,循环中不要持有多个
XWPFDocument实例; - 对超大文档考虑分页加载或使用底层 XML 事件解析,但开发成本高,一般不做。
我遇到过一个 80MB 的.docx,里面有大量图片,用默认参数解析直接 OOM。最后的处理方式是把文档先拆分成多个章节再转换,或者干脆换用底层解析模式。这类极端情况跑一次就能涨不少经验,好在日常工作里 5MB 以下的文档占绝大多数。
6. 常见问题与排查技巧实录
6.1 中文乱码:到底是编码还是字体的问题
我最早遇到中文乱码,第一反应是“编码设置错了”,于是反复在new String(bytes, "UTF-8")和"GBK"之间切换。后来才发现乱码分很多种,先要分清是读取乱码、写入乱码,还是字体缺失导致的显示乱码。
- 读取 TXT 乱码,表现为
��或“锟斤拷”:大概率是读取时用的字符集和文件实际编码不一致; - 写入 TXT 后,在 Windows 记事本打开乱码:大概率是写入时用了 UTF-8 但是没有 BOM 头,Windows 默认按 ANSI 解读;
- 生成的 Word 在别人电脑上打开,中文变成方块或换字体:这不是乱码,是字体缺失,和编码无关。
排查技巧:用十六进制工具看一眼前几个字节。如果文件开头是EF BB BF,是 UTF-8 带 BOM;如果以FF FE开头,是 UTF-16 LE;如果什么都没有,可能是 ANSI/GBK,需要进一步用文本编辑器探测。
6.2 读取 .docx 抛异常或者内容为空
POI 读取.docx最常见的异常有两类。一类是java.lang.NoClassDefFoundError,比如缺少xmlbeans依赖,解决方法是把poi-ooxml完整引入,带齐它的传递依赖。另一类是org.apache.xmlbeans.XmlException,通常是文件本身损坏或不是标准 Office 文件。
还有一种情况:文件后缀是.docx,但实际是旧版.doc改后缀得到的。POI 使用XWPFDocument读这种文件会失败。改用WordExtractor读又能成功。所以在文件格式判断上,不能只看后缀,必要时要做“嗅探”,也就是读文件头字节判断真实格式。
6.3 转换后丢失内容:表格和文本框
我在 3.4 节提过表格和文本框的提取问题。这里再强调一次:如果只用getParagraphs(),表格内容一定会丢。遇到“内容少了”的情况,第一件事是检查源文档里是否有表格、文本框、公式等非段落对象。
想比较完整地导出,可以做一个简单的扩展:先遍历段落,再遍历表格,最后遍历图形文本。但这种做法也无法 100% 保证顺序,因为文档中的对象嵌套关系很复杂。我的建议是按业务需求取舍,明确告诉下游“TXT 只保留正文段落”,避免后续扯皮。
6.4 文件被占用或权限问题
Windows 服务器上做批量转换时,经常遇到文件被打开的报错。POI 读取文件时如果检测到文件被独占,会抛FileNotFoundException或AccessDeniedException。这时候不要立刻重试,先检查是否有 Office 进程占用了目标文件。
输出文件路径也不要有奇怪字符。我遇到过目标路径包含中文空格,导致写入失败的情况。稳妥的做法是统一用英文路径,或者提前用Files.createDirectories创建目录。
6.5 性能太慢:瓶颈往往在 I/O 而不在 POI
有一次批量转换几百个文件,跑了一个小时还没结束。我下意识以为是 POI 解析慢,后来加了耗时统计才发现,绝大多数时间花在读取网络磁盘上。如果你的 PDF/Word 文件放在共享存储或远程目录,Files.newInputStream的耗时常常比解析还高。
优化方向:把远程文件先下载到本地临时目录,再交给 POI 解析;批量场景下用固定线程池控制并发;转换完成后的目标 TXT 不逐行写,而是攒成StringBuilder一次性写出,减少系统调用。
这里要提醒一句:一次性写大字符串也可能爆内存,所以我一般攒到 1MB 左右就 flush 一次。
7. 扩展:从工具方法到可复用服务
7.1 封装一个支持多格式的转换接口
写到这里,你已经有了 Word 转 TXT 和 TXT 转 Word 的代码。但如果你直接把这些方法扔给业务方,对方大概率会嫌“不够好用”。更好的做法是封装一个统一接口,比如FileConverter,让调用方只关心源文件和目标路径。
public interface FileConverter { void convert(String sourcePath, String targetPath) throws IOException; } public class WordToTxtConverter implements FileConverter { @Override public void convert(String sourcePath, String targetPath) throws IOException { if (sourcePath.toLowerCase().endsWith(".docx")) { DocxToTxt.convert(sourcePath, targetPath); } else if (sourcePath.toLowerCase().endsWith(".doc")) { DocToTxt.convert(sourcePath, targetPath); } else { throw new IllegalArgumentException("不支持的文件格式: " + sourcePath); } } }接口的好处是后续可以替换具体实现,比如从 Apache POI 换成别的组件,不需要改调用方代码。
7.2 配合 Spring Boot 暴露上传下载接口
很多人的实际需求是做成一个 HTTP 接口,前端上传 Word,后端返回 TXT。这个场景下,代码和文件流处理几乎是一样的,不同点是要从MultipartFile读取输入流,并把结果写入HttpServletResponse的输出流。
@PostMapping("/convert/word-to-txt") public ResponseEntity<byte[]> convertWordToTxt(@RequestParam("file") MultipartFile file) throws IOException { XWPFDocument document = new XWPFDocument(file.getInputStream()); StringWriter writer = new StringWriter(); for (XWPFParagraph paragraph : document.getParagraphs()) { writer.write(paragraph.getText()); writer.write("\n"); } byte[] result = writer.toString().getBytes(StandardCharsets.UTF_8); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=converted.txt") .contentType(MediaType.TEXT_PLAIN) .body(result); }这个接口看起来简单,但有两个隐患。一是上传文件过大时,MultipartFile会把整个文件加载进内存,容易把服务压垮。二是StringWriter对超大文档同样吃内存。生产环境里要结合项目实际情况做限制,或者换成流式处理。
7.3 定期任务与日志埋点
最后分享一个我自己的习惯:任何文件转换任务都要打结构化日志。不是System.out.println,而是记录文件大小、解析耗时、段落数、字符数。这些数据平时不起眼,你要排查“为什么凌晨批量任务挂了”时就成了救命稻草。
long start = System.currentTimeMillis(); // 执行转换 long cost = System.currentTimeMillis() - start; System.out.printf("file=%s, size=%d, paragraphs=%d, chars=%d, cost=%dms%n", sourcePath, fileSize, paragraphCount, charCount, cost);日志记录多了,你会慢慢摸出规律,比如“超过 3MB 的.docx转换耗时基本在 5 秒以上”“带大量图片的文档容易内存飙升”。这些经验值用在实际容量规划上非常有用。
我自己的体会是,Word 和 TXT 转换本身不算一个高深的技术点,真正考验人的地方在于格式判断、编码处理、异常兜底和批量场景的工程化。很多人写完核心转换代码就以为完事了,等到生产环境跑起来,才发现各种“不按套路出牌”的文件。如果你能把上面这些边界情况都处理好,这套转换功能就能稳定撑起大部分业务需求。最后再分享一个小技巧:凡是转换类的工具代码,我建议一定加一个“转换前后对比”的测试用例,用一个你自己准备的、包含表格、图片、中文、长段落的样例文档,每次改完版本跑一遍,比什么都稳。