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

相关推荐
缺点内向17 小时前
Java:创建、读取或更新 Excel 文档
java·excel
带刺的坐椅18 小时前
Solon v3.4.7, v3.5.6, v3.6.1 发布(国产优秀应用开发框架)
java·spring·solon
四谎真好看19 小时前
Java 黑马程序员学习笔记(进阶篇18)
java·笔记·学习·学习笔记
桦说编程19 小时前
深入解析CompletableFuture源码实现(2)———双源输入
java·后端·源码
java_t_t19 小时前
ZIP工具类
java·zip
lang2015092820 小时前
Spring Boot优雅关闭全解析
java·spring boot·后端
pengzhuofan21 小时前
第10章 Maven
java·maven
百锦再21 小时前
Vue Scoped样式混淆问题详解与解决方案
java·前端·javascript·数据库·vue.js·学习·.net
刘一说21 小时前
Spring Boot 启动慢?启动过程深度解析与优化策略
java·spring boot·后端
壹佰大多21 小时前
【spring如何扫描一个路径下被注解修饰的类】
java·后端·spring