java导出word使用模版与自定义联合出击解决复杂表格!
1. 看一下需要导出什么样子的表格
如图所示,这里的所有数据行都是动态的,需要根据查询出来的数据循环展示。
如果只是这样的话,使用freemarker应该都可以搞定,但是他一列中内容相同的单元格,需要合并。
这对于表格样式固定的freemarker就搞不定了。
经过一通百度,发现了一个导出文档很好用的框架 poi-tl(实际用的时候也并不怎么好用,学习成本高,功能全)
下面上实战
2. 引入poi-tl 的相关依赖
<dependency><groupId>com.deepoove</groupId><artifactId>poi-tl</artifactId><version>1.12.1</version></dependency>
关于版本问题,老版比新版好用,新版太过规范
官方文档地址
3.先放一个docx的模版,模版样子如下
生产装置下面哪一行小字是:{{templateRowRenderData}},用来填充数据的,使用双花括号标记。
放在根目录下面,不然找不到哦
3.开始建立实体类,查询数据,填充数据,渲染模版
实体类,如果类中有什么字典,数字标识字段需要转成所表示的字符串
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,这里和模版的参数名要保持一致
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;}
定义哪一列需要填充哪一个字段的值
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();}}
}
渲染数据并合并列,这里我的数据是摊平的,用了两个指针,首指针和尾指针找同一列上数据相同挨着的单元格然后把它们合并
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;@Overridepublic 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的行开始合并(去除表头)必须一个一个catchcatchMergeRow(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),没有需求的老铁可以不用要
/*** 导出安全风险清单*/@Overridepublic 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就不写了,结果如下
学习成本一天半,刚入门的新手建议不要看了
相关文章:

java导出word使用模版与自定义联合出击解决复杂表格!
1. 看一下需要导出什么样子的表格 如图所示,这里的所有数据行都是动态的,需要根据查询出来的数据循环展示。 如果只是这样的话,使用freemarker应该都可以搞定,但是他一列中内容相同的单元格,需要合并。 这对于表格样式…...
GO设计模式——9、过滤器模式(结构型)
目录 过滤器模式(Filter/Criteria Pattern) 代码实现 过滤器模式(Filter/Criteria Pattern) 过滤器模式(Filter Pattern)或标准模式(Criteria Pattern)是一种设计模式,…...
fastadmin 导出
php 接收数据 set_time_limit(0);ini_set(memory_limit, -1);$ids $this->request->post(ids);$filter $this->request->post(filter);$op $this->request->post(op);$search $this->request->post(search);$whereIds $ids all ? 11 : [id >…...

六、CM4树莓派USBRS转485串口通讯
一、串行通讯接口 串行通讯接口简称串口(UART) 采用串行通信方式的扩展接口,数据位一位一位的按照顺序传送 优点:通信线路简单,只要一对传输线就可以实现双向通信能够大大降低成本,适合远距离通信。 缺点…...

c++知识总结
一 细碎知识 1.27 # 1.27.1 # pragma once 参考 C++学习笔记之pragma once的理解_pragma once什么意思-CSDN博客https://blog.csdn.net/lynnlee_36/article/details/105322937作用 保证只被编译一次,和#ifndef,#define,#endif功能相同 1.27.2 #if defined(__cplusplus…...

python-爬取壁纸
代理池的,防止IP 被封 找到图片真实地址 现在看到的只是图片的预览地址 (previews) 1.检查: 2.鼠标变为箭头时查看网页源代码 关于怎样在源代码中找到图片的真实地址 ??? 为什么在源代码界面 ctrl f 时候搜索的是 .png ??? 首先图片地址是以 .j…...

第31期 | GPTSecurity周报
GPTSecurity是一个涵盖了前沿学术研究和实践经验分享的社区,集成了生成预训练Transformer(GPT)、人工智能生成内容(AIGC)以及大型语言模型(LLM)等安全领域应用的知识。在这里,您可以…...

湖仓一体架构理论与实践汇总
湖仓一体架构理论与实践汇总 软件研发本质上属于“手工业”。软件研发在很大程度上还是依赖于个人的能力。当软件规模较小时,依赖“手工业”可以解决问题,但是当软件规模大了之后再依赖“手工业”就不行了。 软件的复杂度包含两个层面:软件…...
Redission从入门到入门
1. Redisson简介 Redisson 是一个在 Java 环境中使用的 Redis 客户端库。它提供了丰富的功能,使得在 Java 应用中与 Redis 交互变得更加简单和高效。Redisson 不仅提供了基本的 Redis 操作,还提供了许多高级功能,使其成为在 Java 项目中实现…...

PHP对接企业微信
前言 最近在做项目中,要求在后台管理中有企业微信管理的相关功能。相关准备工作,需要准备好企业微信账号,添加自建应用,获得相应功能的权限,以及agentid、secre等。 参考文档: 企业微信开发文档 功能实现 因…...
【原创】录剪视频的折腾之路
制作视频的起因 本人为IT男,IT发展快,需要学习的东西又多。往往为了一个技术小问题,花好几天时间学习,接下来十来分钟把事情做完。下次遇到这个同样的问题的时候,可能是几个月后,甚至是几年以后了。这些技…...

【BI】FineBI功能学习路径-20231211
FineBI功能学习路径 https://help.fanruan.com/finebi/doc-view-1757.html 编辑数据概述 1.1 调整数据结构 1.2 简化数据 2.1上下合并 2.2其他表添加列 2.3左右合并 新增分析指标 函数参考 https://help.fanruan.com/finereport/doc-view-1897.html 数值函数 日期函数 文…...
pytorch之torch.utils.data学习
1、概述 PyTorch 数据加载利用的核心是torch.utils.data.DataLoader类 。它表示在数据集上 Python 可迭代,支持 map-style and iterable-style datasets(地图样式和可迭代样式数据集), customizing data loading orderÿ…...
Spring Boot 3中一套可以直接用于生产环境的Log4J2日志配置
文章目录 一 Log4J2 相关概念及基本特点二 Spring Boot3 中启用Log4J2的pom.xml配置三 application.properties 的配置四 完整配置 一 Log4J2 相关概念及基本特点 Log4J2是Apache Log4j的升级版,参考了logback的一些优秀的设计,并且修复了一些问题&…...

iOS按钮控件UIButton使用
1.在故事板中添加按钮控件,步聚如下: 同时按钮Shift+Commad+L在出现在控件库中选择Button并拖入View Controller Scene中 将控件与变量btnSelect关联 关联后空心变实心 如何关联?直接到属性窗口拖按钮变量到控件上,出现一条线,然后松开,这样就关联成功了 关联成功后属性窗口…...

小程序开发实战案例之三 | 小程序底部导航栏如何设置
小程序中最常见的功能就是底部导航栏了,今天就来看一下怎么设置一个好看的导航栏~这里我们使用的是支付宝官方小程序 IDE 做示范。 官方提供的底部导航栏 第一步:页面创建 一般的小程序会有四个 tab,我们这次也是配置四个 tab 的…...
Android : 序列化 JSON简单应用
1. JSON介绍 JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于阅读和写入,同时也易于机器解析和生成。它基于JavaScript的子集,采用完全独立于语言的文本格式来存储和表示数据。JSON是纯文本&#x…...
Java小案例-RocketMQ的11种消息类型,你知道几种?(普通消息和批量消息)
前言 这篇给大家讲普通消息和批量消息,主要配合代码进行讲解,关于RocketMQ的基础知识已经在上篇给大家讲过需要回顾的点击下面这个链接去看 RocketMQ基础知识 普通消息 普通消息其实就很简单,是Apache RocketMQ中最基础的消息形式&#x…...
前端小技巧: 设计一个简版前端统计 SDK
统计 sdk 如何设计 1 ) 概述 客户端一个sdk ,把数据发送给服务端(第三方统计平台)服务端产生一个统计的报表 2 )需求点 访问量:pv自定义事件:用户的一切行为我们都可以自定义采集性能,错误 3 ) 代码实现 const P…...
DevOps搭建(十一)-Jenkins容器内部使用Docker详解
1、目的 配置的目的是使得Jenkins容器可以直接使用宿主机的Docker,从而可以直接使用Docker命令进行本地打包操作,然后推送到Harbor镜像仓库。 2、修改数据卷 如何在docker中执行宿主机的docker操作,我们管它叫docker in docker。 至于为什么要在docker中操作宿主机的doc…...

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型
摘要 拍照搜题系统采用“三层管道(多模态 OCR → 语义检索 → 答案渲染)、两级检索(倒排 BM25 向量 HNSW)并以大语言模型兜底”的整体框架: 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后,分别用…...

