Java导出复杂Excel升级版(解决占位符遗留问题,通用工具类)

目录

介绍

通用读取工具

通用写入工具

注解表头映射工具

占位符映射工具


介绍

之前的文章Java导出写入固定Excel模板数据里演示了导出固定excel模板数据的方式,但是导出的模板数据还是有些小问题,比如使用占位符导出的数据,如果导出的指定占位符的sheet页集合数据为空集合,此时就会出现占位符在模板数据里的情况,也就是该sheet页面的占位符还在,没有清除占位符,这个一般在使用中是比较不太合适的,比如:

这里传入空集合数据时,渲染出来的excel文件就会单留占位符数据在这里,显得比较突兀

本篇文章直接对工具类进行封装,并且解决这个问题,而且针对一些比较复杂的excel进行处理,比如第一行的数据为sheet页的标题,第二行才为表头,我们渲染数据应该是要从第三行数据进行渲染

还会对读取excel的监听器进行封装,也是封装为通用的读取监听器来进行使用

这里使用的是阿里的easyExcel进行封装使用,依赖如下:

java 复制代码
  <dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>easyexcel</artifactId>
     <version>3.3.2</version>
  </dependency>

通用读取工具

平常在使用阿里的easyExcel时,需要对单个sheet页进行梳理,书写对应实体和对应数据监听器,这里直接封装为一个通用的监听器进行使用

java 复制代码
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;

/**
 * @author 武天
 * 通用excel监听器2
 * @date 2023/7/25 14:10
 */
public class ObjectExcelListener<T> extends AnalysisEventListener<T> {


    List<T> addList = new ArrayList<>();

    /**
     * 每解析一行都会回调invoke()方法
     * @param context 内容
     */

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        //解析结束销毁不用的资源
        //注意不要调用datas.clear(),否则getDatas为null
    }


    @Override
    public void invoke(T t, AnalysisContext analysisContext) {
        addList.add(t);
    }

    /**
     * 返回数据
     *
     * @return 返回读取的数据集合
     **/
    public List<T> getDatas() {
        return addList;
    }

    /**
     * 设置读取的数据集合
     *
     * @param datas 设置读取的数据集合
     **/
    public void setDatas(List<T> datas) {
        this.addList = datas;
    }

    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        System.out.println("✅ 实际读取到的表头(列索引 -> 表头名):");
        headMap.forEach((index, name) -> 
            System.out.println("  列 " + index + ": \"" + name + "\""));
    }
}

使用时需要传入对应数据的建表实体进行使用

准备一个有数据的excel文件,表头注意是在第二行,在使用工具时需制定对应表头所在行

封装对应实体(只展示部分字段演示即可):

java 复制代码
import java.math.BigDecimal;

import com.alibaba.excel.annotation.ExcelProperty;

import lombok.Data;


@Data
public class FundDetail {
 
    
    @ExcelProperty("序号")
    private String serialNo;

    @ExcelProperty("基金全称")
    private String fundFullName;

    @ExcelProperty("基金报备编号")
    private String filingNumber;

    @ExcelProperty("执行状态")
    private String executionStatus;

    @ExcelProperty("所在国家或地区")
    private String countryOrRegion;

    @ExcelProperty("所在省市区")

    private String provinceCityDistrict;


}

测试运行

java 复制代码
import java.util.List;

import com.alibaba.excel.EasyExcel;

import net.coding.excelUtils.entity.FundDetail;
import net.coding.excelUtils.listener.ObjectExcelListener;

public class Test {

    public static void main(String[] args) {
        String fileName = "/workspace/spring-boot-cloud-studio-demo/src/main/java/net/coding/excelUtils/templates/投资基金.xlsx";
        ObjectExcelListener objectExcelListener = new ObjectExcelListener();
            EasyExcel.read(fileName, FundDetail.class, objectExcelListener)
                    .sheet(2) // sheet页索引
                    .headRowNumber(2) // 指定表头所在行
                    .doRead();
        List<FundDetail> connectors = objectExcelListener.getDatas();
        System.out.println("我要的数据====>"+connectors);
    }
}

可以看到数据都正常打印拿取到了

注意这里制定读取的sheet页索引,也可以直接传入sheet页名字进行读取

比如:.sheet('sheetName')

通用写入工具

注解表头映射工具

传统方式还是根据easyExcel的注解映射表头进行映射,但是传统的方式是直接根据实体属性的注解进行导出数据的,这里需要根据固定模板的表头进行映射,而且表头还不在第一行,也是需要指定表头行进行数据渲染

比如excel文件为:

直接上工具类代码

java 复制代码
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.*;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.*;

/**
 * 
 */
public class ExcelWriteUtil {

