EasyExcel 动态多级标题、合并单元格、修改单元格样式实现总结

POM 依赖

xml 复制代码
<dependency>  
    <groupId>com.alibaba</groupId>  
    <artifactId>easyexcel</artifactId>  
    <version>4.0.3</version>  
</dependency>

注解多级标题

java 复制代码
public class AnnotationHeadDemo {  
    public static void main(String[] args) {  
        EasyExcel.write("多级标题.xlsx", WeekData.class).sheet("测试").doWrite(new ArrayList<>());  
    }}
java 复制代码
package com.polaris.excel;  
  
import com.alibaba.excel.annotation.ExcelProperty;  
  
/**  
 * @author xuxx@tsintergy.com  
 * @since 2025/10/1  
 */public class WeekData {  
  
    @ExcelProperty(value = {"第一周","周一"})  
    private String monday;  
    @ExcelProperty(value = {"第一周","周二"})  
    private String tuesday;  
    @ExcelProperty(value = {"第一周","周三"})  
    private String wednesday;  
    @ExcelProperty(value = {"第一周","周四"})  
    private String thursday;  
    @ExcelProperty(value = {"第一周","周五"})  
    private String friday;  
    @ExcelProperty(value = {"第一周","周六"})  
    private String saturday;  
    @ExcelProperty(value = {"第一周","周日"})  
    private String sunday;  
  
    public String getMonday() {  
        return monday;  
    }  
    public void setMonday(String monday) {  
        this.monday = monday;  
    }  
    public String getTuesday() {  
        return tuesday;  
    }  
    public void setTuesday(String tuesday) {  
        this.tuesday = tuesday;  
    }  
    public String getWednesday() {  
        return wednesday;  
    }  
    public void setWednesday(String wednesday) {  
        this.wednesday = wednesday;  
    }  
    public String getThursday() {  
        return thursday;  
    }  
    public void setThursday(String thursday) {  
        this.thursday = thursday;  
    }  
    public String getFriday() {  
        return friday;  
    }  
    public void setFriday(String friday) {  
        this.friday = friday;  
    }  
    public String getSaturday() {  
        return saturday;  
    }  
    public void setSaturday(String saturday) {  
        this.saturday = saturday;  
    }  
    public String getSunday() {  
        return sunday;  
    }  
    public void setSunday(String sunday) {  
        this.sunday = sunday;  
    }}

注解标题适用于固定列的标题。

动态多级标题

java 复制代码
package com.polaris.excel;  
  
import com.alibaba.excel.EasyExcel;  
import com.alibaba.excel.ExcelWriter;  
import com.alibaba.excel.write.metadata.WriteSheet;  
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;  
  
import java.time.LocalDate;  
import java.time.YearMonth;  
import java.time.format.DateTimeFormatter;  
import java.time.temporal.ChronoUnit;  
import java.util.ArrayList;  
import java.util.Arrays;  
import java.util.List;  
  
/**  
 * @author xuxx@tsintergy.com  
 * @since 2025/10/1  
 */public class DynamicHeadDemo {  
  
    private static final List<String> WEEK_ALIAS = Arrays.asList("周一", "周二", "周三", "周四", "周五", "周六", "周日");  
  
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("MM-dd");  
  
    public static void main(String[] args) {  
  
        List<List<String>> headList = headList(YearMonth.of(2025, 6), YearMonth.of(2025, 8));  
        // 多加一行测试
        headList.add(Arrays.asList("第1周", "周一", "test"));
        WriteSheet sheet = EasyExcel.writerSheet("测试sheet").registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())  
                .head(headList)  
                .build();  
        String fileName = "多级动态标题.xlsx";  
        try (ExcelWriter excelWriter = EasyExcel.write(fileName).build()) {  
            excelWriter.write(new ArrayList<>(), sheet);  
        }    }  
    private static List<List<String>> headList(YearMonth startMonth, YearMonth endMonth) {  
        List<List<String>> headList = new ArrayList<>();  
        LocalDate date = startMonth.atDay(1);  
        // 从周一开始  
        LocalDate startDate = date.minusDays((date.getDayOfWeek().ordinal()) % 7);  
        LocalDate endOfMonth = endMonth.atEndOfMonth();  
        // 从周日结束  
        LocalDate endDate = endOfMonth.plusDays((endOfMonth.getDayOfWeek().ordinal()) % 7);  
        long between = ChronoUnit.DAYS.between(startDate, endDate);  
        for (long l = 0; l < between; l++) {  
            LocalDate localDate = startDate.plusDays(l);  
            long index = l / 7 + 1;  
            List<String> dateList = Arrays.asList("第" + index + "周", WEEK_ALIAS.get(localDate.getDayOfWeek().ordinal()), FORMATTER.format(localDate));  
            headList.add(dateList);  
        }        return headList;  
  
    }}
    
    

