使用Poi-tl实现的文档导出

World模板引擎

项目利用这个模板引擎实现了超级复杂的world导出,现在记录一下项目中实际使用的一些操作逻辑。

1、poi-tl简介

至于非常详细的介绍,请看上面的World模板引擎

  • poi-tl(poi template language)是Word模板引擎,基于Word模板和数据生成新的文档。(基于Apache POI)
  • 具体用到的一些功能
引擎功能 描述 标签
文本 将标签渲染为文本
图片 将标签渲染为图片 {{@xx}}
表格 将标签渲染为表格 {{#xx}}
列表 将标签渲染为列表 {{*xx}}
图表 条形图(3D条形图)、柱形图(3D柱形图)、面积图(3D面积图)、折线图(3D折线图)、雷达图、饼图(3D饼图)等图表渲染
文本框 文本框内标签支持
样式 模板即样式,同时代码也可以设置样式
模板嵌套 模板包含子模板,子模板再包含子模板 {{+var}}
合并 Word合并Merge,也可以在指定位置进行合并
用户自定义函数(插件) 在文档任何位置执行函数
If Condition判断 内隐藏或者显示某些文档内容(包括文本、段落、图片、表格、列表、图表等)
Foreach Loop循环 循环某些文档内容(包括文本、段落、图片、表格、列表、图表等)
Loop表格行 循环渲染表格的某一行
Loop表格列 循环渲染表格的某一列
Loop有序列表 支持有序列表的循环,同时支持多级列表
图片替换 将原有图片替换成另一张图片
书签、锚点、超链接 支持设置书签,文档内锚点和超链接功能
强大的表达式 完全支持SpringEL表达式,可以扩展更多的表达式:OGNL, MVEL…
标签定制 支持自定义标签前后缀
  • 对于模板嵌套


    image.png

模板标签

{{?sections}}{{/sections}} 区块对标签
引用标签 : 在图或者图表中添加标签,可直接操作图或者图表数据

2、不足之处

image.png
  • 针对不同的桥型,各个部位下的部件都不相同,所以使用easy-poi需要针对不同的桥梁编辑多个不同的模板
  • 在各个部件位置上的图片,表格,文本数据不能使用一个标签来代替,导致需要针对的标签数量成倍的增加
  • 项目中针对不同的桥梁类型也需要不一样的模板,比如城市桥梁,公路桥梁,公路桥梁里面又分什么单幅桥,双福桥,三幅桥等等,导致会有很多冗余的代码产生
  • 仅仅支持07版本的word也是只能生成后缀是docx的文档
  • 要导出表格的话,需要自己操作XWPFTable对象。在word中插入表格

3、大致实现思路

我们在做的时候是引入的1.9.0版本,目前版本已经更新到1.11.1

  • 把整个文档需要替换的地方使用{{XXXX }}进行标记,并将{{XXXX}}这些里面的内容保存到数据库中
    整个模板中分为三个类型,第一个系统默认的(用F打头),第二个文本,第三个图片(@F打头)
image.png

{{+XXX}}是自定义模板嵌套,模板里面需要子模板


image.png

自定义模板嵌套的具体模板
  • 写一个计算的注解,@CalculateAnn,并写一个bean,利用反射将所有标记有@CalculateAnn注解的方法解析出来,并利缓存起来,后面需要使用


    @CalculateAnn
解析所有使用了此注解的方法
  • 写一个枚举类用来识别对应的标记具体是那个模板,code就是在模板中定义的字段
    image.png
具体有哪些模板
  • 具体实现对应的含有注解的方法,返回Object


    image.png
  • 实现具体导出方法
    1)获取数据库中当前项目的所有模板字段


    image.png

2)配置具体有表格,比如图片,设备表、计算表,人员表,病害统计,总结等等


image.png

这里面的DetailTablePolicyMemberTablePolicy这两个是复写了的,详细代码,贴到了最后面,都是根据实际需求进行的改造

3)初始化数据项目中的很多数据,进行缓存,在具体的数据实现中需要用到(这块就根据自己的业务量来,我们业务数据太多,每次去重复查询不合适,所以临时保存到内存中)

4)准备一个渲染Map,这个map是为了模板输出数据的额

 Map<String, Object> renderMap = new HashMap<>(allField.size());

