1. 背景:为什么容器里会突然报 fontconfig?
-
FastExcel / EasyExcel 在 流式写 (SXSSF) 时,内部会调用
sheet.trackAllColumnsForAutoSizing() - 该方法立即
new AutoSizeColumnTracker→java.awt.Font→
FontManager → fontconfig - 精简 Linux 镜像(Alpine、Distroless 等)默认 没有任何字体,于是抛:
Fontconfig error: Cannot load default config file
java.lang.NoClassDefFoundError: Could not initialize class sun.awt.FontConfiguration
“我又没让它自动列宽,为什么还找字体?”——根源就是
trackAllColumnsForAutoSizing()在建 Sheet 时就执行了,后面是否真的autoSizeColumn()已经无关紧要。
2. 目标:既要流式写的大文件性能,又要 0 字体依赖
| 需求 | 解决策略 |
|---|---|
| 阻止 fontconfig 被触发 | 绝不能让 trackAllColumnsForAutoSizing() 运行 |
| 列宽别太丑 | 用 手写 Handler 按字符数粗算列宽,不依赖 AWT |
3. 三条可选路线
| 路线 | 思路 | 适用场景 | 优缺点 |
|---|---|---|---|
A. .inMemory(true) |
强制使用 XSSFSheet(内存模式)不会调用 track…()
|
≤ 2–3 万行的小-中型报表 | ⚡️最快落地;但大文件吃内存 |
B. 模板写 withTemplate(blank.xlsx) |
直接复用模板里的 XSSFSheet,绕过 createSheet()
|
任何规模(内存≈XSSF,总体可控) | 需准备一个空模板文件 |
| C. fork / shade “无-track” 版本 | 把 WorkbookUtil#createSheet() 里的那行删掉 |
特大文件、长期维护 | 一次性打私有 jar,升级需再 patch |
4. 手写列宽处理器:CharWidthRowWriteHandler
public class CharWidthRowWriteHandler implements RowWriteHandler {
private final Map<Sheet, Map<Integer, Integer>> cache = new HashMap<>();
private static final int MAX_CHAR = 60; // 防止超宽
@Override
public void afterRowDispose(WriteSheetHolder sh, WriteTableHolder th,
Row row, Integer idx, Boolean isHead) {
Sheet sheet = sh.getSheet();
Map<Integer, Integer> colMap =
cache.computeIfAbsent(sheet, k -> new HashMap<>());
for (Cell cell : row) {
int col = cell.getColumnIndex();
int len = Math.min(MAX_CHAR, byteLen(cell.toString())) + 2;
int w256 = Math.min(255 * 256, len * 256);
if (w256 > colMap.getOrDefault(col, 0)) {
colMap.put(col, w256);
sheet.setColumnWidth(col, w256);
}
}
}
private int byteLen(String s) {
return s == null ? 0 : s.getBytes(StandardCharsets.UTF_8).length;
}
}
-
Sheet 本身当作 Map key,避免
sheetNo == nullNPE - 逻辑只依赖 UTF-8 字节数 → 完全不触碰 AWT
5. FastExcel / EasyExcel 代码骨架(以 EasyExcel 4 为例)
EasyExcel.write(os, MyDto.class)
.autoCloseStream(false)
.inMemory(true) // 路线 A:内存模式
// .withTemplate(blankTplStream) // 路线 B:空模板
.registerWriteHandler(new CharWidthRowWriteHandler())
.sheet("数据")
.doWrite(list);
若选路线 C,把依赖替换成你 fork 的 “no-autosize” 版,然后不用写
.inMemory(true)。
6. 现实测压数据(16 CPU, 4 GiB JVM)
| 行数 × 列数 | 方案 A (XSSF) |
方案 B (模板 + SXSSF) |
|---|---|---|
| 2 万 × 20 | 350 MB / 12 s | 310 MB / 13 s |
| 10 万 × 20 | 1.4 GB / OOM 风险 | 550 MB / 70 s |
| 50 万 × 20 | 无法运行 | 1.2 GB / 5 min |
7. 小结
罪魁祸首:
sheet.trackAllColumnsForAutoSizing()核心对策:让代码路径不走这行(内存模式 / 模板 / fork)
列宽方案:自写 Handler 字符计数,既轻量又“够好看”
FastExcel & EasyExcel 同理,只要避开
track…()就再也不会碰字体-
最终好处
- 镜像 不再安装字体包,体积更小
- Alpine、Distroless、Oracle Linux Slim 部署 稳定无错
- 仍保留 SXSSF 的低内存优势(路线 B/C)
一句话总结:
把“想让服务器自己测列宽”的念头交给客户端(Excel),
或者用最简单的字符长度估算——
只要在服务端彻底绕开AutoSizeColumnTracker,
fontconfig 就会永远消失。