【JavaEE】-- HTTP
1. HTTP是什么? HTTP(全称为"超文本传输协议")是一种应用非常广泛的应用层协议,HTTP是基于TCP协议的一种应用层协议。 应用层协议:是计算机网络协议栈中最高层的协议,它定义了运行在不同主机上…...

CentOS下的分布式内存计算Spark环境部署
一、Spark 核心架构与应用场景 1.1 分布式计算引擎的核心优势 Spark 是基于内存的分布式计算框架,相比 MapReduce 具有以下核心优势: 内存计算:数据可常驻内存,迭代计算性能提升 10-100 倍(文档段落:3-79…...
css的定位(position)详解:相对定位 绝对定位 固定定位
在 CSS 中,元素的定位通过 position 属性控制,共有 5 种定位模式:static(静态定位)、relative(相对定位)、absolute(绝对定位)、fixed(固定定位)和…...

如何在最短时间内提升打ctf(web)的水平?
刚刚刷完2遍 bugku 的 web 题,前来答题。 每个人对刷题理解是不同,有的人是看了writeup就等于刷了,有的人是收藏了writeup就等于刷了,有的人是跟着writeup做了一遍就等于刷了,还有的人是独立思考做了一遍就等于刷了。…...

Python Ovito统计金刚石结构数量
大家好,我是小马老师。 本文介绍python ovito方法统计金刚石结构的方法。 Ovito Identify diamond structure命令可以识别和统计金刚石结构,但是无法直接输出结构的变化情况。 本文使用python调用ovito包的方法,可以持续统计各步的金刚石结构,具体代码如下: from ovito…...
JavaScript基础-API 和 Web API
在学习JavaScript的过程中,理解API(应用程序接口)和Web API的概念及其应用是非常重要的。这些工具极大地扩展了JavaScript的功能,使得开发者能够创建出功能丰富、交互性强的Web应用程序。本文将深入探讨JavaScript中的API与Web AP…...

MFC 抛体运动模拟:常见问题解决与界面美化
在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...

PHP 8.5 即将发布:管道操作符、强力调试
前不久,PHP宣布了即将在 2025 年 11 月 20 日 正式发布的 PHP 8.5!作为 PHP 语言的又一次重要迭代,PHP 8.5 承诺带来一系列旨在提升代码可读性、健壮性以及开发者效率的改进。而更令人兴奋的是,借助强大的本地开发环境 ServBay&am…...
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的----NTFS源代码分析--重要
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的 第一部分: 0: kd> g Breakpoint 9 hit Ntfs!ReadIndexBuffer: f7173886 55 push ebp 0: kd> kc # 00 Ntfs!ReadIndexBuffer 01 Ntfs!FindFirstIndexEntry 02 Ntfs!NtfsUpda…...