拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java数据库导出Word文档:JDBC+POI完整实现方案

Java数据库导出Word文档:JDBC+POI完整实现方案

简介:一个面向.NET开发者的C#数据库转Word文档完整工程,演示如何通过ADO.NET连接SQL Server等数据库,读取表结构与字段信息,并借助Word对象模型将数据以表格形式导出为文档。适用于有C#基础、需要实现报表自动生成或数据导出功能的开发人员。压缩包共230个文件,约2.62MB,以51个.cs源码文件为核心,另含46个dll库、32个pdb调试文件、9个csproj工程文件及6个config配置文件,并附3个sql脚本与说明文档,目录结构完整,可直接在Visual Studio中打开运行。目前已有429人学习使用。借助该项目可掌握数据库连接、SQL查询构造、Word表格填充与样式设置的完整流程,同时了解Office Interop与NPOI/OpenXML等替代方案的选型思路,对实现自动化报表与数据导出工作流有直接参考价值。

1. 程序实现数据库生成 word 文档:这不是导出 PDF,是一套能直接改的导出接口

很多朋友拿到“程序实现数据库生成 word 文档”这个需求时,第一反应是打开一篇 POI 教程抄几行代码,结果导出的文档要么中文乱码,要么表格多出一行,要么打开时提示需要修复。这套资源拆解下来其实是一条完整的导出链路:程序从数据库查出记录,按配置好的字段映射和样式规则,生成可编辑的 docx 文档。它适合后端开发接报表导出、课程设计做管理系统,以及每天要从业务库手工复制数据到 Word 里的运维同事。它不是一次性脚本,而是一个能跑通的工程,改改数据库连接和 SQL 就能用在多个项目里。

2. 选型不能拍脑袋:JDBC 读库 + POI 拼文档,先看对比再动手

数据库生成 word 文档的程序,网上能找到很多版本,但大多数是用 HTML 改后缀名冒充 docx。真正的导出程序要能控制标题级别、表格宽度、分页位置,还要能应对中文。我选型时对比过三条路:JDBC 直连 + Apache POI、ORM + docx4j、Word 模板 + XML 占位符。这套程序最终走的是 JDBC + POI,原因很直接:依赖少,导出逻辑肉眼可见,出问题能直接定位到是哪一步写操作出了问题。下面把选型理由和工程结构讲清楚。

2.1 数据读出:JDBC 直连负责“读得干净”,连接池负责“扛得住”

数据读出的关键不只是把 SQL 跑一遍。业务系统里的数据库通常有多个库、多种字符集,导出程序一旦连错库或者忽略编码,后面生成的 Word 全是乱码。这里选择 JDBC 直连,而不引入 MyBatis 或 JPA,是因为导出程序的查询逻辑往往只有几条固定 SQL,用 ORM 反而要维护实体类和 XML 映射,多一层没必要的转换。程序里把连接信息放在 jdbc.properties,方便换环境时直接改配置。

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/report_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true jdbc.username=report_user jdbc.password=report_pwd jdbc.maxActive=10 jdbc.initialSize=2

这段配置里最容易被忽略的是 URL 后面的四个参数。useUnicode 和 characterEncoding 必须成对出现,utf8 决定了数据库返回的字符串进入 Java 后不会变成乱码;serverTimezone 指定数据库服务器的时区,否则新版 MySQL 驱动会在连接阶段直接报 timezone 异常;rewriteBatchedStatements 在批量写入场景下有效,这里虽然只是查询,但保留它对同一条连接执行多次预编译有好处。maxActive 表示连接池最大活跃连接数,导出操作如果同时被多个用户触发,设得太小会互相等待,设得太大又会让数据库连接数爆掉。程序里一般会再封装一个 JdbcManager,用 HikariCP 做连接池,避免每次导出都创建新连接。

