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

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

前言

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

核心功能

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

实现代码

1. 定义合并注解

java 复制代码
import java.lang.annotation.*;

/**
 * 标记需要合并的 Excel 列
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelMerge {
    /**
     * 是否启用合并(默认 true)
     */
    boolean enable() default true;
}

2. Excel 合并工具类

java 复制代码
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);

        // 输出 Excel
        EasyExcel.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;
        }

        @Override
        protected 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. 定义实体类

java 复制代码
public class UserVO {
    @ExcelMerge // 此字段相同值会自动合并
    private String username;
    
    @ExcelMerge(enable = false) // 不合并
    private Integer age;
    
    @ExcelMerge // 此字段相同值会自动合并
    private String department;

    // 省略构造方法、getter/setter
}

2. 导出 Excel

java 复制代码
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 导出功能,提升报表的可读性和美观度。

相关推荐
DokiDoki之父16 分钟前
多线程—飞机大战排行榜功能(2.0版本)
android·java·开发语言
高山上有一只小老虎21 分钟前
走方格的方案数
java·算法
whatever who cares22 分钟前
Java 中表示数据集的常用集合类
java·开发语言
JavaArchJourney1 小时前
TreeMap 源码分析
java
whitepure1 小时前
万字详解Java中的IO及序列化
java·后端
还梦呦1 小时前
2025年09月计算机二级Java选择题每日一练——第一期
java·开发语言
与火星的孩子对话1 小时前
Unity高级开发:反射原理深入解析与实践指南 C#
java·unity·c#·游戏引擎·lucene·反射
花开富贵ii2 小时前
代码随想录算法训练营四十六天|图论part04
java·数据结构·算法·图论
Miraitowa_cheems2 小时前
LeetCode算法日记 - Day 15: 和为 K 的子数组、和可被 K 整除的子数组
java·数据结构·算法·leetcode·职场和发展·哈希算法
答题卡上的情书2 小时前
java第一个接口
java·开发语言