乐于分享
好东西不私藏

高效办公自动化:POI-TL在Word模板引擎中的高级应用技巧

高效办公自动化:POI-TL在Word模板引擎中的高级应用技巧

         
01
⼀、技术介绍
1 POI-TL
1.1 简介
poi-tl(poitemplatelanguage)是⼀个基于ApachePOI的Word模板引擎,也是⼀个免费开源的Java类库,可以⽅便的加⼊到项⽬中,通过模板和数据创建Word⽂档。
模板是Docx格式的Word⽂档,你可以使⽤Microsoftoffice、WPSOffice等任软件制作模板,也可以使⽤ApachePOI代码来⽣成模板。
所有的标签都是以{{开头,以}}结尾,标签可以出现在任何位置,包括⻚眉,⻚脚,表格内部,⽂本框等。
要实现数据在模版中的填充,官⽅提供了多种数据填充⽅式的⽀持,满⾜不同业务场景下的使⽤需要。⽬前公司 内系统⽣成报表最常⽤的数据填充⽅式主要有标签、引⽤标签、配置、插件。
1.1.1 标签
poi-tl是⼀种⽆逻辑「logic-less」的模板引擎,没有复杂的控制结构和变量赋值,只有标签。标签由前后两个⼤括号组成,{{title}}是标签,{{?title}}也是标签,title是这个标签的名称,问号标识了标签类型。
常⽤标签类型:⽂本、图⽚、表格
1.1.2 引⽤标签
引⽤标签是⼀种特殊位置的特殊标签,提供了直接引⽤⽂档中的元素句柄的能⼒,这个重要的特性在我们只想改 变⽂档中某个元素极⼩⼀部分样式和属性的时候特别有⽤,因为其余样式和属性都可以在模板中预置好,真正的 所⻅即所得。
常⽤引⽤标签:图⽚,统计图
1.1.3 配置
poi-tl提供了类Configure来配置常⽤的设置,例如设置标签原有的⽅式。
1.1.4 插件
插件,⼜称为⾃定义函数,它允许⽤⼾在模板标签位置处执⾏预先定义好的函数。由于插件机制的存在,我们⼏乎可以在模板的任何位置执⾏任何操作。
插件是poi-tl的核⼼,默认的标签和引⽤标签都是通过插件加载。
常⽤插件:表格⾏循环、表格列循环
1.2 添加依赖
<dependency><groupId>com.deepoove</groupId><artifactId>poi-tl</artifactId><version>1.9.1</version></dependency>
依赖的apachepoi版本
    <dependency><groupId>org.apache.poi</groupId><artifactId>poi-scratchpad</artifactId><version>4.1.2</version></dependency><dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>4.1.2</version></dependency><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>4.1.2</version></dependency><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml-schemas</artifactId><version>4.1.2</version></dependency>
