使用easyPoi的动态列导出,行转列

项目背景:

有一个导出excel的需求,要求导出部分固定列和部分动态列,固定列使用字段写死,动态列使用list集合存放

成果展示:

思路:

简单说就是一个行转列的处理

1. 使用easypoi的注解方式进行导出,固定列部分使用 @Excel标注

2. 动态列使用一个List集合,用 @ExcelCollection 标注,里面的每一项就是每一个动态列标题,一个字段作为表头名称,一个字段作为对应的数据

代码:

需要的maven依赖及版本

<!--easypoi-->
<dependency>
    <groupId>cn.afterturn</groupId>
    <artifactId>easypoi-base</artifactId>
    <version>4.4.0</version>
</dependency>

<!--commons-beanutils-->
<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.9.4</version>
</dependency>

<!--cglib,动态向实体类添加字段-->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib-nodep</artifactId>
    <version>3.3.0</version>
</dependency>

举个栗子:

模拟自己的业务逻辑,组装好需要的数据格式调用工具类中的方法即可

public void exportTest(HttpServletResponse response) {
        ExportDemo demo1 = new ExportDemo();
        // 固定列
        demo1.setDepartName("住建局");
        demo1.setResolutionRate("82%");
        demo1.setDealCount(204);
        demo1.setTotalEvaCount(62);
        demo1.setValidEvaCount(7);
        // 动态列
        List<ExcelPoiUtils.Demo.Type> types = new ArrayList<>();
        ExcelPoiUtils.Demo.Type type1 = new ExcelPoiUtils.Demo.Type();
        type1.setTypeName("民生满意度");
        type1.setScore("35分");
        ExcelPoiUtils.Demo.Type type2 = new ExcelPoiUtils.Demo.Type();
        type2.setTypeName("卫生治理");
        type2.setScore("46分");
        ExcelPoiUtils.Demo.Type type3 = new ExcelPoiUtils.Demo.Type();
        type3.setTypeName("公共安全");
        type3.setScore("52分");
        ExcelPoiUtils.Demo.Type type4 = new ExcelPoiUtils.Demo.Type();
        type4.setTypeName("绿化面积");
        type4.setScore("65分");
        types.add(type1);
        types.add(type2);
        types.add(type3);
        types.add(type4);
        demo1.setTypes(types);

        ExportDemo demo2 = new ExportDemo();
        demo2.setDepartName("民政局");
        demo2.setResolutionRate("62%");
        demo2.setDealCount(9661);
        demo2.setTotalEvaCount(560);
        demo2.setValidEvaCount(80000);

        List<ExcelPoiUtils.Demo.Type> types2 = new ArrayList<>();
        ExcelPoiUtils.Demo.Type typeA = new ExcelPoiUtils.Demo.Type();
        typeA.setTypeName("民生满意度");
        typeA.setScore("102分");
        ExcelPoiUtils.Demo.Type typeB = new ExcelPoiUtils.Demo.Type();
        typeB.setTypeName("卫生治理");
        typeB.setScore("60分");
        ExcelPoiUtils.Demo.Type typeC = new ExcelPoiUtils.Demo.Type();
        typeC.setTypeName("公共安全");
        typeC.setScore("4分");
        ExcelPoiUtils.Demo.Type typeD = new ExcelPoiUtils.Demo.Type();
        typeD.setTypeName("绿化面积");
        typeD.setScore("88分");
        types2.add(typeA);
        types2.add(typeB);
        types2.add(typeC);
        types2.add(typeD);
        demo2.setTypes(types2);

        List<ExportDemo> list = new ArrayList<>();
        list.add(demo1);
        list.add(demo2);

        // 动态标头名称字段
        final String headerName = "typeName";
        // 动态标头值字段
        final String headerValue = "score";
        try {
            ExcelPoiUtils.dynamicExport(response, System.currentTimeMillis() + "", "我是标题行!",
                    "sheet名称", list, headerName, headerValue);
        } catch (Exception e) {
            log.error("导出错误,", e);
            throw new RuntimeException(e);
        }
    }