    // ==================== 对外暴露的核心方法(直接接收样式参数,支持空集合) ====================
    public static <T> void writeExcelByTemplate(String templatePath, String outputPath,
                                                int sheetIndex, int startRow,
                                                List<T> dataList,
                                                BorderStyle borderStyle,
                                                IndexedColors bgColor,
                                                HorizontalAlignment horizontalAlign,
                                                VerticalAlignment verticalAlign,
                                                String fontName,
                                                short fontSize,
                                                boolean fontBold,
                                                short fontColor,
                                                boolean wrapText) {
        // 校验基础参数
        File templateFile = new File(templatePath);
        if (!templateFile.exists()) {
            throw new RuntimeException("模板文件不存在:" + templatePath);
        }
        if (sheetIndex < 0) {
            throw new IllegalArgumentException("Sheet索引不能为负数");
        }
        if (startRow < 0) {
            throw new IllegalArgumentException("数据起始行号不能为负数");
        }

        // 处理空集合:直接复制模板文件到输出路径,不写入任何数据
        if (dataList == null || dataList.isEmpty()) {
            copyTemplateFile(templatePath, outputPath);
            System.out.println(" 数据列表为空,已直接复制模板文件到:" + outputPath);
            return;
        }

        Workbook workbook = null;
        FileInputStream fis = null;
        FileOutputStream fos = null;

        try {
            fis = new FileInputStream(templateFile);
            workbook = new XSSFWorkbook(fis);
            Sheet sheet = workbook.getSheetAt(sheetIndex);
            if (sheet == null) {
                throw new RuntimeException("模板中不存在索引为" + sheetIndex + "的Sheet");
            }

            // 核心:直接在当前Workbook创建样式(不依赖任何高版本API)
            CellStyle dataCellStyle = createStyleDirectly(
                    workbook,
                    borderStyle,
                    bgColor,
                    horizontalAlign,
                    verticalAlign,
                    fontName,
                    fontSize,
                    fontBold,
                    fontColor,
                    wrapText
            );

            // 反射获取列映射
            Class<?> clazz = dataList.get(0).getClass();
            Map<Integer, Field> columnFieldMap = getColumnFieldMap(clazz);

            // 逐行写入数据+应用样式
            int currentRowNum = startRow;
            for (T data : dataList) {
                Row row = sheet.getRow(currentRowNum);
                if (row == null) {
                    row = sheet.createRow(currentRowNum);
                }

                for (Map.Entry<Integer, Field> entry : columnFieldMap.entrySet()) {
                    int colIndex = entry.getKey();
                    Field field = entry.getValue();
                    Object fieldValue = getFieldValue(data, field);
                    Cell cell = setCellValue(row, colIndex, fieldValue);
                    cell.setCellStyle(dataCellStyle); // 应用样式
                }
                currentRowNum++;
            }

            // 写入输出文件
            File outputFile = new File(outputPath);
            if (!outputFile.getParentFile().exists()) {
                outputFile.getParentFile().mkdirs();
            }
            fos = new FileOutputStream(outputFile);
            workbook.write(fos);

            System.out.println(String.format(" Excel写入成功!输出路径:%s,写入数据条数:%d",
                    outputPath, dataList.size()));

        } catch (Exception e) {
            throw new RuntimeException("Excel模板写入失败", e);
        } finally {
            // 关闭资源
            try {
                if (fis != null) fis.close();
                if (fos != null) fos.close();
                if (workbook != null) workbook.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 重载方法:使用默认样式(边框+居中+微软雅黑10号黑字),支持空集合
     */
    public static <T> void writeExcelByTemplate(String templatePath, String outputPath,
                                                int sheetIndex, int startRow,
                                                List<T> dataList) {
        // 调用核心方法,传入默认样式参数
        writeExcelByTemplate(
                templatePath, outputPath, sheetIndex, startRow, dataList,
                BorderStyle.THIN,
                IndexedColors.WHITE,
                HorizontalAlignment.CENTER,
                VerticalAlignment.CENTER,
                "微软雅黑",
                (short) 10,
                false,
                IndexedColors.BLACK.getIndex(),
                false
        );
    }

    // ==================== 私有方法:复制模板文件(空集合时调用) ====================
    private static void copyTemplateFile(String templatePath, String outputPath) {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            // 创建输出目录
            File outputFile = new File(outputPath);
            if (!outputFile.getParentFile().exists()) {
                outputFile.getParentFile().mkdirs();
            }
            // 复制模板文件到输出路径
            fis = new FileInputStream(templatePath);
            fos = new FileOutputStream(outputPath);
            byte[] buffer = new byte[1024];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            throw new RuntimeException("复制模板文件失败:" + e.getMessage(), e);
        } finally {
            try {
                if (fis != null) fis.close();
                if (fos != null) fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    // ==================== 核心:直接创建样式(无任何高版本API) ====================
    private static CellStyle createStyleDirectly(Workbook workbook,
                                                 BorderStyle borderStyle,
                                                 IndexedColors bgColor,
                                                 HorizontalAlignment horizontalAlign,
                                                 VerticalAlignment verticalAlign,
                                                 String fontName,
                                                 short fontSize,
                                                 boolean fontBold,
                                                 short fontColor,
                                                 boolean wrapText) {
        CellStyle style = workbook.createCellStyle();

        // 1. 边框
        style.setBorderTop(borderStyle);
        style.setBorderBottom(borderStyle);
        style.setBorderLeft(borderStyle);
        style.setBorderRight(borderStyle);
        style.setTopBorderColor(IndexedColors.BLACK.getIndex());
        style.setBottomBorderColor(IndexedColors.BLACK.getIndex());
        style.setLeftBorderColor(IndexedColors.BLACK.getIndex());
        style.setRightBorderColor(IndexedColors.BLACK.getIndex());

        // 2. 背景色:如果是白色,设置为无填充(透明)
        if (bgColor == IndexedColors.WHITE) {
            style.setFillPattern(FillPatternType.NO_FILL); // 无背景色(关键)
        } else {
            style.setFillForegroundColor(bgColor.getIndex());
            style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        }

        // 3. 对齐方式
        style.setAlignment(horizontalAlign);
        style.setVerticalAlignment(verticalAlign);
        style.setWrapText(wrapText);

        // 4. 字体
        Font font = workbook.createFont();
        font.setFontName(fontName);
        font.setFontHeightInPoints(fontSize);
        font.setBold(fontBold);
        font.setColor(fontColor);
        style.setFont(font);

        return style;
    }

    // ==================== 私有通用方法 ====================
    private static <T> Map<Integer, Field> getColumnFieldMap(Class<T> clazz) {
        Map<Integer, Field> columnFieldMap = new TreeMap<>();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            ExcelColumn excelColumn = field.getAnnotation(ExcelColumn.class);
            if (excelColumn != null) {
                field.setAccessible(true);
                columnFieldMap.put(excelColumn.columnIndex(), field);
            }
        }
        if (columnFieldMap.isEmpty()) {
            throw new RuntimeException("实体类" + clazz.getName() + "未配置@ExcelColumn注解");
        }
        return columnFieldMap;
    }

    private static Object getFieldValue(Object obj, Field field) {
        try {
            return field == null ? "" : field.get(obj);
        } catch (IllegalAccessException e) {
            return "";
        }
    }

    private static Cell setCellValue(Row row, int colIndex, Object value) {
        Cell cell = row.getCell(colIndex);
        if (cell == null) {
            cell = row.createCell(colIndex);
        }
        if (value == null || value.toString().isEmpty()) {
            cell.setCellValue("");
        } else if (value instanceof String) {
            cell.setCellValue((String) value);
        } else if (value instanceof BigDecimal) {
            cell.setCellValue(((BigDecimal) value).doubleValue());
        } else if (value instanceof Double) {
            cell.setCellValue((Double) value);
        } else if (value instanceof Integer) {
            cell.setCellValue((Integer) value);
        } else if (value instanceof Long) {
            cell.setCellValue((Long) value);
        } else if (value instanceof Boolean) {
            cell.setCellValue((Boolean) value);
        } else {
            cell.setCellValue(value.toString());
        }
        return cell;
    }

    // ========== 保留原有无模板方法 ==========
    public static <T> void writeExcel(String outputPath, String sheetName, List<T> dataList, Class<T> clazz) {
        if (dataList == null || dataList.isEmpty()) {
            throw new IllegalArgumentException("数据不能为空");
        }
        File outputFile = new File(outputPath);
        if (!outputFile.getParentFile().exists()) {
            outputFile.getParentFile().mkdirs();
        }
        com.alibaba.excel.EasyExcel.write(outputPath, clazz)
                .autoCloseStream(true)
                .sheet(sheetName == null ? "Sheet1" : sheetName)
                .doWrite(dataList);
    }

    public static <T> void writeExcel(String outputPath, List<T> dataList, Class<T> clazz) {
        writeExcel(outputPath, "Sheet1", dataList, clazz);
    }
}

测试:

java 复制代码
import net.coding.excelUtils.entity.FundDetail;
import net.coding.excelUtils.listener.ExcelWriteUtil;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

import org.apache.poi.ss.usermodel.*;

public class TestFillWrite {
    public static void main(String[] args) throws IOException {
        // ========== 核心配置(3.3.2专属) ==========
        String path = "/workspace/spring-boot-cloud-studio-demo/src/main/java/net/coding/excelUtils/templates/";
        String templatePath = path+"投资基金_输出模板.xlsx"; // 你的模板文件
        String outputPath = path+"投资基金_输出指定数据行.xlsx"; // 输出文件
           // 1. 构造测试数据
           List<FundDetail> dataList = new ArrayList<>();
           FundDetail item1 = new FundDetail();
           item1.setSerialNo("1");
           item1.setFundFullName("科技创新基金");
           item1.setFilingNumber("SVJ2025001");
           item1.setExecutionStatus("执行中");
           item1.setCountryOrRegion("中国");
           item1.setProvinceCityDistrict("上海市");
           item1.setLingangArea("是");
           item1.setYangtzeRiverDelta("是");
           item1.setFiveNewTowns("否");
           item1.setIsNewAnnualProject("是");
           item1.setInvestorFullName("XX投资集团");
           item1.setCreditCode("91310000MA1FL12345");
           item1.setIsGroupControllingGp("是");
           item1.setInvestorType("国有企业");
           item1.setTotalCommittedAmount(new BigDecimal("100000"));
           item1.setSelfCommittedAmount(new BigDecimal("60000"));
           item1.setSharePercentage(60.0);
           item1.setSelfPaidAmount(new BigDecimal("30000"));
           item1.setLastYearPaidAmount(new BigDecimal("20000"));
           item1.setGovernmentFunding(new BigDecimal("10000"));
           item1.setOwnFunding(new BigDecimal("20000"));
           item1.setOtherFunding(BigDecimal.ZERO);
           item1.setMajorInvestmentProject("人工智能芯片项目");
           item1.setRemarks("重点项目");
           dataList.add(item1);
   
   
           try {
               // 传入空集合测试
               ExcelWriteUtil.writeExcelByTemplate(
                       templatePath,
                       outputPath,
                       2,  // 第3个Sheet
                       2,  // 数据从第5行开始
                       dataList,
                       BorderStyle.THIN,                // 细边框
                       IndexedColors.WHITE,               // 无背景色
                       HorizontalAlignment.CENTER,      // 水平居中
                       VerticalAlignment.CENTER,        // 垂直居中
                       "楷体",                          // 楷体字体
                       (short) 16,                     // 16号字
                       true,                           // 加粗
                       IndexedColors.BLACK.getIndex(),  // 黑色字体
                       false                           // 不自动换行
               );
   
               System.out.println(" 测试成功!已复制模板文件,写入数据大小为"+dataList);
   
           } catch (Exception e) {
               System.err.println(" 写入失败:" + e.getMessage());
               e.printStackTrace();
           }
    }
}

打开查看数据:

可以看到数据都已经成功渲染

这个优势就是可以直接使用easyExcel提供的注解进行配置,我们指定映射的表头行即可,不用额外处理占位符的问题

占位符映射工具

占位符需要提前在excel模板里进行设置占位符进行处理

但是当有空集合数据时,占位符还是会进行保留,这次直接封装通用工具类来处理这个问题

java 复制代码
package com.hongyin.util;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.fill.FillConfig;
import com.alibaba.excel.write.metadata.fill.FillWrapper;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.*;
import java.lang.reflect.Field;
import java.util.*;
import java.util.regex.Pattern;

/**
 * :修复SheetData构造器报错 + 支持Sheet名称/索引双匹配 + 空集合处理 + 精准样式
 * 兼容JDK8 + EasyExcel 3.3.2
 */
public class ExcelLocalPlaceholderFillUtil {
    private static final Logger log = LoggerFactory.getLogger(ExcelLocalPlaceholderFillUtil.class);
    public static final String DEFAULT_LIST_PLACEHOLDER_PREFIX = "data";
    private static final FillConfig DEFAULT_VERTICAL_FILL_CONFIG;
    private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{\\s*" + DEFAULT_LIST_PLACEHOLDER_PREFIX + "\\.[^}]+\\}");

    static {
        DEFAULT_VERTICAL_FILL_CONFIG = new FillConfig();
        DEFAULT_VERTICAL_FILL_CONFIG.setDirection(com.alibaba.excel.enums.WriteDirectionEnum.VERTICAL);
    }

    // ========== 样式配置类 ==========
    public static class StyleConfig {
        // 单元格样式
        private BorderStyle borderStyle = BorderStyle.THIN;
        private IndexedColors borderColor = IndexedColors.BLACK;
        private IndexedColors bgColor = IndexedColors.WHITE;
        private HorizontalAlignment horizontalAlign = HorizontalAlignment.CENTER;
        private VerticalAlignment verticalAlign = VerticalAlignment.CENTER;
        private boolean wrapText = false;
        // 字体样式
        private String fontName = "微软雅黑";
        private short fontSize = 10;
        private boolean fontBold = false;
        private IndexedColors fontColor = IndexedColors.BLACK;

        public StyleConfig() {}

        // Getter & Setter
        public BorderStyle getBorderStyle() { return borderStyle; }
        public void setBorderStyle(BorderStyle borderStyle) { this.borderStyle = borderStyle; }
        public IndexedColors getBorderColor() { return borderColor; }
        public void setBorderColor(IndexedColors borderColor) { this.borderColor = borderColor; }
        public IndexedColors getBgColor() { return bgColor; }
        public void setBgColor(IndexedColors bgColor) { this.bgColor = bgColor; }
        public HorizontalAlignment getHorizontalAlign() { return horizontalAlign; }
        public void setHorizontalAlign(HorizontalAlignment horizontalAlign) { this.horizontalAlign = horizontalAlign; }
        public VerticalAlignment getVerticalAlign() { return verticalAlign; }
        public void setVerticalAlign(VerticalAlignment verticalAlign) { this.verticalAlign = verticalAlign; }
        public boolean isWrapText() { return wrapText; }
        public void setWrapText(boolean wrapText) { this.wrapText = wrapText; }
        public String getFontName() { return fontName; }
        public void setFontName(String fontName) { this.fontName = fontName; }
        public short getFontSize() { return fontSize; }
        public void setFontSize(short fontSize) { this.fontSize = fontSize; }
        public boolean isFontBold() { return fontBold; }
        public void setFontBold(boolean fontBold) { this.fontBold = fontBold; }
        public IndexedColors getFontColor() { return fontColor; }
        public void setFontColor(IndexedColors fontColor) { this.fontColor = fontColor; }
    }

    // ========== Sheet匹配实体(支持名称/索引) ==========
    public static class SheetData {
        // Sheet名称(优先级高于索引)
        private String sheetName;
        // Sheet索引(名称为空时使用)
        private int sheetIndex;
        // 填充数据
        private List<?> dataList;

        // 构造器1:按名称匹配
        public SheetData(String sheetName, List<?> dataList) {
            this.sheetName = sheetName;
            this.sheetIndex = -1;
            this.dataList = dataList;
        }

        // 构造器2:按索引匹配
        public SheetData(int sheetIndex, List<?> dataList) {
            this.sheetName = null;
            this.sheetIndex = sheetIndex;
            this.dataList = dataList;
        }

        // Getter & Setter
        public String getSheetName() { return sheetName; }
        public void setSheetName(String sheetName) { this.sheetName = sheetName; }
        public int getSheetIndex() { return sheetIndex; }
        public void setSheetIndex(int sheetIndex) { this.sheetIndex = sheetIndex; }
        public List<?> getDataList() { return dataList; }
        public void setDataList(List<?> dataList) { this.dataList = dataList; }
    }

    // ========== 核心方法1:兼容原有Map<String, List<?>>(按名称匹配) ==========
    public static <T> void fillAndWriteToLocal(String templatePath, String outputPath,
                                               T singleData, Map<String, List<?>> multiSheetDataMap,
                                               String listPrefix, StyleConfig styleConfig) throws IOException {
        // 转换为SheetData列表
        List<SheetData> sheetDataList = new ArrayList<>();
        if (multiSheetDataMap != null && !multiSheetDataMap.isEmpty()) {
            for (Map.Entry<String, List<?>> entry : multiSheetDataMap.entrySet()) {
                sheetDataList.add(new SheetData(entry.getKey(), entry.getValue()));
            }
        }
        // 调用核心方法2
        fillAndWriteToLocal(templatePath, outputPath, singleData, sheetDataList, listPrefix, styleConfig);
    }

    // ========== 核心方法2:新增支持SheetData列表(名称/索引双匹配) ==========
    public static <T> void fillAndWriteToLocal(String templatePath, String outputPath,
                                               T singleData, List<SheetData> sheetDataList,
                                               String listPrefix, StyleConfig styleConfig) throws IOException {
        // 1. 参数校验
        if (StringUtils.isBlank(templatePath) || StringUtils.isBlank(outputPath)) {
            throw new IllegalArgumentException("模板/输出路径不能为空");
        }
        String prefix = StringUtils.isBlank(listPrefix) ? DEFAULT_LIST_PLACEHOLDER_PREFIX : listPrefix;
        StyleConfig finalStyle = styleConfig == null ? new StyleConfig() : styleConfig;

        // 2. 预处理空集合:为空集合填充空对象(清除占位符)
        List<SheetData> processedSheetDataList = processEmptySheetData(sheetDataList);

        // 3. 读取模板,获取Sheet名称-索引映射(用于索引转名称)
        Map<Integer, String> sheetIndexToNameMap = getSheetIndexToNameMap(templatePath);
        Map<String, Integer> sheetNameToIndexMap = new HashMap<>();
        for (Map.Entry<Integer, String> entry : sheetIndexToNameMap.entrySet()) {
            sheetNameToIndexMap.put(entry.getValue(), entry.getKey());
        }

        // 4. 识别模板中的占位符行(按名称)
        Map<String, Set<Integer>> placeholderRowMap = new HashMap<>();
        for (SheetData sheetData : processedSheetDataList) {
            String sheetName = getSheetName(sheetData, sheetIndexToNameMap);
            if (sheetName == null) {
                log.warn("Sheet不存在:名称={}, 索引={}", sheetData.getSheetName(), sheetData.getSheetIndex());
                continue;
            }
            if (!placeholderRowMap.containsKey(sheetName)) {
                placeholderRowMap.put(sheetName, getTemplatePlaceholderRows(templatePath, sheetName, prefix));
            }
        }

        // 5. 记录模板原有单元格(排除占位符行)
        Map<String, Set<String>> templateCellMap = new HashMap<>();
        for (String sheetName : placeholderRowMap.keySet()) {
            templateCellMap.put(sheetName, getTemplateCellMapExcludePlaceholder(templatePath, sheetName, placeholderRowMap.get(sheetName)));
        }

        // 6. EasyExcel填充数据(支持名称/索引)
        File tempFile = File.createTempFile("excel_temp_", ".xlsx");
        try {
            ExcelWriter writer = EasyExcel.write(tempFile)
                    .withTemplate(new File(templatePath))
                    .inMemory(true)
                    .build();

            // 填充单数据
            if (singleData != null) {
                writer.fill(singleData, EasyExcel.writerSheet().build());
            }

            // 填充多Sheet列表(支持名称/索引)
            if (processedSheetDataList != null && !processedSheetDataList.isEmpty()) {
                for (SheetData sheetData : processedSheetDataList) {
                    String sheetName = getSheetName(sheetData, sheetIndexToNameMap);
                    if (sheetName == null) {
                        continue;
                    }
                    int sheetIndex = sheetNameToIndexMap.get(sheetName);
                    List<?> dataList = sheetData.getDataList();

                    WriteSheet sheet = EasyExcel.writerSheet(sheetIndex, sheetName).build();
                    writer.fill(new FillWrapper(prefix, dataList), DEFAULT_VERTICAL_FILL_CONFIG, sheet);
                }
            }
            writer.finish();

            // 7. 精准设置样式
            setPreciseCellStyle(tempFile, outputPath, finalStyle, templateCellMap, placeholderRowMap);

            log.info("导出成功!路径:{}", outputPath);
        } finally {
            // 删除临时文件
            tempFile.delete();
        }
    }

    // ========== 重载方法 ==========
    // 重载1:默认前缀+样式
    public static <T> void fillAndWriteToLocal(String templatePath, String outputPath,
                                               T singleData, List<SheetData> sheetDataList) throws IOException {
        fillAndWriteToLocal(templatePath, outputPath, singleData, sheetDataList, DEFAULT_LIST_PLACEHOLDER_PREFIX, new StyleConfig());
    }

    // 重载2:兼容原有Map,默认前缀+样式
    public static <T> void fillAndWriteToLocal(String templatePath, String outputPath,
                                               T singleData, Map<String, List<?>> multiSheetDataMap) throws IOException {
        fillAndWriteToLocal(templatePath, outputPath, singleData, multiSheetDataMap, DEFAULT_LIST_PLACEHOLDER_PREFIX, new StyleConfig());
    }

    // ========== 工具方法:获取Sheet名称(兼容名称/索引) ==========
    private static String getSheetName(SheetData sheetData, Map<Integer, String> sheetIndexToNameMap) {
        // 优先使用名称
        if (StringUtils.isNotBlank(sheetData.getSheetName())) {
            for (String name : sheetIndexToNameMap.values()) {
                if (name.equals(sheetData.getSheetName())) {
                    return name;
                }
            }
            return null;
        }
        // 名称为空时使用索引
        int index = sheetData.getSheetIndex();
        return sheetIndexToNameMap.getOrDefault(index, null);
    }

    // ========== 获取模板Sheet索引-名称映射 ==========
    private static Map<Integer, String> getSheetIndexToNameMap(String templatePath) throws IOException {
        Map<Integer, String> map = new HashMap<>();
        Workbook workbook = new XSSFWorkbook(new FileInputStream(templatePath));
        for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
            map.put(i, workbook.getSheetName(i));
        }
        workbook.close();
        return map;
    }

    // ========== 预处理空集合(SheetData版本) ==========
    private static List<SheetData> processEmptySheetData(List<SheetData> originalSheetDataList) {
        if (originalSheetDataList == null || originalSheetDataList.isEmpty()) {
            return new ArrayList<>();
        }

        List<SheetData> processedList = new ArrayList<>();
        for (SheetData sheetData : originalSheetDataList) {
            List<?> dataList = sheetData.getDataList();
            // 空集合:创建空对象
            if (CollectionUtils.isEmpty(dataList)) {
                try {
                    List<Object> emptyList = new ArrayList<>();
                    if (dataList != null && !dataList.getClass().isAssignableFrom(ArrayList.class)) {
                        Object emptyObj = createEmptyObject(dataList);
                        emptyList.add(emptyObj);
                    } else {
                        emptyList.add(new HashMap<String, String>() {{
                            put("serialNo", "");
                            put("fundFullName", "");
                            put("filingNumber", "");
                            put("executionStatus", "");
                            put("remarks", "");
                            put("majorInvestmentProject", "");
                        }});
                    }
                    // ========== 修复:根据类型选择构造器 ==========
                    SheetData newSheetData;
                    if (StringUtils.isNotBlank(sheetData.getSheetName())) {
                        // 按名称构造
                        newSheetData = new SheetData(sheetData.getSheetName(), emptyList);
                    } else {
                        // 按索引构造
                        newSheetData = new SheetData(sheetData.getSheetIndex(), emptyList);
                    }
                    processedList.add(newSheetData);
                } catch (Exception e) {
                    List<String> emptyList = new ArrayList<>();
                    emptyList.add("");
                    // ========== 修复:根据类型选择构造器 ==========
                    SheetData newSheetData;
                    if (StringUtils.isNotBlank(sheetData.getSheetName())) {
                        newSheetData = new SheetData(sheetData.getSheetName(), emptyList);
                    } else {
                        newSheetData = new SheetData(sheetData.getSheetIndex(), emptyList);
                    }
                    processedList.add(newSheetData);
                }
            } else {
                // 非空集合:直接使用
                processedList.add(sheetData);
            }
        }
        return processedList;
    }

    // ========== 识别指定Sheet的占位符行 ==========
    private static Set<Integer> getTemplatePlaceholderRows(String templatePath, String sheetName, String prefix) throws IOException {
        Set<Integer> placeholderRows = new HashSet<>();
        Workbook workbook = new XSSFWorkbook(new FileInputStream(templatePath));
        Sheet sheet = workbook.getSheet(sheetName);
        if (sheet == null) {
            workbook.close();
            return placeholderRows;
        }

        Pattern pattern = Pattern.compile("\\{\\s*" + prefix + "\\.[^}]+\\}");
        for (Row row : sheet) {
            int rowNum = row.getRowNum();
            boolean isPlaceholderRow = false;

            for (Cell cell : row) {
                String cellValue = getCellStringValue(cell);
                if (cellValue != null && pattern.matcher(cellValue).find()) {
                    isPlaceholderRow = true;
                    break;
                }
            }

            if (isPlaceholderRow) {
                placeholderRows.add(rowNum);
            }
        }
        workbook.close();
        return placeholderRows;
    }

    // ========== 记录指定Sheet的模板单元格(排除占位符行) ==========
    private static Set<String> getTemplateCellMapExcludePlaceholder(String templatePath, String sheetName, Set<Integer> placeholderRows) throws IOException {
        Set<String> cellSet = new HashSet<>();
        Workbook workbook = new XSSFWorkbook(new FileInputStream(templatePath));
        Sheet sheet = workbook.getSheet(sheetName);
        if (sheet == null) {
            workbook.close();
            return cellSet;
        }

        for (Row row : sheet) {
            int rowNum = row.getRowNum();
            if (placeholderRows.contains(rowNum)) {
                continue;
            }

            for (Cell cell : row) {
                String cellKey = rowNum + "_" + cell.getColumnIndex();
                cellSet.add(cellKey);
            }
        }
        workbook.close();
        return cellSet;
    }

    // ========== 精准设置样式 ==========
    private static void setPreciseCellStyle(File tempFile, String outputPath,
                                            StyleConfig styleConfig,
                                            Map<String, Set<String>> templateCellMap,
                                            Map<String, Set<Integer>> placeholderRowMap) throws IOException {
        Workbook workbook = new XSSFWorkbook(new FileInputStream(tempFile));
        CellStyle dataCellStyle = createPoiCellStyle(workbook, styleConfig);

        for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
            Sheet sheet = workbook.getSheetAt(i);
            String sheetName = sheet.getSheetName();
            Set<String> templateCells = templateCellMap.getOrDefault(sheetName, new HashSet<>());
            Set<Integer> placeholderRows = placeholderRowMap.getOrDefault(sheetName, new HashSet<>());

            for (Row row : sheet) {
                int rowNum = row.getRowNum();
                for (Cell cell : row) {
                    int colNum = cell.getColumnIndex();
                    String cellKey = rowNum + "_" + colNum;

                    boolean isPlaceholderRow = placeholderRows.contains(rowNum);
                    boolean isNewDataCell = !templateCells.contains(cellKey) && cell.getCellType() != CellType.BLANK;

                    if (isPlaceholderRow || isNewDataCell) {
                        cell.setCellStyle(dataCellStyle);
                    }
                }
            }
        }

        // 写入最终文件
        File outputFile = new File(outputPath);
        if (!outputFile.getParentFile().exists()) {
            outputFile.getParentFile().mkdirs();
        }
        try (FileOutputStream fos = new FileOutputStream(outputFile)) {
            workbook.write(fos);
        }
        workbook.close();
    }

    // ========== POI创建样式 ==========
    private static CellStyle createPoiCellStyle(Workbook workbook, StyleConfig config) {
        CellStyle style = workbook.createCellStyle();

        // 边框
        style.setBorderTop(config.getBorderStyle());
        style.setBorderBottom(config.getBorderStyle());
        style.setBorderLeft(config.getBorderStyle());
        style.setBorderRight(config.getBorderStyle());
        style.setTopBorderColor(config.getBorderColor().getIndex());
        style.setBottomBorderColor(config.getBorderColor().getIndex());
        style.setLeftBorderColor(config.getBorderColor().getIndex());
        style.setRightBorderColor(config.getBorderColor().getIndex());

        // 背景色
        if (config.getBgColor() != IndexedColors.WHITE) {
            style.setFillForegroundColor(config.getBgColor().getIndex());
            style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        } else {
            style.setFillPattern(FillPatternType.NO_FILL);
        }

        // 对齐
        style.setAlignment(config.getHorizontalAlign());
        style.setVerticalAlignment(config.getVerticalAlign());
        style.setWrapText(config.isWrapText());

        // 字体
        Font font = workbook.createFont();
        font.setFontName(config.getFontName());
        font.setFontHeightInPoints(config.getFontSize());
        font.setBold(config.isFontBold());
        font.setColor(config.getFontColor().getIndex());
        style.setFont(font);

        return style;
    }

    // ========== 反射创建空对象 ==========
    private static Object createEmptyObject(List<?> dataList) throws Exception {
        Class<?> elementClass = dataList.getClass().getGenericSuperclass().getClass();
        if (dataList.size() == 0) {
            return new HashMap<String, String>();
        }
        elementClass = dataList.get(0).getClass();
        Object emptyObj = elementClass.getDeclaredConstructor().newInstance();

        Field[] fields = elementClass.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            Class<?> fieldType = field.getType();
            if (fieldType == String.class) {
                field.set(emptyObj, "");
            } else if (fieldType == Integer.class || fieldType == int.class) {
                field.set(emptyObj, 0);
            } else if (fieldType == Long.class || fieldType == long.class) {
                field.set(emptyObj, 0L);
            } else if (fieldType == Double.class || fieldType == double.class) {
                field.set(emptyObj, 0.0);
            } else {
                field.set(emptyObj, "");
            }
        }
        return emptyObj;
    }

