当遇到要生成一个word文档(证明文件等)的需求时,就可以考虑使用word模板生成.doc和.wps文件
一、需求
1、生成如下这样的订单数据.doc文件,红框部分是变化的,其余部分是固定的
2、生成如下这样的书籍列表,书的个数不固定是动态的。
二、使用Docx4j实现
1、引入依赖
<!-- Docx4j 核心依赖 -->
<dependency>
<groupId>org.docx4j</groupId>
<artifactId>docx4j-core</artifactId>
<version>8.3.0</version> <!-- 兼容 JDK 1.8 的稳定版本 -->
</dependency>
<!-- 处理变量替换所需依赖 -->
<dependency>
<groupId>org.docx4j</groupId>
<artifactId>docx4j-JAXB-ReferenceImpl</artifactId>
<version>8.3.0</version>
</dependency>
<!-- Velocity模板引擎 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>org.docx4j</groupId>
<artifactId>docx4j-JAXB-Internal</artifactId>
<version>8.3.0</version>
</dependency>
注: 前两个完成第一个需求就够了,后两个使用模板完成后一个需求
2、制作模板
变量的地方用${}
模板contract_template.docx内容如下:
模板book_template.docx内容如下:
使用foreach进行循环
3、代码
Docx4jService类
package com.example.demo.demos;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;
import org.docx4j.wml.Document;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import javax.xml.bind.JAXBElement;
import java.io.ByteArrayOutputStream;
import java.util.List;
import java.util.Map;
@Service
public class Docx4jService {
/**
* 根据模板生成Word文档(修复没有write方法的问题)
*/
public byte[] generateWord(String templatePath, Map<String, String> data) throws Exception {
// 1. 加载模板
ClassPathResource resource = new ClassPathResource(templatePath);
WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.load(resource.getInputStream());
MainDocumentPart mainDocumentPart = wordMLPackage.getMainDocumentPart();
// 2. 获取文档XML内容
Document document = mainDocumentPart.getJaxbElement();
List<Object> content = document.getBody().getContent();
// 3. 手动替换变量
replaceVariables(content, data);
// 4. 写回并生成字节数组(使用save方法替代write方法)
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
// 关键修复:用save方法替代write方法
wordMLPackage.save(out);
return out.toByteArray();
}
}
/**
* 递归替换文档中的变量(修复Text节点无法识别问题)
*/
@SuppressWarnings("unchecked")
private void replaceVariables(List<Object> content, Map<String, String> data) {
for (int i = 0; i < content.size(); i++) {
Object obj = content.get(i);
// 处理JAXB包装的元素(解开外层包装)
if (obj instanceof JAXBElement) {
obj = ((JAXBElement<?>) obj).getValue();
}
// 关键修复:处理运行元素(R),Text节点一定在R里面
if (obj instanceof org.docx4j.wml.R) {
org.docx4j.wml.R run = (org.docx4j.wml.R) obj;
// 遍历R元素中的内容,寻找Text节点
replaceVariables(run.getContent(), data);
}
// 现在可以找到Text节点了
else if (obj instanceof org.docx4j.wml.Text) {
org.docx4j.wml.Text text = (org.docx4j.wml.Text) obj;
String value = text.getValue();
// 遍历所有变量进行替换
for (Map.Entry<String, String> entry : data.entrySet()) {
String key = "${" + entry.getKey() + "}";
if (value.contains(key)) {
value = value.replace(key, entry.getValue());
text.setValue(value);
}
}
}
// 处理段落(P):继续遍历段落中的内容(会包含R元素)
else if (obj instanceof org.docx4j.wml.P) {
org.docx4j.wml.P p = (org.docx4j.wml.P) obj;
replaceVariables(p.getContent(), data);
}
// 处理表格(Tbl):继续遍历表格中的内容
else if (obj instanceof org.docx4j.wml.Tbl) {
org.docx4j.wml.Tbl tbl = (org.docx4j.wml.Tbl) obj;
replaceVariables(tbl.getContent(), data);
}
}
}
}
Book类
package com.example.demo.demos;
public class Book {
private String name;
private String author;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public Book(String name, String author) {
this.name = name;
this.author = author;
}
}
WordController类
package com.example.demo.demos;
import org.docx4j.jaxb.Context;
import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.stream.StreamSource;
import java.io.*;
import java.net.URLEncoder;
import java.util.*;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.docx4j.Docx4J;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.wml.Document;
@RestController
@RequestMapping("/word")
public class WordController {
@Autowired
private Docx4jService docx4jService;
/**
* 生成并下载 Word 文档
*/
@GetMapping("/generate")
public ResponseEntity<byte[]> generateWord() throws Exception {
// 1. 准备模板变量数据
Map<String, String> data = new HashMap<>();
data.put("username", "张三");
data.put("orderNo", "ORD20250911001");
data.put("amount", "1000.00");
data.put("date", "2025-09-11");
// 2. 调用服务生成 Word 字节数组
byte[] wordBytes = docx4jService.generateWord("templates/contract_template.docx", data);
// 3. 配置响应头,实现文件下载
String fileName = URLEncoder.encode("订单详情.docx", "UTF-8");
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition", "attachment; filename=" + fileName);
headers.add("Content-Type", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
return new ResponseEntity<>(wordBytes, headers, HttpStatus.OK);
}
static {
try {
Properties props = new Properties();
props.setProperty("resource.loader", "class");
props.setProperty("class.resource.loader.class",
"org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
Velocity.init(props);
} catch (Exception e) {
throw new RuntimeException("Velocity引擎初始化失败", e);
}
}
@GetMapping("/export")
public void exportBooks(HttpServletResponse response) {
try (InputStream templateStream = new ClassPathResource("templates/book_template.docx").getInputStream()) {
WordprocessingMLPackage template = WordprocessingMLPackage.load(templateStream);
MainDocumentPart mainPart = template.getMainDocumentPart();
//生成books内容
List<Book> books = new ArrayList<>();
for (int i = 1; i <= 3; i++) {
books.add(new Book("Java开发指南" + i, "作者" + i));
}
VelocityContext context = new VelocityContext();
context.put("books", books);
String xmlContent = mainPart.getXML();
StringWriter writer = new StringWriter();
Velocity.evaluate(context, writer, "bookTemplate", xmlContent);
String processedXml = writer.toString();
// 过滤非法标签
String cleanedXml = cleanSdtElements(processedXml);
// 替换主文档内容(使用docx4j的JAXB工具类)
replaceMainDocumentContent(mainPart, cleanedXml);
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
Docx4J.save(template, baos);
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
String fileName = new String("1.docx".getBytes("UTF-8"), "ISO-8859-1");
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
response.setContentLength(baos.size());
baos.writeTo(response.getOutputStream());
response.flushBuffer();
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 清理内容控件相关标签
private String cleanSdtElements(String xml) {
xml = xml.replaceAll("<w:sdt[^>]*>", "");
xml = xml.replaceAll("<w:sdtPr[^>]*>", "");
xml = xml.replaceAll("<w:dataBinding[^>]*>", "");
xml = xml.replaceAll("<w:sdtContent[^>]*>", "");
xml = xml.replaceAll("</w:sdt>", "");
xml = xml.replaceAll("</w:sdtPr>", "");
xml = xml.replaceAll("</w:dataBinding>", "");
xml = xml.replaceAll("</w:sdtContent>", "");
return xml;
}
// 关键修复:使用docx4j提供的Context获取JAXBContext
private void replaceMainDocumentContent(MainDocumentPart mainPart, String xmlContent)
throws JAXBException, XMLStreamException, UnsupportedEncodingException {
// 使用docx4j的Context工具类获取预配置的JAXBContext
Unmarshaller u = Context.jc.createUnmarshaller();
XMLInputFactory xif = XMLInputFactory.newFactory();
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
try (ByteArrayInputStream bais = new ByteArrayInputStream(xmlContent.getBytes("UTF-8"))) {
XMLStreamReader xsr = xif.createXMLStreamReader(new StreamSource(bais));
Document doc = (Document) u.unmarshal(xsr);
mainPart.setJaxbElement(doc);
} catch (IOException e) {
throw new RuntimeException("XML内容转换失败", e);
}
}
}
4、整体代码结构
5、使用postman 调用controller的接口就能下载到从模板生成的word文档