public class JdbcManager { private static HikariDataSource dataSource; static { Properties props = new Properties(); try (InputStream in = JdbcManager.class.getClassLoader().getResourceAsStream("jdbc.properties")) { props.load(in); } catch (IOException e) { throw new ExceptionInInitializerError(e); } HikariConfig config = new HikariConfig(); config.setJdbcUrl(props.getProperty("jdbc.url")); config.setUsername(props.getProperty("jdbc.username")); config.setPassword(props.getProperty("jdbc.password")); config.setDriverClassName(props.getProperty("jdbc.driver")); config.setMaximumPoolSize(Integer.parseInt(props.getProperty("jdbc.maxActive"))); config.setMinimumIdle(Integer.parseInt(props.getProperty("jdbc.initialSize"))); dataSource = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }

代码的逻辑是在类加载阶段读取 jdbc.properties,初始化 HikariCP 数据源,之后所有操作都通过 getConnection 获取连接。HikariCP 的两个参数这里要额外解释:maximumPoolSize 是对外能同时借出的连接数,minimumIdle 是空闲时保留的最小连接数。导出 Word 是“查数据 + 写文档”交替执行的长任务,连接如果每次都新建,数据库端会出现大量 TIME_WAIT 连接,微信公众号后台那种几分钟一次的导出任务看不出来,但到了批量生成几十份合同的时候就非常明显。

2.2 文档生成:Apache POI 的 XWPF 才是改 Word 的正路

Word 生成方案我见过三类:第一类是直接用 Java 拼 HTML,再把 HTML 后缀改成 doc 或 docx,这种文件打开时 Word 会提示格式与扩展名不匹配,而且不能设置大纲级别,导航窗格完全废掉。第二类是 FreeMarker 结合 Word 转 XML 的模板,适合格式完全固定的合同书,但遇到动态行数的表格,模板里要写大量循环标签,维护成本不低。第三类就是用 Apache POI 的 XWPF 组件直接操作 docx,这也是这套程序采用的方案。

POI 把 docx 文档看成一段 XML 的树形结构:文档对应 XWPFDocument,段落对应 XWPFParagraph,段落里的文字块对应 XWPFRun,表格对应 XWPFTable。最小单位是 Run,同一个段落里如果字体不一样,就必须拆成多个 Run。下面是最简单的一段生成代码:

XWPFDocument document = new XWPFDocument(); XWPFParagraph paragraph = document.createParagraph(); XWPFRun run = paragraph.createRun(); run.setText("示例:从数据库读出来的订单号 AB123"); run.setFontSize(12);

这段代码展示了写入 Word 的基本动作:先建文档对象,再建段落,然后在段落里建 Run 并写入文字。setFontSize 设置的是字号,单位是磅。这里要注意中文字体不能只靠 setFontSize 保证,还需要设置一个中文字体族,比如 setFontFamily("宋体"),否则 Word 在 Windows 下打开时会用默认字体替代,实际显示效果和程序里预期的段落格式不一致。这套程序里专门封装了 FontUtil,统一处理中文段落字体、加粗和颜色,后面第三章会讲具体写法。

2.3 源码包目录结构:拿到程序后先找这几个文件

刚拿到这套程序时,不要急着跑,先把目录结构看一遍。工程是一个标准的 Maven 项目,入口是一个 main 方法,核心代码集中在 export 包下。目录结构如下:

word-export/ ├── pom.xml ├── sql/ │ └── report_query.sql └── src/main/ ├── java/cn/example/wordexporter/ │ ├── Main.java │ ├── config/ │ │ ├── JdbcManager.java │ │ └── ExportConfig.java │ ├── dao/ │ │ └── ReportDao.java │ ├── service/ │ │ └── WordExportService.java │ ├── util/ │ │ ├── FontUtil.java │ │ └── TableUtil.java │ └── export/ │ └── DocxExporter.java └── resources/ ├── jdbc.properties └── field_mapping.properties

这里每个文件职责都很单一:ReportDao 负责执行 SQL 并返回 List 数据,DocxExporter 负责把数据写成 Word,WordExportService 负责协调整个流程。pom.xml 里只放两个核心依赖,poi-ooxml 负责读写 docx,mysql-connector-j 负责数据库驱动,版本号都写在 Maven 仓库里,直接用中央仓库默认的最新稳定版即可。

文件作用改动频率
jdbc.properties数据库连接参数换环境时改
field_mapping.properties数据库字段与 Word 标题的对应关系每次接新报表时改
report_query.sql导出的数据查询 SQL每次接新报表时改
Main.java程序入口,读取参数并调 service基本不动
DocxExporter.javaPOI 写 Word 的核心实现调样式时改

这套结构的好处是数据查询和文档生成分离。改 SQL 不用碰 Java 代码,改 Word 排版不用碰 SQL,两个人并行也互相干扰。有些课程设计里会把 SQL 直接写在 main 方法里,改一次需求就要重新编译一次,而这里只需要替换 properties 和 sql 文件。下一章就顺着这个结构,把核心实现拆开看。

3. 核心实现:查数据、写标题、建表格、插分页,完整程序骨架

这一章是复现的关键。整套导出流程可以拆成四步:先把数据库结果集变成 Java 对象,再把查询结果写成标题段,接着把明细数据写成表格,最后按规则插入分页符和页眉页脚。每一步都对应源码包里的一个类,下面按顺序过一遍。

3.1 查询数据:把 ResultSet 映射成 List<Map<String, Object>>

数据库查询的结果是一个 ResultSet,直接把它传给 Word 写入逻辑会非常别扭,因为 ResultSet 的光标只能向前移动一次。程序里先在 ReportDao 中把结果转成 List,每个 Map 代表一行记录,Map 的 key 是 SQL 里的列名。这里用 LinkedHashMap 而不是 HashMap,是为了保持列顺序和 SELECT 语句中的顺序一致,写表格时就不用额外排序。

public List<Map<String, Object>> queryForList(Connection conn, String sql, Object... params) throws SQLException { List<Map<String, Object>> rows = new ArrayList<>(); try (PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); } try (ResultSet rs = ps.executeQuery()) { ResultSetMetaData metaData = rs.getMetaData(); int columnCount = metaData.getColumnCount(); while (rs.next()) { Map<String, Object> row = new LinkedHashMap<>(); for (int i = 1; i <= columnCount; i++) { row.put(metaData.getColumnLabel(i), rs.getObject(i)); } rows.add(row); } } } return rows; }

