news 2026/10/15 5:42:37

Java后端动态生成Word,Poi-tl模板引擎从入门到实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java后端动态生成Word,Poi-tl模板引擎从入门到实战

做后端开发的同学,十有八九都写过“导出Word”这种需求。尤其是企业级系统里动不动就要导合同、导验收报告、导培训记录,你要是用原生POI一行行去画画样式,能把人画崩溃;你要是用Freemarker拼Word的XML结构,调试起来也够你喝一壶。后来我换成了基于模板的渲染方式,直接把项目里一个“根据Word模板动态生成Word”的需求做成了一套通用能力,实测下来方案又稳又省事,这个方案的主角就是Poi-tl。

Poi-tl是一款基于Apache POI的Word模板引擎,它允许你在docx文档里用{{变量}}、{{?条件}}、{{*循环}}这些标签提前占好位置,后端只需要把数据准备好交给它,它就能按模板原样生成一份新的Word文件。这个方案特别适合两类人:一类是正在做报表/合同/审批类系统、天天被导Word折腾的Java后端;另一类是准备接手老项目、发现代码里全是原生POI操作、想找机会重构的技术同学。这篇文章我会把Poi-tl的选型理由、模板制作规范、SpringBoot集成流程、常见坑全部分享出来,代码都是可以直接抄的。

1. 为什么选Poi-tl而不是直接操作POI

1.1 常见的Word导出方案对比

先说一个背景,Java生态里导出Word向来有几种路子,我把自己踩过的方案放在一起对比过:

方案优点坑点
原生Apache POI功能最全,能控制每一个单元格代码量大,样式计算复杂,一个模板稍微改样式就得动代码
Freemarker/Thymeleaf拼接XML利用模板语法,成本低只能生成doc格式的XML,兼容性差,表格和图片处理麻烦
Aspose.Words功能强大,效果接近Office原生商业授权贵,公司预算紧的团队基本不会选
Poi-tl模板思想接近Freemarker,但直接操作docx,支持图片/表格/条件/循环需要学习模板标签;模板要求是docx而不能是doc

这里说下为什么原生POI最让人头疼。你一旦用XWPFDocument手动建表格,就要重复做一遍“创建行→创建单元格→写段落→写run→设置边框→设置字体”这套动作,如果表格有十几行,代码量直接爆炸,等Word模板一调整单元格宽度,你可能得对着坐标改半天。而Poi-tl的核心思路是**“模板里画什么,导出来就是什么”**,边界单纯:样式归模板管,数据归程序管,两边互不干扰。这让我在后来改需求的时候轻松很多,产品说“加一行合计”“标题换个颜色”,都不用来回改Java代码。

1.2 Poi-tl的模板设计思想

