EasyExcel 合并單元格最佳實踐:基于注解的自動合并與樣式控制
前言
在日常開發中,我們經常需要導出 Excel 報表,而合并單元格是提升報表可讀性的常見需求。本文將介紹如何基于 EasyExcel 實現智能的單元格合并功能,通過自定義注解 @ExcelMerge
標記需要合并的字段,并確保合并后的內容完美居中對齊。
核心功能
- 注解驅動:通過
@ExcelMerge
注解標記需要合并的字段 - 自動合并:相鄰行相同值的單元格自動合并
- 樣式控制:合并后的單元格內容水平和垂直居中
- 兼容性:支持 EasyExcel 原生功能(自動列寬、下拉框等)
實現代碼
easyexcel 版本
<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.3.4</version></dependency>
1. 定義合并注解
import java.lang.annotation.*;/*** 標記需要合并的 Excel 列*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelMerge {/*** 是否啟用合并(默認 true)*/boolean enable() default true;
}
2. Excel 合并工具類
package cn.iocoder.yudao.framework.excel.core.util;import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.converters.longconverter.LongStringConverter;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;/*** Excel 合并單元格工具類(支持注解驅動)*/
public class ExcelMergeUtils {/*** 導出 Excel 并自動合并標記字段** @param outputStream 響應* @param filename 文件名* @param sheetName Sheet 名稱* @param head 表頭類* @param data 數據列表* @param <T> 數據類型* @throws IOException 寫入異常*/public static <T> void write(OutputStream outputStream, String filename, String sheetName,Class<T> head, List<T> data) throws IOException {// 內容樣式:水平 + 垂直居中WriteCellStyle contentStyle = new WriteCellStyle();contentStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);contentStyle.setVerticalAlignment(VerticalAlignment.CENTER);// 注冊樣式策略HorizontalCellStyleStrategy styleStrategy = new HorizontalCellStyleStrategy(null, contentStyle);// 自動合并策略(基于注解)AbstractMergeStrategy mergeStrategy = new AnnotationBasedMergeStrategy<>(data, head);// 輸出 ExcelEasyExcel.write(outputStream, head).autoCloseStream(false).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 自動列寬.registerWriteHandler(new SelectSheetWriteHandler(head)) // 下拉框支持.registerWriteHandler(mergeStrategy) // 自動合并.registerWriteHandler(styleStrategy) // 居中對齊.registerConverter(new LongStringConverter()) // Long 轉 String.sheet(sheetName).doWrite(data);}public static <T> void write(HttpServletResponse response, String filename, String sheetName,Class<T> head, List<T> data) throws IOException {write(response.getOutputStream(), filename, sheetName, head, data);// 設置響應頭response.addHeader("Content-Disposition", "attachment;filename=" +URLEncoder.encode(filename, StandardCharsets.UTF_8.name()));response.setContentType("application/vnd.ms-excel;charset=UTF-8");}/*** 基于注解的合并策略*/private static class AnnotationBasedMergeStrategy<T> extends AbstractMergeStrategy {private final List<T> dataList;private final Class<T> clazz;public AnnotationBasedMergeStrategy(List<T> dataList, Class<T> clazz) {this.dataList = dataList != null ? dataList : new ArrayList<>();this.clazz = clazz;}@Overrideprotected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {if (relativeRowIndex != 0) return; // 只在第一行處理int columnIndex = cell.getColumnIndex();Field field = clazz.getDeclaredFields()[columnIndex];if (field.isAnnotationPresent(ExcelMerge.class)) {mergeColumn(sheet, columnIndex);}}private void mergeColumn(Sheet sheet, int columnIndex) {List<CellRangeAddress> ranges = new ArrayList<>();if (dataList.isEmpty()) return;try {Field field = clazz.getDeclaredFields()[columnIndex];field.setAccessible(true);Object currentValue = field.get(dataList.get(0));int startRow = 1; // 從第2行開始(第1行是標題)for (int i = 1; i < dataList.size(); i++) {Object value = field.get(dataList.get(i));if (!value.equals(currentValue)) {if (startRow < i) {ranges.add(new CellRangeAddress(startRow, i, columnIndex, columnIndex));}currentValue = value;startRow = i + 1;}}// 處理最后一段if (startRow < dataList.size()) {ranges.add(new CellRangeAddress(startRow, dataList.size(), columnIndex, columnIndex));}// 應用合并for (CellRangeAddress range : ranges) {sheet.addMergedRegion(range);}} catch (IllegalAccessException e) {throw new RuntimeException("反射獲取字段值失敗", e);}}}
}
使用示例
1. 定義實體類
public class UserVO {@ExcelMerge // 此字段相同值會自動合并private String username;@ExcelMerge(enable = false) // 不合并private Integer age;@ExcelMerge // 此字段相同值會自動合并private String department;// 省略構造方法、getter/setter
}
2. 導出 Excel
List<UserVO> users = Arrays.asList(new UserVO("張三", 25, "研發部"),new UserVO("張三", 30, "研發部"), // username 和 department 相同,會自動合并new UserVO("李四", 28, "市場部")
);// HTTP 響應方式
ExcelMergeUtils.write(response, "users.xlsx", "用戶列表", UserVO.class, users);// 或者輸出流方式
try (OutputStream out = new FileOutputStream("users.xlsx")) {ExcelMergeUtils.write(out, "users.xlsx", "用戶列表", UserVO.class, users);
}
技術要點解析
-
合并策略實現:
- 繼承
AbstractMergeStrategy
實現自定義合并邏輯 - 通過反射獲取標記了
@ExcelMerge
的字段值 - 計算需要合并的單元格區域(
CellRangeAddress
)
- 繼承
-
樣式控制:
- 使用
HorizontalCellStyleStrategy
設置內容居中對齊 - 表頭使用默認樣式,內容使用自定義樣式
- 使用
-
性能優化:
- 只在第一行數據時執行合并操作(
relativeRowIndex == 0
) - 按列處理,避免重復計算
- 只在第一行數據時執行合并操作(
常見問題解決
1. 合并區域重疊問題
錯誤信息:
Cannot add merged region A2:A6 to sheet because it overlaps with an existing merged region
解決方案:
- 確保每個合并操作只執行一次
- 可以使用
Set
記錄已處理的列,避免重復合并
2. 字段順序問題
確保實體類字段順序與 Excel 列順序一致:
- 保持字段聲明順序
- 或使用
@ExcelProperty
注解指定順序
3. 大數據量性能優化
當數據量較大時:
- 考慮分批處理
- 緩存字段信息,減少反射調用
總結
本文實現的 Excel 合并工具具有以下優勢:
- 簡單易用:通過注解標記即可實現自動合并
- 靈活可控:可以單獨控制每個字段是否合并
- 樣式美觀:合并后的單元格自動居中對齊
- 功能完善:兼容 EasyExcel 的各項特性
通過這種方式,我們可以輕松實現專業級的 Excel 導出功能,提升報表的可讀性和美觀度。