这段代码用了 try-with-resources,PreparedStatement 和 ResultSet 都会在方法结束后自动关闭。使用 PreparedStatement 是为了防止 SQL 注入,导出程序要接收外部传入的查询条件,不能把字符串直接拼进 SQL。getColumnLabel 取的是 SQL 别名,如果查询里写的是select order_no as orderNo from t_order,那么 Map 的 key 就是 orderNo,后面配置字段映射时会用这个 key 去取值。getObject 返回的是 Java 对象的原始类型,日期、BigDecimal 在这一步先不做转换,等写入 Word 时统一转字符串,避免出现格式不一致。

3.2 写标题:动态生成带大纲级别的章节标题

很多导出程序生成的 Word 没有大纲级别,导航窗格里什么也看不到,打印目录也找不到标题。这个问题在选型章节提过,现在看实现。POI 中设置大纲级别需要操作底层 XML,通过 setStyle 设置 Heading1 等内置样式,同时还要给段落设置 outlineLvl。

private void addHeading(XWPFDocument document, String text, int level) { XWPFParagraph paragraph = document.createParagraph(); paragraph.setAlignment(ParagraphAlignment.LEFT); paragraph.setStyle("Heading" + level); paragraph.getCTP().getPPr().addNewOutlineLvl().setVal(level - 1); XWPFRun run = paragraph.createRun(); run.setText(text); run.setBold(true); run.setFontSize(level == 1 ? 16 : 14); run.setFontFamily("微软雅黑"); }

这里有两个细节容易翻车。一是 setStyle("Heading1") 只是使用了内置样式,Word 会根据当前主题决定字体颜色,这不代表你想要的格式;真正决定它在导航窗格中层级的是 outlineLvl 的值,level 1 对应值 0,level 2 对应值 1,依次类推。二是同一个段落里同时设置了 setBold 和 setFontSize,但这两个属性都作用在 Run 上,如果后面还要追加文字,并且希望新文字不加粗,就必须另建一个 Run,而不是接着在同一个 Run 上 setText。

标题的数据来源通常是主表里的某个字段,比如“某某公司的 2024 年 12 月报表”。程序从 Map 中取出这个字段值,再拼上固定前缀。这个拼接动作放在 WordExportService 里,而不是写在 DocxExporter 里,因为标题规则是业务层面的,换一个项目可能就变了。

3.3 建表格:列头、数据行、空值兜底,一个方法搞定明细区

明细数据是 Word 里最常见的内容。POI 创建表格有两种方式:先 createTable 再逐行写内容,或者直接传入行列数创建空表格然后填充。第二种方式更适合程序里的动态数据,因为行数可以直接用 list.size() + 1 算出来。