如果你用过Freemarker渲染HTML,Poi-tl的上手难度会低很多。它的基础标签只有几类:

  • {{title}}普通文本标签,直接替换为字符串
  • {{?flag}}内容{{/flag}}条件标签,flag为true才显示内容
  • {{*rows}}循环标签,配合表格或段落循环渲染
  • {{@image}}图片标签,后端传入图片对象
  • {{#table}}表格引用标签,直接绑定一个表格模板

我举个最直观的例子。原来写POI导出,要实现一个“用户列表表格”,你得在代码里foreach用户,然后XWPFTableRow循环去添加行。现在用Poi-tl,只需要先在Word里画好一张一行两列的表格,表头写上“姓名”“部门”,第二行写{{*userList}}、{{name}}、{{dept}},后端拿着List对象往userList字段里一塞,渲染出来的就是从第二行开始动态扩展的完整表格。模板标签和Word表格的原生结构融合得很自然,英文单词虽然多,但学习成本主要在记忆标签含义上,半天就能上手。

1.3 选型时要考虑的额外因素

除了开发效率,选型还应该考虑项目运行环境和后续维护成本。Poi-tl底层还是Apache POI,所以你项目的SpringBoot版本、JDK版本基本不会成为阻碍;它渲染出来的文件就是标准docx,不会像XML拼接那样出现“文件损坏”的尴尬;而且它是一个纯Java库,Linux服务器上跑也不依赖Office软件,这一点很关键,因为很多开Word服务的公司服务器都是CentOS环境,总不能为了导出Word还去装办公套件。

我在实际项目中还关注了另一个点:可测试性。因为Poi-tl把模板渲染成InputStream/OutputStream流,我可以直接跑单元测试,断言生成的docx文件能正常打开、能包含指定关键词,而原生POI虽然也能测试,但动辄几百行的构建代码让测试用例变得很臃肿。所以我后来定的选型结论很简单:凡是文档样式要频繁改的项目,无脑选Poi-tl;如果你只是生成一个格式完全固定的纯文本doc,怎么省事怎么来。但实际业务里“格式固定”几乎是伪命题,所以Poi-tl基本是通用答案。

2. 环境准备与模板制作规范

2.1 SpringBoot工程依赖引入与版本选择

Poi-tl的接入方式很简单,SpringBoot项目只需要在pom.xml里加一个依赖。以我用的版本为例:

<dependency> <groupId>com.deepoove</groupId> <artifactId>poi-tl</artifactId> <version>1.12.1</version> </dependency>

这里提醒一下,1.12.1是基于POI 5.2.x的,如果你项目里本来就有POI,要留意版本冲突。最稳妥的做法是把Poi-tl依赖写在其他POI依赖之前,并且检查依赖树:

mvn dependency:tree -Dincludes=org.apache.poi

如果发现apache.poi相关版本被冲掉了,尽量在SpringBoot的dependencyManagement里统一指定POI版本,但不能随便选过老的版本,否则会报一些NoClassDefFoundError。我用1.12.1的时候搭配SpringBoot 2.7.x,在Java 8和Java 11上都跑过,没出现什么问题;如果你用的是SpringBoot 3.x,建议尝试更高的Poi-tl版本,或者注意javax/xml相关依赖的兼容性。

2.2 制作Word模板时的标签规范

模板是整个方案的核心资产,模板做得规范,后端就省心;模板做乱了,后端代码再优雅也白搭。制作模板时我习惯用Microsoft Word或者WPS新建一个docx,在里面手动输入标签,而不是用代码去生成模板。

几个关键规范:

  • 占位符写在Word正文里,直接输入{{name}},不需要设置什么特殊格式,但要注意使用半角花括号,全角{{}}是识别不了的。
  • 普通变量建议加“上下标”区分吗?不用,Poi-tl支持直接扫描整个Word文档,你只要保证标签完整就行。
  • 表格里的循环标签,写在一行单元格内。比如要动态生成多行项目记录,先让表格有表头行和一行数据行,数据行的每一列写{{*projList}}的引用方式按列绑定更常见:实际使用中我更喜欢把循环标签写在表格首列单元格,比如{{*list}},然后同一行其他列写{{name}}这样的字段标签,渲染时Poi-tl会自动复制这一行结构作为循环体。这个细节从1.10版本开始支持得很好。
  • 图片占位符是{{@image}},后面的代码要传入带Width和Height的图片对象,否则默认使用原始图片尺寸,很容易撑爆页面。
  • 条件标签要成对出现:{{?show}}和{{/show}}之间包住的内容,根据Boolean值决定是否展示。

另外还要注意,模板里不要出现颜色很花哨的“底纹”,因为部分复杂底纹渲染后可能丢失;如果模板里已经有一行空的普通文本,后端的标签变量,最好确保模板里待渲染的变量都有值。Poi-tl对null值默认是空字符串渲染,但要注意循环里的对象属性如果为null,取到的可能是“null”字符串,这需要后端转义。

2.3 模板文件放哪里以及资源加载方式

模板存放位置,我推荐放在项目的src/main/resources/templates/目录下,因为打包成jar之后仍然可以读取。比如路径:

src/main/resources/templates/contract/report_template.docx

读取方式不要用new File(),因为jar包内路径不是普通文件路径。直接在代码里用ClassPathResource:

Resource resource = new ClassPathResource("templates/contract/report_template.docx"); InputStream inputStream = resource.getInputStream(); XWPFTemplate template = XWPFTemplate.compile(inputStream).render(data);

这种方式在本地IDE里能跑,在Linux服务器上打包成jar之后也能跑,是我踩坑之后固定下来的标准写法。还有个小习惯:模板文件名不要用中文,避免部分服务器解压jar时出现编码问题;如果你一定要用中文,麻烦把文件编码和服务器系统编码都检查一遍,否则读不到文件非常难排查。

3. SpringBoot集成Poi-tl的核心实现

3.1 配置Poi-tl默认字体,避免中文乱码

Poi-tl渲染出来的Word默认字体可能跟模板原有字体不一致,尤其是中文场景,很容易变成宋体以外的字体,甚至出现乱码或字体不适配的问题。我后来直接在渲染前设置默认样式:

Configure config = Configure.builder() .defaultTextStyle(TextStyle.of().fontFamily("微软雅黑").fontSize(10.5f).build()) .build();

这么做的好处是,模板里没显式设置字体的地方都按统一风格走,导出的文件不会一会儿宋体一会儿黑体。如果你模板里已经定义了每个标题的字体,那这个配置基本是兜底方案,优先级比模板内样式低,不会把Word里的既有样式冲掉。

3.2 普通变量与日期数据的渲染

最基础的做法就是构建一个Map,key对应模板里的变量名,value放实际值:

Map<String, Object> data = new HashMap<>(); data.put("title", "2025年上半年项目验收报告"); data.put("reportDate", formatDate(new Date())); data.put("projectName", "模拟跨平台业务系统项目");

关于日期我多说一句,模板里如果直接放{{reportDate}},后端传的必须是字符串,所以提前用SimpleDateFormat格式化好,格式一般是yyyy年MM月dd日。我见过有同事直接把Date对象塞进去,结果渲染出来是一串英文时间戳,Word里看着非常突兀。遇到BigDecimal金额也是同理,后端先转成两位小数的字符串再放进Map,不要在模板层去做金额格式化。

3.3 循环表格与条件内容的数据组织

循环标签是这类方案里最容易出问题的部分。假设模板里有一个项目清单表格,表格结构是这样:

序号项目名称预算金额
{{*itemList}}{{name}}{{budget}}

后端的组织方式:

List<Map<String, Object>> itemList = new ArrayList<>(); Map<String, Object> item1 = new HashMap<>(); item1.put("name", "模块A建设"); item1.put("budget", "100000.00"); itemList.add(item1); Map<String, Object> item2 = new HashMap<>(); item2.put("name", "模块B升级"); item2.put("budget", "50000.00"); itemList.add(item2); data.put("itemList", itemList);

这里的核心是:表格中写{{*itemList}}的那一行,会被当作循环模板行,循环体里引用的字段是{{name}}、{{budget}}这些List中Map的key。如果模板表格只有表头加一行数据行,那渲染出来的表格就是表头加两行数据,行数跟List大小一致。

条件内容类似:

{{?hasSign}} 验收日期:2025年06月30日 {{/hasSign}}

后端只要data.put("hasSign", true),这段话就会出现在文档里;设为false就整段隐藏。这个特性很适合做“不同项目是否需要展示某些段落”的灵活性控制。

3.4 图片填充的三种方式

图片是Poi-tl里面最有价值的能力。做一个带签章、带证件照、带效果图的Word报告,用原生POI处理图片真的不是一般麻烦,而Poi-tl只需要传入图片对象。

模板里放{{@sign}},后端代码:

// 方式一:本地图片 Picture picture = Pictures.of("filePath/sign.png").size(120, 40).create(); data.put("sign", picture); // 方式二:输入流 InputStream imageStream = new FileInputStream("report.png"); Picture picture2 = Pictures.of(imageStream).size(120, 40).create(); data.put("sign", picture2); // 方式三:网络图片,需先下载成本地字节 byte[] bytes = downloadFromUrl("https://example.com/sign.png"); Picture picture3 = Pictures.ofBytes(bytes).size(120, 40).create(); data.put("sign", picture3);

size方法的单位是磅,120磅大概是一张4厘米宽的图。如果不想失真,原图尺寸和设置尺寸比例要一致,不然会变形。我还遇到过一个问题:图片变量在循环表格里反复渲染时,每行的图片如果都用同一个文件InputStream,Poi-tl内部如果读完了没重置,可能第二行图片就渲染不出来了。我的解决办法是循环里每次new一个InputStream或者用byte[]构造图片对象。

3.5 模板整体渲染与输出文件

所有数据准备完成后,渲染的核心代码就三行:

XWPFTemplate template = XWPFTemplate.compile(inputStream, config).render(data); template.write(outputStream); template.close();

在SpringBoot的接口里,通常用HttpServletResponse把输出流写回去,形成浏览器直接下载的效果;也可以先写入服务器磁盘,再把文件地址给前端。两种方式都有使用场景,如果是批量生成任务,建议先落盘再批量打包;如果是用户点一下按钮,直接用Response输出比较方便。

4. 实操过程与完整代码:导出项目验收报告

4.1 模拟场景与模板设计

为了把流程讲透,我虚构一个场景:某公司在做项目管理系统,系统里需要导出一份“项目验收报告”,内容包括项目基本信息、承建单位、项目清单表格、监理总结和验收签字区。需求特点是要动态生成清单行、根据项目情况显示“需要整改”的提示、并且右下角要放监理单位公章图片。

我的第一步一定是先做模板,而不是写代码。用Word新建模板,在每一处需要动态变化的位置放好Poi-tl标签,结构大致如下:

  • 标题行:{{projectName}}验收报告
  • 基本信息区:项目编号{{projectCode}}、承建单位{{contractor}}、验收日期{{acceptDate}}
  • 项目清单表格:表头为“序号/交付物名称/交付时间/状态”,数据行第一列写{{*deliverables}},其余列写{{name}}、{{time}}、{{status}}
  • 整改提示:用{{?needRectify}}包住一句“该项目仍存在需整改问题”的段落
  • 签字区:左边填写单位名称{{signUnit}},右边放图片标签{{@companySeal}}

模板里这些标签全部是普通文本,我甚至没有给它们单独设置字体颜色,因为我希望它们被渲染后跟正文风格一致,只需要在段落本身设置好常用字体即可。

4.2 完整代码结构

工程包结构我习惯这样划分:

com.example.report.controller.ReportExportController.java com.example.report.service.ReportExportService.java com.example.report.model.Deliverable.java com.example.report.util.PoiTlUtils.java

Controller只负责接收请求和输出文件:

@RestController @RequestMapping("/report") public class ReportExportController { @Resource private ReportExportService reportExportService; @GetMapping("/export") public void export(HttpServletResponse response) throws IOException { // 设置下载响应头 String fileName = URLEncoder.encode("项目验收报告.docx", "UTF-8"); response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); reportExportService.exportReport(response.getOutputStream()); } }

Service是重点,里面组织数据并调用Poi-tl:

@Service public class ReportExportService { public void exportReport(OutputStream outputStream) throws IOException { // 从数据库或者业务接口取数,这里直接构造模拟数据 Map<String, Object> data = new HashMap<>(); data.put("projectName", "某跨平台业务系统"); data.put("projectCode", "PRJ-2025-0713"); data.put("contractor", "某某科技有限公司"); data.put("acceptDate", "2025年07月13日"); // 项目清单,对应{{*deliverables}} List<Deliverable> deliverableList = new ArrayList<>(); deliverableList.add(new Deliverable(1, "需求调研报告", "2025-05-10", "已完成")); deliverableList.add(new Deliverable(2, "系统设计文档", "2025-06-01", "已完成")); deliverableList.add(new Deliverable(3, "核心模块编码", "2025-06-20", "已完成")); deliverableList.add(new Deliverable(4, "用户培训材料", "2025-07-05", "待完善")); data.put("deliverables", deliverableList); // 是否显示整改提示 data.put("needRectify", true); // 签字区 data.put("signUnit", "某某科技有限公司"); // 使用byte[]方式构造印章图片,避免InputStream多次读问题 byte[] sealBytes = Files.readAllBytes(Paths.get("/data/seal.png")); data.put("companySeal", Pictures.ofBytes(sealBytes).size(90, 90).create()); // 加载模板并渲染 Resource resource = new ClassPathResource("templates/report_template.docx"); Configure config = Configure.builder() .defaultTextStyle(TextStyle.of().fontFamily("微软雅黑").fontSize(10.5f).build()) .build(); try (InputStream templateStream = resource.getInputStream(); XWPFTemplate template = XWPFTemplate.compile(templateStream, config).render(data)) { template.write(outputStream); } } }

这里Deliverable就是一个普通POJO:

public class Deliverable { private int seq; private String name; private String time; private String status; public Deliverable(int seq, String name, String time, String status) { this.seq = seq; this.name = name; this.time = time; this.status = status; } // getter/setter省略 }

有同学会问,为什么Deliverable是对象而模板循环里能用{{name}}直接取字段?因为Poi-tl对POJO的属性访问做了反射支持,字段名对应属性名就能直接读取。我实际项目里更多用Map,因为很多查询结果直接就是Map,但用POJO更利于代码维护和IDE提示,看团队习惯。

4.3 导出参数的动态计算与空值处理

上面代码只演示了固定数据,实际上数据往往是聚合出来的。比如“交付状态”可能是从数据库查询的字符串,也可能是需要根据日期算出来的“已完成/待完善”;“整改提示”往往需要根据某个规则来判断,比如存在status.equals("待完善")就让needRectify为true。

我写了一段常用的辅助逻辑:

boolean needRectify = deliverableList.stream() .anyMatch(item -> "待完善".equals(item.getStatus())); data.put("needRectify", needRectify);

还有空值处理。模板渲染时如果value为null,默认变成空字符串,但有时候我们希望显示“/”或者“待补充”。这个可以用Poi-tl的哨兵值或者Lambda表达式配置,比如:

Configure config = Configure.builder() .bind("contractor", (model) -> null == model ? "待补充" : model.toString()) .build();

这个写法适合针对个别字段做兜底,比每个字段单独判断要干净。如果整个Map里很多字段可能是null,也可以把配置写成一个方法复用,不然后期维护一段段if判断会让人头大。

4.4 前端调用与下载注意事项

前端只要一个普通下载请求即可:

window.location.href = '/report/export';

或者使用axios的话要设置responseType为'blob',不然下载下来的文件可能是一串乱码:

axios.get('/report/export', { responseType: 'blob' }).then(response => { const blob = new Blob([response.data]); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = '项目验收报告.docx'; link.click(); });

一个容易踩的坑是Content-Disposition里的文件名,如果直接用中文且不回显,浏览器下载时可能变成一堆编码;Java后端用URLEncoder.encode(fileName, "UTF-8")处理过,前端再拿到的就正常了。此外,模板文件里的中文字体在Linux服务器上可能缺失,导致导出的Word在Windows上打开正常,在Linux上用LibreOffice预览却字体异常,这个不是Poi-tl的问题,是服务器字体库不全,别在这上面浪费时间排查。

5. 常见问题与排查技巧实录

5.1 中文乱码问题

乱码出现的原因,我排查下来基本是两种:一是模板本身就是doc而不是docx,Poi-tl不支持;二是渲染后字体缺失。处理方式很简单:模板统一另存为docx,配置里设置默认中文字体。另外要注意,如果模板里存在文本框内文字或页眉页脚里的变量,Poi-tl默认是不渲染的,需要针对Gramer或IncludeText做额外配置,但一般需求很少碰页眉页脚,遇到时再查官方文档就行。

5.2 图片无法显示或变成红叉

图片不显示的第一嫌疑是图片数据没传进去。检查点:

  • 模板占位符是不是{{@image}},少一个@就不认
  • 图片构造是否调用.create(),没创建就会空指针
  • 图片路径如果是相对路径,在Linux上可能找不到,直接用字节流或绝对路径

遇到“红叉”一般是图片格式未被识别,比如ICO格式的图表,POI只支持常见png/jpg格式,转成png即可。我之前从某系统下载的印章图是webp格式,传进去直接整个表格渲染失败,转成png就好了。

5.3 模板标签被当成普通文本原样输出

如果你导出后发现Word正文里还留着{{name}},说明Poi-tl没有解析这个标签。最常见原因是模板里的花括号是全角,或者标签中间包含隐藏的换行符。解决办法是检查模板源,在Word里把{{name}}删掉重新输入一遍;如果是循环标签,还要确认标签是否位于表格单元格内,且没有跨单元格。

5.4 循环表格渲染结果不对

循环表格的坑集中在标签位置和数据结构上。比如你希望在表格里循环多行,模板的第二行第一列必须写{{*list}},该行的其他单元格才能写变量;如果你把{{*list}}写在表格外面的段落里,Poi-tl只会复制那段文字内容,不会扩展表格行。再比如List里的Map,key必须和模板变量完全一致,少一个字母就渲染不出值。这属于排错时最容易让人抓狂的一类问题,但定位到标签层级后就特别简单。

5.5 大文档导出内存溢出

Poi-tl底层还是POI的docx处理机制,如果模板里有一张大尺寸图片、循环几千行数据,JVM内存可能被打满。我的优化思路是:

  • 循环数据如果巨大,优先输出流式接口,而不是把整个字节数组保留在内存里
  • 图片压缩之后再传入,避免原图一个就好几MB
  • 模板本身尽量精简,复杂样式越多,解压占用的临时内存越多

如果只是偶尔导出几百行表格,一般业务系统不会有问题。但如果做了批处理,比如凌晨把全年的报表全部导出成docx,建议分批执行,每生成完一个文件就及时关闭流,不要把所有XWPFTemplate对象堆在List里。

5.6 多线程并发渲染的线程安全问题

Poi-tl的模板编译对象是否可以复用?我的经验是:每次渲染都单独调用XWPFTemplate.compile,不要试图复用一个已经render过的template实例,因为render之后内部状态已经变化,再render会出问题。如果追求性能,可以把编译流程放在方法局部,配合线程池也没问题,因为compile本身并不会修改模板文件。

另外,数据Map中的对象在多线程下要避免共享修改。比如同一个List传给了多个导出任务,其中一个任务对List做了排序,另一个任务可能拿到错乱的数据。数据组装和渲染尽量保持无状态。

5.7 问题速查表

现象排查方向解决方案
导出的docx打开提示损坏模板可能是doc格式,或响应头ContentType不对模板统一转docx;接口返回正确ContentType
中文全部乱码字体配置缺失或模板本身编码问题Configure设置默认字体;重新另存docx模板
{{name}}原样出现在文档里标签写法或特殊字符问题重新输入半角花括号标签
图片红叉图片格式不被支持或路径不正确转成png/jpg,使用InputStream或byte[]
循环表格只渲染一行循环标签位置不对把{{*list}}移到表格数据行的首列单元格内
导出为空文件outputStream自行关闭或未write渲染后及时调用template.write
模板读取不到ClassPathResource路径不对确认模板在resources/templates目录且文件名正确

6. 实操心得与后续扩展建议

6.1 模板版本管理比代码版本管理更重要

整套方案跑起来后,你会发现真正的“需求变更入口”变成了Word模板,而不是Java代码。所以我建议把模板文件放进代码仓库的resources目录一起做版本管理,改模板和改代码走同样的评审和发布流程,不要直接在服务器上替换模板文件,否则哪天模板被改坏了,线上导出的文件会非常诡异。我们团队后来还约定:模板文件名末尾带版本号,比如report_template_v3.docx,每次大改就升版本,旧模板先不删,留一份回滚的余地。

6.2 水印、批注、加密与动态图表怎么扩展

Poi-tl发布版还提供了更丰富的插件能力,比如Word水印可以用WatermarkPlugin,批注可以用CommentPlugin,这些插件大多数人用不上,但其中水印确实是合同报告场景的常见需求。如果你也是导出合同,还可以顺便研究一下POI本身对docx加密的支持,但要注意加密后文件在某些老版本Office打开会提示兼容问题,所以上线前要拿目标用户的Office版本测一下。

动态图表是另一个方向,如果导出的Word里要伴随柱状图、饼图,纯Poi-tl实现不了。我遇到这种场景的做法是:先用Apache POI在模板里插入图表对象,或者渲染前用代码往docx的图表xml位置填充数据。这部分比较复杂,不是一句两句能说清,建议需求的优先级不高就别强行做,用截图+静态图片展示效果也是一种合理妥协。

6.3 导出Word后的二次加工与业务化扩展

导出Word通常不是终点,后面还跟着“转PDF”“加签章”“推送邮件”这些动作。我的经验是把导出和后续加工拆成独立服务,导出模块只负责生成标准的pptx/docx文件,转PDF可以再引入其他容器能力,推送交给消息服务。这样每个环节出错都能单独重试,而不是一个大流程串在一起,失败一次从头再来。

另外,我现在处理“导出模板动态生成Word”这类需求时,一定会在Service层写一个generateDocxData(model)方法,专门把业务对象转换成模板数据Map,这样将来接入其他模板引擎也能复用。这个小设计帮我避过很多“改了业务对象导致导出字段报错”的坑,某个字段找不到时,直接看Map的构建逻辑就清楚了。

最后再分享一个小技巧:Poi-tl的模板调试环不用重启整个SpringBoot。模板文件在resources里改完,有时IDEA不会主动把新文件复制到target目录,导致本地测了半天还是旧模板。遇到这种情况,先执行一下mvn clean compile强制刷新,或者直接把模板文件拷贝到target/classes对应的目录下,再刷新页面测试,能省下很多莫名其妙的时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 5:42:35

Cherry Studio:本地离线多模型AI客户端,支持LLaMA-3/Phi-4跨平台部署

简介&#xff1a;本资源是一份关于AI桌面客户端Cherry Studio的深度介绍文档&#xff0c;面向希望提升日常效率的开发者、内容创作者、教育工作者及企业协作团队&#xff0c;解决多模型切换难、本地与云端AI工具割裂、文档与代码协同低效等实际问题。文档为单个16KB的Word&…

作者头像 李华
网站建设 2026/10/15 5:41:38

JDK 18新特性深度解析:从实用升级到迁移避坑指南

各位做Java开发的朋友&#xff0c;最近几年JDK的更新节奏确实快&#xff0c;从17到21&#xff0c;基本上每年都在折腾新东西。JDK 18是2022年3月发布的短期版本&#xff0c;虽然不像17或者21那样有长期支持光环&#xff0c;但这并不代表它没有看头。实际用过之后&#xff0c;我…

作者头像 李华
网站建设 2026/10/15 5:40:25

IPv6工程落地实战:地址规划、NDP排错与华为设备特有陷阱

1. 这份IPv6笔记到底在解决什么问题&#xff1f;“【HCIE备考笔记】【03-IPv6】重点难点整理”——光看标题&#xff0c;你可能以为这只是某位考生随手记下的几页草稿。但实际翻过几十份真实备考资料后你会发现&#xff1a;绝大多数人卡在IPv6&#xff0c;不是因为协议本身多难…

作者头像 李华
网站建设 2026/10/15 5:39:39

XPD737规格书解读:XPD-LINK互联如何省掉MCU实现多口PD功率分配

简介&#xff1a;XPD737 规格书是一份面向 USB PD 快充控制器选型与开发的完整 PDF 技术资料&#xff0c;适合电源适配器、带 USB 端口排插及多口充电设备的研发工程师使用。文档围绕富满电子 XPD737 展开&#xff0c;涵盖 USB PD2.0/3.0 与 PPS 协议、QC3.0/2.0、华为 FCP/SCP…

作者头像 李华
网站建设 2026/10/15 5:39:33

NLP论文汇总实战:主题聚类与影响力分层方法

1. 从一篇论文汇总说起&#xff1a;为什么值得花时间做这件事做自然语言处理这行的人&#xff0c;大概都有过这样的体验&#xff1a;每天醒来&#xff0c;arXiv 上 cs.CL 分区又挂出来几十篇新论文&#xff0c;标题扫一遍&#xff0c;摘要翻两页&#xff0c;真正能跟自己手头工…

作者头像 李华
网站建设 2026/10/15 5:39:32

无GPU也能修推理引擎崩溃:纯CPU调试与开源贡献实战

1. 一个没有 GPU 的人&#xff0c;为什么要去碰推理引擎的 bug先把话说在前头&#xff1a;我手上没有能跑得动大模型的显卡&#xff0c;日常开发机就是一台普通笔记本&#xff0c;核显&#xff0c;内存 16G。但我最近花了大半个月&#xff0c;给一个主流的大模型推理引擎提交了…

作者头像 李华