欢迎转载,原文链接:https://corningsun.github.io/Apache-POI-SXSSF-RichText-Bold/
先说结论:失效的不是整个单元格的 CellStyle,而是单元格内部的富文本字体片段。SXSSFWorkbook 默认使用 inline string,写出时只保留了纯文字,没有把 XSSFRichTextString 中的多个 text run 写入 xlsx。解决办法是让 SXSSF 使用 Shared Strings Table,或者在数据量允许时改用 XSSFWorkbook。
先固定现场,不急着改代码
复现项目 test_jdk17_poi 的环境很简单:
- JDK 17
- Apache POI 5.5.1
- Maven 3.9+
为了让读者不依赖本地复现项目,下面给出两个可独立运行的完整示例。两段代码都生成“标题加粗:普通文本”,区别只有 Workbook 的实现。
流式版本:PoiRichTextDemo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| package com.example;
import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path;
import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Font; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook; import org.apache.poi.xssf.usermodel.XSSFRichTextString;
public final class PoiRichTextDemo { private PoiRichTextDemo() { }
public static void main(String[] args) throws IOException { Path outputPath = Path.of("target", "sxssf-demo.xlsx"); Files.createDirectories(outputPath.getParent());
try (SXSSFWorkbook workbook = new SXSSFWorkbook()) { Sheet sheet = workbook.createSheet("demo"); Row row = sheet.createRow(0); Cell cell = row.createCell(0);
String boldText = "标题加粗"; String normalText = ":普通文本";
Font boldFont = workbook.createFont(); boldFont.setBold(true);
XSSFRichTextString richText = new XSSFRichTextString(boldText + normalText); richText.applyFont(0, boldText.length(), boldFont); cell.setCellValue(richText);
try (OutputStream outputStream = Files.newOutputStream(outputPath)) { workbook.write(outputStream); } }
System.out.println("Generated: " + outputPath.toAbsolutePath()); } }
|
非流式对照版本:PoiRichTextXssfDemo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| package com.example;
import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path;
import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Font; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
public final class PoiRichTextXssfDemo { private PoiRichTextXssfDemo() { }
public static void main(String[] args) throws IOException { Path outputPath = Path.of("target", "xssf-demo.xlsx"); Files.createDirectories(outputPath.getParent());
try (XSSFWorkbook workbook = new XSSFWorkbook()) { Sheet sheet = workbook.createSheet("demo"); Row row = sheet.createRow(0); Cell cell = row.createCell(0);
String boldText = "标题加粗"; String normalText = ":普通文本";
Font boldFont = workbook.createFont(); boldFont.setBold(true);
XSSFRichTextString richText = new XSSFRichTextString(boldText + normalText); richText.applyFont(0, boldText.length(), boldFont); cell.setCellValue(richText);
try (OutputStream outputStream = Files.newOutputStream(outputPath)) { workbook.write(outputStream); } }
System.out.println("Generated: " + outputPath.toAbsolutePath()); } }
|
把无关代码折叠后,真正的差异只有下面两处:
1 2 3 4 5
| - import org.apache.poi.xssf.streaming.SXSSFWorkbook; + import org.apache.poi.xssf.usermodel.XSSFWorkbook;
- try (SXSSFWorkbook workbook = new SXSSFWorkbook()) { + try (XSSFWorkbook workbook = new XSSFWorkbook()) {
|
Font、XSSFRichTextString、applyFont 的区间以及 setCellValue 的调用顺序完全相同。这样就建立了一个只改变 Workbook 实现的对照实验。
先看实际效果
分别运行两个程序并用 WPS 打开输出文件,可以直观看到差异:

左侧是 SXSSF 生成的 示例. xlsx,文字内容完整,但 “标题加粗” 的局部粗体没有保留;右侧是 XSSF 生成的 XSSF - 示例. xlsx,只有 “标题加粗” 部分为粗体,“:普通文本”保持普通字重。
这个对照很重要。它说明 setBold(true)、applyFont 的区间和文字内容大概率都没有问题,变量已经缩小到 Workbook 实现及其序列化过程。
按优先级列出假设
遇到 “内存中设置成功,文件中却失效” 的问题,我通常按下面的顺序排查:
applyFont 的范围是否写错。它使用左闭右开区间 [start, end)。
- 是否在
cell.setCellValue(richText) 之后又用普通字符串覆盖了单元格。
- 字体是否来自另一个 Workbook,或者字体对象是否被后续修改。
- SXSSF 的行是否已经刷入临时文件,导致再修改单元格无效。
- SXSSF 写 xlsx 时是否丢弃了富文本 run。
本项目只有一行数据,SXSSF 默认窗口可以容纳它;富文本也在写文件前设置,因此 “行过早 flush” 不是本次根因。XSSF 对照程序使用同样的字体和下标又能正常工作,前面三个假设也基本可以排除。
截图已经确认现象真实存在。接下来才进入文件结构和 POI 源码,解释格式为什么会丢失。
深入 xlsx 文件结构,解释格式为什么丢失
xlsx 本质上是 ZIP 包。解压检查 OOXML 不是为了代替截图判断视觉效果,而是为了确认丢失发生在写文件阶段。先检查 SXSSF 生成文件的工作表 XML:
1 2
| unzip -p target/sxssf-demo.xlsx xl/worksheets/sheet1.xml \ | xmllint --format -
|
关键内容如下:
1 2 3 4 5
| <c r="A1" t="inlineStr"> <is> <t>标题加粗:普通文本</t> </is> </c>
|
这里仅有一个 <t>,没有表示富文本片段的 <r>,也没有字体属性 <rPr>。它解释了截图中 SXSSF 为什么无法呈现局部粗体:格式信息没有进入最终文件。
再看 XSSF 对照文件中的 xl/sharedStrings.xml:
1 2
| unzip -p target/xssf-demo.xlsx xl/sharedStrings.xml \ | xmllint --format -
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| <si> <r> <rPr> <b val="true"/> <sz val="11.0"/> <rFont val="Calibri"/> </rPr> <t>标题加粗</t> </r> <r> <t>:普通文本</t> </r> </si>
|
粗体片段和普通片段都被写成了独立的 <r>。到这里,问题已经从 “为什么 Excel 不加粗” 变成了“为什么 SXSSF 只写纯文字”。
顺着 Apache POI 源码定位根因
SXSSFWorkbook 的说明中写明:它默认使用 inline strings,而不是 Shared Strings Table。这样更节省内存,但兼容性和功能表现需要调用方权衡。参见 SXSSFWorkbook 源码说明。
真正决定本例结果的是 SheetDataWriter 写字符串单元格的分支:
1 2 3 4 5 6 7 8
| if (_sharedStringSource != null) { RichTextString rt = cell.getRichStringCellValue(); int sRef = _sharedStringSource.addSharedStringItem(rt); } else { outputEscapedString(cell.getStringCellValue()); }
|
两条路径的差异非常直接:
- 有 Shared Strings Table 时,传入的是完整的
RichTextString,字体片段可以保留。
- 默认 inline string 路径调用
getStringCellValue(),得到的只是纯文字,富文本 run 信息已经不在返回值里。
所以,本次问题和 JDK 17 没有直接关系,也不是 applyFont 没执行。根因是 new SXSSFWorkbook() 选择了默认的 inline string 写出路径,而这条路径没有序列化单元格内部的富文本格式。
方案一:保留 SXSSF,启用 Shared Strings Table
如果导出数据量较大,仍然需要 SXSSF 的滑动窗口,可以使用四参数构造方法,将最后一个参数设为 true:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path;
import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Font; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.xssf.streaming.SXSSFWorkbook; import org.apache.poi.xssf.usermodel.XSSFRichTextString; import org.apache.poi.xssf.usermodel.XSSFWorkbook;
Path outputPath = Path.of("target", "sxssf-rich-text.xlsx");
try (SXSSFWorkbook workbook = new SXSSFWorkbook( new XSSFWorkbook(), 100, false, true)) { Sheet sheet = workbook.createSheet("demo"); Row row = sheet.createRow(0); Cell cell = row.createCell(0);
String boldText = "标题加粗"; String normalText = ":普通文本";
Font boldFont = workbook.createFont(); boldFont.setBold(true);
XSSFRichTextString richText = new XSSFRichTextString(boldText + normalText); richText.applyFont(0, boldText.length(), boldFont); cell.setCellValue(richText);
try (OutputStream outputStream = Files.newOutputStream(outputPath)) { workbook.write(outputStream); } }
|
这仍然是流式写行,但不再是 “所有字符串内容都无需驻留内存” 的模式。Shared Strings Table 会保存工作簿中的唯一字符串;如果数据量很大且大量字符串互不重复,内存占用会明显增加。上线前应使用真实数据做堆内存和耗时测试,不能只用一行示例判断。
字体也应在 Workbook 级别创建并复用,不要在循环中为每个单元格创建一个新字体,否则会增加样式记录并最终触碰 Excel 的格式数量限制。
方案二:数据量可控时使用 XSSFWorkbook
项目中的 PoiRichTextXssfDemo 已经证明 XSSFWorkbook 可以正确写出富文本。如果文件规模不大,优先使用它,代码更直接:
1 2 3
| try (XSSFWorkbook workbook = new XSSFWorkbook()) { }
|
代价是所有行、单元格和相关对象都保留在内存中。它适合中小规模、富文本和复杂 Excel 功能优先的场景,不适合不经评估就替换超大数据量导出。
不要把 CellStyle 和 RichTextString 混为一谈
如果整个单元格都要加粗,可以使用 CellStyle:
1 2 3 4 5 6
| Font boldFont = workbook.createFont(); boldFont.setBold(true);
CellStyle style = workbook.createCellStyle(); style.setFont(boldFont); cell.setCellStyle(style);
|
它控制的是整个单元格,无法表达 “标题加粗:普通文本” 这种同一单元格内的局部格式。局部字体必须依赖 RichTextString 的多个 text run。用 CellStyle 绕过本问题,会改变需求,而不是修复需求。
修复后如何验证
不要把 “代码能运行” 当作验收完成。至少做三层验证:
- 用目标客户端打开文件并截图,确认 Microsoft Excel、WPS 等实际使用端的显示符合预期。
- 重新用
XSSFWorkbook 读取 A1,确认 numFormattingRuns() 大于 0。
- 需要继续定位底层原因时,再解压 xlsx,确认
sharedStrings.xml 中存在 <r>、<rPr> 和 <b>。
可以增加一个自动化断言:
1 2 3 4 5 6 7 8 9 10
| try (XSSFWorkbook workbook = new XSSFWorkbook(outputPath.toFile())) { XSSFRichTextString value = workbook.getSheetAt(0) .getRow(0) .getCell(0) .getRichStringCellValue();
if (value.numFormattingRuns() == 0) { throw new AssertionError("Rich text formatting was lost"); } }
|
总结这次排查方法
这类问题最容易在 API 调用上反复试错,例如换下标、换字体、重复调用 setBold。更有效的路径是:
- 做最小复现,并用 XSSF/SXSSF 建立单变量对照。
- 用实际客户端和对比截图确认现象,不用文件结构代替视觉结果。
- 再把 xlsx 当成 ZIP 检查 OOXML,定位丢失发生在哪条序列化路径。
- 沿着差异追到 POI 的写出分支,确认默认配置背后的行为。
- 修复后同时验证语义正确性和内存代价。
最终根因可以压缩成一句话:SXSSF 默认 inline string 写出路径把富文本降成了纯字符串;启用 Shared Strings Table,或改用 XSSFWorkbook,才能保留单元格内的局部粗体。
参考资料