private void addDataTable(XWPFDocument document, String[] headers, List<Map<String, Object>> rows, String[] fields) { XWPFTable table = document.createTable(rows.size() + 1, fields.length); XWPFTableRow headerRow = table.getRow(0); for (int i = 0; i < headers.length; i++) { headerRow.getCell(i).setText(headers[i]); } for (int r = 0; r < rows.size(); r++) { XWPFTableRow tableRow = table.getRow(r + 1); Map<String, Object> row = rows.get(r); for (int c = 0; c < fields.length; c++) { Object value = row.get(fields[c]); tableRow.getCell(c).setText(value == null ? "/" : value.toString()); } } }

这段代码最值得说的是空值兜底。数据库里的 NULL 如果直接转字符串,写进 Word 后单元格会变成空白,看起来像漏了数据。这里统一把 NULL 替换成“/”,既保留了列结构,也方便事后核对。fields 数组的顺序决定了每行单元格的数据来源,程序本身不知道哪一列是订单号、哪一列是金额,它只按数组下标去 Map 里取值,所以这一层必须和 field_mapping.properties 一一对应。表格创建后,列宽默认是平均分配的,如果某一列是超长文本,比如备注字段,Word 会自动换行,这一点后续在第四章讲列宽调整。

3.4 分页符和页眉页脚:让多页文档不再像流水账

数据量超过一页时,Word 会按页面大小自动分页,但自动分页不会照顾章节逻辑。比如第一张表结束、第二张表开始时,可能正好从表格中间断掉。程序里的做法是在每组数据结束之后,手动插入分页符,让下一个章节从新的一页开始。

XWPFParagraph pageBreak = document.createParagraph(); XWPFRun run = pageBreak.createRun(); run.addBreak(BreakType.PAGE);

addBreak(BreakType.PAGE) 会在当前段落后插入一个分页标记。需要注意的是这句话不要把分页符加在表格内部,否则会破坏表格结构。正确位置是上一张表格写完之后,下一章标题写入之前。页眉页脚用 XWPFHeaderFooterPolicy 处理,页眉通常放公司名称,页脚放页码。页码需要插入域代码,不能直接写死文本,否则每页都是同一个页码。POI 对域代码的支持比较底层,程序里封装了 createField 方法,生成PAGE域。这个功能属于锦上添花,如果只是内部使用,可以暂时不接页码,但页眉建议还是加上,因为正式文档没有页眉会显得很不完整。

4. 参数怎么调:连接、字体、分页、空值,五组最关键的配置

程序跑通之后,大家问得最多的问题就是“我改了字段名怎么没效果”“为什么表格这么挤”“页码怎么还是 1”。这些问题基本都能在前面的参数配置里找到答案。这一章把整套程序里最关键的五组参数单拎出来讲,改之前先对照检查一遍。

4.1 JDBC 与连接池参数:超时、编码、最大连接数

数据库连接相关的参数在 jdbc.properties 里集中管理,前面已经看过基础配置。这里补三个容易低估的参数:

参数建议值作用
connectionTimeout30000连接池获取连接的超时时间,单位毫秒
validationTimeout5000连接申请校验的超时时间
maxLifetime1800000连接最长存活时间,避免被防火墙切断

这三个参数在长任务导出时尤其重要。如果数据库端设置了 wait_timeout,而连接池里某个连接空闲时间超过这个阈值,下次使用时会拿到一个已经失效的连接。HikariCP 默认会自动检测,但检测需要额外发送心跳包,在高峰导出时段会明显拖慢第一次查询速度。设置 maxLifetime 略小于数据库 wait_timeout,可以让连接在真正失效前被主动重建。很多同学在自己电脑上跑程序没问题,部署到服务器后第一次导出要等十几秒,基本都是连接池参数没调。

4.2 中文字体和页边距:宋体、行距、表格字号

POI 默认字体是 Calibri,如果不设置中文字体,Windows 环境下打开会看到宋体,但不同操作系统上打开可能变成其他字体,导致段落行数变化、表格变高。程序里 FontUtil 的核心逻辑是同时设置 ascii 字体和 eastAsia 字体,光 setFontFamily 一个方法只能设置西文字体。