实体类:

package com.github.face.user.entity;

import cn.afterturn.easypoi.excel.annotation.Excel;
import cn.afterturn.easypoi.excel.annotation.ExcelCollection;
import com.github.face.utils.ExcelPoiUtils;
import lombok.Data;

import java.io.Serializable;
import java.util.List;

/**
 * 动态列导出测试
 *
 * @author wangcl
 */
@Data
public class ExportDemo implements Serializable {

    /**
     * 部门名称
     */
    @Excel(name = "部门", width = 20)
    private String departName;

    /**
     * 处理事件数
     */
    @Excel(name = "处理事件数", width = 20)
    private Integer dealCount = 0;

    /**
     * 全部评价数
     */
    @Excel(name = "全部评价数", width = 20)
    private Integer totalEvaCount = 0;

    /**
     * 有效评价数
     */
    @Excel(name = "有效评价数", width = 20)
    private Integer validEvaCount = 0;

    /**
     * 解决率
     */
    @Excel(name = "解决率", width = 20)
    private String resolutionRate;

    /**
     * 评价类型及分数
     */
    @ExcelCollection(name = "评价类型")
    private List<ExcelPoiUtils.Demo.Type> types;

    /**
     * 评价类型及分数对象
     */
    @Data
    static class Type implements Serializable {

        /**
         * 评价类型id
         */
        private Long typeId;

        /**
         * 评价类型名称(动态标题名称,此处 name = "typeName"可以随便填,以方法调用时传入的为准)
         */
        @Excel(name = "typeName", width = 20)
        private String typeName;

        /**
         * 评价类型对应分数(动态标题内容,此处 name = "score"可以随便填,以方法调用时传入的为准)
         */
        @Excel(name = "score", width = 20)
        private String score = "0.00";
    }
}

动态导出类:

处理动态表头和动态列字段的对应关系

package com.github.face.utils;

import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.annotation.Excel;
import cn.afterturn.easypoi.excel.annotation.ExcelCollection;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.hutool.core.annotation.AnnotationUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.util.Assert;
import org.springframework.util.StopWatch;
import org.springframework.util.StringUtils;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;

/**
 * 使用easyPoi的动态列导出
 * <br/>固定列使用@Excel标识,动态列使用@ExcelCollection标识的List集合,手动传入集合内作为动态表头名称和值的字段名
 *
 * @author wangcl
 */
@Slf4j
public class ExcelPoiUtils {

