Java利用docx4j与LibreOffice实现动态表格生成与PDF转换实战
1. 为什么需要动态生成表格并转换为PDF在企业级应用开发中经常遇到需要将业务数据动态生成报表的场景。比如财务系统需要导出月度收支明细医疗系统要打印患者检查报告教育系统要生成成绩单等等。这些场景通常有三大共性需求数据动态变化、格式规范统一、便于打印存档。传统做法是后台生成Excel文件但Excel在不同设备上显示效果不一致打印排版也容易出错。而PDF作为工业标准格式能完美解决这些问题。但直接生成PDF的库如iText对复杂表格支持有限这时候docx4jLibreOffice的组合就显现出优势了docx4j能像搭积木一样用代码构建Word文档支持合并单元格、边框样式等高级表格功能LibreOffice提供稳定的文档转换能力确保最终PDF与设计稿完全一致整个过程可完全自动化无需人工干预我去年为某银行开发对账单系统时就遇到过这样的需求每天凌晨需要为10万客户生成包含20数据列的交易明细PDF。最初尝试用POIApache PDFBox方案不仅代码复杂还频繁出现中文乱码。后来切换到docx4jLibreOffice方案后不仅性能提升3倍乱码问题也彻底解决。2. 环境准备与依赖配置2.1 安装LibreOfficeLibreOffice的安装有几个关键注意点版本选择推荐使用最新的稳定版当前是7.4老版本可能存在兼容性问题。Windows用户建议下载MSI安装包Linux用户使用官方PPA源安装路径避免包含中文或空格的路径。我习惯安装在D:\LibreOffice这样的纯英文路径下后续配置会更方便无界面模式配置安装完成后需要配置服务模式这是实现自动化转换的关键。打开cmd执行D:\LibreOffice\program\soffice.exe -headless -acceptsocket,host0.0.0.0,port8100;urp; -nologo -nofirststartwizard参数说明-headless无界面运行host0.0.0.0允许远程连接生产环境建议配合防火墙规则port8100服务监听端口2.2 Maven依赖配置在pom.xml中添加以下依赖dependencies !-- docx4j核心库 -- dependency groupIdorg.docx4j/groupId artifactIddocx4j/artifactId version6.1.2/version /dependency !-- LibreOffice连接器 -- dependency groupIdorg.libreoffice/groupId artifactIdunoloader/artifactId version7.4.0/version /dependency !-- 日志组件 -- dependency groupIdorg.slf4j/groupId artifactIdslf4j-api/artifactId version1.7.36/version /dependency /dependencies注意版本兼容性docx4j 6.x需要Java 8LibreOffice 7.x的API较稳定建议配套使用3. 动态表格生成实战3.1 创建基础文档我们先构建一个包含表格的Word文档骨架// 初始化文档包 WordprocessingMLPackage wordPackage WordprocessingMLPackage.createPackage(); MainDocumentPart mainPart wordPackage.getMainDocumentPart(); // 设置文档默认字体解决中文乱码关键步骤 Body body mainPart.getJaxbElement().getBody(); PPr ppr Context.getWmlObjectFactory().createPPr(); RFonts fonts new RFonts(); fonts.setAscii(宋体); fonts.setEastAsia(宋体); fonts.setHAnsi(宋体); ppr.setRFonts(fonts); body.setPPr(ppr);3.2 构建动态表格假设我们要生成一个员工考勤表包含合并单元格和自定义样式// 创建5行4列的表格 Tbl table factory.createTbl(); String[][] data { {姓名, 部门, 考勤记录, }, {张三, 研发部, 2023-08-01, 正常}, {, , 2023-08-02, 迟到}, {李四, 产品部, 2023-08-01, 请假}, {, , 2023-08-02, 正常} }; // 填充数据 for (int i 0; i data.length; i) { Tr row factory.createTr(); for (int j 0; j data[i].length; j) { Tc cell factory.createTc(); cell.getContent().add(mainPart.createParagraphOfText(data[i][j])); row.getContent().add(cell); } table.getContent().add(row); } // 合并单元格 mergeCells(table, 0, 2, 0, 3); // 合并第一行后两列 mergeCells(table, 1, 2, 0, 0); // 合并张三的姓名单元格 mergeCells(table, 3, 4, 0, 0); // 合并李四的姓名单元格 // 添加表格到文档 mainPart.addObject(table);3.3 样式定制技巧让表格更专业的几个关键设置// 设置表格边框 CTBorder border new CTBorder(); border.setVal(STBorder.SINGLE); border.setSz(BigInteger.valueOf(4)); TblBorders borders new TblBorders(); borders.setTop(border); borders.setBottom(border); borders.setLeft(border); borders.setRight(border); table.setTblPr(new TblPr()); table.getTblPr().setTblBorders(borders); // 设置单元格边距 TblPr tblPr table.getTblPr(); TblCellMar cellMar new TblCellMar(); cellMar.setTop(new TblWidth(100)); cellMar.setBottom(new TblWidth(100)); cellMar.setLeft(new TblWidth(200)); cellMar.setRight(new TblWidth(200)); tblPr.setTblCellMar(cellMar); // 设置列宽 TblGrid grid new TblGrid(); grid.getGridCol().add(new TblGridCol(BigInteger.valueOf(2000))); grid.getGridCol().add(new TblGridCol(BigInteger.valueOf(3000))); grid.getGridCol().add(new TblGridCol(BigInteger.valueOf(4000))); grid.getGridCol().add(new TblGridCol(BigInteger.valueOf(3000))); table.setTblGrid(grid);4. PDF转换与部署实践4.1 本地转换实现通过LibreOffice的UNO API进行转换public void convertToPDF(String docxPath, String pdfPath) throws Exception { String officePath D:\\LibreOffice\\program; XComponentContext context BootstrapSocketConnector.bootstrap(officePath); // 建立连接 XMultiComponentFactory factory context.getServiceManager(); Object desktop factory.createInstanceWithContext( com.sun.star.frame.Desktop, context); // 加载文档 PropertyValue[] loadProps new PropertyValue[1]; loadProps[0] new PropertyValue(); loadProps[0].Name Hidden; loadProps[0].Value true; XComponentLoader loader (XComponentLoader)UnoRuntime.queryInterface( XComponentLoader.class, desktop); XComponent document loader.loadComponentFromURL( file:/// docxPath, _blank, 0, loadProps); // 转换PDF PropertyValue[] exportProps new PropertyValue[2]; exportProps[0] new PropertyValue(); exportProps[0].Name Overwrite; exportProps[0].Value true; exportProps[1] new PropertyValue(); exportProps[1].Name FilterName; exportProps[1].Value writer_pdf_Export; XStorable storable (XStorable)UnoRuntime.queryInterface( XStorable.class, document); storable.storeToURL(file:/// pdfPath, exportProps); // 释放资源 document.dispose(); }4.2 生产环境部署建议在实际部署时我推荐以下架构[应用服务器] --HTTP-- [文档生成服务] --JNI-- [LibreOffice集群]关键优化点LibreOffice服务池启动多个soffice进程不同端口避免单点瓶颈连接超时设置添加30秒超时控制防止进程假死资源清理每天定时重启服务防止内存泄漏错误重试对转换失败的任务自动重试2次典型问题解决方案中文乱码确保系统安装中文字体如sudo apt install fonts-noto-cjk性能优化调整JVM参数-Xmx1024m -XX:MaxDirectMemorySize256m权限问题Linux下用www-data用户运行需配置sudo visudo5. 高级技巧与避坑指南5.1 复杂表格生成处理不规则表格时可以这样操作// 创建跨页表格 TblPr tblPr new TblPr(); TblStyle tblStyle new TblStyle(); tblStyle.setVal(TableGrid); tblPr.setTblStyle(tblStyle); tblPr.setTblp(new TblP().withVertAnchor(TextAnchor.PAGE)); table.setTblPr(tblPr); // 设置行不允许分页 TrPr trPr new TrPr(); TrHeight trHeight new TrHeight(); trHeight.setHRule(HeightRule.EXACT); trHeight.setVal(BigInteger.valueOf(1000)); trPr.setTrHeight(trHeight); trPr.setCantSplit(new BooleanDefaultTrue()); row.setTrPr(trPr);5.2 常见问题解决问题1转换PDF后样式错乱检查LibreOffice版本是否≥7.2确认文档中使用的字体在服务器已安装问题2大量文档转换时内存溢出增加JVM堆内存-Xmx2048m每转换10个文档后重启soffice进程问题3并发转换效率低部署多个LibreOffice实例不同端口使用连接池管理服务连接问题4Linux下字体显示异常# 拷贝Windows字体到服务器 sudo mkdir /usr/share/fonts/win sudo cp *.ttf /usr/share/fonts/win/ sudo fc-cache -fv6. 完整案例演示让我们实现一个完整的报表生成流程准备数据模拟数据库查询ListEmployee employees Arrays.asList( new Employee(E1001, 张三, 研发部, Arrays.asList( new Attendance(2023-08-01, 正常), new Attendance(2023-08-02, 迟到) )), new Employee(E1002, 李四, 市场部, Arrays.asList( new Attendance(2023-08-01, 外出), new Attendance(2023-08-02, 正常) )) );生成文档WordprocessingMLPackage wordPackage WordprocessingMLPackage.createPackage(); MainDocumentPart mainPart wordPackage.getMainDocumentPart(); // 添加标题 mainPart.addParagraphOfText(员工考勤报表) .setPPr(getTitleStyle()); // 生成动态表格 Tbl table createAttendanceTable(employees); mainPart.addObject(table); // 保存文档 wordPackage.save(new File(report.docx));转换PDFOfficeConverter converter new OfficeConverter(); converter.convertToPDF(report.docx, report.pdf);效果优化添加页眉页脚设置文档属性作者、主题等添加目录适用于长文档在实际项目中我们可以将这些操作封装成服务通过REST API提供文档生成能力。例如PostMapping(/generate-report) public ResponseEntitybyte[] generateReport(RequestBody ReportRequest request) { // 生成文档 byte[] docx reportService.generateDocx(request); // 转换PDF byte[] pdf officeService.convertToPDF(docx); // 返回文件流 return ResponseEntity.ok() .header(Content-Type, application/pdf) .header(Content-Disposition, attachment; filenamereport.pdf) .body(pdf); }通过这样的实现前端只需要传递JSON数据后端就能返回完美的PDF报表。我在电商系统中用这种方案处理日均5万的订单导出需求稳定运行两年多未出现重大故障。