基于EasyExcel实现通用版一对一、一对多、多层嵌套结构数据导出并支持自动合并单元格

接口功能

通用

支持一对一数据结构导出

支持一对多数据结构导出

支持多层嵌套数据结构导出

支持单元格自动合并

原文来自:https://blog.csdn.net/qq_40980205/article/details/136564176

新增及修复

基于我自己的使用场景,新增并能修复一下功能:

  • 修复了只有单条一对一数据无法导出问题;
  • 修复了多层嵌套数据结构中存在某个嵌套集合为空无法导出问题;
  • 修复了只有一条一对多数据导出时报错问题;

完整源码解析

  • @MyExcelProperty 作用到导出字段上面,表示这是Excel的一列
  • @MyExcelCollection 表示一个集合,主要针对一对多的导出,比如一个老师对应多个科目,科目就可以用集合表示
  • @MyExcelEntity 表示一个还存在嵌套关系的导出实体

property为基本数据类型的注解类

java 复制代码
package com.example.excel.annotation;


import java.lang.annotation.*;

@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyExcelProperty {
    String name() default "";

    int index() default Integer.MAX_VALUE;

}

property为集合的注解类

java 复制代码
package com.example.excel.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyExcelCollection {
    String name();
}

property为实体对象的注解类

java 复制代码
package com.example.excel.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyExcelEntity {
    String name() default "";
}

导出参数类

java 复制代码
package com.example.excel.excel;

import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
public class ExportParam {
    private String fieldName;
    private String columnName;
    private Integer order;
}

导出实体类

java 复制代码
package com.example.excel.excel;

import lombok.Data;

import java.util.List;
import java.util.Map;

@Data
public class ExportEntity {

    /**
     * 导出参数
     */
    private List<ExportParam> exportParams;

    /**
     * 平铺后的数据
     */
    private List<Map<String,Object>> data;

    /**
     * 每列的合并行数组
     */
    private Map<String, List<Integer[]>> mergeRowMap;
}

导出工具类

java 复制代码
package com.example.excel.excel;

import com.alibaba.excel.EasyExcel;
import com.example.excel.annotation.MyExcelCollection;
import com.example.excel.annotation.MyExcelEntity;
import com.example.excel.annotation.MyExcelProperty;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.net.URLEncoder;
import java.util.*;
import java.util.stream.Collectors;

public class MyExcelUtil {
    private final String DELIMITER = ".";