    /**
     * 使用注解的动态列导出,实体类必须按照规定格式
     * <br/>通过反射实现,注意方法效率
     *
     * @param response    响应
     * @param fileName    文件名
     * @param title       标题
     * @param sheetName   sheet名称
     * @param dataList    导出数据
     * @param headerName  动态列标题
     * @param headerValue 动态列值
     */
    public static <T> void dynamicExport(HttpServletResponse response, String fileName, String title, String sheetName,
                                         List<T> dataList, String headerName, String headerValue) throws Exception {
        Assert.notEmpty(dataList, "没有需要导出的数据");

        StopWatch stopWatch = new StopWatch("自定义列导出");
        stopWatch.start("处理导出字段,使用cglib构建动态属性");
        List<ExcelExportEntity> entityList = new ArrayList<>();
        // 自定义表头列
        List<Object> list = new ArrayList<>();
        for (int i = 0; i < dataList.size(); i++) {
            T t = dataList.get(i);
            // Step1:处理标题
            Field[] fields = t.getClass().getDeclaredFields();
            Map<String, Object> map = new HashMap<>();
            for (int k = 0; k < fields.length; k++) {
                Field field = fields[k];
                field.setAccessible(true);
                Excel excel = field.getAnnotation(Excel.class);
                ExcelCollection excelCollection = field.getAnnotation(ExcelCollection.class);
                // 固定导出列
                if (excel != null) {
                    String excelName = AnnotationUtil.getAnnotationValue(field, Excel.class, "name");
                    // 转换注解修饰的对象
                    if (i == 0) {
                        ExcelExportEntity entity = convert(field, excelName, field.getName());
                        entityList.add(entity);
                    }
                }
                // 自定义导出列,含有@ExcelCollection并且是List
                else if (excelCollection != null && field.getType().getName().equals(List.class.getName())) {
                    Object object;
                    object = field.get(t);
                    List<?> dynamicColl = (List<?>) object;
                    if (CollectionUtils.isEmpty(dynamicColl)) {
                        continue;
                    }
                    for (int m = 0; m < dynamicColl.size(); m++) {
                        Object obj = dynamicColl.get(m);
                        String key = k + "" + m;
                        // 可以在此处设置导出字段为null时的值,默认空字符串
                        String val = null;
                        Field[] typeFields = obj.getClass().getDeclaredFields();
                        for (Field typeField : typeFields) {
                            typeField.setAccessible(true);
                            String fieldName = typeField.getName();
                            Excel excelItem = typeField.getAnnotation(Excel.class);
                            // 只处理@ExcelCollection修饰的List下的@Excel修饰字段,即自定义字段
                            if (excelItem != null) {
                                Object value;
                                if (!Arrays.asList(headerName, headerValue).contains(fieldName)) {
                                    continue;
                                }
                                // 表头字段
                                if (headerName.equals(fieldName)) {
                                    try {
                                        value = typeField.get(obj);
                                        if (value == null) {
                                            continue;
                                        }
                                    } catch (IllegalAccessException e) {
                                        throw new RuntimeException(e);
                                    }
                                    if (i == 0) {
                                        // 转换注解修饰的对象
                                        ExcelExportEntity entity = convert(typeField, value.toString(), key);
                                        entityList.add(entity);
                                    }
                                }
                                // 表头对应的值字段
                                else if (headerValue.equals(fieldName)) {
                                    try {
                                        value = typeField.get(obj);
                                        if (value != null) {
                                            val = value.toString();
                                        }
                                    } catch (IllegalAccessException e) {
                                        throw new RuntimeException(e);
                                    }
                                }
                            }
                        }
                        map.put(key, val);
                        log.debug("map添加元素{},{}", key, val);
                    }
                }
            }
            //  Step2:处理数据,如果有动态列的话,添加到实体类
            if (MapUtils.isNotEmpty(map)) {
                Object object = ReflectKit.getObject(t, map);
                list.add(object);
            } else {
                list.add(t);
            }
        }
        stopWatch.stop();
        log.debug("构建字段:{}", JSONObject.toJSONString(list));

        stopWatch.start("导出");
        //entityList = entityList.stream().filter(distinctByKey(ExcelExportEntity::getName)).collect(Collectors.toList());
        downloadExcelEntityDynamic(response, entityList, list, fileName, title, sheetName);
        stopWatch.stop();
        log.debug(stopWatch.prettyPrint());
    }

