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就不写了,结果如下

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

相关推荐
挺菜的5 分钟前
【算法刷题记录(简单题)003】统计大写字母个数(java代码实现)
java·数据结构·算法
掘金-我是哪吒1 小时前
分布式微服务系统架构第156集:JavaPlus技术文档平台日更-Java线程池使用指南
java·分布式·微服务·云原生·架构
亲爱的非洲野猪1 小时前
Kafka消息积压的多维度解决方案:超越简单扩容的完整策略
java·分布式·中间件·kafka
wfsm1 小时前
spring事件使用
java·后端·spring
微风粼粼2 小时前
程序员在线接单
java·jvm·后端·python·eclipse·tomcat·dubbo
缘来是庄2 小时前
设计模式之中介者模式
java·设计模式·中介者模式
rebel2 小时前
若依框架整合 CXF 实现 WebService 改造流程(后端)
java·后端
代码的余温3 小时前
5种高效解决Maven依赖冲突的方法
java·maven
慕y2743 小时前
Java学习第十六部分——JUnit框架
java·开发语言·学习
paishishaba3 小时前
Maven
java·maven