public static void setChineseFont(XWPFRun run, String fontName, int fontSize, boolean bold) { run.setFontFamily(fontName); run.setFontSize(fontSize); run.setBold(bold); run.getCTR().getRPr().getRFontsArray(0).setEastAsia(fontName); }

run.getCTR().getRPr() 操作的是底层 XML 对象,需要在创建 Run 之后立即调用,否则 rPr 可能为 null。getRFontsArray(0) 取得当前 Run 的字体数组,setEastAsia 就是设置中文字体。页边距也在文档初始化阶段设置,包括上下左右边距、页面宽度和高度。程序里有两个选择:正文用宋体五号,表格内容用小五号;标题用微软雅黑,这样打印和屏幕阅读都能兼顾。

4.3 分页与章节:什么时候插分页符,什么时候靠样式

分页符不是越多越好。如果每组数据固定从新页开始,而每组数据内容不足一页,文档就会有很多半空白页;如果不分页,表格在页中断开,阅读体验又不好。程序里默认按前几章的规则:一级标题出现前强制分页,明细表格和下一个一级标题之间不强制分页。这个开关放在 ExportConfig.java 中,字段叫 forcePageBeforeLevel1,默认 true。如果导出的是 A4 纸打印用的流程图,手动分页符位置要人工确定;如果导出的是屏幕阅读的在线报告,可以让 Word 自动分页,只保留标题段落的“段前分页”属性。POI 通过设置段落属性 pageBreakBefore 实现:

paragraph.getCTP().getPPr().addNewPageBreakBefore().setVal(true);

4.4 字段映射与空值策略:数据库字段到 Word 标题的对照关系

field_mapping.properties 是接新报表时改动最频繁的文件。它的作用是告诉程序:数据库查询结果的哪个列名,对应 Word 表格里的哪个表头,以及该列数据为空时显示什么。

order.orderNo=订单编号|/ order.orderDate=下单日期|/ order.customerName=客户名称|未填写 order.totalAmount=订单金额|0.00

竖线前面的部分是数据库列名,竖线后面是 Word 表头名称,再后面是空值默认显示。解析时按竖线拆分成两个或三个字段,列名和表头名顺序必须一一对应。这里有个很容易弄反的点:properties 文件里“=”右边如果也包含竖线,解析时要先 split("\|"),然后把值的第一段作为表头,第二段作为空值策略,不要把整个字符串直接拿去做表头。程序里专门写了 FieldMappingConfig 类,把配置文件解析成 Map,在写表格时直接查这个 Map。

5. 避坑指南:乱码、空行、列宽和内存溢出的排错记录

这一章把我在真实项目里踩过的坑集中写出来,每一条都按“现象、原因、解决”的顺序记录。你复现这套程序时如果遇到同样问题,直接对照这一章排错,比重新翻 POI 文档快得多。

5.1 一导出就乱码:中文变成问号,或者 Word 提示文件损坏

现象:程序跑完没报错,打开 docx 发现所有中文都变成了“?”,英文和数字正常;有些情况是整个文件打不开,提示“文件已损坏,是否尝试修复”。

原因:中文变问号,多半是 JDBC 连接串里没有 characterEncoding=utf8,数据库返回的 UTF-8 字节被当成默认字符集解码。文件打不开,常见于手动拼接 XML 时没有处理转义字符,比如把数据库里的“&”直接写进票据,而 Word 底层 XML 里必须写成&amp;。POI 的 setText 方法会自动转义,但如果你为了修改底层标记而直接操作 XML 对象,就容易漏掉这一点。

解决:连接的 URL 上把 useUnicode=true&characterEncoding=utf8 写成固定模板,程序启动时校验一次,没有这两个参数直接抛异常。所有文本写入都走 setText,不要自己拼 XML 字符串。如果已经生成了打不开的文件,用文本编辑器打开 docx 的 document.xml 片段检查特殊字符。

5.2 表格多出一行:每个表格最后都有一个空行,怎么删都删不掉

现象:生成的 Word 文档里,每一张表格下面都多出一个空白段落,打印时每张表后面都空一块。

原因:这是 POI createTable 的默认行为。XWPFDocument 在创建表格之后会保留一个段落标记,这个段落不属于表格,但 Word 打开时会在表格后面显示成一个空行。如果你用 createTable(rows, cols) 后来又在同一文档顺序里穿插其他元素,表格后的空段落会自动补位。

