持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
前言
工作中经常会遇到生成word这样的需求, 以前都是使用FreeMarker
来生成的
我们先了解一下FreeMarker
是如何生成word的
- 制作好word模板
- 转换为xml文档并格式化, 文件后缀改为ftl(这一步生成的xml格式可能会错乱...)
- 在模板代码(上万行)中加各种逻辑代码来实现需求
- FreeMarker模板引擎渲染生成word(doc格式的)
- doc转docx
ps: word doc格式和docx格式的区别 doc为二进制文件, 非常大, 打开速度非常慢; docx为压缩文件, 非常小, 打开速度很快
上万行的xml
代码简直是维护地狱, 要是这时候老板觉得这个word模板不太好看, 改改字体, 换下颜色, 调整表格样式等等, 除非你对word的xml结构特比特别熟悉, 否则你只能重复上面的步骤再来一次, 心态崩了!!!
有没有一种更容易维护的方式来生成word呢, 答案就是:
POI-TL
poi-tl简介
poi-tl(poi template language)
是基于Apache POI
的Word模板引擎,纯Java组件,跨平台,代码短小精悍,通过插件机制使其具有高度扩展性。
注意: POI-TL可能会和项目中使用到xml相关操作的包产生冲突 建议将POI-TL单独写成一个服务, 包装需要使用到的相关特性以避免冲突
poi-tl使用
使用步骤十分简单:
- 准备好word模板
- 准备好数据 下面我们来试一下
maven
<!-- poi-tl -->
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.12.0</version>
</dependency>
<!-- spire.doc -->
<dependency>
<groupId>e-iceblue</groupId>
<artifactId>spire.doc.free</artifactId>
<version>3.9.0</version>
</dependency>
<!-- spring el -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.3.18</version>
</dependency>
先看下我的模板
这是生成后的word效果
我在模板中尝试了
POI-TL
的以下特性
- 链接
- 图片
语法:
{{@logo}}
- 表格
POI-TL
表格中列加超链接没有找到相关的插件, 所以我扩展了LoopRowTableRenderPolicy
插件, 实现了给表格中的列添加超链接的功能
/**
* 表格内设置超链接
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
public class LoopRowTableHyperlinkRenderPolicy extends LoopRowTableRenderPolicy {
/**
* 列
*/
private int hyperlinkIndex;
/**
* 超链接url key
*/
private String hyperlinkName = "url";
/**
* 超链接text key
*/
private String hyperlinkValue;
public LoopRowTableHyperlinkRenderPolicy(int hyperlinkIndex, String hyperlinkValue) {
this.hyperlinkIndex = hyperlinkIndex;
this.hyperlinkValue = hyperlinkValue;
}
@Override
protected void afterloop(XWPFTable table, Object data) {
try {
List<JSONObject> jsonObjectList = JSONArray.parseArray(JSON.toJSONString(data), JSONObject.class);
List<XWPFTableRow> rows = table.getRows();
// index从1开始, 跳过表头
for (int i = 1; i < rows.size(); i++) {
XWPFTableCell cell = rows.get(i).getCell(hyperlinkIndex);
List<XWPFParagraph> paragraphs = cell.getParagraphs();
for (XWPFParagraph paragraph : paragraphs) {
JSONObject js = jsonObjectList.get(i - 1);
String url = String.valueOf(js.get(hyperlinkName));
String value = String.valueOf(js.get(hyperlinkValue));
if (StrUtil.isBlank(url) || "null".equals(url)) {
// 不满足条件, 正常填充值
cell.setText(value);
} else {
// 满足条件, 设置超链接
setLink(url, value, paragraph);
}
// 居中
paragraph.setAlignment(ParagraphAlignment.CENTER);
paragraph.setVerticalAlignment(TextAlignment.CENTER);
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
private void setLink(String url, String text, XWPFParagraph e) {
String id = e.getDocument().getPackagePart().addExternalRelationship(url,
XWPFRelation.HYPERLINK.getRelation()).getId();
CTHyperlink ctHyperlink = e.getCTP().addNewHyperlink();
ctHyperlink.setId(id);
CTText ctText = CTText.Factory.newInstance();
// 设置值
ctText.setStringValue(text);
CTR ctr = CTR.Factory.newInstance();
CTRPr rpr = ctr.addNewRPr();
CTColor color = CTColor.Factory.newInstance();
// 设置颜色
color.setVal("0000FF");
rpr.setColorArray(new CTColor[]{color});
// 单下划线
// rpr.addNewU().setVal(STUnderline.SINGLE);
// 无下划线
rpr.addNewU().setVal(STUnderline.NONE);
ctr.setTArray(new CTText[]{ctText});
ctHyperlink.setRArray(new CTR[]{ctr});
}
}
- 柱状图/饼图(柱状图可以在模板中调整柱间距来改变柱的粗细)
- 嵌套
语法:
{{+nested}}
sub.docx
这里是{{province}}
{{city}} - {{district}}
- 循环
{{?sections}}集合第[{{_index}}]个元素: {{name}}{{/sections}}
- 区块对
{{?isEmpty}}
******数据不为null******
{{/}}
{{?isEmpty == null}}
******数据为null******
{{/}}
- SpingEL表达式
{{new java.text.SimpleDateFormat('yyyy-MM-dd HH:mm:ss').format(time)}}
配置并生成文件
Configure config = Configure.builder()
// isStrict = false , 关闭严格模式, 严格模式下key不存在会报错
// 关闭后key不存在会被忽略
.useSpringEL(false)
// 表格绑定policy
.bind("books", policy)
.build();
XWPFTemplate template = XWPFTemplate.
compile(getFile("", "template/template.docx"), config).render(data);
String path = OUT_PATH + IdUtil.fastSimpleUUID() + ".docx";
template.writeToFile(path);
private File getFile(String path, String name) throws IOException {
InputStream input = this.getClass().getClassLoader().getResourceAsStream(path + name);
File file = new File(name);
FileUtils.copyInputStreamToFile(input, file);
return file;
}
POI-TL
的TOCRenderPolicy
插件还不太好使(Beta实验功能:目录,打开文档时会提示更新域)
这里我使用的是spire.doc
来更新目录, 为了避免更新目录造成内容位置错乱, 我在模板文件的目录后加了一个分页符
/**
* 更新目录
*
* @param source 源文件
* @param dir 目录
* @return 更新后文件
*/
public static File updateTOCBySpire(String source, String dir) {
Document document = new Document(source);
String path = newPath(dir);
File file = new File(path);
try {
document.updateTableOfContents();
document.saveToFile(path, FileFormat.Docx_2013);
} finally {
document.close();
}
return file;
}
注意spire.doc免费版
的最多支持500个段落
, 25个表格
, 超过部分会被截断
(一般情况下这个已经能够满足了)
也可以使用docx4j
来更新目录(文档很多页会特别慢, 并且DTM ID
可能被耗尽, 导致无法使用)
如果真的是文档特别大的话可以使用jacob(不能跨平台, 只能在windows系统下使用)
或者使用付费版的spire.doc(可以跨平台的)