java导出word使用模版与自定义联合出击解决复杂表格!

1. 看一下需要导出什么样子的表格
如图所示,这里的所有数据行都是动态的,需要根据查询出来的数据循环展示。

如果只是这样的话,使用freemarker应该都可以搞定,但是他一列中内容相同的单元格,需要合并。

这对于表格样式固定的freemarker就搞不定了。
经过一通百度,发现了一个导出文档很好用的框架 poi-tl(实际用的时候也并不怎么好用,学习成本高,功能全)
下面上实战
2. 引入poi-tl 的相关依赖

java 复制代码
    <dependency>
            <groupId>com.deepoove</groupId>
            <artifactId>poi-tl</artifactId>
            <version>1.12.1</version>
        </dependency>

关于版本问题,老版比新版好用,新版太过规范
官方文档地址

3.先放一个docx的模版,模版样子如下
生产装置下面哪一行小字是:{{templateRowRenderData}} ,用来填充数据的,使用双花括号标记。

放在根目录下面,不然找不到哦
3.开始建立实体类,查询数据,填充数据,渲染模版
实体类,如果类中有什么字典,数字标识字段需要转成所表示的字符串

java 复制代码
package com.ruoyi.prevention.inventory.domain.vo;

import lombok.Data;

/**
 * 安全风险管控措施对象 prevention_risk_measures
 *
 * @author ruoyi
 * @date 2023-06-27
 */
@Data
public class AllInventoryVo {
    private String id;
    /**
     * 管控对象(分析对象名称)
     */
    private Integer zoneType;
    private String zoneTypeStr;

    /**
     * 责任部门名称
     */
    private String responsibleDepartmentName;

    /**
     * 责任人名称
     */
    private String responsibleStaffName;

    /**
     * 风险分析单元名称
     */
    private String riskUnitName;

    /**
     * 风险单元id
     */
    private String riskUnitId;

    /**
     * 安全风险事件
     */
    private String riskEventName;

    /** 风险事件id */
    private String riskEventId;

    /** 管控措施分类 1 */
    private String controlMeasuresClassify1;

    /** 管控措施分类 2 */
    private String controlMeasuresClassify2;

    /** 管控措施分类 3 */
    private String controlMeasuresClassify3;

    /** 管控措施描述 */
    private String controlMeasuresDescription;

    /** 隐患排查内容 */
    private String treacherousContent;

    /** 管控措施id */
    private String riskMeasuresId;

    /** 岗位负责人 */
    private String postResponsible;

    /** 巡检周期 */
    private Integer checkCycle;

    /** 巡检周期单位 */
    private Integer checkCycleUnit;
    private String checkCycleUnitStr;

}

渲染数据到templateRowRenderData,这里和模版的参数名要保持一致

java 复制代码
package com.ruoyi.prevention.inventory.word;

import com.deepoove.poi.expression.Name;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;


/**
 * @author: fanbaolin
 * @Date: 2023/12/12
 * @Description: 基础数据+动态数据即
 * @Version: 1.0
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TemplateData {
    @Name("templateRowRenderData")
    private TemplateRowRenderData templateRowRenderData;

}

定义哪一列需要填充哪一个字段的值

java 复制代码
package com.ruoyi.prevention.inventory.word;

import com.deepoove.poi.data.CellRenderData;
import com.deepoove.poi.data.ParagraphRenderData;
import com.deepoove.poi.data.RowRenderData;
import com.deepoove.poi.data.style.CellStyle;
import com.deepoove.poi.data.style.ParagraphStyle;
import com.deepoove.poi.data.style.RowStyle;
import com.deepoove.poi.data.style.Style;
import com.ruoyi.prevention.inventory.domain.vo.AllInventoryVo;
import lombok.Data;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * @author: fanbaolin
 * @Date: 2023/12/12
 * @Description: 将实体类转化为一个表格 因为渲染只接受RowRenderData类型
 * ps:新版真的难用
 * @Version: 1.0
 */
@Data
public class TemplateRowRenderData {
    /**
     * 管控分类措施
     */
    private List<RowRenderData> typeRowRenderDataList;

    private RowStyle rowStyle;

    public TemplateRowRenderData(List<AllInventoryVo> inventoryVos) {
        // 初始化样式
        initStyle();
        // 初始化动态数据
        initData(inventoryVos);
    }