    /**
     * @param sourceData 导出数据源
     * @param fileName   导出文件名
     * @param response
     */
    public void exportData(Collection<Object> sourceData, String sheetName, HttpServletResponse response) {
        if (CollectionUtils.isEmpty(sourceData)) return;
        ExportEntity exportEntity = flattenObject(sourceData);
        List<String> columnList = exportEntity.getExportParams().stream()
                .sorted(Comparator.comparing(ExportParam::getOrder))
                .map(ExportParam::getFieldName)
                .collect(Collectors.toList());
        List<List<Object>> exportDataList = new ArrayList<>();
        //导出数据集
        for (Map<String, Object> objectMap : exportEntity.getData()) {
            List<Object> data = new ArrayList<>();
            columnList.stream().forEach(columnName -> {
                data.add(objectMap.get(columnName));
            });
            exportDataList.add(data);
        }

        List<List<String>> headList = columnList.stream().map(Lists::newArrayList).collect(Collectors.toList());
        try {
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding("utf-8");
            EasyExcel.write(response.getOutputStream()).sheet(sheetName)
                    .head(headList)
                    .registerWriteHandler(new MergeStrategy(exportEntity))
                    .registerWriteHandler(new HeadHandler(exportEntity))
                    .doWrite(exportDataList);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    public ExportEntity flattenObject(Collection<Object> sourceData) {
        Map<String, List<Integer[]>> mergeMap = new HashMap<>();
        Class aClass = sourceData.stream().findFirst().get().getClass();
        List<ExportParam> exportParams = getAllExportField(aClass, "");
        Preconditions.checkArgument(!exportParams.isEmpty(), "export field not found !");
        List<Map<String, Object>> target = new ArrayList<>();
        Integer startRow = 0;
        for (Object sourceDataLine : sourceData) {
            List<Map<String, Object>> flatData = flattenObject(sourceDataLine, "", startRow, mergeMap);
            startRow += flatData.size();
            target.addAll(flatData);
        }
        ExportEntity exportEntity = new ExportEntity();
        exportEntity.setExportParams(exportParams);
        exportEntity.setData(target);
        exportEntity.setMergeRowMap(mergeMap);
        return exportEntity;
    }


    /**
     * @param data     数据行
     * @param preNode  上一层级字段名
     * @param startRow 当前数据行所在行数
     * @param mergeMap 各字段合并行数组
     * @return
     */
    private List<Map<String, Object>> flattenObject(Object data, String preNode, Integer startRow, Map<String, List<Integer[]>> mergeMap) {
        List<Map<String, Object>> flatList = new ArrayList<>();
        if (null == data) return flatList;

        Class<?> aClass = data.getClass();
        //获取不为null的excel导出字段
        List<Field> collectionFields = new ArrayList<>();
        List<Field> entityFields = new ArrayList<>();
        List<Field> propertyFields = new ArrayList<>();
        for (Field field : aClass.getDeclaredFields()) {
            Object value = getFieldValue(field, data);
            if (null == value) continue;
            if (isCollection(field)) {
                collectionFields.add(field);
            } else if (isEntity(field)) {
                entityFields.add(field);
            } else if (isProperty(field)) {
                propertyFields.add(field);
            }
        }

        List<Map<String, Object>> entityFlatData = flattenEntityFields(entityFields, data, preNode, startRow, mergeMap);
        List<List<Map<String, Object>>> collectionFlatData = flattenCollectionFields(collectionFields, data, preNode, startRow, mergeMap);
        Map<String, Object> objectMap = Collections.emptyMap();
        if (collectionFields.isEmpty() && entityFields.isEmpty()) {
            objectMap = flattenPropertyFields(propertyFields, data, preNode);
        }
        if (collectionFlatData.isEmpty() && entityFlatData.isEmpty()) {
            objectMap = flattenPropertyFields(propertyFields, data, preNode);
        }
        if (!objectMap.isEmpty()) {
            flatList.add(objectMap);
        }

        //当前层级所有平铺列表
        List<List<Map<String, Object>>> allFlatData = Lists.newArrayList();
        if (!entityFlatData.isEmpty()) {
            allFlatData.add(entityFlatData);
        }
        if (!collectionFlatData.isEmpty()) {
            allFlatData.addAll(collectionFlatData);
        }

        List<Map<String, Object>> mergeList = mergeList(data, allFlatData, propertyFields, preNode, startRow, mergeMap);
        if (!mergeList.isEmpty()) {
            flatList.addAll(mergeList);
        }

        return flatList;
    }


    /**
     * @param preNode   上一层级字段名
     * @param fieldName 当前字段名
     * @return
     */
    private String buildPreNode(String preNode, String fieldName) {
        StringBuffer sb = new StringBuffer();
        return StringUtils.isEmpty(preNode) ? fieldName : sb.append(preNode).append(DELIMITER).append(fieldName).toString();
    }


    /**
     * @param mergeFields   需要合并的字段
     * @param preNode       上一层级字段名
     * @param mergeStartRow 合并开始行
     * @param mergeEndRow   合并结束行
     * @param mergeMap      各字段合并行数组
     */
    private void buildMerge(List<Field> mergeFields, String preNode, Integer mergeStartRow, Integer mergeEndRow, Map<String, List<Integer[]>> mergeMap) {
        for (int k = 0; k < mergeFields.size(); k++) {
            String fieldName = buildPreNode(preNode, mergeFields.get(k).getName());
            List<Integer[]> list = mergeMap.get(fieldName);
            if (null == list) {
                mergeMap.put(fieldName, new ArrayList<>());
            }

            Integer[] rowInterval = new Integer[2];
            //合并开始行
            rowInterval[0] = mergeStartRow;
            //合并结束行
            rowInterval[1] = mergeEndRow;
            mergeMap.get(fieldName).add(rowInterval);
        }
    }

    private List<Map<String, Object>> flattenEntityFields(List<Field> entityFields, Object data, String preNode, Integer startRow, Map<String, List<Integer[]>> mergeMap) {
        List<Map<String, Object>> entityFlatData = Lists.newArrayList();
        for (Field entityField : entityFields) {
            entityFlatData = flattenObject(getFieldValue(entityField, data), buildPreNode(preNode, entityField.getName()), startRow, mergeMap);
        }
        return entityFlatData;
    }

    private List<List<Map<String, Object>>> flattenCollectionFields(List<Field> collectionFields, Object data, String preNode, Integer startRow, Map<String, List<Integer[]>> mergeMap) {
        List<List<Map<String, Object>>> collectionFlatData = Lists.newArrayList();
        for (Field collectionField : collectionFields) {
            Collection collectionValue = (Collection) getFieldValue(collectionField, data);

            //当前集合字段平铺而成的数据列表
            List<Map<String, Object>> collectionObjectValue = new ArrayList<>();
            //间隔行数
            Integer row = 0;
            for (Object value : collectionValue) {
                List<Map<String, Object>> flatData = flattenObject(value, buildPreNode(preNode, collectionField.getName()), startRow + row, mergeMap);
                if (!flatData.isEmpty()) {
                    collectionObjectValue.addAll(flatData);
                    //下条数据的起始间隔行
                    row += flatData.size();
                }
            }
            if (!collectionObjectValue.isEmpty()) {
                collectionFlatData.add(collectionObjectValue);
            }
        }
        return collectionFlatData;
    }

    private Map<String, Object> flattenPropertyFields(List<Field> propertyFields, Object data, String preNode) {
        Map<String, Object> flatMap = new HashMap<>();
        for (Field field : propertyFields) {
            flatMap.put(buildPreNode(preNode, field.getName()), getFieldValue(field, data));
        }
        return flatMap;
    }


    private List<Map<String, Object>> mergeList(Object data, List<List<Map<String, Object>>> allFlatData, List<Field> propertyFields, String preNode, Integer startRow, Map<String, List<Integer[]>> mergeMap) {
        List<Map<String, Object>> flatList = new ArrayList<>();

        //当前层级下多个集合字段的最大长度即为当前层级其他字段的合并行数
        Integer maxSize = 0;
        for (List<Map<String, Object>> list : allFlatData) {
            maxSize = Math.max(maxSize, list.size());
        }

        //记录集合同层级其他字段的合并行数
        if (maxSize > 0) {
            buildMerge(propertyFields, preNode, startRow, startRow + maxSize, mergeMap);
        }

        //需要合并
        if (maxSize > 1) {
            //重新构建平铺的list
            List<Map<String, Object>> mergeFlatData = new ArrayList<>(maxSize);
            for (int i = 0; i < maxSize; i++) {
                mergeFlatData.add(new HashMap<>());
            }

            for (List<Map<String, Object>> flatData : allFlatData) {
                for (int i = 0; i < flatData.size(); i++) {
                    Map<String, Object> flatMap = new HashMap<>();
                    flatMap.putAll(flatData.get(i));

                    //添加同层级字段值
                    if (CollectionUtils.isNotEmpty(propertyFields)) {
                        for (Field field : propertyFields) {
                            flatMap.put(buildPreNode(preNode, field.getName()), getFieldValue(field, data));
                        }
                    }
                    mergeFlatData.get(i).putAll(flatMap);
                }
            }
            flatList.addAll(mergeFlatData);
        } else {
            for (List<Map<String, Object>> list : allFlatData) {
                flatList.addAll(list);
            }
        }
        return flatList;
    }


    /**
     * @param clazz   导出类
     * @param preNode 上一层级字段名
     * @return
     */
    private List<ExportParam> getAllExportField(Class<?> clazz, String preNode) {
        List<ExportParam> exportFields = new ArrayList<>();
        for (Field declaredField : clazz.getDeclaredFields()) {
            MyExcelProperty myExcelProperty = declaredField.getDeclaredAnnotation(MyExcelProperty.class);
            MyExcelEntity myExcelEntity = declaredField.getDeclaredAnnotation(MyExcelEntity.class);
            MyExcelCollection myExcelCollection = declaredField.getDeclaredAnnotation(MyExcelCollection.class);
            String fieldName = buildPreNode(preNode, declaredField.getName());
            if (null != myExcelProperty) {
                ExportParam exportParam = new ExportParam()
                        .setFieldName(fieldName)
                        .setColumnName(myExcelProperty.name())
                        .setOrder(myExcelProperty.index());
                exportFields.add(exportParam);
            } else if (null != myExcelEntity) {
                exportFields.addAll(getAllExportField(declaredField.getType(), fieldName));
            } else if (null != myExcelCollection) {
                boolean isCollection = Collection.class.isAssignableFrom(declaredField.getType());
                if (!isCollection) continue;
                ParameterizedType pt = (ParameterizedType) declaredField.getGenericType();
                Class<?> clz = (Class<?>) pt.getActualTypeArguments()[0];
                exportFields.addAll(getAllExportField(clz, fieldName));
            }
        }
        return exportFields;
    }

    private boolean isProperty(Field field) {
        return null != field.getDeclaredAnnotation(MyExcelProperty.class) ? true : false;
    }


    private boolean isEntity(Field field) {
        return null != field.getDeclaredAnnotation(MyExcelEntity.class) ? true : false;
    }

    private boolean isCollection(Field field) {
        boolean isCollection = Collection.class.isAssignableFrom(field.getType());
        return isCollection && null != field.getAnnotation(MyExcelCollection.class) ? true : false;
    }

    private Object getFieldValue(Field field, Object sourceObject) {
        Object fieldValue;
        try {
            field.setAccessible(true);
            fieldValue = field.get(sourceObject);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        return fieldValue;
    }

}

单元格合并策略

java 复制代码
```java
package com.example.excel.excel;

import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
import org.springframework.util.CollectionUtils;

import java.util.List;

public class MergeStrategy extends AbstractMergeStrategy {

    private ExportEntity exportEntity;

    public MergeStrategy(ExportEntity exportEntity) {
        this.exportEntity = exportEntity;
    }


    @Override
    protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
        //mergeMap需要加上head的行数
        String fieldName = head.getHeadNameList().get(0);
        List<Integer[]> mergeRow = exportEntity.getMergeRowMap().get(fieldName);
        if (CollectionUtils.isEmpty(mergeRow)) return;
        int currentColumnIndex = cell.getColumnIndex();
        int currentRowIndex = cell.getRowIndex();
        //表头所占行数
        int headHeight = head.getHeadNameList().size();
        for (int i = 0; i < mergeRow.size(); i++) {
            if (currentRowIndex == mergeRow.get(i)[1] + headHeight - 1) {
                sheet.addMergedRegion(new CellRangeAddress(mergeRow.get(i)[0] + headHeight, mergeRow.get(i)[1] + headHeight - 1,
                        currentColumnIndex, currentColumnIndex));
            }
        }

    }

}

表头导出处理

java 复制代码
```java
package com.example.excel.excel;

import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import org.apache.poi.ss.usermodel.Cell;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class HeadHandler implements CellWriteHandler {

    private ExportEntity exportEntity;


    public HeadHandler(ExportEntity exportEntity) {
        this.exportEntity = exportEntity;
    }

    @Override
    public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
                                 List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
        if (!isHead) return;
        List<String> headNameList = head.getHeadNameList();
        Map<String, String> collect = exportEntity.getExportParams().stream().collect(Collectors.toMap(ExportParam::getFieldName, ExportParam::getColumnName));
        cell.setCellValue(collect.get(headNameList.get(0)));
    }
}

导出示例

被导出实体

java 复制代码
 public class CompanyConfigEnntity{
    @MyExcelProperty(name = "公司名称")
    private String companyName;

    @MyExcelCollection(name = "线路配置")
    private List<LineConfigEntity> lineConfigs;
 }

 public class LineConfigEntity{
    @MyExcelProperty(name = "线路号")
    private String lineNumber;

    @MyExcelCollection(name = "消息配置")
    private List<MsgConfigEntity> msgConfigs;
 }

public class MsgConfigEntity{
    @MyExcelProperty(name = "消息类型")
    private String msgType;

    @MyExcelCollection(name = "子类型配置")
    private List<SubTypeConfigEntity> subTypeConfigs;
 }

public class SubTypeConfigEntity{
    @MyExcelProperty(name = "子类型")
    private String subType;

    @MyExcelCollection(name = "集中站配置")
    private List<RtuConfigEntity> rtuConfigs;
 }
public class RtuConfigEntity{
    @MyExcelProperty(name = "集中站号")
    private String rtuNumber;

 }

接口调用

java 复制代码
@RestController
@ReqeutMapping("/companyConfig/companyConfig")
public class CompanyConfigController{
    @AutoWired
    private CompanyConfigService companyConfigService;
    
    @PostMappinng("/export")
    public void expooort(HttpServletResponse response){
        List<CompanyConfigEnityt> list = companyConfigService.queryAll();
        new MyExcelUtil().exportData(list, "公司配置内容数据", resposne);
    }
}

效果展示

相关推荐
selt7914 小时前
Redisson之RedissonLock源码完全解析
android·java·javascript
Yao_YongChao4 小时前
Android MVI处理副作用(Side Effect)
android·mvi·mvi副作用
非凡ghost5 小时前
JRiver Media Center(媒体管理软件)
android·学习·智能手机·媒体·软件需求
席卷全城5 小时前
Android 推箱子实现(引流文章)
android
齊家治國平天下6 小时前
Android 14 系统中 Tombstone 深度分析与解决指南
android·crash·系统服务·tombstone·android 14
maycho1238 小时前
MATLAB环境下基于双向长短时记忆网络的时间序列预测探索
android
思成不止于此8 小时前
【MySQL 零基础入门】MySQL 函数精讲(二):日期函数与流程控制函数篇
android·数据库·笔记·sql·学习·mysql
brave_zhao8 小时前
达梦数据库(DM8)支持全文索引功能,但并不直接兼容 MySQL 的 FULLTEXT 索引语法
android·adb
sheji34168 小时前
【开题答辩全过程】以 基于Android的网上订餐系统为例,包含答辩的问题和答案
android
easyboot9 小时前
C#使用SqlSugar操作mysql数据库
android·sqlsugar