第一次遇到这种奇葩需求,正常导出 Excel 后单元格一般自动为“常规”并不影响使用,结果需求一定要导出为“文本”。  
一开始我以为只是给几列加个样式就行,后来发现是**整张表所有 sheet、所有单元格**都要是文本格式,而且还要保证导出的内容显示不变。

这篇记录下我最终落地的方案,给同样踩坑的同学一个可复用模板。

 一、问题本质

Excel 里“常规”看起来没问题,但在业务场景下经常翻车:

- 身份证、银行卡这类长数字可能被转科学计数法
- 前导 0 被吞(如编码 `00123` 变 `123`)
- 某些日期/编号被自动识别成日期格式
- 上游系统二次导入时类型校验失败

所以需求本质是:  
**导出的最终 xlsx 文件中,单元格 dataFormat 必须是 `@`(文本)。**

 二、常见误区

很多人会这么做:

cell.setCellValue("123456");

只写字符串值并不一定够。  
如果单元格样式还是“常规”,Excel 打开时仍可能自动推断类型。

正确做法是:  
**值写字符串 + 样式设文本格式**,两步都要做。

三、我的落地方案(导出后统一文本化)

考虑项目里有两种导出路径(POI 手工写模板、ExcelUtils 对象导出),我采用统一后处理:

1. 先按原流程导出 xlsx
2. 再重新读取该文件
3. 遍历所有 sheet/row/cell
4. 将样式 dataFormat 设为 `@`
5. 用 `DataFormatter` 取当前显示值,再以字符串写回
6. 覆盖写回文件

这样不用入侵每个导出分支,兼容性最好。

 四、核心代码

 

private void forceExcelCellsAsText(String outputPath) {
    Workbook workbook = null;
    try (InputStream inputStream = new FileInputStream(outputPath)) {
        workbook = WorkbookFactory.create(inputStream);
        short textFormat = workbook.createDataFormat().getFormat("@");
        Map<Short, CellStyle> styleMap = new HashMap<>();
        DataFormatter formatter = new DataFormatter();

        for (int sheetIndex = 0; sheetIndex < workbook.getNumberOfSheets(); sheetIndex++) {
            Sheet sheet = workbook.getSheetAt(sheetIndex);
            for (Row row : sheet) {
                for (Cell cell : row) {
                    String value = formatter.formatCellValue(cell);
                    CellStyle textStyle = getTextStyle(workbook, cell.getCellStyle(), textFormat, styleMap);
                    cell.setCellStyle(textStyle);
                    cell.setCellValue(value);
                }
            }
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }

    try (FileOutputStream outputStream = new FileOutputStream(outputPath)) {
        workbook.write(outputStream);
    } catch (Exception e) {
        throw new RuntimeException(e);
    } finally {
        if (workbook != null) {
            try {
                workbook.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

private CellStyle getTextStyle(Workbook workbook, CellStyle baseStyle, short textFormat, Map<Short, CellStyle> styleMap) {
    short styleIndex = baseStyle == null ? -1 : baseStyle.getIndex();
    if (styleMap.containsKey(styleIndex)) {
        return styleMap.get(styleIndex);
    }
    CellStyle textStyle = workbook.createCellStyle();
    if (baseStyle != null) {
        textStyle.cloneStyleFrom(baseStyle);
    }
    textStyle.setDataFormat(textFormat);
    styleMap.put(styleIndex, textStyle);
    return textStyle;
}

五、为什么要用 `DataFormatter`

如果你直接 `cell.toString()`,在日期、公式、数值场景里容易出现显示值变化。  
`DataFormatter` 能拿到接近“Excel界面展示值”的字符串,作为写回值更稳。

六、性能与边界注意事项

- 大文件建议分块导出后再文本化,避免一次性处理超大工作簿
- 样式对象要缓存(上面 `styleMap`),否则会创建过多样式导致性能下降
- 如果有公式单元格,文本化后会变成公式结果文本(不再保留公式语义),要提前和业务确认

七、总结

这类需求本质不是“导出”,而是“**导出结果的类型一致性控制**”。  
实践结论就一句话:

> 想让 Excel 真正稳定地“全单元格文本”,请在导出后做一次统一文本化处理,而不是只改少量列或只改 setCellValue。

如果你也在做类似需求,可以直接复用这套“导出后文本化”的方案,兼容 POI 和模板导出两种路径。

扩展

Apache POI 里常用的 DataFormat

- 文本: "@"
- 整数: "0"
- 两位小数: "0.00"
- 千分位整数: "#,##0"
- 千分位两位小数: "#,##0.00"
- 百分比(无小数): "0%"
- 百分比(两位小数): "0.00%"
- 科学计数法: "0.00E+00"
- 分数: "# ?/?"
- 日期(年-月-日): "yyyy-mm-dd"
- 日期时间: "yyyy-mm-dd hh:mm:ss"
- 时间: "hh:mm:ss"
- 会计格式(可自定义更复杂):如 "_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)"

DataFormat df = workbook.createDataFormat();
short textFmt = df.getFormat("@");
short numFmt = df.getFormat("#,##0.00");
short dateFmt = df.getFormat("yyyy-mm-dd");

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