easyexcel快速填充大数据量不覆盖后面的行解决方式

背景:使用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,支持批注添加、背景色设置等功能。主要特点包括:

  1. 使用EasyExcel实现高性能导出;

  2. 通过模板文件定义数据填充区域;

  3. 支持批注内容添加和不同级别的背景色设置;

  4. 处理大量数据时保留模板样式和备注行;

  5. 提供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;
    }
}
相关推荐
范什么特西2 小时前
Maven中dependencies和dependencyManagement区别
java·开发语言·maven
SunnyDays10112 小时前
Java 操作 Word 超链接:添加网页、邮箱、文件和图片链接
java·word·超链接
DFT计算杂谈2 小时前
WannierTools输入文件wt.in一键批量生成脚本
java·前端·chrome·python·算法·conda
大神15732 小时前
Cordova Android 签名三种方式详解:证书生成、命令行直接签名与配置文件自动签名
android·java
武子康2 小时前
调查研究-170 Vert.x 是什么?它和 Netty 到底是什么关系?一张图讲清 Java 异步技术栈选型
java·后端
zzz_23682 小时前
【Java基础】泛型的门道:伪泛型的真相
java·开发语言
我登哥MVP2 小时前
SpringCloud 核心组件解析:服务链路追踪
java·spring boot·后端·spring·spring cloud·java-ee·maven
PixelBai2 小时前
JSON差异比较高级用法技巧
java·服务器·json
iiiiyu2 小时前
IO流相关编程题
java·大数据·开发语言·数据结构·数据库·mysql