实现效果:

动态多级标题通过List<List<String>>实现,可以理解为:外层的List表示Sheet中的列(column),内层的List表示Sheet中的行(row),在上述实现中,一个单元格(cell)中有三级标题,所以有:

java 复制代码
List<String> dateList = Arrays.asList("第" + index + "周", WEEK_ALIAS.get(localDate.getDayOfWeek().ordinal()), FORMATTER.format(localDate)); 

从图中可以看出,05-26到06-01,它们dateList的第一个值为第1周,因此,这7列的第一级标题就合并成一个单元格。然而,在headList最后加上了

java 复制代码
headList.add(Arrays.asList("第1周", "周一", "test"));

这一项不会合并到开始的第1周 去,所以可得EasyExcel这里的实现是根据同行不同列的值相同,并且是连续列相同值,才合并到一个标题

合并单元格

注解形式

java 复制代码
package com.polaris.excel;  
  
import com.alibaba.excel.annotation.ExcelProperty;  
import com.alibaba.excel.annotation.write.style.ContentLoopMerge;  
  
/**  
 * @author xuxx@tsintergy.com  
 * @since 2025/10/1  
 */public class MergeData {  
  
    @ContentLoopMerge(eachRow = 2 ,columnExtend = 2)  
    @ExcelProperty("合并的列")
    private String merge;  
  
    @ExcelProperty("被覆盖的列")  
    private String cover;  
  
    @ExcelProperty("普通列")  
    private String common;  
  
    public MergeData() {  
    }  
    public MergeData(String merge, String cover, String common) {  
        this.merge = merge;  
        this.cover = cover;  
        this.common = common;  
    }  
    public String getCover() {  
        return cover;  
    }  
    public void setCover(String cover) {  
        this.cover = cover;  
    }  
    public String getMerge() {  
        return merge;  
    }  
    public void setMerge(String merge) {  
        this.merge = merge;  
    }  
    public String getCommon() {  
        return common;  
    }  
    public void setCommon(String common) {  
        this.common = common;  
    }}
java 复制代码
package com.polaris.excel;  
  
import com.alibaba.excel.EasyExcel;  
import com.alibaba.excel.ExcelWriter;  
import com.alibaba.excel.write.metadata.WriteSheet;  
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;  
  
import java.util.ArrayList;  
import java.util.List;  
import java.util.concurrent.ThreadLocalRandom;  
  
/**  
 * @author xuxx@tsintergy.com  
 * @since 2025/10/2  
 */public class MergeCellDemo {  
    public static void main(String[] args) {  
        String filename = "合并单元.xlsx";  
        ThreadLocalRandom current = ThreadLocalRandom.current();  
        int nextInt = current.nextInt(20, 50);  
        List<MergeData> mergeData = new ArrayList<>();  
        for (int i = 0; i < nextInt; i++) {  
  
            mergeData.add(new MergeData(i + "", "被覆盖的列" + i, "普通列" + i));  
        }  
//        EasyExcel.write(filename, MergeData.class)  
//                .registerWriteHandler(new CustomCellStyleStrategy())  
//                .sheet("ddd")  
//                // 注册自定义样式类  
//                .doWrite(mergeData);  
  
        try (ExcelWriter writer = EasyExcel.write(filename).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build()) {  
            WriteSheet sheet = EasyExcel.writerSheet("test")  
//                    .registerWriteHandler(new CustomCellStyleStrategy())  
                    .head(MergeData.class)  
//                    .registerWriteHandler(new MergeStrategy())  
                    .build();  
            WriteSheet test1 = EasyExcel.writerSheet("test1")  
                    .head(MergeData.class)  
//                    .registerWriteHandler(new CustomCellStyleStrategy())  
                    .build();  
            writer.write(mergeData, sheet);  
            writer.write(mergeData, test1);  
        }  
  
    }}

