当前位置: 首页 > news >正文

EasyExcel 合并单元格最佳实践:基于注解的自动合并与样式控制

EasyExcel 合并单元格最佳实践:基于注解的自动合并与样式控制

前言

在日常开发中,我们经常需要导出 Excel 报表,而合并单元格是提升报表可读性的常见需求。本文将介绍如何基于 EasyExcel 实现智能的单元格合并功能,通过自定义注解 @ExcelMerge 标记需要合并的字段,并确保合并后的内容完美居中对齐。

核心功能

  1. 注解驱动:通过 @ExcelMerge 注解标记需要合并的字段
  2. 自动合并:相邻行相同值的单元格自动合并
  3. 样式控制:合并后的单元格内容水平和垂直居中
  4. 兼容性:支持 EasyExcel 原生功能(自动列宽、下拉框等)

实现代码

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);
}

技术要点解析

  1. 合并策略实现

    • 继承 AbstractMergeStrategy 实现自定义合并逻辑
    • 通过反射获取标记了 @ExcelMerge 的字段值
    • 计算需要合并的单元格区域(CellRangeAddress
  2. 样式控制

    • 使用 HorizontalCellStyleStrategy 设置内容居中对齐
    • 表头使用默认样式,内容使用自定义样式
  3. 性能优化

    • 只在第一行数据时执行合并操作(relativeRowIndex == 0
    • 按列处理,避免重复计算

常见问题解决

1. 合并区域重叠问题

错误信息:

Cannot add merged region A2:A6 to sheet because it overlaps with an existing merged region

解决方案:

  • 确保每个合并操作只执行一次
  • 可以使用 Set 记录已处理的列,避免重复合并

2. 字段顺序问题

确保实体类字段顺序与 Excel 列顺序一致:

  1. 保持字段声明顺序
  2. 或使用 @ExcelProperty 注解指定顺序

3. 大数据量性能优化

当数据量较大时:

  1. 考虑分批处理
  2. 缓存字段信息,减少反射调用

总结

本文实现的 Excel 合并工具具有以下优势:

  1. 简单易用:通过注解标记即可实现自动合并
  2. 灵活可控:可以单独控制每个字段是否合并
  3. 样式美观:合并后的单元格自动居中对齐
  4. 功能完善:兼容 EasyExcel 的各项特性

通过这种方式,我们可以轻松实现专业级的 Excel 导出功能,提升报表的可读性和美观度。

http://www.dtcms.com/a/339648.html

相关文章:

  • 大模型如何通过人工指标来评估效果?有哪些常用的人工指标?
  • FGF21对牛磺胆酸钠诱导的急性胰腺损伤的应答
  • DAY48 随机函数与广播机制
  • 补偿电流控制的APF并联型有源电力滤波器simulink
  • 2025年09月计算机二级MySQL选择题每日一练——第一期
  • 《深入源码理解webpack构建流程》
  • springboot 项目正常启动后自动结束
  • 当硅基生命遇见碳基萌宠:Deepoc具身智能如何重新定义“宠物监护者”
  • 【QT入门到晋级】进程间通信(IPC)-socket(包含性能优化案例)
  • UX 设计入门第二课:如何洞察人心?用户研究方法论导览
  • elementplus组件文本框设置前缀
  • 07复杂度分析实战习题集
  • PG靶机 - Pebbles
  • 无人机/航测/三维建模领域常见的“航线规划或建模方式
  • 基于单片机智能拐杖/导盲杖/老人防摔倒设计
  • python-林粒粒的视频笔记1
  • 网络数据包
  • 用relation-graph构建关系图谱 vue版
  • 单片 、物联网、51单片机、软硬件之基于STM32与蓝牙的仓储管控系统的设计与实现/基于物联网的仓库管理系统
  • 【实时Linux实战系列】基于实时Linux的物联网系统设计
  • AI硬件 - AMD显卡架构演进及产品线
  • 快速傅里叶变换:数字信号处理的基石算法
  • ubuntu24.04 用apt安装的mysql修改存储路径(文件夹、目录)
  • Linux 系统下安装 uv 并在 PyCharm 中使用指南
  • Z-Score归一化:原理、作用与实战解析
  • UGUI源码剖析(10):总结——基于源码分析的UGUI设计原则与性能优化策略
  • Spring框架-数据访问层和事务管理
  • 不止是耳机,运动与商务的结合!雷登A7开放式蓝牙耳机
  • Linux下的软件编程——IPC机制(信号和共享内存)
  • QT6(常用界面组件的使用和布局管理)