    private void initStyle() {
        // 字体样式
        Style style = new Style("宋体", 10);

        // 段落样式
        ParagraphStyle paragraphStyle = new ParagraphStyle();
        paragraphStyle.setDefaultTextStyle(style);
        // ps:这里才是字体居中对齐
        paragraphStyle.setAlign(ParagraphAlignment.CENTER);

        // 表格样式
        CellStyle cellStyle = new CellStyle();
        // ps:表格也需要居中,否则字体不在正中间,会偏上
        cellStyle.setVertAlign(XWPFTableCell.XWPFVertAlign.CENTER);
        cellStyle.setDefaultParagraphStyle(paragraphStyle);

        // 行样式
        this.rowStyle = new RowStyle();
        rowStyle.setDefaultCellStyle(cellStyle);
    }

    private void initData(List<AllInventoryVo> inventoryVos) {
        // 管控分类
        List<RowRenderData> newTypeRowRenderDataList = new ArrayList<>();
        if (CollectionUtils.isNotEmpty(inventoryVos)) {
            for (AllInventoryVo inventoryVo : inventoryVos) {
                // 共有14列
                List<CellRenderData> cellDataList = new ArrayList<>();
                cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getZoneTypeStr())));
                cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getResponsibleDepartmentName())));
                cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getResponsibleStaffName())));
                cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getRiskUnitName())));
                cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getRiskEventName())));
                cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getControlMeasuresClassify1())));
                cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getControlMeasuresClassify2())));
                cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getControlMeasuresClassify3())));
                cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getControlMeasuresDescription())));
                cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getTreacherousContent())));
                cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getPostResponsible())));
                cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getCheckCycle() == null ? "":inventoryVo.getCheckCycle() + "")));
                cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getCheckCycleUnitStr())));
                // 备注先空着
                cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText("")));
                RowRenderData rowRenderData = new RowRenderData();
                // 样式
                rowRenderData.setRowStyle(rowStyle);
                rowRenderData.setCells(cellDataList);
                newTypeRowRenderDataList.add(rowRenderData);
            }
            this.typeRowRenderDataList = newTypeRowRenderDataList;
        }else{
            this.typeRowRenderDataList = Collections.emptyList();
        }
    }
}

渲染数据并合并列,这里我的数据是摊平的,用了两个指针,首指针和尾指针找同一列上数据相同挨着的单元格然后把它们合并

java 复制代码
package com.ruoyi.prevention.inventory.word;

import com.deepoove.poi.data.RowRenderData;
import com.deepoove.poi.policy.DynamicTableRenderPolicy;
import com.deepoove.poi.policy.TableRenderPolicy;
import com.deepoove.poi.util.TableTools;
import lombok.NoArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;

import java.util.List;


/**
 * @author: fanbaolin
 * @Date: 2023/12/12
 * @Description: 自定义渲染插件-for循环
 * 这里因为需要操作表格-所以集成DynamicTableRenderPolicy:动态表格插件,允许直接操作表格对象
 * 详情请看:http://deepoove.com/poi-tl/#_%E9%BB%98%E8%AE%A4%E6%8F%92%E4%BB%B6
 * @Version: 1.0
 */
@NoArgsConstructor
public class TemplateTableRenderPolicy extends DynamicTableRenderPolicy {

    private XWPFTable xwpfTable;

    @Override
    public void render(XWPFTable xwpfTable, Object data) throws Exception {
        if (null == data) {
            return;
        }
        this.xwpfTable = xwpfTable;

        TemplateRowRenderData templateRowRenderData = (TemplateRowRenderData) data;

        // 分类和管控措施的数据
        List<RowRenderData> typeRowRenderDataList = templateRowRenderData.getTypeRowRenderDataList();

        if (CollectionUtils.isNotEmpty(typeRowRenderDataList)) {
            // 表头下面那一行
            int typeMemberRow = 1;
            // 移除空白的表头下面那一行
            xwpfTable.removeRow(typeMemberRow);
            // 得到表头那一行的数据
            XWPFTableRow xwpfTableRow = xwpfTable.getRow(0);
            for (int i = typeRowRenderDataList.size() - 1; i > -1; i--) {
                // 重新插入表格
                XWPFTableRow insertNewTableRow = xwpfTable.insertNewTableRow(typeMemberRow);
                // 统一高度
                insertNewTableRow.setHeight(xwpfTableRow.getHeight());

                for (int j = 0; j < 14; j++) {
                    insertNewTableRow.createCell();
                }
                // 渲染数据
                TableRenderPolicy.Helper.renderRow(xwpfTable.getRow(typeMemberRow), typeRowRenderDataList.get(i));
            }
            // 合并行 下标为1的行开始合并(去除表头)必须一个一个catch
            catchMergeRow(typeRowRenderDataList,0);
            catchMergeRow(typeRowRenderDataList,1);
            catchMergeRow(typeRowRenderDataList,2);
            catchMergeRow(typeRowRenderDataList,3);
            catchMergeRow(typeRowRenderDataList,4);
            catchMergeRow(typeRowRenderDataList,5);
            catchMergeRow(typeRowRenderDataList,6);
            catchMergeRow(typeRowRenderDataList,7);
        }
    }