合并的值为合并对应项的第一行第一列的单元格的值,并且两个sheet都合并了,

下图还可以发现test列宽做了处理:

java 复制代码
ExcelWriter writer = EasyExcel.write(filename).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build()

说明这个构建writer,默认给了test LongestMatchColumnWidthStyleStrategy,而test1没有享受到该策略。

复杂的单元格

通过继承com.alibaba.excel.write.merge.AbstractMergeStrategy 并实现其merge方法

java 复制代码
package com.alibaba.excel.write.merge;  
  
import com.alibaba.excel.metadata.Head;  
import com.alibaba.excel.write.handler.CellWriteHandler;  
import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;  
  
import org.apache.poi.ss.usermodel.Cell;  
import org.apache.poi.ss.usermodel.Sheet;  
  
/**  
 * Merge strategy * * @author Jiaju Zhuang  
 */public abstract class AbstractMergeStrategy implements CellWriteHandler {  
  
    @Override  
    public void afterCellDispose(CellWriteHandlerContext context) {  
        if (context.getHead()) {  
            return;  
        }        merge(context.getWriteSheetHolder().getSheet(), context.getCell(), context.getHeadData(),  
            context.getRelativeRowIndex());  
    }  
    /**  
     * merge     
     * @param sheet  
     * @param cell  
     * @param head  
     * @param relativeRowIndex  
     */  
    protected abstract void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex);  
}
  • sheet 当前所在的sheet
  • cell 当前所在的单元格,调用顺序为从非标题行开始的第0-N列,下一行的第0-N列
  • head 当前单元格拥有的表头,可以根据这个筛选指定列
  • relativeRowIndex 相对行,从非标题行开始算,0开始

以注解形式的例子

java 复制代码
//    @ContentLoopMerge(eachRow = 2 ,columnExtend = 2)
java 复制代码
// .registerWriteHandler(new MergeStrategy())
  1. 注释掉实体类的注解
  2. 继承AbstractMergeStrategy类并重写merge方法
  3. 取消 registerWriteHandler(new MergeStrategy())注释
  4. 使用MergeCellDemo中的代码创建excel。
java 复制代码
package com.polaris.excel;  
  
import com.alibaba.excel.metadata.Head;  
import com.alibaba.excel.write.merge.AbstractMergeStrategy;  
import org.apache.poi.ss.usermodel.Cell;  
import org.apache.poi.ss.usermodel.Sheet;  
import org.apache.poi.ss.util.CellAddress;  
import org.apache.poi.ss.util.CellRangeAddress;  
  
/**  
 * @author xuxx@tsintergy.com  
 * @since 2025/10/2  
 */public class MergeStrategy extends AbstractMergeStrategy {  
  
    @Override  
    protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {  
        if (relativeRowIndex % 2 == 0 && cell.getColumnIndex() == 0) {  
            CellRangeAddress cellAddress = new CellRangeAddress(cell.getRowIndex(), cell.getRowIndex() + 1, 0, 1);  
            sheet.addMergedRegion(cellAddress);  
        }    }}

如下图:使用registerWriteHandler(new MergeStrategy())可以使得特定的sheet才采用特定的合并策略,更加灵活。

单元格样式

可以通过实现com.alibaba.excel.write.handler.CellWriteHandler接口来修改单元格的样式。官方有个抽象类com.alibaba.excel.write.style.AbstractCellStyleStrategy实现该接口,通过继承AbstractCellStyleStrategy并重写setHeadCellStyle方法setContentCellStyle方法来修改样式,前者为修改标题的样式,后者为内容的样式

代码实现

java 复制代码
package com.polaris.excel;  
  
import com.alibaba.excel.constant.OrderConstant;  
import com.alibaba.excel.metadata.Head;  
import com.alibaba.excel.write.style.AbstractCellStyleStrategy;  
import org.apache.poi.ss.usermodel.*;  
  
