自定义注解完善easyPoi的动态表头生成

基本思路

easyPoi的@Excel注解目前只支持静态表头(列名).
我这两天遇到的一个场景是要基于一个属性,生成很多列名。easyPoi给出的方案是让我们自己去创建 ExcelExportEntity 这个对象,再定义一组数据 List<Map>, 然后调用它提供的下面这个方法来导出。

package cn.afterturn.easypoi.excel;
public final class ExcelExportUtil {
	/**
	  * 根据Map创建对应的Excel
	  *
	  * @param entity     表格标题属性
	  * @param entityList Map对象列表
	  * @param dataSet    Excel对象数据List
	  */
	 public static Workbook exportExcel(ExportParams entity, List<ExcelExportEntity> entityList,
	                                    Collection<?> dataSet) {
	     Workbook workbook = getWorkbook(entity.getType(), dataSet.size());
	     ;
	     new ExcelExportService().createSheetForMap(workbook, entity, entityList, dataSet);
	     return workbook;
	 }
}

不过在创建ExcelExportEntity对象的过程又臭又长,很容易造就屎山。那么核心要解决的问题就是让ExcelExportEntity对象创建的过程自动化,用户无需感知到这个过程最好。
此外,从上面的方法中看到,我们还需要一个Collection<?> dataSet参数,这个参数的构建过程也是又臭又长的,因此也要考虑自动化。
为了迎合easyPoi使用者的习惯,就写个类似的注解来实现这项功能,话不多说,直接上代码:

自定义注解

@CustomExcel

package com.ssy.file.annotation;

import java.lang.annotation.*;

/**
 * Author: SiYang
 * Date: 2023/09/26 10:19
 * Description:
 *  easyPoi并没有给出动态表头的注解方案,
 *  想要使用动态表头,只能自己构建 ExcelExportEntity 这个对象,比较繁琐
 *  我们可以使用自定义注解的方式,使得构建该对象的过程自动化
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomExcel {

    /**
     * 指定列名,有些列名是固定的,不需要生成
     */
    String name() default "";

    /**
     * 指定生成策略,生成策略指的是数据类内部自定义的方法
     * 被CustomExcel注解的属性可以定义多种生成策略
     * 该方法用来指定生成策略
     */
    String columnNameGenerator() default "";

    /**
     * 列的序号
     */
    int orderNum() default 0;

    /**
     * 列序号生成器,需要自定义生成方法,这里的值为方法名称
     * 注:顺序的体现只需要orderNum有大小关系,而不一定要连续
     */
    String orderGenerator() default "";

    /**
     * 是否需要合并列
     * 该字段映射到easyPoi的ExcelExportEntity对象中似乎无效
     * easyPoi的合并功能还是一如既往地令人失望,所以合并还是得靠自己实现
     */
    boolean needMerge() default false;

    /**
     * 标题分组
     */
    String groupName() default "";

    /**
     * 指定生成title的方法
     * 该方法如果返回的值相同,那么就表示这些列的标题需要合并
     */
    String groupGenerator() default "";

    int width() default 10;

    int height() default 10;
}

@CustomExcelCollection

package com.ssy.file.annotation;

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

/**
 * Author: SiYang
 * Date: 2023/09/26 14:49
 * Description: 处理内嵌list的注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomExcelCollection {
}

使用注解的工具类

用来生成 List<ExcelExportEntity> entityList 以及 Collection<?> dataSet
简单思路就是利用反射拿到注解,利用注解给到的信息执行一系列操作

package com.ssy.file.utils;

import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

import com.ssy.file.annotation.CustomExcel;
import com.ssy.file.annotation.CustomExcelCollection;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;

/**
 * Author: SiYang
 * Date: 2023/09/26 10:40
 * Description:
 *  对于导出行为: 生成 ExcelExportEntity 对象
 *  对于导入行为: 以后再说
 */
@Slf4j
@SuppressWarnings("unused")
public class ExcelEntityGenerator {

    /**
     * 基于数据列表生成 easyPoi 中的 ExcelExportEntity 对象列表
     *
     * @param dataList 数据列表: 想被导出的字段需要使用 @CustomExcel 注解
     * @return ExcelExportEntity 对象列表
     */
    public static List<ExcelExportEntity> generateExportEntity(List<Object> dataList) {
        try {
            return generateExportEntity(dataList, null, null);
        } catch (Exception e) {
            log.error("生成动态表头错误: ", e);
            throw new RuntimeException("生成动态表头错误: " + e.getMessage());
        }
    }

