springboot使用xdoc-report包导出word

news2025/5/22 6:29:19

背景:项目需要使用xdoc-report.jar根据设置好的word模版,自动填入数据 导出word

框架使用

我的需求是我做一个模板然后往里面填充内容就导出我想要的word文件,问了下chatgpt还有百度,最后选用了xdocreport这个框架,主要它使用docx模板以及freemarker模板引擎就可以做导出,不用各种转换,而且文档demo也很齐全,虽然最新的版本已经两年没更新了!!参考文档

制作模板

模板效果图如下

文字显示

比如说,我们需要显示一些文档的元数据,例如 文档的标题,文档的时间等等可以单独显示的属性。我们可以使用一个对象来封装这些属性,现在我封装了一个Project对象

class ExportProject() {  
    var title: String? = null  
    var type: String? = null  
    var car: String? = null  
    var className: String? = null  
    var date: String? = null  
    var problem: String? = null  
    var range: String? = null  
    var signature: IImageProvider? = null  
  
    constructor(vo: DriverCheckVo) : this() {  
        type = vo.spec  
        car = vo.usageCode  
        className = vo.shift  
        date = "${vo.date?.year} 年 ${vo.date?.monthValue} 月 ${vo.date?.dayOfMonth} 日"  x
        problem = vo.problemSummary  
    }  
}

生成word文档时,只要new一个对象实例,扔到xdocreport的context中,就可以使用啦。当然具体展示还是需要在docx模板上做特殊处理的。比如我想控制我生成的docx文件中的标题,我直接在标题处,生成一个域即可,具体操作步骤如下,(下列例子中使用了wps),另外模版的文件格式名一定是docx

step-》 将光标放在标题处-》点击插入 -》选择文档部件-》点击域-》选择邮件合并-》输入变量 ${project.title} -》 点击确定 

效果如下!

其他的地方,例如年份,也是一样 输入 变量 ${project.range}即可。这样我们就可以将需要单独显示的文字,控制在xdocreport context里面 project变量里面了,当然具体怎么取名,放在哪,都随便。

列表嵌套

上面说了一些单独显示的文字显示,那列表控制如何显示呢,比如下图

首先还是定义控制对象 

class DriverCheckData {  
/**  
* 大检查项  
*/  
var name: String? = null  
  
/**  
* 是否有小检查项  
*/  
var hasLittleCheck: Boolean? = false  
  
/**  
* 小检查项列表  
*/  
var items: List<DriverCheckItem>? = null  
}  
  
class DriverCheckItem {  
/**  
* 小检查项列表  
*/  
var name: String? = null  
  
/**  
* 序号  
*/  
var sequence: Int? = null  
  
/**  
* 检查内容  
*/  
var content: String? = null  
  
/**  
* 检查结果  
*/  
var result: String? = null  
}

定义了两个类,大检查项类以及小检查项类,大检查项内嵌小检查项。这样我们在输出列表数据时,只需要定义一个大检查项列表就可以展示了,因为是展示的还是文本,所以定义还是跟上面一样,使用域来关联变量,写法么,就跟mybatis,el表达式差不多。

