Poi-tl根据word模板生成动态表格

发布时间:2026/7/8 4:13:01
Poi-tl根据word模板生成动态表格 poi-tl是一个导出word的工具类, 相较于easy-poi来说文档可能是更全乎一点吧。具体的标签什么的看文档就行了主要是一个生成多级合并表头的方法。1. 基础用法1. 基础标签介绍标签描述备注{{text}}纯文本{{?var}}循环块需要配对{{/var}}{{#table}}纯代码拼接的表格需要传入一个TableRenderData对象2. 使用示例Word模板如下用户统计共 {{userCount}}名注册用户用户信息块{{?userList}} 姓名: {{username}} 年龄: {{age}} 性别: {{gender}} 注册时间: {{registerTime}} 联系方式如下 {{#userPhoneTable}} {{/userList}}测试渲染模板方法Slf4jpublicclassUserTemplateTest{DataNoArgsConstructorAllArgsConstructorpublicstaticclassUser{privateStringusername;privateIntegerage;privateStringgender;privateStringregisterTime;privateStringphone;privateStringwx;privateStringemail;privateStringqq;}TestpublicvoidtestExportUserTemplate(){// 2. 组装渲染数据MapString,ObjectdatabuildRenderMap();// 4. 加载模板并渲染StringtemplatePathC:\\Users\\24962\\Desktop\\dept\\user.docx;StringoutputPathC:\\Users\\24962\\Desktop\\dept\\user_output.docx;try(XWPFTemplatetemplateXWPFTemplate.compile(templatePath);FileOutputStreamfosnewFileOutputStream(outputPath)){template.render(data);// 使用原生POI清空表格首行缩进PoiTlUtil.clearAllTableIndent(template.getXWPFDocument());template.write(fos);log.info(导出成功文件路径{},outputPath);}catch(Exceptione){log.error(导出失败{},e.getMessage(),e);}}privateMapString,ObjectbuildRenderMap(){ListUserusersbuildMockUsers();MapString,ObjectresultMapnewHashMap();resultMap.put(userCount,users.size());resultMap.put(userList,buildUserList(users));returnresultMap;}privateListMapString,ObjectbuildUserList(ListUserusers){ListMapString,ObjectresultListnewArrayList();MapString,ObjectuserMap;for(Useruser:users){userMapnewHashMap();userMap.put(username,user.getUsername());userMap.put(age,user.getAge());userMap.put(gender,user.getGender());userMap.put(registerTime,user.getRegisterTime());userMap.put(userPhoneTable,buildUserPhoneTable(user));resultList.add(userMap);}returnresultList;}privateTableRenderDatabuildUserPhoneTable(Useruser){RowRenderDatatableRowPoiTlUtil.buildRowRenderDataByArr(手机号,微信,邮箱,QQ);RowRenderDatamockUserRowsPoiTlUtil.buildRowRenderDataByArr(user.getPhone(),user.getWx(),user.getEmail(),user.getQq());TableRenderDatatableRenderDataTables.of(tableRow,mockUserRows).create();// 设置表格内文字首行无缩进PoiTlUtil.setTableAllCellNoIndentLeft(tableRenderData);returntableRenderData;}privateListUserbuildMockUsers(){ListUseruserListnewArrayList();userList.add(newUser(张三,20,男,2023-01-01,13800000000,123456,zhangsanexample.com,123456));userList.add(newUser(李四,21,女,2023-01-02,13800000001,123457,lisiexample.com,123457));userList.add(newUser(王五,22,男,2023-01-03,13800000002,123458,wangwuexample.com,123458));returnuserList;}}消除表格文字首行缩进工具类importcom.deepoove.poi.data.*;importcom.deepoove.poi.data.style.ParagraphStyle;importorg.apache.poi.xwpf.usermodel.*;importorg.openxmlformats.schemas.wordprocessingml.x2006.main.CTInd;importorg.openxmlformats.schemas.wordprocessingml.x2006.main.CTP;importorg.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPr;importjava.math.BigInteger;importjava.util.ArrayList;importjava.util.List;/** * * author eleven * since 2026/7/2 18:30 */publicclassPoiTlUtil{/** * 段落样式 首行无缩进 无边距 */privatestaticfinalParagraphStyleLEFT_NO_INDENT_STYLEParagraphStyle.builder().withAlign(ParagraphAlignment.LEFT).withIndentFirstLineChars(0)// 取消字符首行缩进核心.withIndentLeftChars(0)// 整体段落左边距 0.build();/** * 设置word表格无首行缩进 * param tableRenderData 表格数据 */publicstaticvoidsetTableAllCellNoIndentLeft(TableRenderDatatableRenderData){// 遍历所有行表头数据行ListRowRenderDatarowstableRenderData.getRows();for(RowRenderDatarow:rows){ListCellRenderDatacellsrow.getCells();for(CellRenderDatacell:cells){// 单元格内所有段落统一替换样式ListParagraphRenderDataparagraphscell.getParagraphs();for(ParagraphRenderDatapara:paragraphs){para.setParagraphStyle(LEFT_NO_INDENT_STYLE);}}}}publicstaticRowRenderDatabuildRowRenderData(ListStringlist){returnbuildRowRenderDataByArr(list.toArray(newString[]{}));}publicstaticRowRenderDatabuildRowRenderDataByArr(String...arr){//return Rows.of(arr).horizontalCenter().center().create();returnbuildRowRenderDataByArr2(arr);}// 通用构建无缩进单元格工具publicstaticCellRenderDatabuildNoIndentCell(Stringtext){ParagraphRenderDataparanewParagraphRenderData().addText(text);para.setParagraphStyle(LEFT_NO_INDENT_STYLE);returnCells.of().addParagraph(para).create();}// 改造表头构建方法privatestaticRowRenderDatabuildRowRenderDataByArr2(String...texts){ListCellRenderDatacellsnewArrayList();for(Stringtext:texts){cells.add(buildNoIndentCell(text));}returnRows.of(cells.toArray(newCellRenderData[]{})).create();}/** * 原生POI强制清除文档内所有表格单元格段落首行缩进、整体左缩进 * param document poi-tl编译后得到的XWPFDocument */publicstaticvoidclearAllTableIndent(XWPFDocumentdocument){// 遍历文档内全部表格for(XWPFTabletable:document.getTables()){for(XWPFTableRowrow:table.getRows()){for(XWPFTableCellcell:row.getTableCells()){// 单元格内所有段落统一重置for(XWPFParagraphparagraph:cell.getParagraphs()){// 强制左对齐paragraph.setAlignment(ParagraphAlignment.LEFT);CTPctpparagraph.getCTP();CTPPrpprctp.getPPr()null?ctp.addNewPPr():ctp.getPPr();CTIndindentppr.isSetInd()?ppr.getInd():ppr.addNewInd();// 首行缩进置0消除2字符缩进indent.setFirstLine(BigInteger.ZERO);// 段落整体左侧缩进置0indent.setLeft(BigInteger.ZERO);}}}}}}2. 全代码生成动态表头姓名年龄联系方式性别注册时间QQ微信邮箱word里边写一个{{#userHeader }}标签用来占位就行importcom.deepoove.poi.XWPFTemplate;importcom.deepoove.poi.data.*;importcom.deepoove.poi.data.MergeCellRule.Grid;importjava.io.FileInputStream;importjava.io.FileOutputStream;importjava.util.HashMap;importjava.util.Map;/** * 使用 poi-tl {link Tables#of} {link Tables.TableBuilder#mergeRule} 生成用户固定表头。 * * pre * 表头结构2行 × 7列 * ┌──────┬──────┬───────────────┬──────┬──────────┐ * │ 姓名 │ 年龄 │ 联系方式 │ 性别 │ 注册时间 │ * │ │ ├────┬────┬────┤ │ │ * │ │ │ qq │微信│邮箱│ │ │ * └──────┴──────┴────┴────┴────┴──────┴──────────┘ * /pre * * author eleven * since 2026/7/7 */publicclassUserFixedHeadUtil{publicstaticvoidmain(String[]args)throwsException{StringoutputPathuserFixedHead.docx;StringtemplatePathuserFixedHeadTemplate.docx;// 1. 构建合并规则MergeCellRulemergeRuleMergeCellRule.builder()// 联系方式 横向合并 cols 2→4行0.map(Grid.of(0,2),Grid.of(0,4))// 左侧列纵向合并行0→1.map(Grid.of(0,0),Grid.of(1,0))// 姓名.map(Grid.of(0,1),Grid.of(1,1))// 年龄// 右侧列纵向合并行0→1.map(Grid.of(0,5),Grid.of(1,5))// 性别.map(Grid.of(0,6),Grid.of(1,6))// 注册时间.build();// 2. 构建表头数据行// 行0: 主表头cols 3~4 为 null被横向合并覆盖RowRenderDatarow0Rows.of(姓名,年龄,联系方式,null,null,性别,注册时间).center().textBold().create();// 行1: 子表头纵向合并的列填 nullRowRenderDatarow1Rows.of(null,null,qq,微信,邮箱,null,null).center().textBold().create();// 3. 构建表格注入合并规则TableRenderDataheaderTableTables.of(row0,row1).mergeRule(mergeRule).create();MapString,ObjectmapnewHashMap();map.put(userHeader,headerTable);try(FileInputStreamfisnewFileInputStream(templatePath);XWPFTemplatedocXWPFTemplate.compile(fis);FileOutputStreamfosnewFileOutputStream(outputPath)){doc.render(map);doc.write(fos);}catch(Exceptione){e.printStackTrace();}System.out.println(已生成: outputPath);}}3. 固定表头根据数据生成表格这种固定表头的就有意思了需要把循环标签写到循环数据的上一行有这种合并单元格的写在拆分的上一行。word里边就写成下边的表格这样就行了。姓名年龄联系方式性别注册时间QQ微信邮箱 {{userList}}[name][age][qq][wx][email][age][regTime]针对这个表格呢进行渲染表格的时候就有些不一样了需要在compile的时候指定config配置importcom.deepoove.poi.XWPFTemplate;importcom.deepoove.poi.config.Configure;importcom.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;importlombok.AllArgsConstructor;importlombok.Data;importlombok.NoArgsConstructor;importlombok.experimental.Accessors;importorg.junit.jupiter.api.Test;importjava.io.FileInputStream;importjava.io.FileOutputStream;importjava.util.ArrayList;importjava.util.HashMap;importjava.util.List;importjava.util.Map;publicclassUserReportTest{TestpublicvoiduserReportTest(){try{// 1. 组装业务数据ListUseruserListgetUserList();// 2. 封装模板参数key 必须和模板 {{userList}} 一致MapString,ObjectrenderDatanewHashMap();renderData.put(userList,userList);// 3. 加载模板并渲染导出MapString,ObjectmapnewHashMap();try(FileInputStreamfisnewFileInputStream(user_template_hackloop.docx);FileOutputStreamfosnewFileOutputStream(output3.docx)){LoopRowTableRenderPolicypolicynewLoopRowTableRenderPolicy();ConfigureconfigConfigure.builder().bind(userList,policy).build();XWPFTemplatedocXWPFTemplate.compile(fis,config);map.put(userList,getUserList());doc.render(map);doc.write(fos);}}catch(Exceptione){e.printStackTrace();}}privateListUsergetUserList(){ListUseruserListnewArrayList();returnuserList;}}