背景:使用easyexcel向模板文件中某行开始写入大量数据,要求不覆盖后面行(以下称为备注行)的内容,示例模板如下,从第三行开始填充:

easyexcel大致实现代码:
java
FillConfig fillConfig = FillConfig.builder()
.forceNewRow(true)
.build();
WriteSheet writeSheet = EasyExcel.writerSheet(sheetName).build();
excelWriter.fill(dataList, fillConfig, writeSheet);
但是速度非常慢,因为设置了forceNewRow为true,会插入新行,不影响备注行,如果设置为false,则填充很快,但是会覆盖调备注行。
下面的代码旨在解决这个矛盾点,该代码实现了一个基于模板的Excel导出工具类FileCheckResultExportWriter,支持批注添加、背景色设置等功能。主要特点包括:
-
使用EasyExcel实现高性能导出;
-
通过模板文件定义数据填充区域;
-
支持批注内容添加和不同级别的背景色设置;
-
处理大量数据时保留模板样式和备注行;
-
提供SheetEndRowData类缓存和恢复备注行样式。
使用示例展示了如何配置列名、添加批注和颜色标记,并导出包含5万行数据的Excel文件。该工具适用于需要保持模板格式同时处理大数据量
java
package com.zhou.util.easyexcel;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.handler.RowWriteHandler;
import com.alibaba.excel.write.handler.WorkbookWriteHandler;
import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import com.alibaba.excel.write.handler.context.RowWriteHandlerContext;
import com.alibaba.excel.write.handler.context.WorkbookWriteHandlerContext;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.fill.FillConfig;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import com.zhou.common.enums.VerifyLevelEnum;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.*;
import com.zhou.common.model.Point;
import com.zhou.util.ExcelUtils;
import java.io.File;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
/**
* 基于模板的Excel导出工具类
* 支持添加批注和背景色
* <p>不设置forceNewRow为true的情况下不丢失备注行,同时保证速度</p>
*
* @author lang.zhou
* @since 2025-12-16
*/
@Data
@Slf4j
public class FileCheckResultExportWriter {
/**
* 模板文件
* <p>数据开始行必须带有{.COLUMN_NAME}占位符标记,用于easyexcel识别填充数据区域</p>
*/
private final File templateFile;
private final ExportOptions options;
/**
* 导出索引和字段名信息
* <p>key:列索引</p>
* <p>value:字段名</p>
*/
private TreeMap<Integer, String> columns = new TreeMap<>();
/**
* 单元格批注枚举,
* <p>key:单元格位置</p>
* <p>value:单元格批注列表</p>
*/
private Map<Point, List<String>> comments = new HashMap<>() ;
/**
* 单元格背景色枚举,
* <p>key:单元格位置</p>
* <p>value:单元格背景色枚举</p>
* <p>与getStyleMap配合使用</p>
*/
private Map<Point, Integer> levelMap = new HashMap<>();
/**
* 构造函数
* @param templateFile 模板文件
* @param options 配置项
*/
public FileCheckResultExportWriter(File templateFile, ExportOptions options) {
this.templateFile = templateFile;
this.options = options;
}
/**
* 添加单个批注
* @param row 行索引(从0开始)
* @param col 列索引(从0开始)
* @param comment 批注内容
*/
public FileCheckResultExportWriter addComment(int row, int col, String comment) {
Point point = new Point(row, col);
comments.computeIfAbsent(point, k -> new ArrayList<>()).add(comment);
return this;
}
/**
* 添加多个批注
* @param row 行数据索引
* @param col 列数据索引
* @param commentList 批注内容列表
* <p>注意是数据行相对索引,而非excel文件中的行索引</p>
*/
public void addComments(int row, int col, Collection<String> commentList) {
Point point = new Point(row, col);
comments.computeIfAbsent(point, k -> new ArrayList<>()).addAll(commentList);
}
/**
* 添加单个颜色
* @param row 行索引(从0开始)
* @param col 列索引(从0开始)
* <p>注意是数据行相对索引,而非excel文件中的行索引</p>
*/
public void addLevel(int row, int col, Integer level) {
Point point = new Point(row, col);
levelMap.put(point, level);
}
/**
* 缓存数据开始行样式
* <p>同存一份原始样式,同时创建不同级别的颜色样式</p>
*/
protected void initStyleMap(CellWriteHandlerContext context, Map<Integer,Map<Integer,CellStyle>> styleMap){
log.info("开始读取数据开始行样式");
Sheet sheet = context.getWriteSheetHolder().getSheet();
Row row = context.getRow();
for (Integer colIndex : columns.keySet()) {
Cell cell = row.getCell(colIndex);
CellStyle style = null;
if(cell != null){
style = cell.getCellStyle();
}
if(style == null){
style = sheet.getColumnStyle(colIndex);
}
if(style != null){
Map<Integer, CellStyle> groupMap = styleMap.computeIfAbsent(colIndex, m -> new HashMap<>());
this.getStyleMap(sheet, style, groupMap);
}
}
log.info("读取数据开始行样式完成");
}
/**
* 自定义样式克隆
* 默认使用勾稽颜色样式,数据对比或其他样式,需要覆盖此方法
*/
protected void getStyleMap(Sheet sheet, CellStyle style, Map<Integer, CellStyle> groupMap){
groupMap.put(0,style);
CellStyle warn = ExcelUtils.cloneStyle(sheet.getWorkbook(),style, VerifyLevelEnum.getColor(VerifyLevelEnum.WARN.getValue()));
groupMap.put(VerifyLevelEnum.WARN.getValue(),warn);
CellStyle force = ExcelUtils.cloneStyle(sheet.getWorkbook(),style, VerifyLevelEnum.getColor(VerifyLevelEnum.FORCE.getValue()));
groupMap.put(VerifyLevelEnum.FORCE.getValue(),force);
}
/**
* 填充数据并保存文件
* @param sheetName sheet名称
* @param tableName 表格区域名称
* @param dataList 数据
* @param savePath excel保存路径
*/
public void save(String sheetName, String tableName, List<Map<String, Object>> dataList, File savePath) {
long startTime = System.currentTimeMillis();
int dataCount = dataList.size();
Map<Integer,Map<Integer,CellStyle>> styleMap = new HashMap<>();
CellWriteHandler cellWriteHandler = this.getCellWriteHandler(styleMap, dataCount);
//缓存备注行样式、数据、合并单元格
AtomicReference<SheetEndRowData> sheetEndRowData = new AtomicReference<>();
RowWriteHandler rowWriteHandler = new RowWriteHandler() {
@Override
public void afterRowDispose(RowWriteHandlerContext context) {
Integer relativeRowIndex = context.getRelativeRowIndex();
if (relativeRowIndex != null && relativeRowIndex >= dataCount - 1) {
Row row = context.getRow();
Sheet sheet = row.getSheet();
if(sheetEndRowData.get() != null){
//将备注行还原
sheetEndRowData.get().write(sheet, dataCount -1);
}
return;
}
handleN();
}
};
WorkbookWriteHandler workbookWriteHandler = new WorkbookWriteHandler() {
@Override
public void afterWorkbookCreate(WriteWorkbookHolder writeWorkbookHolder) {
if(dataCount > 1){
//读取workbook完成后回调,拿到poi原生workbook
XSSFWorkbook xssfWorkbook = getXSSFWorkbook(writeWorkbookHolder.getWorkbook());
XSSFSheet sheet = xssfWorkbook.getSheet(sheetName);
//读取备注行并缓存
sheet.getTables().stream().filter(t -> t.getName().equals(tableName)).findFirst().ifPresent(table -> {
int startIndex = table.getStartRowIndex();
if(startIndex == sheet.getLastRowNum()){
return;
}
//找到备注行并且剪切
SheetEndRowData end = SheetEndRowData.cut(sheet, startIndex + 1);
sheetEndRowData.set(end);
});
}
}
@Override
public void afterWorkbookDispose(WorkbookWriteHandlerContext context) {
Workbook workbook = context.getWriteWorkbookHolder().getWorkbook();
XSSFWorkbook xssfWorkbook = getXSSFWorkbook(workbook);
if(dataCount > 1){
XSSFSheet xssfSheet = xssfWorkbook.getSheet(sheetName);
xssfSheet.getTables().stream().filter(t -> t.getName().equals(tableName)).findFirst().ifPresent(table -> {
updateTableArea(table, dataCount);
afterWriteTable(table, dataCount);
});
}
afterWriteWorkboot(workbook);
}
};
FillConfig fillConfig = FillConfig.builder()
.forceNewRow(false)
.build();
try (ExcelWriter excelWriter = EasyExcel.write(savePath.getAbsolutePath())
.withTemplate(templateFile.getAbsolutePath())
.registerWriteHandler(cellWriteHandler)
.registerWriteHandler(rowWriteHandler)
.registerWriteHandler(workbookWriteHandler)
.build()) {
WriteSheet writeSheet = EasyExcel.writerSheet(sheetName).build();
excelWriter.fill(dataList, fillConfig, writeSheet);
}
long endTime = System.currentTimeMillis();
log.info("导出完成 - 耗时: {}ms", endTime - startTime);
}
private XSSFWorkbook getXSSFWorkbook(Workbook workbook){
XSSFWorkbook xssfWorkbook ;
if (workbook instanceof SXSSFWorkbook) {
xssfWorkbook = ((SXSSFWorkbook) workbook).getXSSFWorkbook();
} else if (workbook instanceof XSSFWorkbook) {
xssfWorkbook = (XSSFWorkbook) workbook;
}else{
throw new IllegalArgumentException("不支持的Workbook类型");
}
return xssfWorkbook;
}
private CellWriteHandler getCellWriteHandler(Map<Integer, Map<Integer, CellStyle>> styleMap, int dataCount) {
AtomicReference<Drawing<?>> drawingHolder = new AtomicReference<>();
return new CellWriteHandler() {
@Override
public void afterCellDispose(CellWriteHandlerContext context) {
Integer relativeRowIndex = context.getRelativeRowIndex();
if (relativeRowIndex != null && relativeRowIndex >= dataCount) {
return;
}
if(relativeRowIndex == 0 && styleMap.isEmpty()){
initStyleMap(context, styleMap);
}
Cell cell = context.getCell();
if (cell == null) return;
int colIndex = cell.getColumnIndex();
// 添加批注(检查是否有预定义的批注)
Point point = new Point(relativeRowIndex, colIndex);
List<String> commentList = comments.get(point);
if (commentList != null && !commentList.isEmpty()) {
if (drawingHolder.get() == null) {
drawingHolder.set(cell.getSheet().createDrawingPatriarch());
}
String commentText = String.join("\n", commentList);
ExcelUtils.addComment(drawingHolder.get(), cell, commentText);
}
Integer level = levelMap.get(point);
// 设置背景色样式
if (level != null) {
Map<Integer, CellStyle> colStyleMap = styleMap.get(colIndex);
if(colStyleMap != null){
CellStyle style = colStyleMap.get(level);
if(style != null && context.getCellDataList() != null && !context.getCellDataList().isEmpty()){
context.getCellDataList().get(0).setOriginCellStyle(style);
}
}
}
}
};
}
/**
* 数据全部写入完毕回调
*/
public void afterWriteTable(XSSFTable table, int dataCount) {
}
/**
* 数据全部写入完毕回调
*/
public void handleN() {
}
/**
* 文件全部写入完毕回调
*/
public void afterWriteWorkboot(Workbook workbook) {
}
private void updateTableArea(XSSFTable table, int dataCount) {
try {
int tableStartRow = table.getStartRowIndex();
int tableStartCol = table.getStartColIndex();
int tableEndCol = table.getEndColIndex();
int tableEndRow = tableStartRow + dataCount - 1;
CellRangeAddress newRange = new CellRangeAddress(tableStartRow, tableEndRow, tableStartCol, tableEndCol);
table.getCTTable().setRef(newRange.formatAsString());
log.info("表格区域已更新: {}", newRange.formatAsString());
} catch (Exception e) {
log.error("更新表格区域失败: ", e);
}
}
}
java
package com.zhou.util.easyexcel;
import com.zhou.common.model.Point;
import com.zhou.util.ExcelUtils;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 将sheet页某一行开始的行样式、数据全部缓存起来,然后从这一行开始全部删除
* 最后将这些移除的行添加到sheet页末尾
* @author lang.zhou
* @since 2025-12-16 10:34
*/
@Slf4j
@Data
public class SheetEndRowData {
private int startRow;
private Map<Integer,Short> rowHeightMap = new HashMap<>();
private Map<Point,Object> valueMap = new HashMap<>();
private Map<Point,CellStyle> styleMap = new HashMap<>();
private List<CellRangeAddress> regionList = new ArrayList<>();
/**
* 剪切样式、数据
* @param startRow 剪切开始行
*/
public static SheetEndRowData cut(Sheet sheet, int startRow){
FormulaEvaluator evaluator = sheet.getWorkbook().getCreationHelper().createFormulaEvaluator();
SheetEndRowData cutStyle = new SheetEndRowData();
cutStyle.setStartRow(startRow);
for (int i = startRow; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if(row != null){
cutStyle.rowHeightMap.put(i - startRow, row.getHeight());
for (int j = row.getFirstCellNum(); j <= row.getLastCellNum(); j++) {
Cell cell = row.getCell(j);
if(cell != null){
Point point = new Point(i - startRow, j);
if(cell.getCellStyle() != null){
cutStyle.styleMap.put(point, cell.getCellStyle());
}
cutStyle.valueMap.put(point, ExcelUtils.getCellValue(cell,evaluator));
}
}
}
}
List<Integer> indexes = new ArrayList<>();
List<CellRangeAddress> mergedRegions = sheet.getMergedRegions();
for (int i = 0; i < mergedRegions.size(); i++) {
CellRangeAddress region = mergedRegions.get(i);
if(region.getFirstRow() >= startRow){
cutStyle.regionList.add(region);
indexes.add(i);
}
}
if(!indexes.isEmpty()){
sheet.removeMergedRegions(indexes);
}
for (int i = startRow; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if(row != null){
sheet.removeRow(row);
}
}
return cutStyle;
}
/**
* 将样式、数据写回sheet页末尾
* @param addRows 添加的行数
*/
public void write(Sheet sheet, int addRows){
// 对于SXSSFSheet,需要特殊处理已刷新到磁盘的行
for (Map.Entry<Integer, Short> entry : rowHeightMap.entrySet()) {
int rowNum = entry.getKey() + this.startRow + addRows;
Row row = getRowOrNull(sheet, rowNum);
if(row != null){
row.setHeight(entry.getValue());
}
}
for (Map.Entry<Point, CellStyle> entry : styleMap.entrySet()) {
Point point = entry.getKey();
int rowNum = point.getX() + this.startRow + addRows;
Row row = getRowOrNull(sheet, rowNum);
if(row != null){
Cell cell = ExcelUtils.safeCell(row, point.getY());
cell.setCellStyle(entry.getValue());
ExcelUtils.setCellValue(cell, valueMap.get(point));
}
}
for (CellRangeAddress a : regionList) {
int sRow = a.getFirstRow() + addRows;
int eRow = a.getLastRow() + addRows;
sheet.addMergedRegion(new CellRangeAddress(sRow, eRow, a.getFirstColumn(), a.getLastColumn()));
}
}
/**
* 获取行,如果行不存在或已被刷新到磁盘,返回null而不是创建新行
* 这避免了SXSSFSheet中"尝试创建已刷新的行"的错误
*/
private Row getRowOrNull(Sheet sheet, int rowNum){
try {
Row row = sheet.getRow(rowNum);
if(row == null){
row = sheet.createRow(rowNum);
}
return row;
} catch (Exception e) {
// 对于SXSSFSheet,如果行已被刷新到磁盘,可能抛出异常
// 记录警告日志,便于问题排查
log.warn("无法访问Sheet[{}]的第{}行,该行可能已被刷新到磁盘。这是正常现象,将跳过此行。错误类型: {},错误信息{}",
sheet.getSheetName(), rowNum, e.getClass().getSimpleName(),e);
return null;
}
}
}
使用示例
java
package com.zhou.util.easyexcel;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* FileCheckResultExportWriter 使用示例
*
* @author lang.zhou
* @since 2025-12-16
*/
public class SimpleSXSSFInsertExample {
private static final String SHEET_NAME = "J1024前200大投资者";
private static final String TABLE_NAME = "table_ee9374eadebf4cc08e2c726828c82970";
private static final String[] COLUMNS = {
"col1", "col2", "col3", "col4", "col5", "col6", "col7",
"col8", "col9", "col10", "col11", "col12", "col13", "col14"
};
public static void main(String[] args) throws Exception{
long startTime = System.currentTimeMillis();
File templateFile = new File("C:\\Users\\Administrator\\Desktop\\1.xlsx");
File outFile = new File("C:\\Users\\Administrator\\Desktop\\out.xlsx");
FileCheckResultExportWriter writer = new FileCheckResultExportWriter(templateFile, ExportOptions.forceNewRow(0));
writer.setColumns(buildColumns());
writer.addComment(1, 1, "这是第二行第二列的批注");
writer.addComment(1, 2, "这是第二行第三列的批注");
writer.addLevel(0, 0, 2);
writer.addLevel(1, 2, 2);
writer.addLevel(2, 3, 1);
writer.addLevel(2, 4, 1);
int total = 50000;
List<Map<String, Object>> dataList = buildData(total);
writer.save(SHEET_NAME, TABLE_NAME, dataList, outFile);
System.out.println("耗时:" + (System.currentTimeMillis() - startTime) + "ms");
System.out.println("输出文件:" + outFile.getAbsolutePath());
}
private static TreeMap<Integer, String> buildColumns() {
TreeMap<Integer, String> columns = new TreeMap<>();
for (int i = 0; i < COLUMNS.length; i++) {
columns.put(i, COLUMNS[i]);
}
return columns;
}
private static List<Map<String, Object>> buildData(int total) {
List<Map<String, Object>> dataList = new ArrayList<>(total);
for (int i = 0; i < total; i++) {
Map<String, Object> row = new HashMap<>();
for (int j = 0; j < COLUMNS.length; j++) {
row.put(COLUMNS[j], "数据" + (i + 1) + "-列" + (j + 1));
}
dataList.add(row);
}
return dataList;
}
}