背景
我们需要通过 Java 动态导出 Word 文档,基于预定义的 模板文件(如 .docx
格式)。模板中包含 表格,程序需要完成以下操作:
-
替换模板中的文本(如占位符 ${设备类型} 等)。
-
替换模板中的图片(如占位符 {{图片_作业现场}} )。
模板示例
模板文件(如 template.docx
)结构大致如下:
maven依赖
<dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>3.17</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>3.17</version> </dependency> <dependency> <groupId>com.deepoove</groupId> <artifactId>poi-tl</artifactId> <version>1.12.1</version> </dependency>
Controller
@ApiOperation(notes = "模板导出", value = "使用模板导出文档")
@RequestMapping(value = "/exportByTemplate", method = RequestMethod.GET)
public void exportByTemplate(HttpServletResponse response) {
try {
// 1. 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
response.setHeader("Content-Disposition", "attachment;filename=report.docx");
// 2. 准备数据
Map<String, Object> data = new HashMap<>();
data.put("设备类型", "开关");
data.put("属地运维单位", "湘江公司");
data.put("作业现场", new String[]{"D:\\upload\\upload\\2025\\04\\14\\20250414070702.jpg","D:\\upload\\upload\\2025\\04\\14\\20250414070720.jpg"});
// 3. 调用生成方法
pdPointProblemService.generateFromTemplate(
response,
"D:\\1.docx", // 模板路径
data
);
} catch (Exception e) {
e.printStackTrace();
// 异常处理(略)
}
}
ServiceImpl
@Override
public void generateFromTemplate(HttpServletResponse response,
String templatePath,
Map<String, Object> data) throws Exception {
// 1. 初始化文档(不使用try-with-resources)
FileInputStream fis = new FileInputStream(templatePath);
XWPFDocument doc = new XWPFDocument(fis);
try {
// 2. 执行替换
replaceText(doc, data);
replaceImages(doc, data);
OutputStream out = response.getOutputStream();
doc.write(out);
out.flush();
} finally {
if (fis != null) {
fis.close();
}
}
}
private void replaceText(XWPFDocument doc, Map<String, Object> data) {
// 替换段落中的文本
for (XWPFParagraph p : doc.getParagraphs()) {
replaceTextInParagraph(p, data);
}
// 替换表格中的文本
for (XWPFTable table : doc.getTables()) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
for (XWPFParagraph p : cell.getParagraphs()) {
replaceTextInParagraph(p, data);
}
}
}
}
}
private void replaceTextInParagraph(XWPFParagraph paragraph, Map<String, Object> data) {
// 1. 合并段落内所有Run的文本
String fullText = mergeAllRuns(paragraph);
if (!fullText.contains("${")) return;
// 2. 执行全局替换
String newText = replacePlaceholders(fullText, data);
// 3. 清空原有Run的文本(保留样式)
clearRunTexts(paragraph);
// 4. 将新文本写入第一个Run(保留原始格式)
if (!paragraph.getRuns().isEmpty()) {
XWPFRun firstRun = paragraph.getRuns().get(0);
firstRun.setText(newText, 0);
} else {
paragraph.createRun().setText(newText);
}
}
/**
* 正则替换完整文本
*/
private String replacePlaceholders(String text, Map<String, Object> data) {
Pattern pattern = Pattern.compile("\\$\\{(.+?)}");
Matcher matcher = pattern.matcher(text);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String key = matcher.group(1);
Object value = data.getOrDefault(key, "");
matcher.appendReplacement(sb, Matcher.quoteReplacement(value.toString()));
}
matcher.appendTail(sb);
return sb.toString();
}
/**
* 清空所有Run的文本(保留样式)
*/
private void clearRunTexts(XWPFParagraph paragraph) {
for (XWPFRun run : paragraph.getRuns()) {
run.setText("", 0); // 清空文本但保留Run对象
}
}
private void replaceImages(XWPFDocument doc, Map<String, Object> data) throws Exception {
// 1. 处理普通段落
for (XWPFParagraph p : doc.getParagraphs()) {
processParagraphForImages(p, data);
}
// 2. 处理表格内的段落
for (XWPFTable table : doc.getTables()) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
for (XWPFParagraph p : cell.getParagraphs()) {
processParagraphForImages(p, data);
}
}
}
}
}
/**
* 统一处理段落中的图片占位符
*/
private void processParagraphForImages(XWPFParagraph p, Map<String, Object> data) throws Exception {
// 合并段落内所有Run的文本
String mergedText = mergeAllRuns(p);
if (mergedText.isEmpty()) return;
// 正则匹配图片占位符
Matcher matcher = Pattern.compile("\\{\\{图片_(.+?)}}").matcher(mergedText);
if (!matcher.find()) return;
String placeholder = matcher.group(0);
String fieldName = matcher.group(1);
// 清理占位符
clearPlaceholderRuns(p, placeholder);
// 插入图片
if (data.containsKey(fieldName)) {
// String imagePath = (String) data.get(fieldName);
// insertImage(p, imagePath);
String[] imageList = (String[]) data.get(fieldName);
insertImageList(p,imageList);
}
}
private void insertImageList(XWPFParagraph paragraph, String[] imagePaths) throws Exception {
for (String imagePath : imagePaths) {
File imageFile = new File(imagePath);
if (!imageFile.exists()) {
System.out.println("图片文件不存在: " + imagePath);
}
FileInputStream fis = new FileInputStream(imageFile);
byte[] bytes = IOUtils.toByteArray(fis);
fis.close();
int format = getImageFormat(imagePath);
// 添加图片到文档中,返回的是图片ID
String blipId = paragraph.getDocument().addPictureData(bytes, format);
// 创建图片关联的 CTDrawing
int id = paragraph.getDocument().getNextPicNameNumber(format);
XWPFRun run = paragraph.createRun();
int width = 300; // px
int height = 200; // px
int widthEmu = Units.toEMU(width);
int heightEmu = Units.toEMU(height);
String picXml = getPicXml(blipId, widthEmu, heightEmu, id);
// 读取为 CTInline
CTInline inline = run.getCTR().addNewDrawing().addNewInline();
XmlToken xmlToken = XmlToken.Factory.parse(picXml);
inline.set(xmlToken);
// 设置图片的大小和描述
inline.setDistT(0);
inline.setDistB(0);
inline.setDistL(0);
inline.setDistR(0);
CTPositiveSize2D extent = inline.addNewExtent();
extent.setCx(widthEmu);
extent.setCy(heightEmu);
CTNonVisualDrawingProps docPr = inline.addNewDocPr();
docPr.setId(id);
docPr.setName("图片_" + id);
docPr.setDescr("描述_" + id);
// 可选:图片之间加个换行
run.addBreak();
}
}
/**
* 合并段落内所有Run的文本
*/
private String mergeAllRuns(XWPFParagraph paragraph) {
StringBuilder sb = new StringBuilder();
for (XWPFRun run : paragraph.getRuns()) {
String text = run.getText(0);
if (text != null) {
sb.append(text);
}
}
return sb.toString();
}
/**
* 处理占位符跨多个Run的情况,并删除相关Run
*/
private void clearPlaceholderRuns(XWPFParagraph paragraph, String placeholder) {
List<XWPFRun> runs = paragraph.getRuns();
if (runs == null || runs.isEmpty()) {
return;
}
StringBuilder allText = new StringBuilder();
List<Integer> runPositions = new ArrayList<>();
// 收集每个run的起始位置
for (XWPFRun run : runs) {
runPositions.add(allText.length());
String text = run.getText(0);
if (text != null) {
allText.append(text);
}
}
String fullText = allText.toString();
int startIndex = fullText.indexOf(placeholder);
if (startIndex == -1) {
return; // 找不到占位符,不处理
}
int endIndex = startIndex + placeholder.length();
// 找到涉及到的 run 范围
int runStart = -1;
int runEnd = -1;
for (int i = 0; i < runPositions.size(); i++) {
int runPos = runPositions.get(i);
if (runStart == -1 && runPos <= startIndex && (i == runPositions.size() - 1 || runPositions.get(i + 1) > startIndex)) {
runStart = i;
}
if (runPos <= endIndex && (i == runPositions.size() - 1 || runPositions.get(i + 1) >= endIndex)) {
runEnd = i;
break;
}
}
// 删除 run,注意:从后往前删,避免下标错乱
for (int i = runEnd; i >= runStart; i--) {
paragraph.removeRun(i);
}
}
/**
* 获取图片格式类型
*/
private int getImageFormat(String fileName) {
String extension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
switch (extension) {
case "jpg":
case "jpeg": return XWPFDocument.PICTURE_TYPE_JPEG;
case "png": return XWPFDocument.PICTURE_TYPE_PNG;
default: return XWPFDocument.PICTURE_TYPE_JPEG;
}
}
private static String getPicXml(String blipId, int widthEmu, int heightEmu, int id) {
return
"<a:graphic xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\">" +
" <a:graphicData uri=\"http://schemas.openxmlformats.org/drawingml/2006/picture\">" +
" <pic:pic xmlns:pic=\"http://schemas.openxmlformats.org/drawingml/2006/picture\">" +
" <pic:nvPicPr>" +
" <pic:cNvPr id=\"" + id + "\" name=\"Generated\"/>" +
" <pic:cNvPicPr/>" +
" </pic:nvPicPr>" +
" <pic:blipFill>" +
" <a:blip r:embed=\"" + blipId + "\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\"/>" +
" <a:stretch><a:fillRect/></a:stretch>" +
" </pic:blipFill>" +
" <pic:spPr>" +
" <a:xfrm>" +
" <a:off x=\"0\" y=\"0\"/>" +
" <a:ext cx=\"" + widthEmu + "\" cy=\"" + heightEmu + "\"/>" +
" </a:xfrm>" +
" <a:prstGeom prst=\"rect\">" +
" <a:avLst/>" +
" </a:prstGeom>" +
" </pic:spPr>" +
" </pic:pic>" +
" </a:graphicData>" +
"</a:graphic>";
}