解决:在创建表格之前先记录文档当前段落数,表格写完后手动删除多余段落,或者更简单的做法是接受这个空行,把它当作表格和下一个标题之间的唯一间隔。如果一定要删,需要通过 getDocument().getBodyElements() 找到新创建的段落对象,调用 XMLObject.remove() 移除。删除时要先判断下一个 bodyElement 是不是段落,否则会把表格误删。

5.3 列宽设置不生效:表格看起来还是等宽的,设了宽度没有变化

现象:代码里通过 table.setWidth("5000") 设置表格宽度,又对每个表格单元格 setWidth,导出后仍然全部列等宽,长文本列没有变宽。

原因:Word 表格的实际宽度由三层决定:表格级宽度、单元格级宽度、表格布局类型。POI 只设置表格宽度而不设置每个单元格的 tcW,Word 会按内容自动分配列宽;更隐蔽的是表格的 tblLayout 默认可能是 autofit,autofit 模式下 word 会自动忽略手动设置的固定值。

解决:先把表格布局设置为 fixed,再设置表格总宽度,然后再逐列设置列宽。列宽的计算方式是 页面宽度减页边距 除以期望比例。程序里 TableUtil 的 setColumnWidths 方法专门处理这个,最后还要给每个单元格单独调用 setWidth,因为 Word 的列宽以单元格 tcW 为准。

5.4 大数据量导出时内存溢出:导出三万行明细,程序直接 OOM

现象:查询结果只有三万条记录,不算特别大,但导出过程中堆内存持续上涨,最后发生 OutOfMemoryError,连日志都没打完整。

原因:一是查询方法把全部数据先放进 List,三万行每个字段都存一份 Map,占用已经不小;二是 POI 默认把所有段落、表格、Runs 都保存在内存里,一次性写出时会维持整篇文档对象模型。这两个叠加,内存自然不够。

解决:先分页查询,比如每 5000 行刷一批,每批写完就刷新到磁盘。POI 的 XWPFDocument 不像 SXSSFWorkbook 那样有专门流式版本,但可以通过临时文件的方式分批写入,最后合并。更实用的做法是按业务拆分:明细数据超过预设行数时,不再生成一个巨型表格,而是按分组拆成多个小表格并插入分页符,这样每一张表格只在内存里存活很短时间。程序里 ExportConfig 中有一个 pageSize 参数,默认 5000,超过就拆表,这是防止 OOM 的第一道防线。

6. 进阶用法:把导出程序打包成命令行工具,再做一次自动验证

程序在 IDE 里跑通只是第一步,真正落地要能脱离 IDE 运行。源码包里的工程可以打成可执行 jar,然后用命令行参数控制数据库配置、SQL 文件和输出路径。Maven 打包命令和调用方式如下:

mvn clean package -DskipTests java -jar word-export.jar --config /etc/wordexport/jdbc.properties --sql /tmp/report_query.sql --out /tmp/report.docx

main 方法里会解析这三个参数,先从 config 初始化连接池,然后读取 sql,把查询结果交给 service。打包时要注意把依赖一起打进去,用 maven-assembly-plugin 打 fat jar,否则在服务器上会报 ClassNotFound。我一般习惯把 jar 放在单独目录,和配置文件、SQL 文件分开,这样以后换报表只需要改配置和 SQL,不用重新打 jar。

程序写完后要回归验证。我的验证方式不是肉眼打开 Word 看一遍,而是再写一个几十行的校验程序,用 POI 把生成好的 docx 重新读一遍,检查标题数量和表格数量是否符合预期。核心校验代码很简单:

try (XWPFDocument checker = new XWPFDocument(new FileInputStream("/tmp/report.docx"))) { if (checker.getTables().size() != expectedTables) { throw new IllegalStateException("表格数量不对,导出失败"); } }

用 getTables().size() 可以精确统计文档里的表格数,再对比 SQL 查询结果的分组数量,能快速发现漏数据、重复分页的问题。这个校验脚本不算优雅,但它能在没有打开 Word 的情况下完成大部分回归验证。从那以后,我每次改完字段映射,都强制走一遍部署、命令行导出、再读取校验两个动作,确认这份程序不是只在 IDE 里能跑。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表