最近在实际项目中遇到了 Java 填充 Word 模板的需求,涉及文本、列表以及复选框的勾选状态。这里整理了一套基于 Aspose.Words 和 Poi-tl 的工具方案,记录一下核心实现细节。
一、设置 Word 模板
在 Word 中选中需要填充的位置,点击'插入'->'文档部件'->'域',选择域名(MergeField),填入变量名称即可。
普通字段
直接在文档中插入域代码,例如 ${name}。

填充效果如下:

列表字段
操作与普通字段类似,区别在于需要在首行第一列插入列表开始域,首行最后一列插入结束域,中间放置正常字段。格式规范为:StartTable:<数组字段名> 和 EndTable:<数组字段名>。

复选框
复选框的处理稍微特殊一些,直接通过域可能无法完美控制勾选状态。我们采用占位符配合代码逻辑的方式处理。

二、代码实现
1. 引入依赖
主要使用 Hutool、Poi-tl、Aspose.Words 和 Gson。
<!-- hutool 工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.14</version>
</dependency>
<!-- word 模板数据解析 -->
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.9.0-beta</version>
</dependency>
<!-- word/pdf 操作 -->
<dependency>
<groupId>com.aspose</groupId>
<artifactId>aspose-words</artifactId>
<version>18.8</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.9</version>
</dependency>
2. 资源准备
将模板文件放入项目的 resources 目录下。如果需要使用 Aspose.Words 且不希望出现水印,需配置有效的 License 文件。
注意: 生产环境请务必使用正版授权文件,避免法律风险。将 License.xml 放在 resources 根路径下。
3. 核心代码
实体类定义
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FillWordDTO implements Serializable {
private String name;
private String age;
private String yuyan;
private String yingyu;
private String deyu;
private String fayu;
private String zhengshu;
private String yiji;
private String erji;
private List<ExperienceList> experienceList;
public static FillWordDTO create() {
FillWordDTO fillWordDTO = new FillWordDTO();
fillWordDTO.setName("小王");
fillWordDTO.setAge("18");
// 模拟复选框状态
fillWordDTO.setYuyan("☑");
fillWordDTO.setYingyu("☑");
fillWordDTO.setDeyu("□");
fillWordDTO.setFayu("☑");
fillWordDTO.setZhengshu("☑");
fillWordDTO.setYiji("☑");
fillWordDTO.setErji("□");
fillWordDTO.setExperienceList(Arrays.asList(
new ExperienceList("学校 A", "2020-01-01", "2020-01-01", "备注 A"),
new ExperienceList("学校 B", "2020-01-01", "2020-01-01", "备注 B")
));
return fillWordDTO;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class ExperienceList {
private String school;
private String startTime;
private String endTime;
private String remark;
}
工具类实现
import com.aspose.words.*;
import com.aspose.words.net.System.Data.DataRow;
import com.aspose.words.net.System.Data.DataTable;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.beans.PropertyDescriptor;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ContractUtil {
private ContractUtil() {}
/**
* 调整 BufferedImage 大小
*/
public static BufferedImage resizeBufferedImage(BufferedImage source, int targetW, int targetH, boolean flag) {
int type = source.getType();
BufferedImage target = null;
double sx = (double) targetW / source.getWidth();
double sy = (double) targetH / source.getHeight();
if (flag && sx > sy) {
sx = sy;
targetW = (int) (sx * source.getWidth());
} else if (flag && sx <= sy) {
sy = sx;
targetH = (int) (sy * source.getHeight());
}
if (type == BufferedImage.TYPE_CUSTOM) {
ColorModel cm = source.getColorModel();
WritableRaster raster = cm.createCompatibleWritableRaster(targetW, targetH);
boolean alphaPremultiplied = cm.isAlphaPremultiplied();
target = new BufferedImage(cm, raster, alphaPremultiplied, null);
} else {
target = new BufferedImage(targetW, targetH, type);
}
Graphics2D g = target.createGraphics();
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.drawRenderedImage(source, AffineTransform.getScaleInstance(sx, sy));
g.dispose();
return target;
}
/**
* 填充 Word 模板(Object 数据格式)
*/
public static byte[] fillWordDataByDomain(byte[] modelWordByte, Object obj) {
try {
Class<?> aClass = obj.getClass();
Field[] fields = aClass.getDeclaredFields();
Map<String, Object> data = new HashMap<>(fields.length);
for (Field field : fields) {
PropertyDescriptor pd = new PropertyDescriptor(field.getName(), aClass);
Method method = pd.getReadMethod();
String key = field.getName();
Object value = method.invoke(obj);
if (value != null) {
data.put(key, value);
}
}
return fillWordDataByMap(modelWordByte, data);
} catch (Exception e) {
e.printStackTrace();
return new byte[0];
}
}
/**
* 填充 Word 模板(Map 数据格式)
*/
public static byte[] fillWordDataByMap(byte[] file, Map<String, Object> data) throws Exception {
byte[] ret = null;
if (data == null || data.isEmpty()) {
return ret;
}
try (InputStream is = new ByteArrayInputStream(file);
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
Document doc = new Document(is);
DocumentBuilder builder = new DocumentBuilder(doc);
Map<String, String> toData = new HashMap<>();
for (Map.Entry<String, Object> entry : data.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
// 处理表格数据
if (value instanceof List && !key.equals("checkboxOptions")) {
DataTable dataTable = fillListData((List) value, key, builder);
doc.getMailMerge().executeWithRegions(dataTable);
}
// 图片插入
else if (value instanceof BufferedImage) {
builder.moveToMergeField(key);
builder.insertImage((BufferedImage) value);
}
// 其他普通字段正常填充
else {
String valueStr = String.valueOf(value);
if (value != null && !"null".equals(valueStr)) {
toData.put(key, valueStr);
}
}
}
// 执行普通字段合并
String[] fieldNames = new String[toData.size()];
String[] values = new String[toData.size()];
int i = 0;
for (Map.Entry<String, String> entry : toData.entrySet()) {
fieldNames[i] = entry.getKey();
values[i] = entry.getValue();
i++;
}
doc.getMailMerge().execute(fieldNames, values);
doc.save(out, SaveOptions.createSaveOptions(SaveFormat.DOCX));
ret = out.toByteArray();
}
return ret;
}
/**
* 勾选段落中的复选框字段
*/
private static void checkTheCheckbox(Paragraph paragraph) throws Exception {
FieldCollection fields = paragraph.getRange().getFields();
int count = fields.getCount();
for (int i = 0; i < count; i++) {
com.aspose.words.Field field = fields.get(i);
if (field.getType() == FieldType.FIELD_FORM_CHECK_BOX) {
setCheckboxChecked(field, true);
}
}
}
private static void setCheckboxChecked(com.aspose.words.Field field, boolean checked) throws Exception {
if (checked) {
field.setResult("✓");
} else {
field.setResult("□");
}
}
/**
* 封装 list 数据到 Word 模板中(Word 表格)
*/
private static DataTable fillListData(List<Object> list, String tableName, DocumentBuilder builder) throws Exception {
DataTable dataTable = new DataTable(tableName);
for (Object obj : list) {
DataRow dataRow = dataTable.newRow();
Class<?> objClass = obj.getClass();
Field[] fields = objClass.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
dataTable.getColumns().add(fields[i].getName());
PropertyDescriptor pd = new PropertyDescriptor(field.getName(), objClass);
Method method = pd.getReadMethod();
dataRow.set(i, method.invoke(obj));
}
dataTable.getRows().add(dataRow);
}
return dataTable;
}
/**
* 加载 License
* 由于 Aspose 是收费软件,若没有有效 License,则会出现水印。
*/
static {
try {
InputStream is = ContractUtil.class.getResourceAsStream("/License.xml");
if (is != null) {
License license = new License();
license.setLicense(is);
}
} catch (Exception e) {
// 忽略异常,若无 License 文件则保留默认行为
}
}
}
三、测试
在 main 方法中调用,实际场景可改为返回 Response 或上传至 OSS 后返回链接。
public static void main(String[] args) throws IOException {
FillWordDTO fillWordDTO = FillWordDTO.create();
// 读取模板文件
byte[] modelByte = Files.readAllBytes(Paths.get("src/main/resources/templates/test.docx"));
// 调用工具类
byte[] resultByte = ContractUtil.fillWordDataByDomain(modelByte, fillWordDTO);
// 输出结果
File resultFile = new File("demo.docx");
FileOutputStream fos = new FileOutputStream(resultFile);
fos.write(resultByte);
fos.close();
}
四、注意事项
编辑 Word 域代码时,有时隐藏代码会导致填充失败,报错信息如:
Found end of mail merge region 'experienceList' that does not match start of mail merge region 'jlList'.
排查方法: 在 Word 中依次点击「文件→选项→高级」,在「显示文档内容」区域勾选「显示域代码而非域值」。找到报错的域代码后删除,重新添加正确的域即可解决。