    /**
     * 根据指定字段去重
     */
    private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
        Set<Object> seen = ConcurrentHashMap.newKeySet();
        return t -> seen.add(keyExtractor.apply(t));
    }

    /**
     * 动态表头导出
     *
     * @param response   响应
     * @param entityList 表头列表
     * @param list       导出数据
     * @param fileName   文件名
     * @param title      标题
     * @param sheetName  sheet名称
     */
    public static void downloadExcelEntityDynamic(HttpServletResponse response, List<ExcelExportEntity> entityList,
                                                  Collection<?> list, String fileName, String title,
                                                  String sheetName) throws Exception {
        makeResponse(response, fileName);
        ExportParams exportParams;
        if (StringUtils.hasText(title)) {
            exportParams = new ExportParams(title, sheetName);
        } else {
            exportParams = new ExportParams();
            exportParams.setSheetName(sheetName);
        }
        Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entityList, list);
        ServletOutputStream output = response.getOutputStream();
        BufferedOutputStream bufferedOutPut = new BufferedOutputStream(output);
        workbook.write(bufferedOutPut);
        bufferedOutPut.flush();
        bufferedOutPut.close();
        output.close();
    }

    public static void makeResponse(HttpServletResponse response, String fileName) {
        response.setHeader("Content-Disposition",
                "attachment;filename=" + URLEncoder.encode(fileName + ".xlsx", StandardCharsets.UTF_8));
        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
    }

    /**
     * 将@Excel修饰的字段转为ExcelExportEntity
     *
     * @param typeField 字段
     * @param name      列名
     * @param key       唯一表示key
     * @return ExcelExportEntity
     */
    private static ExcelExportEntity convert(Field typeField, String name, String key) {
        Map<String, Object> annotationValueMap = AnnotationUtil.getAnnotationValueMap(typeField, Excel.class);
        ExcelExportEntity entity = JSONObject.parseObject(JSONObject.toJSONBytes(annotationValueMap), ExcelExportEntity.class);
        // 字段名和@Excel的name一致,视为动态表头列
        entity.setName(name);
        // !!!如果使用name作为key,而name中恰好含有英文的“ (,;”等特殊字符,cglib构建动态属性会报错,所以使用一个自定义的唯一值作为key
        entity.setKey(key);
        return entity;
    }

    /**
     * 动态导出demo
     */
    @Data
    public static class Demo implements Serializable {

        /**
         * 部门id
         */
        private Long departId;

        /**
         * 部门名称
         */
        @Excel(name = "部门", width = 20)
        private String departName;

        /**
         * 处理事件数
         */
        @Excel(name = "处理事件数", width = 20)
        private Integer dealCount = 0;

        /**
         * 全部评价数
         */
        @Excel(name = "全部评价数", width = 20)
        private Integer totalEvaCount = 0;

        /**
         * 有效评价数
         */
        @Excel(name = "有效评价数", width = 20)
        private Integer validEvaCount = 0;

        /**
         * 解决率
         */
        @Excel(name = "解决率", width = 20)
        private String resolutionRate;

        /**
         * 评价类型及分数
         */
        @ExcelCollection(name = "部门")
        private List<Type> types;

        /**
         * 评价类型及分数
         */
        @ExcelCollection(name = "部门2")
        private List<Type2> types2;

        /**
         * 评价类型及分数对象
         */
        @Data
        public static class Type implements Serializable {

            /**
             * 评价类型id
             */
            private Long typeId;

            /**
             * 评价类型名称(动态标题名称,此处 name = "typeName"可以随便填,以方法调用时传入的为准)
             */
            @Excel(name = "typeName", width = 20, groupName = "动态分组1")
            private String typeName;

            /**
             * 评价类型对应分数(动态标题内容,此处 name = "score"可以随便填,以方法调用时传入的为准)
             */
            @Excel(name = "score", width = 20)
            private String score = "0.00";
        }

        /**
         * 评价类型及分数对象
         */
        @Data
        public static class Type2 implements Serializable {

            /**
             * 评价类型id
             */
            private Long typeId;

            /**
             * 评价类型名称(动态标题名称,此处 name = "typeName"可以随便填,以方法调用时传入的为准)
             */
            @Excel(name = "typeName", width = 20, groupName = "动态分组2")
            private String typeName;

            /**
             * 评价类型对应分数(动态标题内容,此处 name = "score"可以随便填,以方法调用时传入的为准)
             */
            @Excel(name = "score", width = 20)
            private String score = "0.00";
        }
    }

}

反射处理类:

使用cglib动态向实体类内添加字段,添加的字段为需要动态添加的表头和对应的值

package com.github.face.utils;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.PropertyUtilsBean;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

/**
 * 反射获取动态对象
 *
 * @author wangcl
 */
@Slf4j
public class ReflectKit {

