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

相关推荐
珹洺5 分钟前
Java-Spring入门指南(五)Spring自动装配
android·java·spring
帧栈11 分钟前
并发编程原理与实战(二十七)深入剖析synchronized底层基石ObjectMonitor与对象头Mark Word
java
imHanweihu16 分钟前
基于POI-TL实现动态Word模板数据填充(含图表):从需求到落地的完整开发实践
java·onlyoffice·poi-tl
月夕·花晨19 分钟前
Gateway -网关
java·服务器·分布式·后端·spring cloud·微服务·gateway
失散1319 分钟前
分布式专题——6 Redis缓存设计与性能优化
java·redis·分布式·缓存·架构
杏花春雨江南20 分钟前
Spring Cloud Gateway 作为一个独立的服务进行部署吗
java·开发语言
GSDjisidi20 分钟前
东京本社招聘 | 财务负责人 & 多个日本IT岗位(Java/C++/Python/AWS 等),IT营业同步招募
java·开发语言·aws
叫我阿柒啊32 分钟前
Java全栈开发面试实战:从基础到微服务的完整技术栈解析
java·spring boot·微服务·前端框架·vue·jwt·全栈开发
前行的小黑炭33 分钟前
Android:在项目当中可能会遇到的ANR,应该如何解决?
android·java·kotlin