在 Linux 容器里优雅地导出 Excel:彻底摆脱 fontconfig 的曲折之旅

1. 背景:为什么容器里会突然报 fontconfig

  • FastExcel / EasyExcel流式写 (SXSSF) 时,内部会调用
    sheet.trackAllColumnsForAutoSizing()
  • 该方法立即 new AutoSizeColumnTrackerjava.awt.Font
    FontManagerfontconfig
  • 精简 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 == null NPE
  • 逻辑只依赖 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. 小结

  1. 罪魁祸首sheet.trackAllColumnsForAutoSizing()

  2. 核心对策:让代码路径不走这行(内存模式 / 模板 / fork)

  3. 列宽方案:自写 Handler 字符计数,既轻量又“够好看”

  4. FastExcel & EasyExcel 同理,只要避开 track…() 就再也不会碰字体

  5. 最终好处

    • 镜像 不再安装字体包,体积更小
    • Alpine、Distroless、Oracle Linux Slim 部署 稳定无错
    • 仍保留 SXSSF 的低内存优势(路线 B/C)

一句话总结
把“想让服务器自己测列宽”的念头交给客户端(Excel),
或者用最简单的字符长度估算——
只要在服务端彻底绕开 AutoSizeColumnTracker
fontconfig 就会永远消失

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容