在使用 POI 开发 Excel 导出功能时,写了一个合并单元格的工具类方法,却遇到了一个诡异的问题:合并单元格后仅左上角显示边框样式,字体、背景色等其他样式均正常生效。本文将梳理这个问题的成因、多种解决方案,以及不同方案的适用场景,给同样踩坑的小伙伴一个参考~(第一次写博客,大家轻点喷😜)
问题如下
封装的 POI 合并单元格工具类,仅给合并区域左上角单元格设置样式,导出后发现边框样式仅左上角生效,其他区域无边框;而字体、背景色、对齐方式等样式能正常应用到整个合并区域,效果如下:

问题成因
网上常规说法是合并单元格只需给左上角单元格设置样式,样式会覆盖整个合并区域,但这个结论对边框样式不成立(只有部分的情况才会成立)
POI 中合并单元格的边框样式是特殊的,无法通过给左上角单元格单独设置的方式应用到整个合并区域,必须对边框做针对性的特殊处理;且在老版本 Excel(03 版)中,背景色、条件格式、数据验证等样式也可能出现类似的 “局部失效” 问题
初始问题代码
这是我最开始写的给合并单元格添加样式的工具类,最初的工具类仅给合并区域左上角单元格赋值和设置样式,导致边框样式失效,核心代码如下:
public class POIUtil{
/**
* 向工作表中添加合并单元格
* @param sheet 工作表对象
* @param cellRangeAddress 合并单元格范围对象
* @param value 合并单元格中要显示的值
* @param style 单元格样式
*/
public static void addMergedRegion(
Sheet sheet,
CellRangeAddress cellRangeAddress,
String cellValue,
CellStyle style
) {
// 参数校验
if (sheet == null || cellRangeAddress == null) {
throw new IllegalArgumentException("sheet和cellRangeAddress不能为null");
}
// 添加合并区域(单元格个数大于大于2个再合并)
if(cellRangeAddress.getNumberOfCells() >= 2){
sheet.addMergedRegion(cellRangeAddress);
}
// 获取合并区域的起始行和起始列
int firstRow = cellRangeAddress.getFirstRow();
int firstCol = cellRangeAddress.getFirstColumn();
// 获取起始行,如果行不存在则创建
Row row = sheet.getRow(firstRow) == null ? sheet.createRow(firstRow) : sheet.getRow(firstRow);
// 获取起始单元格,如果单元格不存在则创建
Cell cell = row.getCell(firstCol) == null ? row.createCell(firstCol) : row.getCell(firstCol);
// 设置单元格值
cell.setCellValue(cellValue);
// 设置单元格样式
if (style != null) {
cell.setCellStyle(style); // 为合并单元格的左上角单元格设置样式可以运用于整个合并单元格(边框样式除外)
}
}
}
具体效果如上图所示,背景颜色、垂直水平居中等样式只给左上角设置还是可以覆盖到整个合并单元格区域的,但是边框就不行
上图使用的是新版的Excel版本,也就是.xlsx后缀的文件,如果使用老版本的Excel版本也就是03版本的话,背景色、条件格式、数据验证等样式也可能出现类似的 “局部失效” 问题
解决方案 1:针对性处理边框(推荐,性能最优)
POI 提供了RegionUtil工具类,专门用于给合并单元格区域设置边框样式,可基于原有代码做轻量改造,仅对边框做特殊处理,兼顾效果和性能,改造后能实现虚线、双线、混合边框全区域正常显示。
核心思路:
1、保留原有给左上角单元格设置样式的逻辑(保证字体、背景色等样式生效);
2、新增边框处理方法,通过RegionUtil提取原样式中的边框属性,批量设置到整个合并区域;
3、增加非空判断,避免无边框样式时的无效执行。
public class POIUtil{
/**
* 向工作表中添加合并单元格
* @param sheet 工作表对象
* @param cellRangeAddress 合并单元格范围对象
* @param value 合并单元格中要显示的值
* @param style 单元格样式
*/
public static void addMergedRegion(
Sheet sheet,
CellRangeAddress cellRangeAddress,
String cellValue,
CellStyle style
) {
// 参数校验
if (sheet == null || cellRangeAddress == null) {
throw new IllegalArgumentException("sheet和cellRangeAddress不能为null");
}
// 添加合并区域(单元格个数大于大于2个再合并)
if(cellRangeAddress.getNumberOfCells() >= 2){
sheet.addMergedRegion(cellRangeAddress);
}
// 获取合并区域的起始行和起始列
int firstRow = cellRangeAddress.getFirstRow();
int firstCol = cellRangeAddress.getFirstColumn();
// 获取起始行,如果行不存在则创建
Row row = sheet.getRow(firstRow) == null ? sheet.createRow(firstRow) : sheet.getRow(firstRow);
// 获取起始单元格,如果单元格不存在则创建
Cell cell = row.getCell(firstCol) == null ? row.createCell(firstCol) : row.getCell(firstCol);
// 设置单元格值
cell.setCellValue(cellValue);
// 设置单元格样式
if (style != null) {
cell.setCellStyle(style); // 为合并单元格的左上角单元格设置样式可以运用于整个合并单元格(边框样式除外)
setBordersToMergedRegion(sheet, cellRangeAddress, style); // 为合并区域设置边框样式(边框样式必须这么写)
}
}
/**
* 为合并单元格区域设置边框样式
* @param sheet 工作表对象
* @param region 合并单元格区域
* @param style 包含边框样式的单元格样式
*/
private static void setBordersToMergedRegion(Sheet sheet, CellRangeAddress region, CellStyle style) {
// 获取边框样式
BorderStyle borderTop = style.getBorderTop();
BorderStyle borderBottom = style.getBorderBottom();
BorderStyle borderLeft = style.getBorderLeft();
BorderStyle borderRight = style.getBorderRight();
// 只有在有边框样式时才设置边框(防止报错)
if (borderTop != BorderStyle.NONE) {
RegionUtil.setBorderTop(borderTop, region, sheet);
RegionUtil.setTopBorderColor(style.getTopBorderColor(), region, sheet);
}
if (borderBottom != BorderStyle.NONE) {
RegionUtil.setBorderBottom(borderBottom, region, sheet);
RegionUtil.setBottomBorderColor(style.getBottomBorderColor(), region, sheet);
}
if (borderLeft != BorderStyle.NONE) {
RegionUtil.setBorderLeft(borderLeft, region, sheet);
RegionUtil.setLeftBorderColor(style.getLeftBorderColor(), region, sheet);
}
if (borderRight != BorderStyle.NONE) {
RegionUtil.setBorderRight(borderRight, region, sheet);
RegionUtil.setRightBorderColor(style.getRightBorderColor(), region, sheet);
}
}
}
最后测试的效果如下图所示:

