EasyExcel 模板填充(Fill)
官方文档:https://easyexcel.opensource.alibaba.com/docs/current/quickstart/fill 官方示例:https://github.com/alibaba/easyexcel/blob/master/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/demo/fill/FillTest.java 当前版本:
com.alibaba:easyexcel:4.0.3(Apache-2.0,仓库已归档,仅做 Bug 修复)
本技能专注于 "按预制模板填充" 场景。当用户给出一份已经画好样式的 .xlsx 模板,要求把数据库/接口里的数据填进去(工资条、合同、发票、报表、统计单等),应当使用本技能。
Capability Boundaries
✅ 强项
- 简单对象填充(Map / 单个 DTO)
- 列表填充(模板
{.}语法,单向纵向) - 复杂填充(list 后还有内容 →
forceNewRow=true) - 大数据量复杂填充(list 必须放最后一行,后续统计行用
write追加) - 横向多列组合填充(
WriteDirectionEnum.HORIZONTAL+FillWrapper) - 模板转义字符处理(
\{\}) - Spring Boot 集成(模板文件可来自 resources/ 或 OSS)
⚠️ 限制
- 仅支持
.xlsx(07 版),03 版(.xls)不支持复杂/大数据量填充 forceNewRow=true会全量驻内存,数据量大时慎用withTemplate模板会全量加载到内存,模板文件过大(>几十 MB)会 OOM- Map 填充 list 时必须包含所有 list 的 key,否则报 NPE
❌ Out of Scope(不该用本技能的场景,请改用其它技能)
- 不基于模板的程序化生成 Excel(无预制模板)→ 不适用本技能,使用
easyexcel-write - 读取 Excel 数据 → 不适用本技能,使用
easyexcel-read - CSV 文件处理 → 不适用本技能,自行使用 commons-csv / OpenCSV
- Word/PPT → 不适用本技能,使用 Apache POI / Apache POI-XML
Data Privacy
本技能不收集、存储或传输任何用户数据。所有代码示例仅用于本地开发参考。
When to use this skill
- 用户说"按模板生成 Excel"、"填充 Excel 模板"、"导出工资条/合同/发票"
- 用户提供
.xlsx模板文件,要求把数据写进变量占位符 - 报表存在固定格式,需要保留样式/合并单元格/函数/图表
- 业务上有"先 fill 列表,再追加合计行/统计行"的需求
Quick Start
典型调用方式(4 选 1,按文件大小递增):
// 方案 A:最简单的对象填充(since 2.1.1)
String fileName = "output_simple.xlsx";
EasyExcel.write(fileName).withTemplate("templates/simple.xlsx")
.sheet().doFill(fillData);
// 方案 B:列表填充(一次放入内存)
EasyExcel.write(fileName).withTemplate("templates/list.xlsx")
.sheet().doFill(list);
// 方案 C:列表分次填充(文件缓存,省内存)
try (ExcelWriter excelWriter = EasyExcel.write(fileName)
.withTemplate("templates/list.xlsx").build()) {
WriteSheet writeSheet = EasyExcel.writerSheet().build();
excelWriter.fill(list, writeSheet); // 多次 fill,自动文件缓存
excelWriter.fill(list, writeSheet);
}
// 方案 D:先 fill 列表 + 后 write 追加合计行(推荐:复杂报表 + 大数据量)
try (ExcelWriter excelWriter = EasyExcel.write(fileName)
.withTemplate("templates/complex.xlsx").build()) {
WriteSheet writeSheet = EasyExcel.writerSheet().build();
excelWriter.fill(list, writeSheet); // 1. 填列表
excelWriter.fill(headerMap, writeSheet); // 2. 填头部变量
excelWriter.write(totalListList, writeSheet);// 3. 手动追加合计行
}
Workflow
Step 1. 确认场景 — 用户是否提供了 .xlsx 模板?模板里是否有 {var} {.} {prefix.} 占位符?
Step 2. 评估数据量 — < 1 万行:方案 A/B;> 1 万行:方案 C/D(list 必须放最后一行)
Step 3. 选择填充模式 — 单对象 / 列表 / 复杂 / 横向多列
Step 4. 构造数据 — Map / DTO / List;Map 填充 list 必须包含所有 key
Step 5. 执行填充 — try-with-resources 关闭 ExcelWriter
Step 6. 验证输出 — 打开生成的 .xlsx 检查占位符是否被替换、有无残留 {.} {}
Critical: 模板语法速查
| 模板占位符 | 含义 | Java 数据结构 |
|---|---|---|
{name} {number} {date} |
单值占位符 | Map.put("name", value) 或 DTO 字段 |
{.} |
列表占位符(默认纵向) | List<DTO> 整体作为 fill 第一个参数 |
{data1.} {data2.} |
带前缀的列表(多列表区分) | new FillWrapper("data1", list) |
\{ \} |
转义:当模板中真有 { } 字符时 |
—— |
官方原文:"{} 普通变量,{.} list 的变量。如果本来就有'{'、'}' 特殊字符 用'\{'、'\}' 代替。{.前缀可以区分不同的list。"
Critical: 4 种填充场景完整示例
场景 1:简单对象填充
DTO:
@Getter
@Setter
@EqualsAndHashCode
public class FillData {
private String name;
private double number;
private Date date;
}
模板(templates/simple.xlsx):单元格含 {name} {number} {date}
代码:
@Test
public void simpleFill() {
String templateFileName = "templates/simple.xlsx";
String fileName = "output_simpleFill_" + System.currentTimeMillis() + ".xlsx";
// 方案 1:根据对象填充
FillData fillData = new FillData();
fillData.setName("张三");
fillData.setNumber(5.2);
EasyExcel.write(fileName).withTemplate(templateFileName).sheet().doFill(fillData);
// 方案 2:根据 Map 填充
Map<String, Object> map = new HashMap<>();
map.put("name", "张三");
map.put("number", 5.2);
EasyExcel.write(fileName).withTemplate(templateFileName).sheet().doFill(map);
}
场景 2:列表填充(模板 {.} 占位一行)
模板(templates/list.xlsx):一行单元格含 {.} 表示 list 起点
代码:
@Test
public void listFill() {
String templateFileName = "templates/list.xlsx";
String fileName = "output_listFill_" + System.currentTimeMillis() + ".xlsx";
// 方案 1:一次性放入内存
EasyExcel.write(fileName).withTemplate(templateFileName).sheet().doFill(data());
// 方案 2:分次填充(文件缓存,省内存,推荐)
try (ExcelWriter excelWriter = EasyExcel.write(fileName)
.withTemplate(templateFileName).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet().build();
excelWriter.fill(data(), writeSheet);
excelWriter.fill(data(), writeSheet);
}
}
官方提示:"分多次 填充 会使用文件缓存"
场景 3:复杂填充(list 后还有合计行/总计行)
场景特征:模板中 {.} 列表行下面还有非列表内容(如"总计:xxx")。
@Test
public void complexFill() {
String templateFileName = "templates/complex.xlsx";
String fileName = "output_complexFill_" + System.currentTimeMillis() + ".xlsx";
try (ExcelWriter excelWriter = EasyExcel.write(fileName)
.withTemplate(templateFileName).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet().build();
// ⚠️ 关键:forceNewRow=true 才能让 list 后面再追加内容
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
excelWriter.fill(data(), fillConfig, writeSheet);
excelWriter.fill(data(), fillConfig, writeSheet);
Map<String, Object> map = new HashMap<>();
map.put("date", "2019年10月9日13:28:28");
map.put("total", 1000);
excelWriter.fill(map, writeSheet);
}
}
官方原文:"在 02 版上面 模板 list 不是最后一行 下面还有数据 需要设置
forceNewRow=true但是有一个缺点 就是他会把所有的数据都放到内存了 慎用" 官方原文:"如果数据量大 list不是最后一行 参照下一个(complexFillWithTable)"
场景 4:数据量大的复杂填充(list 放最后一行 + write 追加)
最佳实践模板设计:删除模板中 list 之后的"合计行"占位行,让 list 占最后一行;后续"统计行"用 write 手动追加。
@Test
public void complexFillWithTable() {
String templateFileName = "templates/complexFillWithTable.xlsx";
String fileName = "output_complexFillWithTable_" + System.currentTimeMillis() + ".xlsx";
try (ExcelWriter excelWriter = EasyExcel.write(fileName)
.withTemplate(templateFileName).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet().build();
// 1. 写入 list 数据
excelWriter.fill(data(), writeSheet);
excelWriter.fill(data(), writeSheet);
// 2. 写入 list 之前的变量
Map<String, Object> map = new HashMap<>();
map.put("date", "2019年10月9日13:28:28");
excelWriter.fill(map, writeSheet);
// 3. list 后面手动 write 统计行
List<List<String>> totalListList = new ArrayList<>();
List<String> totalList = new ArrayList<>();
totalListList.add(totalList);
totalList.add(null);
totalList.add(null);
totalList.add(null);
totalList.add("统计:1000");
excelWriter.write(totalListList, writeSheet);
}
}
官方原文:"这里是write 别和fill 搞错了"
场景 5:横向填充(多列横向并列)
@Test
public void horizontalFill() {
String templateFileName = "templates/horizontal.xlsx";
String fileName = "output_horizontalFill_" + System.currentTimeMillis() + ".xlsx";
try (ExcelWriter excelWriter = EasyExcel.write(fileName)
.withTemplate(templateFileName).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet().build();
// 横向:列方向展开
FillConfig fillConfig = FillConfig.builder()
.direction(WriteDirectionEnum.HORIZONTAL).build();
excelWriter.fill(data(), fillConfig, writeSheet);
excelWriter.fill(data(), fillConfig, writeSheet);
Map<String, Object> map = new HashMap<>();
map.put("date", "2019年10月9日13:28:28");
excelWriter.fill(map, writeSheet);
}
}
场景 6:多列表组合填充(同时填多个 list)
模板:包含 {data1.} {data2.} {data3.} 三种带前缀的列表
@Test
public void compositeFill() {
String templateFileName = "templates/composite.xlsx";
String fileName = "output_compositeFill_" + System.currentTimeMillis() + ".xlsx";
try (ExcelWriter excelWriter = EasyExcel.write(fileName)
.withTemplate(templateFileName).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet().build();
FillConfig fillConfig = FillConfig.builder()
.direction(WriteDirectionEnum.HORIZONTAL).build();
// ⚠️ 关键:多个 list 必须用 FillWrapper 包裹,prefix 必须与模板占位符前缀一致
excelWriter.fill(new FillWrapper("data1", data()), fillConfig, writeSheet);
excelWriter.fill(new FillWrapper("data2", data()), writeSheet); // data2 纵向
excelWriter.fill(new FillWrapper("data3", data()), writeSheet);
Map<String, Object> map = new HashMap<>();
map.put("date", new Date());
excelWriter.fill(map, writeSheet);
}
}
官方原文:"多个 list 必须用
FillWrapper包裹;{前缀.}前缀可以区分不同的 list;不同 list 可以指定不同方向(HORIZONTAL/VERTICAL)"
Critical: Spring Boot 集成
1) 模板放在 resources/templates/
@Service
public class SalaryFillService {
public void fillSalary(String employeeId, OutputStream out) {
InputStream templateIn = getClass()
.getResourceAsStream("/templates/salary.xlsx");
SalaryDTO data = salaryMapper.findByEmployee(employeeId);
// 模板以流方式加载,避免临时文件
try (ExcelWriter writer = EasyExcel.write(out)
.withTemplate(templateIn).build()) {
WriteSheet sheet = EasyExcel.writerSheet().build();
writer.fill(data, sheet);
}
}
}
2) Controller 返回
@GetMapping("/export/salary/{employeeId}")
public void exportSalary(@PathVariable String employeeId,
HttpServletResponse response) throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("工资条", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
salaryFillService.fillSalary(employeeId, response.getOutputStream());
}
3) 复杂场景:工资条批量打包
public void batchExportSalary(List<String> employeeIds, OutputStream zipOut) {
try (ZipOutputStream zos = new ZipOutputStream(zipOut);
ExcelWriter writer = EasyExcel.write(/* unused */).build()) { // 实际逐个写入临时文件
for (String id : employeeIds) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
fillSalary(id, baos);
ZipEntry entry = new ZipEntry("salary_" + id + ".xlsx");
zos.putNextEntry(entry);
zos.write(baos.toByteArray());
zos.closeEntry();
}
}
}
Critical: 业务模板设计规范
| 业务场景 | 推荐模板设计 | 关键代码 |
|---|---|---|
| 工资条 | 单对象 {name} {base} {bonus} {total} |
简单填充 方案 1 |
| 员工合同 | 头部 {company} {name} {id} + 列表 {items.} |
简单 + 列表 |
| 对账单 | 头部 {date} {account} + 列表 {details.} + 底部"合计" |
complexFillWithTable(先 fill 列表再 write 合计行) |
| 财务报表 | 横向多列 {months.} + 纵向 {rows.} |
compositeFill(多个 FillWrapper) |
| 发票 | 单对象 + 函数 =SUM() |
简单填充(公式由模板预置) |
| 商品清单 | 列表 {products.} + 图片 |
列表 + 图片(图片需读为 byte[]) |
| 汇总表(按月) | 头部 {year} + 列表 {data1.}(横向多列展开) |
horizontalFill |
Gotchas(常见错误)
| # | 错误 | 正确做法 |
|---|---|---|
| 1 | Map 填充 list 漏掉某些 key | Map 填充时必须 put 所有 list 的 key(可 value=null) |
| 2 | 模板有 { } 字符未转义 |
用 \{ \} 转义 |
| 3 | 复杂填充忘记 forceNewRow=true |
list 下面还有内容时必须设置 |
| 4 | forceNewRow=true 用在百万级数据 |
改用 complexFillWithTable(list 放最后一行) |
| 5 | 03 版(.xls)做复杂填充 |
改用 07 版(.xlsx) |
| 6 | list 后追加合计行用 fill |
用 write("这里是write 别和fill 搞错了") |
| 7 | 横向多列忘了 FillWrapper |
多 list 必须用 new FillWrapper("prefix", list) |
| 8 | 横向多列方向忘了配 | 必须 FillConfig.builder().direction(HORIZONTAL) |
| 9 | 模板文件 withTemplate(InputStream) 用完未关 |
模板流是缓存的,try-with-resources 关闭 |
| 10 | 关闭 ExcelWriter 时漏掉 finish() |
用 try-with-resources 让 ExcelWriter.close() 触发 finish |
| 11 | 模板文件 > 几十 MB | 改用多个小模板分批填充 |
| 12 | 模板变量写错(大小写 / 漏 { }) |
模板变量名必须与 Map key / DTO 字段完全一致 |
Key Classes / Methods
| 类型 | 名称 | 用途 |
|---|---|---|
| 入口 | EasyExcel.write(path).withTemplate(template) |
指定模板路径或流 |
| 入口 | EasyExcel.write(OutputStream).withTemplate(stream) |
Web 输出流场景 |
| 核心 | ExcelWriter |
多次 fill 的容器,需 try-with-resources |
| 核心 | WriteSheet |
单个 sheet 配置 |
| 配置 | FillConfig.builder().forceNewRow(true) |
复杂填充(list 后还有内容) |
| 配置 | FillConfig.builder().direction(HORIZONTAL) |
横向填充 |
| 包装 | FillWrapper("prefix", list) |
多列表分组,前缀匹配模板 {prefix.} |
| 工具 | EasyExcel.writerSheet().build() |
创建默认 WriteSheet |
| 工具 | EasyExcel.writerSheet(0, "sheet名").build() |
指定 sheetNo 和名称 |
References
- 官方文档:https://easyexcel.opensource.alibaba.com/docs/current/quickstart/fill
- 官方示例:https://github.com/alibaba/easyexcel/blob/master/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/demo/fill/FillTest.java
- 项目主页:https://easyexcel.opensource.alibaba.com/
- GitHub:https://github.com/alibaba/easyexcel
- API 参考:https://easyexcel.opensource.alibaba.com/docs/current/api/
- references/api-reference.md — 完整 API 与注解索引
- references/template-design.md — 模板设计规范与真实业务模板
- references/business-scenarios.md — 工资条/合同/对账单/发票等真实案例
- examples/real-cases.md — 6 种填充模式完整可复制代码
- examples/spring-boot-integration.md — Spring Boot 集成完整示例