2 XWPFDocument
2.1 简介
XWPFDocument是ApachePOI库中⽤于处理MicrosoftWord2007及以上版本(.docx格式)⽂档的⼀个重要类。XWPFDocument属于POI-XWPF(XMLWordProcessingFormat)模块,这个模块主要⽤于处理新的.docx格式⽂档,它基于OfficeOpenXML标准。
该⽅法可⽤于word⽂档的读取和写⼊,实际使⽤中⼀般只⽤作读取⽂档,写⼊⼀般使⽤模版加载的⽅式。
2.2 添加依赖
同上POI-TL1.2章节中的apachepoi依赖
02
⼆、使⽤⽰例
1 POI-TL
1.1 快速⼊⻔⽰例
1.1.1 创建模版
新创建⼀个word⽂档,命名为快速开始⽰例.docx,填充占位符{{title}}
1.1.2 填充代码
通过代码对占位符的数据进⾏填充
publicstaticvoid main(String[] args) {//获取文件模版        InputStream inputStream = ClassLoader.getSystemResourceAsStream("word/快速开始示例.docx");if (inputStream == null) {thrownew IllegalArgumentException("模板文件未找到:word/快速开始示例.docx");        }//模板数据        Map<StringObject> data = new HashMap<>();        data.put("title""hello world");//模板引擎初始化,加载模板文件,开始渲染数据(word模板中默认以{{}}双层花括号符号填充占位符,//可以在配置中修改为自己喜欢的符号类型,如{},在配置demo中会详细介绍)try(XWPFTemplate template = XWPFTemplate.compile(inputStream).render(data)) {            template.write(new FileOutputStream("生成结果-快速开始示例.docx"));        } catch (Exception e) {            e.printStackTrace();        }    }
1.1.3 结果展⽰
1.2 标签⽰例
1.2.1 ⽂本
⽂本标签以{{}}填充模板,包括普通⽂本,样式⽂本,超链接⽂本
1.2.1.1 创建模版
1.2.1.2 填充代码
//普通文本填充data.put("title""hello world");//带格式文本填充data.put("colorTitle", Texts.of("hello world").color("FF00FF").create());//链接文本填充data.put("link", Texts.of("baidu").link("https://www.baidu.com/").create());
1.2.1.3 结果展⽰
1.2.2 图⽚
图⽚标签以{{@}}填充模板,并且可设置图⽚的⼤⼩
1.2.2.1 创建模版
1.2.2.2 填充代码
//读取图片文件try {            InputStream picInStream = ClassLoader.getSystemResourceAsStream("word/photo.jpg");if (picInStream == null) {thrownew IllegalArgumentException("模板文件未找到:word/photo.jpg");            }            data.put("image", Pictures.ofStream(picInStream, PictureType.JPEG).size(200220).create());        } catch (Exception e) {            e.printStackTrace();        }
1.2.3 表格
表格标签以{{#}}填充模板,并且可设置表格的样式,合并单元格
1.2.3.1 创建模板
1.2.3.2 填充代码
//表格填充String[][] tables = {newString[] { "姓名""年龄"},newString[] { "张三""23"},newString[] { "李四""24"}};        data.put("table0", Tables.of(tables).border(TableStyle.BorderStyle.DEFAULT).create());//设置格式填充表格        RowRenderData row0 = Rows.of("类型""颜色").textColor("FF00FF").bgColor("4472C4").center().create();        RowRenderData row1 = Rows.create("小狗""白色");        RowRenderData row2 = Rows.create("小猫""黑色");        RowRenderData row3 = Rows.create("小兔子""灰色");        data.put("table1", Tables.create(row0, row1,row2, row3));//设置合并单元格        RowRenderData rowMerge = Rows.of("菜名""价格""数量").center().bgColor("4472C4").create();        RowRenderData rowMerge1 = Rows.create("没有数据"nullnull);        RowRenderData rowMerge2 = Rows.create("红烧肉""35元""2份");        RowRenderData rowMerge3 = Rows.create("牛肉粉""28元""3份");        MergeCellRule rule = MergeCellRule.builder().map(MergeCellRule.Grid.of(10), MergeCellRule.Grid.of(12)).build();        data.put("table2", Tables.of(rowMerge, rowMerge1, rowMerge2, rowMerge3).mergeRule(rule).create());
1.2.3.3 结果展⽰
1.2.4 列表
列表标签以{{*}}填充模板,可以填充多种序列列表
1.2.4.1 创建模板
1.2.4.2 填充代码
//列表默认填充data.put("list", Numberings.create("春节""清明""五一""端午""中秋""国庆"));//有序数字列表data.put("list1",Numberings.of(NumberingFormat.DECIMAL) // 可以有多种有序编号方式                .addItem("星期一")                .addItem("星期二")                .addItem("星期三")                .addItem("星期四")                .addItem("星期五")                .addItem("星期六")                .addItem("星期日")                .create());
1.2.4.3 结果展⽰
1.2.5 区块对
区块对由前后两个标签组成,开始标签为{{?}},结束标签为{{/}}。位于区块对中的⽂档元素可以被渲染零次,⼀次或N次,这取决于区块对的取值。当填充区块对的value为False或空集合时,隐藏区块中的所有⽂档元素。当填充 区块对的value为⾮False且不是集合时,显⽰区块对中的⽂档元素,当填充区块对的value为⾮空集合时,根据集 合的⼤⼩,循环渲染区块中的⽂档元素。
1.2.5.1 创建模板
1.2.5.2 填充代码
//填充false区块对填充        data.put("falseBlock"false);//填充非false区块对填充        data.put("notFalseBlock"true);//填充自定义对象区块对填充        data.put("BlockMap"new Person("张三"));//填充自定义对象列表区块对填充        data.put("BlockListMap", Arrays.asList(new Person("李四"), new Person("王五"), new Person("周六")));
1.2.5.3 结果展⽰
1.3 引⽤标签⽰例
1.3.1 图⽚
引⽤图⽚标签以{{}}填充模板,标签位置在:设置图⽚格式—可选⽂字—标题或者说明(新版本MicrosoftOffice标签位置在:编辑替换⽂字-替换⽂字),在模板中内置好⼀个图⽚,设置好格式,填充好标签,会被代码中新的图 ⽚所替换,格式不变。
1.3.1.1 创建模板
1.3.1.2 填充代码
//引用图片填充        InputStream picInStream = ClassLoader.getSystemResourceAsStream("word/quotePhoto.jpg");if (picInStream == null) {thrownew IllegalArgumentException("模板文件未找到:word/quotePhoto.jpg");        }
1.3.2 多系列图表
引⽤多系列图表标签以{{}}填充模板,在图表设置-⽂本选项-可选⽂字中进⾏填写占位符。多系列图表指的是同⼀种类型的图表,可以⽣成多个系列,⽐如多个柱状图,多个条形图,多个⾯积图,多个折线图,可以设置图表显 ⽰的格式,⽐如⽹格线,数据标签,数据表等。
1.3.2.1 创建模板
1.3.2.2 填充代码
//引用表格填充        ChartMultiSeriesRenderData chart = Charts//填充的为图表标题跟X轴                .ofMultiSeries("折线图"newString[] { "00:15""00:30""00:45""01:00""01:15""01:30""01:45""02:00"})//Y轴数据依次在模板中填充                .addSeries("价格1"new Double[] { 370.13384.12325.12396.12427.12368.12329.12300.12 })                .addSeries("价格2"new Double[] { 410.13356.12432.12323.12367.12432.12321.12345.12 })                .addSeries("价格3"new Double[] { 368.12382.12443.12364.12325.12356.12307.12298.12 })                .create();        data.put("lineChart", chart);
1.3.2.3 结果展⽰
1.3.2.4 内容补充
由于默认⽣成的图表只有三个系列,⽐如默认模板中的折线图,只有三条,当业务数据需求⾮三条时,应当作出 调整。假如数据⼩于三条,则模板不⽤改动,因为模板中多余的线条默认不显⽰。当数据⼤于3条时,需要⼿动设 置模板,增加线条个数。
⾸先右击图表模板,点击编辑数据,在当前数据列中,增加所需的列数。如下图,原先只有系列1,系列2,系列3,现在增加⼀列系列4。
增加完系列4之后,保存返回到图表模板,然后右击图表模板,点击选择数据,将所需要的列选中,点击确认即 可,此时模板中将会有四个系列,代码填充时,可填充四列数据。
1.3.3 单系列图表
引⽤单系列图表标签以{{}}填充模板,在图表设置-⽂本选项-可选⽂字中进⾏填写占位符。多系列图表指的是同⼀种类型的图表,只能⽣成⼀个系列,⽐如饼状图,圆环图,可以设置图表显⽰的格式,⽐如数据标签等。
1.3.3.1 创建模板
1.3.3.2 填充代码
//引用单系列图表填充        ChartSingleSeriesRenderData pie = Charts                .ofSingleSeries("类型占比"newString[] { "类型1""类型2","类型3","类型4" })                .series("比例"new Integer[] { 3035,25,10 })                .create();        data.put("cakeChart", pie);
1.3.3.3 结果展⽰
1.3.3.4 内容补充
由于默认⽣成的饼图只分成了四个⾯积,当与业务数据不对等时,应当作出调整。假如数据⼩于四条,则模板不 ⽤改动,因为模板中多余的⾯积默认不显⽰。当数据⼤于四条时,需要⼿动设置模板,增加⾯积个数。
⾸先右击图表模板,点击编辑数据,在当前数据⾏中,增加所需的数据。如下图,原先只有第⼀类,第⼆类,第 三类,第四类,现在增加⼀⾏第五类。
增加完第五类之后,保存返回到图表模板,然后右击图表模板,点击选择数据,将所需要的数据选中,点击确认 即可,此时模板中将会有五类,代码填充时,可填充五类数据。
1.3.4 组合图表
引⽤组合图表标签以{{}}填充模板,在图表设置-⽂本选项-可选⽂字中进⾏填写占位符。组合图表指的是不同类型的混合图表,在⼀个图表中可同时⽣活柱状图,⾯积图,折线图,可以设置图表显⽰的格式,⽐如⽹格线,数据 标签,数据表等,可在创建图表时,设置双坐标轴。
1.3.4.1 创建模板
1.3.4.2 填充代码
//引用组合图表填充        ChartMultiSeriesRenderData comb = Charts//填充的为图表标题跟X轴                .ofComboSeries("市场"newString[] { "00:15""00:30""00:45""01:00""01:15""01:30""01:45""02:00"})//填充面积图                .addAreaSeries("预测1"new Double[] { 1456.211234.561356.211123.451245.561323.451434.561623.45 })//填充柱状图                .addBarSeries("预测2"new Double[] { 1256.211324.561135.621212.341345.561223.451134.561223.45 })//填充折线图(折线图在模板中设置的时副Y轴)                .addLineSeries("预测3"new Double[] { 123.4587.5696.6766.78112.89123.9089.0190.12 }).create();        data.put("combinationChart", comb);
1.3.4.3 结果展⽰
1.3.4.4 内容补充
由于默认⽣成的图表只有三个系列,⽐如默认模板中的组合图,只有三条,当业务数据需求⾮三条时,应当作出 调整。假如数据⼩于三条,则模板不⽤改动,因为模板中多余的默认不显⽰。当数据⼤于3条时,需要⼿动设置模 板,增加个数。
⾸先右击图表模板,点击编辑数据,在当前数据列中,增加所需的列数。如下图,原先只有系列1,系列2,系列3,现在增加⼀列系列4。
增加完系列4之后,保存返回到图表模板,然后右击图表模板,点击选择数据,将所需要的列选中,点击确认即 可,此时模板中将会有四个系列,代码填充时,可填充四列数据。
此时回到图表模板,右击选择更改图表类型,可为系列四选择图表类型以及坐标轴
1.4 配置⽰例
1.4.1 ⽂本
poi-tl原本提供的⽂本标签为{{}},可通过配置来更改为⾃⼰喜欢的标签。例如将{{}}改成{}
1.4.1.1 创建模板
1.4.1.2 填充代码
//配置标签        ConfigureBuilder builder = Configure.builder();//自定义标签为{}        builder.buildGramer("{""}");//模板数据Map<StringObject> data = new HashMap<>();        data.put("title""hello world");
1.4.1.3 结果展⽰
1.4.2 图⽚
poi-tl原本提供的图⽚标签为{{@}},可通过配置来更改为⾃⼰喜欢的标签。例如将{{@}}改成{%}
1.4.2.1 创建模板
1.4.2.2 填充代码
//配置标签        ConfigureBuilder builder = Configure.builder();//自定义标签为{}        builder.buildGramer("{""}");//自定义图片标签        builder.addPlugin('%'new PictureRenderPolicy());//模板数据Map<StringObject> data = new HashMap<>();        data.put("title""hello world");//读取图片文件try {            InputStream picInStream = ClassLoader.getSystemResourceAsStream("word/photo.jpg");if (picInStream == null) {thrownew IllegalArgumentException("模板文件未找到:word/photo.jpg");            }            data.put("image", Pictures.ofStream(picInStream, PictureType.JPEG).size(200220).create());        } catch (Exception e) {            e.printStackTrace();        }
1.5 插件⽰例
1.5.1 开发插件
在poi-tl中,可以⾃定义开发插件,可以实现⽤⼀个标签做特定的事情。例如⾃定义⼀个插件,模板⽣成时,标签 中填充的值都会拼接上’helloword’显⽰。
1.5.1.1 创建模板
1.5.1.2 填充代码
publicvoid render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {        XWPFRun run = ((RunTemplate) eleTemplate).getRun();//"Hello, world" + 标签的内容String thing = "Hello, world".concat(String.valueOf(data));        run.setText(thing, 0);    }
//配置        ConfigureBuilder builder = Configure.builder();//title标签 采用HelloWorldRenderPolicy渲染策略        builder.bind("title"new HelloWorldRenderPolicy());Map<StringObject> data = new HashMap<>();        data.put("title""你好世界");
1.5.1.3 结果展⽰
1.5.2 表格⾏循环插件
表格⾏循环插件,根据模板中内置好的表格,对表格中的⾏进⾏循环渲染数据,标签需要放在循环⾏的上⼀⾏, 且放标签的单元格不能是合并单元格。循环的数据占位符,需要⽤[]中括号占位。
1.5.2.1 创建模板
1.5.2.2 填充代码
//行填充策略        HackLoopTableRenderPolicy rowRenderPolicy = new HackLoopTableRenderPolicy();        List<Information> informationList = new ArrayList<>();        informationList.add(new Information("张三""18""男""医生""跑步"));        informationList.add(new Information("李四""21""女""科学家""骑车"));        informationList.add(new Information("王五""25""男""教师""游泳"));//informationList标签 采用rowRenderPolicy渲染策略        builder.bind("informationList", rowRenderPolicy);        data.put("informationList", informationList);
1.5.2.3 结果展⽰
1.5.3 表格列循环插件
表格列循环插件,根据模板中内置好的表格,对表格中的列进⾏循环渲染数据,标签需要放在循环列的前⼀列, 且放标签的单元格不能是合并单元格。循环的数据占位符,需要⽤[]中括号占位。
1.5.3.1 创建模板
1.5.3.2 填充代码
// 列填充策略        LoopColumnTableRenderPolicy columnRenderPolicy = new LoopColumnTableRenderPolicy();        List<Information> informationColumnList = new ArrayList<>();        informationColumnList.add(new Information("张三""18""男""医生""跑步"));        informationColumnList.add(new Information("李四""21""女""科学家""骑车"));        informationColumnList.add(new Information("王五""25""男""教师""游泳"));//informationColumnList标签 采用columnRenderPolicy渲染策略        builder.bind("informationColumnList", columnRenderPolicy);        data.put("informationColumnList", informationColumnList);
1.5.3.3 结果展⽰
1.5.4 表格动态循环插件
表格动态循环插件,是匹配模板⽆规则的表格插件,根据内置好的表格模板,预设好需要填充的值,然后对表格 进⾏循环渲染数据,标签可以放在表格模板中的任何位置,因为需要填充的数据⾏列,已经在插件中⾃定义了。
1.5.4.1 创建模板
1.5.4.2 填充代码
// 拿到数据        Map<String, List<RowRenderData>> map = (Map<String, List<RowRenderData>>) data;//自定义数据是从第二行和第四行开始int dogRow = 2;int peopleRow = 4;        List<RowRenderData> dogs = map.get("dogs");        List<RowRenderData> people = map.get("peoples");//从表格下部开始填充数据,可以保证索引正确if (people!= null) {            table.removeRow(peopleRow);for(int i=0;i<people.size();i++){                XWPFTableRow insertNewTableRow = table.insertNewTableRow(peopleRow);for (int j = 0; j < 5; j++) insertNewTableRow.createCell();// 合并单元格                TableTools.mergeCellsHorizonal(table, peopleRow, 02);// 单行渲染                TableRenderPolicy.Helper.renderRow(table.getRow(peopleRow), people.get(i));            }        }if (dogs!= null) {            table.removeRow(dogRow);for(int i=0;i<dogs.size();i++){                XWPFTableRow insertNewTableRow = table.insertNewTableRow(dogRow);for (int j = 0; j < 5; j++) insertNewTableRow.createCell();// 单行渲染                TableRenderPolicy.Helper.renderRow(table.getRow(dogRow), dogs.get(i));            }        }
1.5.4.3 结果展⽰
2 XWPFDocument读取⽰例
XWPFDocument常⽤于word内容读取,这⾥只做读取⽰例,读取可以读取⽂本段落跟表格数据。
try {              InputStream inputStream = ClassLoader.getSystemResourceAsStream("word/读取解析文件示例.docx");              XWPFDocument doc = new XWPFDocument(inputStream);// 移除文档中的第3个段落              doc.getXWPFDocument().removeBodyElement(3);// 获取文档中的段落List<XWPFParagraph> paras = doc.getParagraphs();// 遍历段落for (XWPFParagraph para : paras) {// 获取段落中的文本List<XWPFRun> runs = para.getRuns();// 遍历文本for (XWPFRun run : runs) {                    System.out.println(run.getText(0));                }            }// 获取文档中的表格List<XWPFTable> tables = doc.getTables();// 遍历表格for (XWPFTable table : tables) {// 遍历表格中的行List<XWPFTableRow> rows = table.getRows();for (XWPFTableRow row : rows) {// 遍历表格中该行所有的列List<XWPFTableCell> cells = row.getTableCells();for (XWPFTableCell cell : cells) {// 获取单元格中的段落List<XWPFParagraph> paragraphs = cell.getParagraphs();// 遍历单元格中的段落for (XWPFParagraph paragraph : paragraphs) {// 获取段落中的文本List<XWPFRun> runs = paragraph.getRuns();// 遍历文本for (XWPFRun run : runs) {                                  System.out.println(run.getText(0));                              }                          }                      }                  }              }/*List<String> stringList = paras.stream().map(x -> x.getText()).collect(Collectors.toList());              System.out.println(stringList);*/        } catch (Exception e) {            e.printStackTrace();        }
03
常⻅问题
  1. poi-tl程序执⾏过程出现NoSuchMethodError、ClassNotFoundException、NoClassDefFoundError异常?
    解决⽅案:poi-tl依赖的apache-poi缺失或对应poi版本不兼容,尝试更换版本。
  2. 刚开始在使⽤poi-tl时,可能会遇到模板标签⽆法正确解析的问题。例如,标签未被替换或替换后的内容样式 丢失?解决⽅案:确保模板中的标签格式正确,例如{{tag}}。标签必须严格按照项⽬⽂档中的规范书写,并注意⼤⼩写敏感。
  3. poi-tl表格的⾏循环数据加载,当表格表头存在合并单元格时,加载数据出现格式错乱?解决⽅案:需将⻚签放在⾮合并单元格内以保证格式正常。
  4. poi-tl在填充段落数据的过程中,假如某⼀个标签的数据为空,数据就会填充不上去,建议当数据为空时,给占位符填充空字符串。

往期推荐

Spring Boot 3.x + GraalVM 原生镜像:启动0.3秒,内存直接砍半!

ElasticSearch入门详解(基础概念+REST实操)

TDengine数据库部署全攻略:从环境准备到日常操作

面试必问:CountDownLatch、CyclicBarrier、Semaphore 到底怎么用?一篇彻底分清!

手把手教你:Linux环境安装Docker的详细步骤

90% 的微服务都栽在这个坑上:Resilience4j 熔断器避坑指南

别再 try-catch 重试了!Resilience4j Retry 才是微服务正确姿势

别再用 Guava 限流了!Resilience4j RateLimiter 才是微服务限流王者

揭秘Dockerfile:构建应用镜像的终极指南

别再乱找了!SpringBoot 集成 Kafka 看这篇就够

Spring AI:5 分钟搭建 Java 大模型聊天机器人

Java 26新特性全解析:值对象、泛型专业化与性能优化

揭秘Dubbo集成全过程:从依赖到服务调用,手把手带你飞!

Docker入门指南:应用封装与部署的革命性工具

北京朝阳惊现“一人公司”社区,AI如何助力个体创业者?

彻底搞懂Java阻塞队列:从源码解读到性能优化

手把手教你在Docker中部署Kafka:配置、启动与验证

Kafka,凭什么扛住万亿级消息?

探索Modbus:揭开工业领域最广泛协议的神秘面纱

MQTT协议全解析:物联网通信的轻量级利器

分享

收藏

点赞

在看