    @SuppressWarnings("rawtypes")
    private static List<ExcelExportEntity> generateExportEntity(
            List<Object> dataList, List<ExcelExportEntity> result, Set<String> colNameSet)
            throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        colNameSet = CollectionUtils.isEmpty(colNameSet) ? new HashSet<>() : colNameSet;
        result = CollectionUtils.isEmpty(result) ? new ArrayList<>() : result;
        if (CollectionUtils.isEmpty(dataList)) {
            return Collections.emptyList();
        }
        Class<?> clazz = dataList.get(0).getClass();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            CustomExcel customExcel = field.getAnnotation(CustomExcel.class);
            CustomExcelCollection customExcelCollection =
                    field.getAnnotation(CustomExcelCollection.class);
            if (Objects.nonNull(customExcel)) {
                // 第一优先
                String methodName = customExcel.columnNameGenerator();
                // 第二优先
                String colName = customExcel.name();
                ExcelExportEntityBuilder builder = new ExcelExportEntityBuilder();
                if (StringUtils.isNotBlank(methodName)) {
                    Method method = clazz.getDeclaredMethod(methodName);
                    method.setAccessible(true);
                    for (Object data : dataList) {
                        String dynamicColName = (String) method.invoke(data);
                        if (colNameSet.contains(dynamicColName)
                                || StringUtils.isBlank(dynamicColName)) {
                            continue;
                        }
                        ExcelExportEntity entity =
                                builder.name(dynamicColName)
                                        .key(dynamicColName)
                                        .width(customExcel.width())
                                        .height(customExcel.height())
                                        .needMerge(customExcel.needMerge())
                                        .orderNum(getOrderNum(clazz, data, customExcel))
                                        .groupName(getGroupName(clazz, data, customExcel))
                                        .build();
                        result.add(entity);
                        colNameSet.add(dynamicColName);
                    }
                } else {
                    if (colNameSet.contains(colName) || StringUtils.isBlank(colName)) {
                        continue;
                    }
                    ExcelExportEntity entity =
                            builder.name(colName)
                                    .key(field.getName())
                                    .width(customExcel.width())
                                    .height(customExcel.height())
                                    .needMerge(customExcel.needMerge())
                                    .orderNum(customExcel.orderNum())
                                    .groupName(customExcel.groupName())
                                    .build();
                    result.add(entity);
                    colNameSet.add(colName);
                }
            } else if (Objects.nonNull(customExcelCollection)) {
                for (Object data : dataList) {
                    generateExportEntity(
                            new ArrayList<Object>((List) field.get(data)), result, colNameSet);
                }
            }
        }
        return result;
    }

    /**
     * 将被 @CustomExcel 注解的属性值提取到map
     *
     * @param dataList 数据列表
     * @return 键值对列表
     */
    public static List<Map<String, Object>> generateDataCollection(List<Object> dataList) {
        try {
            return generateDataCollection(dataList, null, null, true);
        } catch (Exception e) {
            log.error("生成excel数据错误: ", e);
            throw new RuntimeException("生成excel数据错误: " + e.getMessage());
        }
    }

    @SuppressWarnings(value = {"rawtypes", "unchecked"})
    private static List<Map<String, Object>> generateDataCollection(
            List<Object> dataList,
            List<Map<String, Object>> result,
            Map<String, Object> dataMap,
            boolean isRoot)
            throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        result = CollectionUtils.isEmpty(result) ? new ArrayList<>() : result;
        if (CollectionUtils.isEmpty(dataList)) {
            return Collections.emptyList();
        }
        Class<?> clazz = dataList.get(0).getClass();
        Field[] fields = clazz.getDeclaredFields();

        for (Object data : dataList) {
            if (isRoot) {
                dataMap = new LinkedHashMap<>();
                result.add(dataMap);
            }
            for (Field field : fields) {
                field.setAccessible(true);
                CustomExcel customExcel = field.getAnnotation(CustomExcel.class);
                CustomExcelCollection customExcelCollection =
                        field.getAnnotation(CustomExcelCollection.class);
                if (Objects.nonNull(customExcel)) {
                    Object val = field.get(data);

                    // 第一优先
                    String methodName = customExcel.columnNameGenerator();
                    // 第二优先
                    String colName = customExcel.name();

                    if (StringUtils.isNotBlank(methodName)) {
                        Method method = clazz.getDeclaredMethod(methodName);
                        method.setAccessible(true);
                        String dynamicColName = (String) method.invoke(data);
                        if (StringUtils.isBlank(dynamicColName)) {
                            continue;
                        }
                        dataMap.put(dynamicColName, val);
                    } else if (StringUtils.isNotBlank(colName)) {
                        dataMap.put(field.getName(), val);
                    }

                } else if (Objects.nonNull(customExcelCollection)) {
                    generateDataCollection(
                            new ArrayList<>((List) field.get(data)), result, dataMap, false);
                }
            }
        }
        return result;
    }

    /**
     * 优先按指定的序号生成器来获取,如果没有指定生成器则使用orderNum
     *
     * @param clazz 数据类字节码对象
     * @param obj 当前处理的对象
     * @param customExcel 注解对象
     * @return 序号
     */
    private static int getOrderNum(Class<?> clazz, Object obj, CustomExcel customExcel)
            throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        String methodName = customExcel.orderGenerator();
        if (StringUtils.isNotBlank(methodName)) {
            Method method = clazz.getDeclaredMethod(methodName);
            method.setAccessible(true);
            return (Integer) method.invoke(obj);
        } else {
            return customExcel.orderNum();
        }
    }

    /**
     * 优先按照group生成策略生成groupName
     *
     * @param clazz 数据类字节码对象
     * @param obj 当前处理的对象
     * @param customExcel 注解对象
     * @return groupName
     */
    private static String getGroupName(Class<?> clazz, Object obj, CustomExcel customExcel)
            throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        String methodName = customExcel.groupGenerator();
        if (StringUtils.isNotBlank(methodName)) {
            Method method = clazz.getDeclaredMethod(methodName);
            method.setAccessible(true);
            return (String) method.invoke(obj);
        } else {
            return customExcel.groupName();
        }
    }

    @NoArgsConstructor
    private static class ExcelExportEntityBuilder {
        private Object key;
        private String name;
        private int width = 10;
        private int height = 10;
        private int orderNum = 0;
        private boolean needMerge;
        private String groupName;

        public ExcelExportEntityBuilder key(Object key){
            this.key = key;
            return this;
        }

        public ExcelExportEntityBuilder name(String name){
            this.name = name;
            return this;
        }

        public ExcelExportEntityBuilder width(int width) {
            if (width > 0) {
                this.width = width;
            }
            return this;
        }

        public ExcelExportEntityBuilder height(int height){
            if (height > 0) {
                this.height = height;
            }
            return this;
        }

        public ExcelExportEntityBuilder orderNum(int orderNum){
            this.orderNum = orderNum;
            return this;
        }

        public ExcelExportEntityBuilder needMerge(boolean needMerge){
            this.needMerge = needMerge;
            return this;
        }

        public ExcelExportEntityBuilder groupName(String groupName){
            this.groupName = groupName;
            return this;
        }

        public ExcelExportEntity build() {
            ExcelExportEntity entity = new ExcelExportEntity(name, key, width);
            entity.setHeight(height);
            entity.setOrderNum(orderNum);
            entity.setNeedMerge(needMerge);
            entity.setGroupName(groupName);
            return entity;
        }
    }
}

导出代码示例

其中的convert方法,以及额外计算合并区域的方法细节就不给出了。

// 报表原始数据
List<CarbonThingEnergyDictDTO> originalDataList = energyPriceReport(request);

// 数据类型转换
List<CarbonThingEnergyDictExcel> excelDataList = CarbonThingEnergyDictExcel.convertFromDto(originalDataList, request.getTimeType());

// 生成表头 & 生成数据集合
List<ExcelExportEntity> excelExportEntities = ExcelEntityGenerator.generateExportEntity(new ArrayList<>(excelDataList));
List<Map<String, Object>> dataCollection = ExcelEntityGenerator.generateDataCollection(new ArrayList<>(excelDataList));

// 计算合并区域
List<CellRangeAddress> regions = CarbonThingEnergyDictExcel.calculateMergeRegion(excelDataList, request.getTimeType());
Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams(null, "报表"), excelExportEntities, dataCollection);
regions.forEach(workbook.getSheetAt(0)::addMergedRegion);

// 导出
ExcelUtils.downLoadExcel("报表.xls", response, workbook);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值