/**  
 * @author xuxx@tsintergy.com  
 * @since 2025/10/1  
 */public class CustomCellStyleStrategy extends AbstractCellStyleStrategy {  
//    @Override  
//    public int order() {  
//        return OrderConstant.FILL_STYLE+1;  
//    }  
  
    @Override  
    protected void setHeadCellStyle(Cell cell, Head head, Integer relativeRowIndex) {  
    }  
  
    @Override  
    protected void setContentCellStyle(Cell cell, Head head, Integer relativeRowIndex) {  
        IndexedColors indexedColors;  
        int i = relativeRowIndex % 3;  
        if(i ==0){  
            indexedColors = IndexedColors.LIGHT_YELLOW;  
        } else if (i == 1) {  
            indexedColors = IndexedColors.LIGHT_GREEN;  
        }else {  
            indexedColors = IndexedColors.TAN;  
        }        Workbook workbook = cell.getSheet().getWorkbook();  
        CellStyle cellStyle = workbook.createCellStyle();  
        cellStyle.setBorderTop(BorderStyle.THIN);  
        cellStyle.setBorderBottom(BorderStyle.THIN);  
        cellStyle.setBorderLeft(BorderStyle.THIN);  
        cellStyle.setBorderRight(BorderStyle.THIN);  
        cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);  
        cellStyle.setAlignment(HorizontalAlignment.CENTER);  
        cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);  
        cellStyle.setFillForegroundColor(indexedColors.getIndex());  
        cellStyle.setWrapText(true);  
        cell.setCellStyle(cellStyle);  
    }}

注意:此时的代码是注释了order方法,就是使用了AbstractCellStyleStrategy默认的order方法 ,还是使用MergeCellDemo中的代码创建excel

此时数据没有效果,在com.alibaba.excel.write.metadata.holder.AbstractWriteHolder中的sortAndClearUpHandler方法断点,我们自定义的处理器在前面,而buildChain是构建过滤器链,所以最后的处理器可能会覆盖之前的样式。

EasyExcel中的过滤器链的顺序是由小到大 ,其中,默认的定义的顺序在OrderConstant类中,可知最大的级别为:FILL_STYLE

java 复制代码
package com.alibaba.excel.event;  
  
import com.alibaba.excel.constant.OrderConstant;  
  
/**  
 * Intercepts handle some business logic * * @author Jiaju Zhuang  
 **/public interface Handler extends Order {  
  
    /**  
     * handler order    
     * @return order  
     */    @Override  
    default int order() {  
        return OrderConstant.DEFAULT_ORDER;  
    }}
java 复制代码
package com.alibaba.excel.constant;  
  
/**  
 * Order constant. * * @author Jiaju Zhuang  
 */public class OrderConstant {  
  
    /**  
     * The system's own style     
     */
     public static int DEFAULT_DEFINE_STYLE = -70000;  
  
    /**  
     * Annotation style definition     
     */
     public static int ANNOTATION_DEFINE_STYLE = -60000;  
  
    /**  
     * Define style.     
     */
     public static final int DEFINE_STYLE = -50000;  
  
    /**  
     * default order.    
     */ 
     public static int DEFAULT_ORDER = 0;  
  
    /**  
     * Sorting of styles written to cells.     
     */
     public static int FILL_STYLE = 50000;  
}

接下来,把CustomCellStyleStrategy的order方法取消注释,重新断点可得:

所以,如果想我们自定义的样式的优先级最高,尽量讲顺序定义比com.alibaba.excel.constant.OrderConstant#FILL_STYLE的级别更高。

参考文档

EasyExcel

相关推荐
玩毛线的包子2 小时前
Android Gradle学习(十)- java字节码指令集解读
java
华农第一蒟蒻2 小时前
谈谈跨域问题
java·后端·nginx·安全·okhttp·c5全栈
菜鸟plus+3 小时前
MinIO
java
艾菜籽3 小时前
JVM中的垃圾回收机制
java·jvm
敲代码的嘎仔3 小时前
JavaWeb零基础学习Day1——HTML&CSS
java·开发语言·前端·css·学习·html·学习方法
Terio_my9 小时前
Java bean 数据校验
java·开发语言·python
超级大只老咪9 小时前
何为“类”?(Java基础语法)
java·开发语言·前端
我笑了OvO10 小时前
C++类和对象(1)
java·开发语言·c++·类和对象
weixin_4365250711 小时前
Gitee - IDEA 主支 master 和分支 dev 的使用
java·ide·intellij-idea