5)循环模板字段列表,用invoke调用具体方法,并将结果放到渲染Map中


image.png

6)最后清缓存数据,以及将所有的数据放到模板中进行具体的结果输出

  XWPFTemplate template = XWPFTemplate.compile(path, config);
  template.render(renderMap);
        template.writeAndClose(outputStream);

4、部分的代码

MethodName
 @AllArgsConstructor
    @Getter
    /**按各个部位分组
     */
    public enum MethodName {
        //构件按照部件分组,存放在不同的表格中,这里面的message是公路桥梁的
        COMPONENT_TOP1("F_componentInfo_top1", "public/component_template.docx", ".1."),
        COMPONENT_LOW1("F_componentInfo_low1", "public/component_template.docx", ".2."),
        COMPONENT_DECK1("F_componentInfo_deck1", "public/component_template.docx", ".3."),

        COMPONENT_TOP_L("F_componentInfo_top_l", "public/component_template.docx", ".1.1."),
        COMPONENT_LOW_L("F_componentInfo_low_l", "public/component_template.docx", ".1.2."),
        COMPONENT_DECK_L("F_componentInfo_deck_l", "public/component_template.docx", ".1.3."),

        COMPONENT_TOP_M("F_componentInfo_top_m", "public/component_template.docx", ".3.1."),
        COMPONENT_LOW_M("F_componentInfo_low_m", "public/component_template.docx", ".3.2."),
        COMPONENT_DECK_M("F_componentInfo_deck_m", "public/component_template.docx", ".3.3."),

        COMPONENT_TOP_R("F_componentInfo_top_r", "public/component_template.docx", ".2.1."),
        COMPONENT_LOW_R("F_componentInfo_low_r", "public/component_template.docx", ".2.2."),
        COMPONENT_DECK_R("F_componentInfo_deck_r", "public/component_template.docx", ".2.3."),

        //具体病害图片及描述
//        DISEASE("F_diseaseInfo", "", "病害信息"),
        //上部结构计算结果
        CALCULATION_TOP("F_calculation_top", "public/calculation_template_highway.docx", ".1."),
        //桥面系计算结果
        CALCULATION_DECK("F_calculation_deck", "public/calculation_template_highway.docx", ".3."),
        //下部结构计算结果
        CALCULATION_LOW("F_calculation_low", "public/calculation_template_highway.docx", ".2."),
        //各个部位的评定计算结果
        CALCULATION_CATEGORY("F_calculation_category", "public/calculation_category.docx", ".4."),
        //总的计算结果
        CALCULATION_RESULT("F_result","","计算结果"),
//        CALCULATION_RESULT_TOP("F_calculation_result_top","",""),
//        CALCULATION_RESULT_DECK("F_calculation_result_deck","",""),
//        CALCULATION_RESULT_LOW("F_calculation_result_low","",""),

        //参与人员的信息表格
        PERSONNEL("F_personnel", "public/person_template.docx", "人员信息"),
        //项目中使用的设备,
        DEVICE("F_device", "public/device_template.docx", "设备库信息"),

        FRONT_IMG("F_ZhengMianZhao", "", "正面照"),
        FACADE_IMG("F_LiMianZhao", "", "立面照"),
        PLAN_IMG("F_PingMianTu", "", "平面示意图"),
        SKETCH_IMG("F_LiMianTu", "", "立面示意图"),
        CROSS_IMG("F_HengDuanMianTu", "", "横断面图"),
        GPS_IMG("F_DingWeiTu", "", "GPS图"),
        MEMBER("F_member", "public/member_template.docx", ".1"),
        //检测目的及内容  {{F_checkContent}}
        CHECK_CONTENT("F_checkContent", "", "检测目的及内容"),
        //检测结论test_conclusion_text_city.docx
        TEST_CONCLUSION("F_test_conclusion", "public/test_conclusion_text_highway.docx", ""),
        //重点关注
        MAIN_DISEASE_TABLE("F_main_disease_table", "public/main_disease_table.docx", "重点关注表"),
        //病害数量统计
        DISEASE_STATISTICS_TABLE("F_disease_statistics_table","public/disease_statistics.docx","病害数量统计表"),
        //技术评定汇总表
        CALCULATION_SUMMARY_TABLE("F_calculation_summary_table","public/calculation_summary_table.docx",".4."),

        PROJECT_INFO("F_info", "", "基础信息,包含项目中的信息:建设单元。项目名称等"),

        BRIDGE_DISEASE("F_bridge_disease", "", "桥梁主要病害统计");

        private String code;
        private String templatePath;
        private String message;


        public static MethodName findByField(String field) {
            for (MethodName value : MethodName.values()) {
                if (value.getCode().equals(field)) {
                    return value;
                }
            }
            return null;
        }
DetailTablePolicy
public class DetailTablePolicy extends DynamicTableRenderPolicy {

    @Override
    public void render(XWPFTable table, Object data) throws Exception {

        if (null == data) {
            return;
        }
        DetailData detailData = (DetailData) data;
        int startRow = detailData.getStartRow() == null ? 1 : detailData.getStartRow();
        List<RowRenderData> dataList = detailData.getRowRenderData();
        if (ComUtil.isNotEmpty(dataList)) {
            int index = dataList.get(0).getCells().size();
            table.removeRow(startRow);
            for (int i = 0; i < dataList.size(); i++) {
                XWPFTableRow insertNewTableRow = table.insertNewTableRow(startRow);
                for (int j = 0; j < dataList.get(i).getCells().size(); j++) {
                    insertNewTableRow.createCell();
                }
                TableRenderPolicy.Helper.renderRow(table.getRow(startRow++), dataList.get(i));
            }
           if (detailData.isMerge()){
               TableTools.mergeCellsVertically(table, index -1  , 1,  dataList.size());
           }
        }
    }

}

MemberTablePolicy
public class MemberTablePolicy implements RenderPolicy {

    int startRow;

    public MemberTablePolicy() {

        this(Constant.GenericNumber.NUMBER_TWO);
    }

    public MemberTablePolicy(int startRow) {
        this.startRow = startRow;
    }

    @Override
    public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {

        if (null == data) {
            return;
        }
        MemberTable detailData = (MemberTable) data;
        if (ComUtil.isEmpty(detailData.getCells()) || ComUtil.isEmpty(detailData.getRows())) {
            return;
        }
        RunTemplate runTemplate = (RunTemplate) eleTemplate;
        XWPFRun run = runTemplate.getRun();
        try {
            if (!TableTools.isInsideTable(run)) {
                throw new IllegalStateException(
                        "The template tag " + runTemplate.getSource() + " must be inside a table");
            }
            XWPFTableCell tagCell = (XWPFTableCell) ((XWPFParagraph) run.getParent()).getBody();
            XWPFTable table = tagCell.getTableRow().getTable();
            run.setText("", 0);
            //insert cells
            processCel(table, tagCell, template, detailData.getCells());
            //insert rows
            final List<List<RowRenderData>> rows = detailData.getRows();
            table.removeRow(startRow);
            processRow(table, rows, startRow);

        } catch (Exception e) {
            throw new RenderException("HackLoopTable for " + eleTemplate + "error: " + e.getMessage(), e);
        }
    }

    private void processRow(XWPFTable table, List<List<RowRenderData>> dataList, int startRow) throws Exception {
        if (ComUtil.isEmpty(dataList)){
            return;
        }
        int index = startRow;
        for (List<RowRenderData> data : dataList) {
            for (int i = data.size() - 1; i >= 0; i--) {
                RowRenderData rowRenderData = data.get(i);
                XWPFTableRow insertNewTableRow = table.insertNewTableRow(startRow);
                for (int j = 0; j < rowRenderData.getCells().size(); j++) {
                    insertNewTableRow.createCell();
                }
                TableRenderPolicy.Helper.renderRow(table.getRow(startRow), rowRenderData);
            }
            if (data.size() > 1){
                TableTools.mergeCellsVertically(table, 1, index, index + data.size() - 1);
            }
            startRow = startRow + data.size();
            index = index + data.size();
        }
    }

    private void processCel(XWPFTable table, XWPFTableCell tagCell, XWPFTemplate template, List<List<Object>> dataList) throws Exception {

        if (ComUtil.isEmpty(dataList)){
            return;
        }
        int templateColIndex = getTemplateColIndex(tagCell);
        int actualColIndex = getActualInsertPosition(tagCell.getTableRow(), templateColIndex);
        XWPFTableCell firstCell = tagCell.getTableRow().getCell(actualColIndex);
        int width = firstCell.getWidth();
        TableWidthType widthType = firstCell.getWidthType();
        if (TableWidthType.DXA != widthType || width == 0) {
            throw new IllegalArgumentException("template col must set width in centimeters.");
        }
        int rowSize = table.getRows().size();
        int count = dataList.stream().mapToInt(List::size).sum();
        int colWidth = processLoopColWidth(table, width, templateColIndex, count);

        int index = templateColIndex;
        for (List<Object> data : dataList) {
            if (data == null || data.size() == 0) {
                continue;
            }
            processLoop(data, table, templateColIndex, rowSize, template, colWidth);
            if (data.size() > 1){
                TableTools.mergeCellsHorizonal(table, 0, index, index + data.size() - 1);
            }
            templateColIndex = templateColIndex + data.size();
            index++;
        }
        //delete last cell
        for (int i = 0; i < rowSize; i++) {
            XWPFTableRow row = table.getRow(i);
            int actualInsertPosition = getActualInsertPosition(row, templateColIndex);
            if (-1 == actualInsertPosition) {
                minusGridSpan(row, templateColIndex);
                continue;
            }
            removeCell(row, actualInsertPosition);
        }
    }

    private void processLoop(List<Object> data, XWPFTable table, int templateColIndex,
                             int rowSize, XWPFTemplate template, int colWidth) throws Exception {

        Iterator<?> iterator = ((Iterable<?>) data).iterator();
        int insertPosition;
        TemplateResolver resolver = new TemplateResolver(template.getConfig().copy("[", "]"));
        while (iterator.hasNext()) {
            insertPosition = templateColIndex++;
            List<XWPFTableCell> cells = new ArrayList<>();

            for (int i = 0; i < rowSize; i++) {
                XWPFTableRow row = table.getRow(i);
                int actualInsertPosition = getActualInsertPosition(row, insertPosition);
                if (-1 == actualInsertPosition) {
                    addColGridSpan(row, insertPosition);
                    continue;
                }
                XWPFTableCell templateCell = row.getCell(actualInsertPosition);
                templateCell.setWidth(colWidth + "");
                XWPFTableCell nextCell = insertCell(row, actualInsertPosition);
                setTableCell(row, templateCell, actualInsertPosition);

                // double set row
                XmlCursor newCursor = templateCell.getCTTc().newCursor();
                newCursor.toPrevSibling();
                XmlObject object = newCursor.getObject();
                nextCell = new XWPFTableCell((CTTc) object, row, (IBody) nextCell.getPart());
                setTableCell(row, nextCell, actualInsertPosition);

                cells.add(nextCell);
            }

            RenderDataCompute dataCompute = template.getConfig().getRenderDataComputeFactory()
                    .newCompute(iterator.next());
            cells.forEach(cell -> {
                List<MetaTemplate> templates = resolver.resolveBodyElements(cell.getBodyElements());
                new DocumentProcessor(template, resolver, dataCompute).process(templates);
            });
        }
    }

    private int getTemplateColIndex(XWPFTableCell tagCell) {
        return (getColIndex(tagCell) + 1);
    }

    private void minusGridSpan(XWPFTableRow row, int templateColIndex) {
        XWPFTableCell actualCell = getActualCell(row, templateColIndex);
        CTTcPr tcPr = actualCell.getCTTc().getTcPr();
        CTDecimalNumber gridSpan = tcPr.getGridSpan();
        gridSpan.setVal(BigInteger.valueOf(gridSpan.getVal().longValue() - 1));
    }

    private void addColGridSpan(XWPFTableRow row, int insertPosition) {
        XWPFTableCell actualCell = getActualCell(row, insertPosition);
        CTTcPr tcPr = actualCell.getCTTc().getTcPr();
        CTDecimalNumber gridSpan = tcPr.getGridSpan();
        gridSpan.setVal(BigInteger.valueOf(gridSpan.getVal().longValue() + 1));
    }

    private int processLoopColWidth(XWPFTable table, int width, int templateColIndex, int count) {
        CTTblGrid tblGrid = TableTools.getTblGrid(table);
        count = count == 0 ? 1 : count;
        int colWidth = width / count;
        for (int j = 0; j < count; j++) {
            CTTblGridCol newGridCol = tblGrid.insertNewGridCol(templateColIndex);
            newGridCol.setW(BigInteger.valueOf(colWidth));
        }
        tblGrid.removeGridCol(templateColIndex + count);
        return colWidth;
    }

    private int getSize(Iterable<?> data) {
        int size = 0;
        Iterator<?> iterator = data.iterator();
        while (iterator.hasNext()) {
            iterator.next();
            size++;
        }
        return size;
    }

    @SuppressWarnings("unchecked")
    private void removeCell(XWPFTableRow row, int actualInsertPosition) {
        List<XWPFTableCell> cells = (List<XWPFTableCell>) ReflectionUtils.getValue("tableCells", row);
        cells.remove(actualInsertPosition);
        row.getCtRow().removeTc(actualInsertPosition);

    }

    @SuppressWarnings("unchecked")
    private XWPFTableCell insertCell(XWPFTableRow tableRow, int actualInsertPosition) {
        CTRow row = tableRow.getCtRow();
        CTTc newTc = row.insertNewTc(actualInsertPosition);
        XWPFTableCell cell = new XWPFTableCell(newTc, tableRow, tableRow.getTable().getBody());

        List<XWPFTableCell> cells = (List<XWPFTableCell>) ReflectionUtils.getValue("tableCells", tableRow);
        cells.add(actualInsertPosition, cell);
        return cell;
    }

    @SuppressWarnings("unchecked")
    private void setTableCell(XWPFTableRow row, XWPFTableCell templateCell, int pos) {
        List<XWPFTableCell> rows = (List<XWPFTableCell>) ReflectionUtils.getValue("tableCells", row);
        rows.set(pos, templateCell);
        row.getCtRow().setTcArray(pos, templateCell.getCTTc());
    }

    private int getColIndex(XWPFTableCell cell) {
        XWPFTableRow tableRow = cell.getTableRow();
        int orginalCol = 0;
        for (int i = 0; i < tableRow.getTableCells().size(); i++) {
            XWPFTableCell current = tableRow.getCell(i);
            int intValue = 1;
            CTTcPr tcPr = current.getCTTc().getTcPr();
            if (null != tcPr) {
                CTDecimalNumber gridSpan = tcPr.getGridSpan();
                if (null != gridSpan) {
                    intValue = gridSpan.getVal().intValue();
                }
            }
            orginalCol += intValue;
            if (current == cell) {
                return orginalCol - intValue;
            }
        }
        return -1;
    }

    private int getActualInsertPosition(XWPFTableRow tableRow, int insertPosition) {
        int orginalCol = 0;
        for (int i = 0; i < tableRow.getTableCells().size(); i++) {
            XWPFTableCell current = tableRow.getCell(i);
            int intValue = 1;
            CTTcPr tcPr = current.getCTTc().getTcPr();
            if (null != tcPr) {
                CTDecimalNumber gridSpan = tcPr.getGridSpan();
                if (null != gridSpan) {
                    intValue = gridSpan.getVal().intValue();
                }
            }
            orginalCol += intValue;
            if (orginalCol - intValue == insertPosition && intValue == 1) {
                return i;
            }
        }
        return -1;
    }

    private XWPFTableCell getActualCell(XWPFTableRow tableRow, int insertPosition) {
        int orginalCol = 0;
        for (int i = 0; i < tableRow.getTableCells().size(); i++) {
            XWPFTableCell current = tableRow.getCell(i);
            int intValue = 1;
            CTTcPr tcPr = current.getCTTc().getTcPr();
            if (null != tcPr) {
                CTDecimalNumber gridSpan = tcPr.getGridSpan();
                if (null != gridSpan) {
                    intValue = gridSpan.getVal().intValue();
                }
            }
            orginalCol += intValue;
            if (orginalCol - 1 >= insertPosition) {
                return current;
            }
        }
        return null;
    }

}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,230评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,261评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,089评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,542评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,542评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,544评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,922评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,578评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,816评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,576评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,658评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,359评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,920评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,859评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,381评论 2 342

推荐阅读更多精彩内容