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())
- 注释掉实体类的注解
- 继承
AbstractMergeStrategy
类并重写merge方法 - 取消
registerWriteHandler(new MergeStrategy())
注释 - 使用
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
的级别更高。