    /**
     * 获取添加动态字段的新对象
     *
     * @param dest        原始对象
     * @param newValueMap 要添加的字段和值
     */
    public static Object getObject(Object dest, Map<String, Object> newValueMap) throws
            InvocationTargetException, IllegalAccessException {
        PropertyUtilsBean propertyUtilsBean = new PropertyUtilsBean();

        //1.获取原对象的字段数组
        PropertyDescriptor[] descriptorArr = propertyUtilsBean.getPropertyDescriptors(dest);

        //2.遍历原对象的字段数组,并将其封装到Map
        Map<String, Class<?>> oldKeyMap = new HashMap<>(4);
        for (PropertyDescriptor it : descriptorArr) {
            if (!"class".equalsIgnoreCase(it.getName())) {
                oldKeyMap.put(it.getName(), it.getPropertyType());
                newValueMap.put(it.getName(), it.getReadMethod().invoke(dest));
            }
        }

        //3.将扩展字段Map合并到原字段Map中
        newValueMap.forEach((k, v) -> {
            // 扩展字段value为null时不扩展此字段
            if (v != null) {
                oldKeyMap.put(k, v.getClass());
            }
        });

        //4.根据新的字段组合生成子类对象
        DynamicBean dynamicBean = new DynamicBean(dest.getClass(), oldKeyMap);

        //5.放回合并后的属性集合
        newValueMap.forEach((k, v) -> {
            try {
                dynamicBean.setValue(k, v);
            } catch (Exception e) {
                log.error("动态添加字段【值】出错", e);
            }
        });
        return dynamicBean.getTarget();
    }
}

动态代理类:

package com.github.face.utils;

import net.sf.cglib.beans.BeanGenerator;
import net.sf.cglib.beans.BeanMap;

import java.util.Map;

/**
 * 动态代理类
 *
 * @author wangcl
 */
public class DynamicBean {

    /**
     * 目标对象
     */
    private final Object target;

    /**
     * 属性集合
     */
    private final BeanMap beanMap;

    public DynamicBean(Class<?> superclass, Map<String, Class<?>> propertyMap) {
        this.target = generateBean(superclass, propertyMap);
        this.beanMap = BeanMap.create(this.target);
    }


    /**
     * bean 添加属性和值
     *
     * @param property 属性名
     * @param value    属性值
     */
    public void setValue(String property, Object value) {
        beanMap.put(property, value);
    }

    /**
     * 获取属性值
     *
     * @param property 属性名
     * @return 属性名对应的值
     */
    public Object getValue(String property) {
        return beanMap.get(property);
    }

    /**
     * 获取对象
     *
     * @return
     */
    public Object getTarget() {
        return this.target;
    }


    /**
     * 根据属性生成对象
     *
     * @param superclass  class类型
     * @param propertyMap 要生成的map对象
     * @return 生成的对象
     */
    private Object generateBean(Class<?> superclass, Map<String, Class<?>> propertyMap) {
        BeanGenerator generator = new BeanGenerator();

        if (null != superclass) {
            generator.setSuperclass(superclass);
        }
        BeanGenerator.addProperties(generator, propertyMap);
        return generator.create();
    }
}


提示:

jdk9以上默认关闭反射,需要手动开启,需要手动添加启动参数

针对本项目:
--add-opens java.base/java.lang=ALL-UNNAMED
针对jdk9以上版本的反射:
--add-opens=java.base/java.lang=ALL-UNNAMED
--add-opens=java.base/java.io=ALL-UNNAMED
--add-opens=java.base/java.util=ALL-UNNAMED
--add-opens=java.base/java.util.concurrent=ALL-UNNAMED
--add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED
--add-opens=java.base/java.util=ALL-UNNAMED
--add-opens=java.base/java.math=ALL-UNNAMED

idea开启方式如下

有错误的地方欢迎大家指正

优化:

2024-06-12:ExcelPoiUtils中dynamicExport(...) 方法校验是否填入自定义列,是否需要动态添加实体类属性;

2024-06-18:优化了传入字段不规范导致的空指针;增加了耗时打印;

2024-07-05:优化了自定义表头含有特殊字符时cglib构建报错的bug;

                      优化了多个表头含有重复名称时只会导出一个同名表头的bug;

2024-07-08:优化了自定义列数量和导出条数一致的bug;

2024-07-09:优化了动态列值为null,反射获取动态对象报错的bug(这里早就改了忘记上传相关代码了);

                      丰富了使用场景:导出的动态列需要分成多组的情况,配置多个@ExcelCollection修饰的list,并且list中的表头列支持分组

  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值