使用freemarker生成docx文件
一、依赖引入
<!-- freemarker文档处理器 开始 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
<version>2.4.3</version>
</dependency>
<!-- freemarker文档处理器 结束 -->
<!-- 配置文件处理器 开始 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
<version>2.4.3</version>
</dependency>
<!-- 配置文件处理器 结束 -->
<!-- pdf依赖 开始 -->
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>xdocreport</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>com.lowagie</groupId>
<artifactId>itext</artifactId>
<version>2.1.7</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext7-core</artifactId>
<version>7.1.12</version>
<type>pom</type>
</dependency>
<!-- pdf依赖 结束 -->
二、pom修改
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<!--防止对resources下文件编码-->
<nonFilteredFileExtensions>
<nonFilteredFileExtension>zip</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
</plugins>
</build>
三、封装方法
import fr.opensagres.poi.xwpf.converter.pdf.PdfConverter;
import fr.opensagres.poi.xwpf.converter.pdf.PdfOptions;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.springframework.util.Assert;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import static freemarker.template.Configuration.VERSION_2_3_28;
/**
* @author author
* @date 2022/1/20 14:49
*/
@Slf4j
public class WordUtils {
/**
* 导出docx格式Word文档
*
* @param templateXmlPath 模板XML地址
* @param docxZipPath 模板ZIP地址
* @param dataMap 填充数据MAP
* @param outputStream 输出流
*/
public static void exportDocx(String templateXmlPath, String docxZipPath, Map<String, Object> dataMap, OutputStream outputStream) {
String templateFileName = templateXmlPath.substring(templateXmlPath.lastIndexOf("/") + 1);
InputStream templateXmlInputStream = null;
InputStreamReader inputStreamReader = null;
ByteArrayOutputStream byteArrayOutputStream = null;
OutputStreamWriter outputStreamWriter = null;
ByteArrayInputStream byteArrayInputStream = null;
InputStream docxZipInputStream = null;
try {
templateXmlInputStream = NbpWordUtils.class.getClassLoader().getResourceAsStream(templateXmlPath);
Assert.notNull(templateXmlInputStream, String.format("模板XML文件不存在, 路径:%s", templateXmlPath));
Configuration configuration = new Configuration(Configuration.VERSION_2_3_28);
configuration.setDefaultEncoding(StandardCharsets.UTF_8.name());
inputStreamReader = new InputStreamReader(templateXmlInputStream);
Template template = new Template(templateFileName, inputStreamReader, configuration);
byteArrayOutputStream = new ByteArrayOutputStream();
outputStreamWriter = new OutputStreamWriter(byteArrayOutputStream);
template.process(dataMap, outputStreamWriter);
byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
docxZipInputStream = NbpWordUtils.class.getClassLoader().getResourceAsStream(docxZipPath);
Assert.notNull(docxZipInputStream, String.format("模板ZIP文件不存在, 路径:%s", docxZipPath));
ZipUtils.replaceItem(docxZipInputStream, outputStream, "word/document.xml", byteArrayInputStream);
} catch (IOException | TemplateException e) {
log.error(e.getMessage(), e.getCause());
throw new RuntimeException("获取DOCX文件异常");
} finally {
NbpIoUtils.close(docxZipInputStream);
NbpIoUtils.close(byteArrayInputStream);
NbpIoUtils.close(outputStreamWriter);
NbpIoUtils.close(byteArrayOutputStream);
NbpIoUtils.close(inputStreamReader);
NbpIoUtils.close(templateXmlInputStream);
}
}
/**
* 导出PDF格式文档(由docx格式Word文档转换而成)
*
* @param templateXmlPath 模板XML地址
* @param docxZipPath 模板ZIP地址
* @param dataMap 填充数据MAP
* @param outputStream 输出流
*/
public static void exportPdf(String templateXmlPath, String docxZipPath, Map<String, Object> dataMap, OutputStream outputStream) {
ByteArrayOutputStream docxByteArrayOutputStream = null;
ByteArrayInputStream swapStream = null;
try {
docxByteArrayOutputStream = new ByteArrayOutputStream();
NbpWordUtils.exportDocx(templateXmlPath, docxZipPath, dataMap, docxByteArrayOutputStream);
swapStream = new ByteArrayInputStream(docxByteArrayOutputStream.toByteArray());
XWPFDocument xwpfDocument = new XWPFDocument(swapStream);
PdfOptions pdfOptions = PdfOptions.create();
PdfConverter.getInstance().convert(xwpfDocument, outputStream, pdfOptions);
} catch (IOException e) {
log.error(e.getMessage(), e.getCause());
throw new RuntimeException("获取PDF文件异常");
} finally {
NbpIoUtils.close(swapStream);
NbpIoUtils.close(docxByteArrayOutputStream);
}
}
}
四、准备模板
1、准备docx格式文件模板
2、修改后缀名称为zip
3、使用压缩工具打开zip文件,复制document.xml (路径word/document.xml )粘贴至桌面
4、使用文本编辑工具(sublime、idea等)打开桌面的xml文件,使用变量占位符替换后续展示值,参考如下修改为 n a m e ! " " 及 {name!""}及 name!""及{bz!“”}
<w:tr w:rsidR="00272276" w:rsidRPr="00272276" w14:paraId="6808549E" w14:textId="0DC638F3"
w:rsidTr="00272276">
<w:trPr>
<w:trHeight w:val="454"/>
<w:jc w:val="center"/>
</w:trPr>
<w:tc>
<w:tcPr>
<w:tcW w:w="2270" w:type="dxa"/>
<w:vAlign w:val="center"/>
</w:tcPr>
<w:p w14:paraId="7C1263BB" w14:textId="77777777" w:rsidR="00272276" w:rsidRPr="00272276"
w:rsidRDefault="00272276">
<w:pPr>
<w:widowControl/>
<w:jc w:val="center"/>
<w:rPr>
<w:bCs/>
<w:kern w:val="0"/>
<w:szCs w:val="21"/>
</w:rPr>
</w:pPr>
<w:r w:rsidRPr="00272276">
<w:rPr>
<w:rFonts w:ascii="宋体" w:hAnsi="宋体" w:hint="eastAsia"/>
<w:bCs/>
<w:kern w:val="0"/>
<w:szCs w:val="21"/>
</w:rPr>
<w:t>姓名</w:t>
</w:r>
</w:p>
</w:tc>
<w:tc>
<w:tcPr>
<w:tcW w:w="7081" w:type="dxa"/>
<w:gridSpan w:val="2"/>
<w:vAlign w:val="center"/>
</w:tcPr>
<w:p w14:paraId="6888ED66" w14:textId="686E13B9" w:rsidR="00272276" w:rsidRPr="00272276"
w:rsidRDefault="00272276" w:rsidP="000C68A5">
<w:pPr>
<w:widowControl/>
<w:rPr>
<w:bCs/>
<w:kern w:val="0"/>
<w:szCs w:val="21"/>
</w:rPr>
</w:pPr>
<w:r w:rsidRPr="00272276">
<w:rPr>
<w:rFonts w:ascii="宋体" w:hAnsi="宋体" w:hint="eastAsia"/>
<w:bCs/>
<w:kern w:val="0"/>
<w:szCs w:val="21"/>
</w:rPr>
<!-- 修改内容 -->
<w:t>${name!""}</w:t>
</w:r>
</w:p>
</w:tc>
</w:tr>
<w:tr w:rsidR="00272276" w:rsidRPr="00272276" w14:paraId="6808549E" w14:textId="0DC638F3"
w:rsidTr="00272276">
<w:trPr>
<w:trHeight w:val="454"/>
<w:jc w:val="center"/>
</w:trPr>
<w:tc>
<w:tcPr>
<w:tcW w:w="2270" w:type="dxa"/>
<w:vAlign w:val="center"/>
</w:tcPr>
<w:p w14:paraId="7C1263BB" w14:textId="77777777" w:rsidR="00272276" w:rsidRPr="00272276"
w:rsidRDefault="00272276">
<w:pPr>
<w:widowControl/>
<w:jc w:val="center"/>
<w:rPr>
<w:bCs/>
<w:kern w:val="0"/>
<w:szCs w:val="21"/>
</w:rPr>
</w:pPr>
<w:r w:rsidRPr="00272276">
<w:rPr>
<w:rFonts w:ascii="宋体" w:hAnsi="宋体" w:hint="eastAsia"/>
<w:bCs/>
<w:kern w:val="0"/>
<w:szCs w:val="21"/>
</w:rPr>
<w:t>备注</w:t>
</w:r>
</w:p>
</w:tc>
<w:tc>
<w:tcPr>
<w:tcW w:w="7081" w:type="dxa"/>
<w:gridSpan w:val="2"/>
<w:vAlign w:val="center"/>
</w:tcPr>
<w:p w14:paraId="6888ED66" w14:textId="686E13B9" w:rsidR="00272276" w:rsidRPr="00272276"
w:rsidRDefault="00272276" w:rsidP="000C68A5">
<w:pPr>
<w:widowControl/>
<w:rPr>
<w:bCs/>
<w:kern w:val="0"/>
<w:szCs w:val="21"/>
</w:rPr>
</w:pPr>
<w:r w:rsidRPr="00272276">
<w:rPr>
<w:rFonts w:ascii="宋体" w:hAnsi="宋体" w:hint="eastAsia"/>
<w:bCs/>
<w:kern w:val="0"/>
<w:szCs w:val="21"/>
</w:rPr>
<!-- 修改内容 -->
<w:t>${bz!""}</w:t>
</w:r>
</w:p>
</w:tc>
</w:tr>
6、有的xml文件打开后document中缺少引入可能影响后续导出结果,建议添加如下信息
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing"
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
xmlns:w10="urn:schemas-microsoft-com:office:word"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"
xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup"
xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk"
xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml"
xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
mc:Ignorable="w14 w15 wp14">
7、将xml文件及zip文件放置在项目的resources下
五、生成文件并导出(示例中是模拟浏览器下载,如需下载到指定路径可以替换outputStream)
/**
* 导出DOCX文件
*
*/
public void exportElementWord() {
// 路径是resources下路径开始
String templateXmlPath = "template/word/模板.xml";
String templateZipPath = "template/word/模板.zip";
// 模拟测试数据
HashMap<String, Object> dataMap = Maps.newHashMap();
dataMap.put("name", "李四");
dataMap.put("remark", "备注1\n备注2");
// 注意事项 因为xml中的换行标签是<w:br/>,所以如果“\n”的换行符需要转换,实现如下
dataMap.put("remark", String.join("</w:t><w:br/><w:t>", dataMap.get("remark").toString().split("\n")));
String templateFileName = templateXmlPath.substring(templateXmlPath.lastIndexOf("/") + 1);
String currentDate = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
// 拼接导出文件名称
String fileName = String.format("%s_%s", dataMap.get("name").toString(), templateFileName).replace(".xml", String.format("_%s.docx", currentDate));
HttpServletResponse response = WebUtils.getResponse();
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "attachment;filename=" + EncoderUtils.encodeToUtf8(fileName));
try (ServletOutputStream outputStream = response.getOutputStream()) {
WordUtils.exportDocx(templateXmlPath, templateZipPath, dataMap, outputStream);
} catch (IOException e) {
log.error(e.getMessage(), e);
throw new RuntimeException("文件导出异常");
}
}
/**
* 导出PDF文件
*
*/
public void exportElementPdf() {
// 路径是resources下路径开始
String templateXmlPath = "template/word/模板.xml";
String templateZipPath = "template/word/模板.zip";
// 模拟测试数据
HashMap<String, Object> dataMap = Maps.newHashMap();
dataMap.put("name", "李四");
dataMap.put("remark", "备注1\n备注2");
// 注意事项 因为xml中的换行标签是<w:br/>,所以如果“\n”的换行符需要转换,实现如下
dataMap.put("remark", String.join("</w:t><w:br/><w:t>", dataMap.get("remark").toString().split("\n")));
String templateFileName = templateXmlPath.substring(templateXmlPath.lastIndexOf("/") + 1);
String currentDate = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
// 拼接导出文件名称
String fileName = String.format("%s_%s", dataMap.get("cpmc").toString(), templateFileName).replace(".xml", String.format("_%s.pdf", currentDate));
HttpServletResponse response = WebUtils.getResponse();
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "attachment;filename=" + EncoderUtils.encodeToUtf8(fileName));
try (ServletOutputStream outputStream = response.getOutputStream()) {
NbpWordUtils.exportPdf(templateXmlPath, templateZipPath, dataMap, outputStream);
} catch (IOException e) {
log.error(e.getMessage(), e);
throw new RuntimeException("文件导出异常");
}
}
六、涉及代码引用
package com.bs.utils;
import lombok.extern.slf4j.Slf4j;
import java.io.*;
/**
* @author admin
* @description IO工具类
* @createDate 2022/6/22
*/
@Slf4j
public class IOUtils {
/**
* 关闭InputStream
*
* @param stream InputStream
*/
public static void close(InputStream stream) {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
log.error("close stream error", e);
}
}
}
/**
* 关闭OutputStream
*
* @param stream OutputStream
*/
public static void close(OutputStream stream) {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
log.error("close stream error", e);
}
}
}
/**
* 关闭Reader
*
* @param stream Reader
*/
public static void close(Reader stream) {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
log.error("close stream error", e);
}
}
}
/**
* 关闭Writer
*
* @param stream Writer
*/
public static void close(Writer stream) {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
log.error("close stream error", e);
}
}
}
}
package com.bs.utils;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletResponse;
/**
* @author admin
* @description Web工具类
* @createDate 2022/6/22
*/
public class WebUtils {
/**
* 获取Response
*
*/
public static HttpServletResponse getResponse() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
Assert.notNull(requestAttributes, "requestAttributes is null");
return requestAttributes.getResponse();
}
}
package com.bs.utils;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
/**
* ZIP工具类
*
* @author author
* @date 2022/8/3
*/
@Slf4j
public class ZipUtils {
/**
* 替换某个 item
*
* @param inputStream 输入流
* @param outputStream 输出流
* @param itemName 要替换的 item 名称
* @param itemInputStream 要替换的 item 的内容输入流
*/
public static void replaceItem(InputStream inputStream, OutputStream outputStream, String itemName, InputStream itemInputStream) {
ZipEntry entryIn;
try (ZipInputStream zipInputStream = new ZipInputStream(inputStream);
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream);
) {
while ((entryIn = zipInputStream.getNextEntry()) != null) {
String entryName = entryIn.getName();
ZipEntry entryOut = new ZipEntry(entryName);
// 只使用 name
zipOutputStream.putNextEntry(entryOut);
// 缓冲区
byte[] buf = new byte[8 * 1024];
int len;
if (entryName.equals(itemName)) {
// 使用替换流
while ((len = (itemInputStream.read(buf))) > 0) {
zipOutputStream.write(buf, 0, len);
}
} else {
// 输出普通Zip流
while ((len = (zipInputStream.read(buf))) > 0) {
zipOutputStream.write(buf, 0, len);
}
}
// 关闭此 entry
zipOutputStream.closeEntry();
}
} catch (IOException e) {
log.error(e.getMessage(), e.getCause());
throw new RuntimeException("替换item失败");
}
}
}