java通过poi-tl生成表格以及源码分析
-
- 依赖
- 模板
- 如何动态生成表格
- 参考文档及分析
- 代码
最近导出的word文件要求是越来越多了,而且对样式也做了很多要求,今天参考文档学习了一下普通表格构建表格、动态构建word表格的方法。
依赖
<dependency><groupId>com.deepoove</groupId><artifactId>poi-tl</artifactId><version>1.10.0</version></dependency>
模板
1. {{ID}}、{{ID}} 普通文本信息占位符;
2. {{#order}} 普通word表格占位符;
3. 最后的表格是动态构建word表格的模板,样式已经设置好;
如何动态生成表格
当需求中的表格更加复杂的时候,我们完全可以设计好那些固定的部分,将需要动态渲染的部分单元格交给自定义模板渲染策略。poi-tl提供了抽象表格策略 DynamicTableRenderPolicy 来实现这样的功能。
public abstract class DynamicTableRenderPolicy implements RenderPolicy {public abstract void render(XWPFTable table, Object data);
}
{{detailTable}}标签可以在表格内的任意单元格内,DynamicTableRenderPolicy会获取XWPFTable对象进而获得操作整个表格的能力。
1、首先新建渲染策略DetailTablePolicy,继承于抽象表格策略,重写其render方法。
public class DetailTablePolicy extends DynamicTableRenderPolicy
2、然后将模板标签{{detailTable}}设置成此策略。
Configure config = Configure.builder().bind("detailTable", new DetailTablePolicy()).build();
参考文档及分析
但是在测试是发现文档中例子始终无法生成最下面的表格,即货物明细表,
debug发现,策略类中的data为空,难怪没有生成表格。
但是数据明明已经初始化好了,那接下来就是数据在传输的是时候在哪丢失或者传进去的参数根本就没被取到。
跟踪代码,
最后在ElementProcessor中,有这样一段代码
void visit(ElementTemplate eleTemplate) {RenderPolicy policy = eleTemplate.findPolicy(template.getConfig());Objects.requireNonNull(policy, "Cannot find render policy: [" + eleTemplate.getTagName() + "]");if (policy instanceof DocxRenderPolicy) return;logger.info("Start render Template {}, Sign:{}, policy:{}", eleTemplate, eleTemplate.getSign(),ClassUtils.getShortClassName(policy.getClass()));policy.render(eleTemplate, renderDataCompute.compute(eleTemplate.getTagName()), template);}
通过policy.render(eleTemplate, renderDataCompute.compute(eleTemplate.getTagName()), template);这个方法最后进入到我们自己写的策略类DetailTablePolicy ,真是妙啊!
那既然是从这里进入我们的策略类,那也是参数的入口,就让我们看看为什么DetailTablePolicy 中data为null?
data就是renderDataCompute.compute(eleTemplate.getTagName()),其中
eleTemplate.getTagName()为detail_table而且renderDataCompute也没有与detail_table相对应的属性,虽然理论上应该是获取detailTable,但是现实是木有取到。因此也验证了我们前面的猜测,数据确实是传过来了,只是没有获取到。因此我就有个大胆的猜测,是不是因为属性字段(detail_table和detailTable)没有完全对应,因此我就把detail_table替换成了detailTable(代码和docx模板全部替换),结果妹想到呀,成了!!!
哈哈哈,真是走了狗屎运了。还望路过的各位大佬指点指点
补充renderDataCompute.compute(eleTemplate.getTagName()):
虽然走了狗屎运,但是还是想探个究竟,compute到底做了些什么
进入DefaultELRenderDataCompute类中
public Object compute(String el) {try {if (null != envObject && !el.contains("#this")) {try {Object val = envObject.eval(el);if (null != val) {return val;}} catch (Exception e) {// ignore}}// 进入这个方法return elObject.eval(el);} catch (ExpressionEvalException e) {if (isStrict) throw e;// Cannot calculate the expression, the default returns nullreturn null;}}
进入DefaultEL类中
public Object eval(String el) {// THIS是什么? private static final String THIS = "#this";if (THIS.equals(el)) {return model;}Dot dot = new Dot(el);// 继续进入return dot.eval(this);}
在Dot类中终于找到了!!!
public Object eval(DefaultEL elObject) {// 如果cache中有el相等的key,则返回cache中相应的valueif (elObject.cache.containsKey(el)) return elObject.cache.get(el);// 如果target为null result就为evalKey(elObject.model)// evalKey 是啥??? 真是要了老命了,我以为结束了呢,继续看方法evalKey吧Object result = null != target ? result = evalKey(target.eval(elObject)) : evalKey(elObject.model);if (null != result) elObject.cache.put(el, result);return result;}private Object evalKey(Object obj) {if (null == obj) {throw new ExpressionEvalException("Error eval " + key + ", the value of " + target + " is null");}final Class<?> objClass = obj.getClass();if (obj instanceof String || obj instanceof Number || obj instanceof java.util.Date || obj instanceof Collection|| obj instanceof Boolean || objClass.isArray() || objClass.isPrimitive()) {throw new ExpressionEvalException("Error eval " + key + ", the type of " + target + "must be Hash, but is " + objClass);}// 重点是这里 如果是Map类型 返回对应的value// 但是我们的elObject.model是Map吗? 看下图if (obj instanceof Map) {return ((Map<?, ?>) obj).get(key);}// introspectorMethod readMethod = ReadMethodFinder.find(objClass, key);if (null != readMethod) {try {return readMethod.invoke(obj);} catch (Exception e) {logger.info("Introspector {} fail: {}", key, e.getMessage());}}// 通过对象的类信息反射返回对应属性的值// reflectField field = FieldFinder.find(objClass, key);if (null == field) {throw new ExpressionEvalException("Cannot find property " + key + " from " + objClass);} else {try {return field.get(obj);} catch (Exception e) {throw new ExpressionEvalException("Error read the property:" + key + " from " + objClass);}}}
elObject.model和elObject.cache的类型
static Field find(Class<?> objClass, String key) {Class<?> clazz = objClass;Field field = null;while (clazz != Object.class) {try {field = findInClazz(clazz, key);// 暴力访问field.setAccessible(true);return field;} catch (NoSuchFieldException e) {// do nothing, go to super class} catch (Exception e) {logger.warn("Error read the property:" + key + " from " + objClass);}clazz = clazz.getSuperclass();}return null;}static Field findInClazz(Class<?> clazz, String key) throws NoSuchFieldException {Field field = null;try {// 通过类信息获取对应字段的值field = clazz.getDeclaredField(key);return field;} catch (Exception e) {}Field[] fields = cache.get(clazz);if (null == fields) {fields = clazz.getDeclaredFields();cache.put(clazz, fields);}for (Field f : fields) {Name annotation = f.getAnnotation(Name.class);if (null != annotation && key.equals(annotation.value())) {return f;}}throw new NoSuchFieldException(key);}
Poi-tl Documentation
附上代码如下:
代码
- 实体类DetailData(动态构建word表格的数据)
public class DetailData {private List<RowRenderData> goods;private List<RowRenderData> labors;public List<RowRenderData> getGoods() {return goods;}public void setGoods(List<RowRenderData> goods) {this.goods = goods;}public List<RowRenderData> getLabors() {return labors;}public void setLabors(List<RowRenderData> labors) {this.labors = labors;}
}
- 实体类PaymentData (相当于数据集,包换word模板中需要的数据都在这里)
public class PaymentData {private String NO;private String ID;private String taitou;private String consignee;private String subtotal;private String tax;private String transform;private String other;private String unpay;private String total;private TableRenderData order;private DetailData detailTable;public String getNO() {return NO;}public void setNO(String NO) {this.NO = NO;}public String getID() {return ID;}public void setID(String ID) {this.ID = ID;}public String getTaitou() {return taitou;}public void setTaitou(String taitou) {this.taitou = taitou;}public String getConsignee() {return consignee;}public void setConsignee(String consignee) {this.consignee = consignee;}public String getSubtotal() {return subtotal;}public void setSubtotal(String subtotal) {this.subtotal = subtotal;}public String getTax() {return tax;}public void setTax(String tax) {this.tax = tax;}public String getTransform() {return transform;}public void setTransform(String transform) {this.transform = transform;}public String getOther() {return other;}public void setOther(String other) {this.other = other;}public String getUnpay() {return unpay;}public void setUnpay(String unpay) {this.unpay = unpay;}public String getTotal() {return total;}public void setTotal(String total) {this.total = total;}public TableRenderData getOrder() {return order;}public void setOrder(TableRenderData order) {this.order = order;}public DetailData getDetailTable() {return detailTable;}public void setDetailTable(DetailData detailTable) {this.detailTable = detailTable;}
}
- 策略类(动态构建表格的策略方法)
public class DetailTablePolicy extends DynamicTableRenderPolicy {// 货品填充数据所在行数int goodsStartRow = 2;// 人工费填充数据所在行数int laborsStartRow = 5;@Overridepublic void render(XWPFTable table, Object data) throws Exception {if (null == data) return;DetailData detailData = (DetailData) data;// 人工费List<RowRenderData> labors = detailData.getLabors();if (null != labors) {table.removeRow(laborsStartRow);// 循环插入行for (int i = 0; i < labors.size(); i++) {XWPFTableRow insertNewTableRow = table.insertNewTableRow(laborsStartRow);for (int j = 0; j < 7; j++) insertNewTableRow.createCell();// 合并单元格TableTools.mergeCellsHorizonal(table, laborsStartRow, 0, 3);// 单行渲染TableRenderPolicy.Helper.renderRow(table.getRow(laborsStartRow), labors.get(i));}}// 货物List<RowRenderData> goods = detailData.getGoods();if (null != goods) {table.removeRow(goodsStartRow);for (int i = 0; i < goods.size(); i++) {XWPFTableRow insertNewTableRow = table.insertNewTableRow(goodsStartRow);for (int j = 0; j < 7; j++) insertNewTableRow.createCell();TableRenderPolicy.Helper.renderRow(table.getRow(goodsStartRow), goods.get(i));}}}
@DisplayName("Example for Table")
public class generateTable{String resource = "src/main/resources/payment.docx";PaymentData datas = new PaymentData();// 初始化数据@BeforeEachpublic void init() {datas.setNO("KB.6890451");datas.setID("ZHANG_SAN_091");datas.setTaitou("深圳XX家装有限公司");datas.setConsignee("丙丁");datas.setSubtotal("8000");datas.setTax("600");datas.setTransform("120");datas.setOther("250");datas.setUnpay("6600");datas.setTotal("总共:7200");RowRenderData header = Rows.of("日期", "订单编号", "销售代表", "离岸价", "发货方式", "条款", "税号").bgColor("F2F2F2").center().textColor("7F7f7F").textFontFamily("Hei").textFontSize(9).create();RowRenderData row = Rows.of("2018-06-12", "SN18090", "李四", "5000元", "快递", "附录A", "T11090").center().create();BorderStyle borderStyle = new BorderStyle();borderStyle.setColor("A6A6A6");borderStyle.setSize(4);borderStyle.setType(XWPFTable.XWPFBorderType.SINGLE);TableRenderData table = Tables.ofA4MediumWidth().addRow(header).addRow(row).border(borderStyle).center().create();datas.setOrder(table);DetailData detailTable = new DetailData();RowRenderData good = Rows.of("4", "墙纸", "书房+卧室", "1500", "/", "400", "1600").center().create();List<RowRenderData> goods = Arrays.asList(good, good, good);RowRenderData labor = Rows.of("油漆工", "2", "200", "400").center().create();List<RowRenderData> labors = Arrays.asList(labor, labor, labor, labor);detailTable.setGoods(goods);detailTable.setLabors(labors);datas.setDetailTable(detailTable);}// 测试方法@Testpublic void testPaymentExample() throws Exception {Configure config = Configure.builder().bind("detailTable", new DetailTablePolicy()).build();XWPFTemplate template = XWPFTemplate.compile(resource, config).render(datas);template.writeToFile("out_example_payment.docx");}}