解决方案 2:遍历单元格逐个设置(兜底方案,兼容所有场景)
如果遇到老版本 Excel(03 版)中背景色、条件格式、数据验证等样式也失效的情况,上述边框处理方法无法解决,此时可以采用循环遍历合并区域所有单元格,逐个设置样式的笨办法,能从根本上解决所有样式的 “局部失效” 问题。
核心思路:
1、获取合并区域的起始 / 结束行、起始 / 结束列;
2、双层循环遍历区域内所有单元格,不存在则创建;
3、给每个单元格统一设置样式,让样式无差别生效。
public class POIUtil {
/**
* 向工作表中添加合并单元格
* @param sheet 工作表对象
* @param cellRangeAddress 合并单元格范围对象
* @param value 合并单元格中要显示的值
* @param style 单元格样式
*/
public static void addMergedRegion(
Sheet sheet,
CellRangeAddress cellRangeAddress,
String cellValue,
CellStyle style
) {
// 参数校验
if (sheet == null || cellRangeAddress == null) {
throw new IllegalArgumentException("sheet和cellRangeAddress不能为null");
}
// 添加合并区域
sheet.addMergedRegion(cellRangeAddress);
// 获取合并区域的起始行和起始列
int firstRow = cellRangeAddress.getFirstRow();
int firstCol = cellRangeAddress.getFirstColumn();
// 获取起始行,如果行不存在则创建
Row row = sheet.getRow(firstRow) == null ? sheet.createRow(firstRow) : sheet.getRow(firstRow);
// 获取起始单元格,如果单元格不存在则创建
Cell cell = row.getCell(firstCol) == null ? row.createCell(firstCol) : row.getCell(firstCol);
// 设置单元格值
cell.setCellValue(cellValue);
// 设置单元格样式
int lastRow = cellRangeAddress.getLastRow();
int lastCol = cellRangeAddress.getLastColumn();
if (style != null) {
// 为合并区域内的每个单元格设置样式
for (int rowIndex = firstRow; rowIndex <= lastRow; rowIndex++) {
Row r = sheet.getRow(rowIndex) == null ? sheet.createRow(rowIndex) : sheet.getRow(rowIndex);
for (int colIndex = firstCol; colIndex <= lastCol; colIndex++) {
Cell c = r.getCell(colIndex) == null ? r.createCell(colIndex) : r.getCell(colIndex);
c.setCellStyle(style);
}
}
}
}
}
注意事项:该方案性能较差,如果导出的 Excel 数据量大、合并单元格多,会显著降低导出速度,因此仅作为老版本 Excel 或特殊样式的兜底方案,不推荐常规场景使用。
实际开发选型建议
1、优先使用 Excel 07 版(.xlsx):新版本 Excel 对 POI 的样式支持更友好,大幅减少样式失效问题;
2、常规场景用方案 1:通过RegionUtil针对性处理边框,轻量改造、性能最优,能解决 99% 的边框失效问题;
3、兼容老版本Excepl / 特殊样式用方案 2:将遍历设置的方法作为兜底,封装成独立方法,仅在需要时调用;
4、工具类拓展:可在工具类中同时保留两种方法,通过方法名区分(如addMergedRegion/addMergedRegionForOldExcel),方便业务按需选择。
总结
POI 合并单元格的样式坑,核心原因是边框样式无法通过左上角单元格透传,且老版本 Excel 对样式的支持存在缺陷:
1、轻量解决用RegionUtil,专门处理边框,兼顾效果和性能;
2、彻底兼容用遍历单元格,牺牲性能换全样式生效;
3、开发中优先使用新版本 Excel,从源头减少问题。
以上就是本次踩坑的全部解决过程,希望能帮助到各位小伙伴,避免再走弯路~
340



被折叠的 条评论
为什么被折叠?