    // ========== 工具方法:获取单元格字符串值 ==========
    private static String getCellStringValue(Cell cell) {
        if (cell == null) {
            return null;
        }
        switch (cell.getCellType()) {
            case STRING:
                return cell.getStringCellValue();
            case NUMERIC:
                return String.valueOf(cell.getNumericCellValue());
            case BOOLEAN:
                return String.valueOf(cell.getBooleanCellValue());
            case FORMULA:
                return cell.getCellFormula();
            default:
                return null;
        }
    }
}

新增自定义注解:

java 复制代码
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Excel列映射注解(通用)
 * 标记实体类字段对应的Excel模板列号
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelColumn {
    /**
     * 模板中的列号(从0开始)
     */
    int columnIndex();

    /**
     * 表头名称(用于校验,可选)
     */
    String columnName() default "";
}

实体属性添加自定义注解:

java 复制代码
import java.math.BigDecimal;

import com.alibaba.excel.annotation.ExcelProperty;

import lombok.Data;
import net.coding.excelUtils.listener.ExcelColumn;


@Data
public class FundDetail {
 
    
    @ExcelProperty("序号")
    @ExcelColumn(columnIndex = 0, columnName = "序号")
    private String serialNo;

    @ExcelProperty("基金全称")
    @ExcelColumn(columnIndex = 1, columnName = "基金全称")
    private String fundFullName;