即使用 [#list noTag as data] 这个将 notag 这个list 里面的子元素 定义变量名为 data,[/#list] 代表列表结束,就跟html的标签对一样,然后是,data的数据展示了,直接使用 ${data.name} 这样的域就可以展示data里面的属性,可以看到,我这个图里面 用的不是[#list noTag as data] ,而是加了前缀,list的结尾也加了后缀,这是为了处理docx里面的表格而做的处理。照猫画虎即可,如果不是在表格里展示,直接使用

以下案例即可

«[#list developers as developer]»

Name: «${developer.name}»

Mail : [${developer.mail}]

Mail2 : [${developer.mail}]

«[/#list]»

图片展示

例如我们展示文本,使用了域,对于图片呢,需要使用书签,如果我说的不清楚,直接看官方demo

流程如下:

step-》添加一个图片(啥图片都行)当做模板,选中图片-》点击插入-》书签-》添加书签名-》添加

这个书签名非常重要,他的名字对应了我们在context里面设置的变量。如果是单独展示,直接设置一级变量名,与书签名对应即可,但是如果是图片列表,而且我们放在对象里面,怎么办呢?使用官方示例,不用[#list noTag as data],而是在java/kotlin程序里面对该列表进行处理,使用列表名中对应的图片属性即可。详见 官方示例

代码展示

springboot加载模板实例

@Component  
@Data  
class ExportInstanceConfig(  
private val resourceLoader: ResourceLoader  
) {  
private var driverExport: IXDocReport? = null  
private var checkExport: IXDocReport? = null  
@PostConstruct  
fun init(){  
driverExport = getInstance("classpath:check.docx")  
checkExport = getInstance("classpath:driver.docx")  
}  
  
private fun getInstance(path:String):IXDocReport{  
var inputStream :InputStream? = null  
try{  
val res = resourceLoader.getResource(path)  
inputStream = res.inputStream  
return XDocReportRegistry  
.getRegistry()  
.loadReport(  
inputStream,  
TemplateEngineKind.Freemarker  
)  
}catch (e:IOException){  
throw e  
}finally {  
inputStream?.close()  
}  
}

将导出的数据转成二进制数组

private fun exportProcess(report: IXDocReport, context: IContext): ByteArray {  
    val bos = ByteArrayOutputStream()  
    val res: ByteArray  
    try {  
        // 导入模板  
        report.process(context, bos)  
        res = bos.toByteArray()  
    } catch (e: IOException) {  
        throw e  
    } finally {  
        bos.close()  
    }  
    return res  
}

进行单独图片导出

private fun exportDriverCheckDocx(param: DriverCheckVo, title: String): ByteArray {  
    val report = exportInstanceConfig.getDriverExport()  
    val metadata = report.createFieldsMetadata()  
    val context = report.createContext()  
    val exportProject = ExportProject(param).apply { this.title = title }  
    // 这里是对上传的图片的base64编码 进行解码
    val image = param.checkPeopleDocumentary?.split(",")?.let { decoder.decode(it[1]) }  
    if (image != null) {  
        // 对应模板中,单独显示的图片
        metadata.addFieldAsImage("signature")  
        // 导出图片时,图片对应的类 的格式
        context.put("signature",ByteArrayImageProvider(ByteArrayInputStream(image)))  
    }  
    val noTags = param.dataList?.filter { it.hasLittleCheck == false }?.toList()  
    val hasTags = param.dataList?.filter { it.hasLittleCheck == true }?.toList()  
    context.put("project", exportProject)  
    context.put("noTag", noTags)  
    context.put("hasTag", hasTags)  
    return exportProcess(report, context)  
}  

图片迭代导出

private fun exportCheckDocx(param: CheckVo, title: String, range: String?): ByteArray {  
    val report = exportInstanceConfig.getCheckExport()  
    val context = report.createContext()  
    val metadata = report.createFieldsMetadata()  
    // 对带图片列表的对象进行load处理,方便模板识别
    metadata.load("re",CheckRecord::class.java,true)  
    val exportProject = ExportProject().apply {  
        this.title = title  
        this.range = range  
    }  
    val noTags = param.dataList?.filter { it.hasLittleCheck == false }?.toList()  
    param.recordList?.forEach { it ->  
        if(it.documentary!=null){  
            val image = it.documentary?.split(",")?.let { decoder.decode(it[1]) }  
            if (image != null) {  
                it.signature = ByteArrayImageProvider(image).apply {  
                    this.setSize(100f,100f)  
                }  
            }  
        }  
    }  
    val hasTags = param.dataList?.filter { it.hasLittleCheck == true }?.toList()  
    context.put("project", exportProject)  
    context.put("noTag", noTags)  
    context.put("hasTag", hasTags)  
    // 将带图片的列表加载进上下文
    context.put("re", param.recordList)  
    return exportProcess(report, context)  
}

带图片的迭代对象定义

class CheckRecord {  
/**  
* 设备型号  
*/  
var spec: String? = null  
  
/**  
* 车号  
*/  
var usageCode: String? = null  
  
/**  
* 检查日期  
*/  
var checkDate: String? = null  
  
/**  
* 检查情况  
*/  
var checkContent: String? = null  
  
/**  
* 整改要求及完成日期  
*/  
var require: String? = null  
  
/**  
* 检查人签名  
*/  
var documentary: String? = null  
/**  
* 图片实体  
*/  
@get:FieldMetadata(images = [ ImageMetadata(name = "signature", behaviour = NullImageBehaviour.RemoveImageTemplate) ])  
var signature: IImageProvider? = null  
}

文件压缩

因为导出docx文件有多个,要求压缩成一个压缩包,这边使用的是

<dependency>  
<groupId>org.apache.commons</groupId>  
<artifactId>commons-compress</artifactId>  
<version>1.23.0</version>  
</dependency>

代码如下,将得到的docx二进制数组转成zip

// 不同类型的文件对应不同的MIME类型  
response.apply {  
    characterEncoding = "UTF-8" // 设置编码字符  
    setHeader("Content-disposition", "attachment;filename=${URLEncoder.encode("下载文件" + ".zip", "utf-8")}")  
    contentType = "application/zip"  
}
val zipOutputStream = ZipArchiveOutputStream(response.outputStream) 
try {   
    var sequence = 0  
    taskExportVo.forEach {  
        // 实例化 ZipEntry 对象,源文件数组中的当前文件  
        sequence++  
        val date = it.date  
        val fileName = when (query.inspType) {  
            EqpInspPlanType.日常检查 ->  "${it.eqpCategoryName}${it.exportType}${date?.year}${date?.monthValue}${date?.dayOfMonth}-$sequence.docx"  
            EqpInspPlanType.点检员点检 -> "${it.eqpCategoryName}${it.exportType}.docx"  
            EqpInspPlanType.专检组专检 -> "${it.eqpCategoryName}${it.exportType}.docx"  
        }  
        zipOutputStream.putArchiveEntry(ZipArchiveEntry(fileName)) 
        // 导出单个docx文件
        val data = memEqpInspCheckRecordService.exportDocx(it)  
        // 写入zip流
        data?.let { it1 -> zipOutputStream.write(it1, 0, it1.size) }  
        zipOutputStream.closeArchiveEntry()  
    }  
} catch (i: IOException) {  
    i.printStackTrace()  
} finally{
   zipOutputStream.close()
}

其他:可视化Word模板设计和导出可以使用NopReport引擎,它不依赖于poi库,直接使用Word进行模板设计。​编辑juejin.cn

附录:Java版本读取数据库数据生成word模板并压缩成Zip中返回

参考:https://juejin.cn/post/7265673876032766015

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2382143.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

重拾GMP

目录 GMP总结 线程协程三家对比GMP调度模型 mgp过一遍流程 g 一个G的生命周期 mpschedt全局队列g0视角看看G的调度流程 四大调度类型 主动调度被动调度正常调度抢占调度 宏观的调度流程上面流程的具体细节 schedule()findRunnable()execute()gosched_m()park_m()与ready()goe…

实验分享|基于千眼狼sCMOS科学相机的流式细胞仪细胞核成像实验

实验背景 流式细胞仪与微流控技术&#xff0c;为细胞及细胞核成像提供新的路径。传统流式细胞仪在细胞核成像检测方面存在检测通量低&#xff0c;荧光信号微弱等局限&#xff0c;故某光学重点实验室开发一种基于高灵敏度sCMOS科学相机并集成在自组荧光显微镜的微流控细胞核成像…

【Linux笔记】——线程池项目与线程安全单例模式

&#x1f525;个人主页&#x1f525;&#xff1a;孤寂大仙V &#x1f308;收录专栏&#x1f308;&#xff1a;Linux &#x1f339;往期回顾&#x1f339;&#xff1a; 【Linux笔记】——简单实习一个日志项目 &#x1f516;流水不争&#xff0c;争的是滔滔不息 一、线程池设计二…

ZooKeeper 原理解析及优劣比较

大家好&#xff0c;这里是架构资源栈&#xff01;点击上方关注&#xff0c;添加“星标”&#xff0c;一起学习大厂前沿架构&#xff01; 引言 在分布式系统中&#xff0c;服务注册、配置管理、分布式锁、选举等场景都需要一个高可用、一致性强的协调服务。Apache ZooKeeper 凭…

是德科技 | 单通道448G未来之路:PAM4? PAM6? PAM8?

内容来源&#xff1a;是德科技 随着数据中心规模的不断扩大以及AI大模型等技术的兴起&#xff0c;市场对高速、大容量数据传输的需求日益增长。例如&#xff0c;AI训练集群中GPU等设备之间的互联需要更高的传输速率来提升效率。在技术升级方面&#xff0c;SerDes技术的不断进步…

OceanBase 开发者大会,拥抱 Data*AI 战略,构建 AI 数据底座

5 月 17 号以“当 SQL 遇见 AI”为主题的 OceanBase 开发者大会在广州举行&#xff0c;因为行程的原因未能现场参会&#xff0c;仍然通过视频直播观看了全部的演讲。总体来说&#xff0c;这届大会既有对未来数据库演进方向的展望&#xff0c;也有 OceanBase 新产品的发布&#…

STM32IIC协议基础及Cube配置

STM32IIC协议基础及Cube配置 一&#xff0c;IC协议简介1&#xff0c;核心特点2&#xff0c;应用场景 二&#xff0c;IC协议基础概念1&#xff0c;总线结构2&#xff0c;主从架构3&#xff0c;设备寻址4&#xff0c;起始和停止条件5&#xff0c;数据传输6&#xff0c;应答机制 三…

CNN vs ViT:图像世界的范式演进

一、图像建模&#xff0c;是不是也可以“大一统” 在前文中我们提到&#xff0c;多模态大模型打破“只能处理文字”的限制。 在 NLP 世界里&#xff0c;Transformer 已经证明自己是理解语言的王者。那么在图像世界&#xff0c;我们是否也能有一种“通用架构”&#xff0c;让模…

cocos creator使用jenkins打包微信小游戏,自动上传资源到cdn,windows版运行jenkins

cocos 版本2.4.11 在windows上jenkins的具体配置和部署&#xff0c;可参考上一篇文章cocos creator使用jenkins打包流程&#xff0c;打包webmobile_jenkins打包,发布,部署cocoscreator-CSDN博客 特别注意&#xff0c;windows上运行jenkins需要关闭windows自己的jenkins服务&a…

定时器的两种实现方式

1、基于优先级队列/堆 队列是先进先出&#xff0c;优先级队列是优先级越高就存放在队列之前&#xff0c;我们可以将过期时间越早设置为优先级越高&#xff0c;那么临近过期时间的任务就会在队列前面&#xff0c;距离过期时间越晚的任务就在队列后面。 可以分配一个线程&#…

[Java实战]Spring Boot整合MinIO:分布式文件存储与管理实战(三十)

[Java实战]Spring Boot整合MinIO&#xff1a;分布式文件存储与管理实战&#xff08;三十&#xff09; 一、MinIO简介与核心原理 MinIO 是一款高性能、开源的分布式对象存储系统&#xff0c;兼容 Amazon S3 API&#xff0c;适用于存储图片、视频、日志等非结构化数据。其核心特…

AI在人力资源领域的应用:把握时代浪潮

借鉴历史经验&#xff0c;引领技术变革 历史总是呈现出惊人的相似性。十年前&#xff0c;众多企业未能及时洞察移动技术与社交技术的潜在价值&#xff0c;迟迟没有将这些创新引入职场环境。随着时间推移&#xff0c;这些组织才意识到BYOD&#xff08;自带设备办公&#xff09;…

vr制作公司提供什么服务?

随着科技的迅猛进步&#xff0c;虚拟现实&#xff08;Virtual Reality&#xff0c;简称VR&#xff09;技术已经悄然渗透到我们的日常生活与工作中&#xff0c;成为推动数字化转型的重要力量。VR制作公司&#xff0c;作为前沿领域的探索者和实践者&#xff0c;以专业的技术和创新…

下一代电子电气架构(EEA)的关键技术

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 钝感力的“钝”,不是木讷、迟钝,而是直面困境的韧劲和耐力,是面对外界噪音的通透淡然。 生活中有两种人,一种人格外在意别人的眼光;另一种人无论…

matlab慕课学习3.5

于20250520 3.5 用while 语句实现循环结构 3.5.1while语句 多用于循环次数不确定的情况&#xff0c;循环次数确定的时候用for更为方便。 3.5.2break语句和continue语句 break用来跳出循环体&#xff0c;结束整个循环。 continue用来结束本次循环&#xff0c;接着执行下一次…

Qt音视频开发过程中一个疑难杂症的解决方法/ffmpeg中采集本地音频设备无法触发超时回调

一、前言 最近在做实时音视频通话的项目中&#xff0c;遇到一个神奇的问题&#xff0c;那就是用ffmpeg采集本地音频设备&#xff0c;当音频设备拔掉后&#xff0c;采集过程会卡死在av_read_frame函数中&#xff0c;尽管设置了超时时间&#xff0c;也设置了超时回调interrupt_c…

PEFT库PromptTuningConfig 配置

PEFT库 PromptTuningConfig 配置 "Prompt Tuning"的参数高效微调 PromptTuningConfig 核心参数解析 1. task_type="CAUSAL_LM" 作用:指定任务类型为因果语言模型(Causal LM)。说明:因果语言模型从左到右生成文本(如GPT系列),这与任务需求匹配(模…

操作系统----软考中级软件工程师(自用学习笔记)

目录 1、计算机系统层次结构 2、程序顺序执行的特征 3、程序并发执行的特征 4、三态模型 5、同步与互斥 6、信号量机制 7、PV操作 8、死锁 9、进程资源图 10、死锁避免 11、线程 12、程序局部性原理 13、分页存储管理 14、单缓冲器 15、双缓冲区 16、磁盘调度算…

基于 Redis 实现短信验证码登录功能的完整方案

&#x1f9f1; 一、技术栈与依赖配置 使用 Spring Boot Redis 实现短信验证码登录&#xff0c;以下是推荐的 Maven 依赖&#xff1a; <dependencies><!-- Spring Boot Web --><dependency><groupId>org.springframework.boot</groupId><ar…

电平匹配电路

1、为什么要电平匹配? 现在很多SOC器件为了降低功耗,都把IO口的电平设计成了1.8V,核电压0.85V,当这种SOC做主平台时,在做接口设计需要格外关注电平的匹配。单板中经常需要将1.8V的电平转换成3.3V或者转成5V。如果没有注意到输入和输出信号之间的电平匹配,系统就无法正常…