    private void catchMergeRow(List<RowRenderData> typeRowRenderDataList, int cell){
        try {
            mergeRow(typeRowRenderDataList,cell,1,1,false);
        }catch (RuntimeException ignore){

        }
    }

    /**
     * 首尾指针递归判断列表下一个值是否和自己相同
     *
     * @param typeRowRenderDataList
     * @param cell
     * @param from
     * @param to
     * @param hasDef
     */
    private void mergeRow(List<RowRenderData> typeRowRenderDataList, int cell, int from, int to, boolean hasDef) {
        if(from == typeRowRenderDataList.size()){
            throw new RuntimeException("跳出递归");
        }else{
            for (int i = from - 1 ; i < typeRowRenderDataList.size() - 1; i++) {
                String content = typeRowRenderDataList.get(i).getCells().get(cell).getParagraphs().get(0).getContents().toString();
                String nextContent = typeRowRenderDataList.get(i + 1).getCells().get(cell).getParagraphs().get(0).getContents().toString();
                if(nextContent.equals(content)){
                    to = to + 1;
                }else{
                    if(from > to){
                        return;
                    }
                    if(from == to){
                        // 整体下移一个单位
                        from += 1;
                        to += 1;
                    }else{
                        // 合并行
                        TableTools.mergeCellsVertically(xwpfTable, cell, from, to);
                        // 合并完成 首指针指向尾端
                        from = to;
                    }
                    // 递归调用
                    mergeRow(typeRowRenderDataList,cell,from,to,true);
                }
            }
        }
        // 如果这一列没有不同的值 全给他合并了
        if(!hasDef){
            if(from >= to){
                return;
            }
            TableTools.mergeCellsVertically(xwpfTable, cell, from, to);
        }
    }
}

4.service层掉用
这里我还做了一个word转pdf的操作(用的aspose),没有需求的老铁可以不用要

java 复制代码
  /**
     * 导出安全风险清单
     */
    @Override
    public void report() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletResponse response = requestAttributes.getResponse();
        response.setContentType("application/pdf");
        response.setHeader("Content-Disposition", "attachment; filename=" + "report.pdf");

        List<AllInventoryVo> allInventoryVos = flatList();
        clearTreacherousContentLabel(allInventoryVos);
        TemplateRowRenderData templateRowRenderData3 = new TemplateRowRenderData(allInventoryVos);
        // 2)完整数据
        TemplateData templateData = new TemplateData(templateRowRenderData3);
        try {
            Configure config = Configure.builder().bind("templateRowRenderData", new TemplateTableRenderPolicy()).build();

            //  四、导出
            ClassPathResource classPathResource = new ClassPathResource("templates" + File.separator + "prevention.docx");
            XWPFTemplate template = XWPFTemplate.compile(classPathResource.getInputStream(), config).render(
                    templateData);

            String filePath = "";
            if (SystemUtil.getOsInfo().isWindows()) {
                filePath = "d:/tmp\\work\\report.doc";
            }else{
                filePath = "/tmp/work/report.doc";
            }
            template.writeAndClose(Files.newOutputStream(Paths.get(filePath)));
            File file = new File(filePath);
            // 生成word filePath是将要被转化的word文档
            Document doc = new Document(filePath);
            // 转换 字体不一样
            doc.save(response.getOutputStream(), SaveFormat.PDF);
            // 删除临时文件
            file.delete();

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

5.controller就不写了,结果如下

学习成本一天半,刚入门的新手建议不要看了

相关推荐
哎呦没几秒前
SpringBoot框架下的资产管理自动化
java·spring boot·后端
m0_571957582 小时前
Java | Leetcode Java题解之第543题二叉树的直径
java·leetcode·题解
魔道不误砍柴功4 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2344 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨4 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
测开小菜鸟6 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity7 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天7 小时前
java的threadlocal为何内存泄漏
java
caridle7 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^7 小时前
数据库连接池的创建
java·开发语言·数据库