Java动态模板导出Word
总结了一下我自己使用过的三种动态模板导出,两个Word导出,用的是不同的方法,一个是POI-TL,另一个是用FTL模板。
1、利用poi-tl工具进行导出word(适合较简单的模板)
我要实现的功能大体是这样的,需要在导出多个部门的数据,然后每个部门中会有多条数据,我要将一个部门的数据全部动态导出到一个word文件中,每条数据占一页,生成单独的word,然后把多个文件生成放入同一个压缩包内,用户导出时下载压缩包
1.1 依赖准备
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-web</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-annotation</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.1</version>
</dependency>
1.2 创建word模板
按照这个形式在word中自定义你的模板,{{item}}的形式会被替换掉,要注意如果要插入图片则需要在前面加个@符号,如{{@qrCode}}
将模板放入resource中,可以单独建立一个新的文件夹
1.3 代码实现
1.3.1 数据处理
private Map<String, Object> convertVo2Map(ExportVo vo, Map<String, List<FileUploadBaseEntity>> fileMap) throws IOException {
Map<String, Object> map = new HashMap<>();
map.put("region", vo.getRegion());
map.put("depName", vo.getDepName());
map.put("capacity", vo.getCapacity());
map.put("type", vo.getType());
map.put("towerNo", vo.getTowerNo());
// 处理 towerInfrastructureList(关联栋舍),每组数据换行
if (vo.getTowerInfrastructureList() != null && !vo.getTowerInfrastructureList().isEmpty()) {
String formattedTowerInfrastructureList = vo.getTowerInfrastructureList().stream()
.map(towerInfrastructure -> towerInfrastructure.getLineName() + ", " + towerInfrastructure.getBuildingName())
.collect(Collectors.joining("\n")); // 每个数据换行
map.put("towerInfrastructureList", formattedTowerInfrastructureList);
} else {
map.put("towerInfrastructureList", "暂无");
}
// 处理 towerFodderList(料号),使用逗号分隔
if (vo.getTowerFodderList() != null && !vo.getTowerFodderList().isEmpty()) {
String formattedTowerFodderList = vo.getTowerFodderList().stream()
.map(towerFodder -> towerFodder.getFodderCode().toString())
.collect(Collectors.joining(", "));
map.put("fodderCode", formattedTowerFodderList);
} else {
map.put("fodderCode", "暂无");
}
//处理图片
List<FileUploadBaseEntity> fileList = fileMap.getOrDefault(vo.getId(), Collections.emptyList());
if (!fileList.isEmpty()) {
FileUploadBaseEntity img = fileList.get(0);
byte[] imageBytes = getImageFromMinio(img.getFileId(), img.getFileName());
map.put("qrCode", Pictures.ofBytes(imageBytes).size(300, 300).create());
}
return map;
}
这个方法是将从数据库中获得到的vo转换成map,然后通过map动态插入到模板中,其实用map和实体类都可以,并且实体类的更好,能够减少耦合,代码足够健壮,只是我使用的是map的方式,写起来简单,代码量少。
代码并不负责,就只是将值put进map,唯一要说的就是将两个list放进map中时需要进行部分处理
//按照逗号分隔
Collectors.joining(", ")
//每个数据换行
Collectors.joining("\n")
1.3.2 Word导出
构建导出列表
//原始数据
List<ExportVo> result = buildResult3(towerInfoList, infrastructureMap, fodderMap);
//放入一个list中
List<ExportVo> exportVos = new ArrayList<>(result);
转换vo为map
List<Map<String, Object>> dataList = new ArrayList<>();
for (ExportVo vo : exportVos) {
Map<String, Object> data = convertVo2Map(vo, fileMap);
dataList.add(data);
}
构造渲染数据,渲染word模板
//构造渲染数据
Map<String, Object> finalData = new HashMap<>();
finalData.put("list", dataList);
//加载模板
ClassPathResource resource = new ClassPathResource("dynamic/template/QRExportTemplate1.docx");
XWPFTemplate tp = XWPFTemplate.compile(resource.getInputStream()).render(finalData);
XWPFDocument doc = tp.getXWPFDocument();
写入本地文件
String dateStr = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String departmentFileName = "料塔二维码" + dateStr + counter++ + ".docx";
File departmentFile = new File(tempDir, departmentFileName);
//写出文件
try (FileOutputStream out = new FileOutputStream(departmentFile)) {
doc.write(out);
}
2、利用FTL模板进行word导出(适合比较复杂的模板)
新建模板
需要先定义一个模板,其中**KaTeX parse error: Double superscript at position 10: {(mat)!' '̲}**这种都是需要填充的数据,…{(mat)}**,那么当业务代码中,变量值为空的时候,此时导出word时就会报错。所以我们需要设置默认值。如果想要插入图片,那么先插入一个图片进去,后续修改ftl文件。
生成XML文件
重命名为ftl
解决word转xml占位符、变量值被分离的问题
转换处理的ftl文件不可以直接使用,需要进行转换一下,转换代码如下
public static void main(String[] args) {
String urlSrc= "C:\\Users\\73108\\Desktop\\archiveCardSrc.ftl";
String urlTarget= "C:\\Users\\73108\\Desktop\\archiveCard.ftl";
//文件读取-FileReader
//默认UTF-8编码,可以在构造中传入第二个参数作为编码
FileReader fileReader = new FileReader(urlSrc);
//从文件中读取每一行数据
List<String> strings = fileReader.readLines();
//文件追加-FileAppender
//destFile – 目标文件
//capacity – 当行数积累多少条时刷入到文件
//isNewLineMode – 追加内容是否为新行
FileAppender appender = new FileAppender(FileUtil.newFile(urlTarget), 16, true);
//遍历得到每一行数据
for (String string : strings) {
//判断每一行数据中不包含'$'的数据先添加进新文件
if (!string.contains("$")) {
appender.append(string);
continue;
}
//如果一行数据中包含'$'变量符将替换为'#$'
string = string.replaceAll("\\$", "#\\$");
//然后以'#'切割成每一行(数组),这样一来'$'都将在每一行的开头
String[] ss = string.split("#");
// 同一行的数据写到同一行,文件追加自动换行了(最后的完整数据)
StringBuilder sb = new StringBuilder();
//遍历每一行(数组ss)
for (int i = 0; i < ss.length; i++) {
//暂存数据
String s1 = ss[i];
//将不是以'$'开头的行数据放进StringBuilder
if (!s1.startsWith("$")) {
sb.append(s1);
continue;
}
//被分离的数据一般都是'${'这样被分开
//匹配以'$'开头的变量找到'}' 得到索引位置
int i1 = s1.lastIndexOf("}");
//先切割得到这个完整体
String substr = s1.substring(0, i1 + 1);
//把变量追加到StringBuilder
sb.append(substr.replaceAll("<[^>]+>", ""));
//再将标签数据追加到StringBuilder包裹变量
sb.append(s1.substring(i1 + 1));
}
appender.append(sb.toString());
}
appender.flush();
appender.toString();
System.out.println("success");
}
修改ftl文件图片相关代码
//修改binData,并设置图片名,item.eraNo和item.pic都是变量
<w:binData w:name="wordml://${item.eraNo}.png" xml:space="preserve">${(item.pic)!'ant'}</w:binData>
这里是为了将固定图片换成动态插入图片
接下来防止遍历的都是同一个图片
接下来需要添加遍历
最后把变量名做统一修改,你是什么就改成什么
最后要注意的一个点,防止刚才修改时把图片那里的修改了
如此,我们的模板就生成成功了,具体的业务代码就不叙述了,和上面poi-tl的差不多
18103497)]
[外链图片转存中…(img-Um7yZ7f2-1748418103497)]
最后把变量名做统一修改,你是什么就改成什么
[外链图片转存中…(img-bcv7wcK9-1748418103498)]
最后要注意的一个点,防止刚才修改时把图片那里的修改了
[外链图片转存中…(img-5JnFKEci-1748418103498)]
如此,我们的模板就生成成功了,具体的业务代码就不叙述了,和上面poi-tl的差不多