    @ExcelProperty("基金报备编号")
    @ExcelColumn(columnIndex = 2, columnName = "基金报备编号")
    private String filingNumber;

    @ExcelProperty("重大投资项目")
    @ExcelColumn(columnIndex = 22, columnName = "重大投资项目")
    private String majorInvestmentProject;

    @ExcelProperty("备注")
    @ExcelColumn(columnIndex = 23, columnName = "备注")
    private String remarks;
}

可以看到其中一个sheet页设置的有占位符,在第6个sheet页也设置下占位符

测试:

java 复制代码
import com.alibaba.excel.write.handler.CellWriteHandler;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.VerticalAlignment;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * EasyExcel 3.3.2 测试类(100%无报错)
 * 注意:所有导入包必须匹配3.3.2版本
 */
public class TestFillWrite {


    public static void main(String[] args) {
        try {
            // 1. 路径配置
            String path = "C:\\Users\\mgl\\Desktop\\文件副本\\";
            String templatePath = path + "投资基金占位符写入模板.xlsx";
            String outputPath = path + "投资基金_索引名称混合版213.xlsx";

            // 2. 测试数据
            List<FundDetail> fundList = new ArrayList<>();
            FundDetail item1 = new FundDetail();
            item1.setSerialNo("1");
            item1.setFundFullName("中国上海基金会");
            item1.setFilingNumber("21654821545465456");
            item1.setExecutionStatus("执行中");
            item1.setRemarks("上海的备注");
            item1.setMajorInvestmentProject("上海的类型");
            fundList.add(item1);

            FundDetail item2 = new FundDetail();
            item2.setSerialNo("2");
            item2.setFundFullName("中国北京基金会");
            item2.setFilingNumber("21487231251315");
            item2.setExecutionStatus("未执行");
            item2.setRemarks("北京的备注");
            item2.setMajorInvestmentProject("北京的类型");
            fundList.add(item2);

            // 3. Sheet数据配置(混合名称/索引)
            List<ExcelLocalPlaceholderFillUtil.SheetData> sheetDataList = new ArrayList<>();
            sheetDataList.add(new ExcelLocalPlaceholderFillUtil.SheetData("25基", fundList)); // 名称
            sheetDataList.add(new ExcelLocalPlaceholderFillUtil.SheetData(5, new ArrayList<>()));      // 索引

            // 4. 样式配置
            ExcelLocalPlaceholderFillUtil.StyleConfig styleConfig = new ExcelLocalPlaceholderFillUtil.StyleConfig();
            //styleConfig.setBorderStyle(BorderStyle.MEDIUM);
            styleConfig.setBorderColor(IndexedColors.BLACK);
            styleConfig.setHorizontalAlign(HorizontalAlignment.CENTER);
            styleConfig.setVerticalAlign(VerticalAlignment.CENTER);
            styleConfig.setWrapText(true);
            styleConfig.setFontName("微软雅黑");
            styleConfig.setFontSize((short) 12);
            styleConfig.setFontBold(true);
            styleConfig.setFontColor(IndexedColors.BLUE);

            // 5. 核心调用
            ExcelLocalPlaceholderFillUtil.fillAndWriteToLocal(
                    templatePath,
                    outputPath,
                    null,
                    sheetDataList,
                    "data",
                    styleConfig
            );

            System.out.println("✅ 导出完成!无构造器报错,功能正常");
        } catch (Exception e) {
            System.err.println("❌ 失败:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

查看效果

可以看到数据正常渲染,而且可以自定义字体和单元格样式

再看第6个sheet页

可以看到第6个sheet页原有的占位符也被清掉了.因为我们给第6个sheet页传递的是空集合数据

如果传递的是空集合数据,不需要额外进行处理了,工具类里都已经对sheet页进行处理

相关推荐
cike_y2 小时前
Servlet原理&Mapping问题&ServletContext对象
java·安全·javaweb
lalala_lulu2 小时前
Jsp的四种作用域(超详细)
java·开发语言·hive
好奇的候选人面向对象3 小时前
企业微信接入自定义系统(Java+Vue3)实现共享文档创建与数据统计
java·状态模式·企业微信
橙露3 小时前
Nginx Location配置全解析:从基础到实战避坑
java·linux·服务器
无敌最俊朗@9 小时前
STL-vector面试剖析(面试复习4)
java·面试·职场和发展
PPPPickup9 小时前
easychat项目复盘---获取联系人列表,联系人详细,删除拉黑联系人
java·前端·javascript
LiamTuc9 小时前
Java构造函数
java·开发语言
长安er10 小时前
LeetCode 206/92/25 链表翻转问题-“盒子-标签-纸条模型”
java·数据结构·算法·leetcode·链表·链表翻转
菜鸟plus+10 小时前
N+1查询
java·服务器·数据库