easyExcel - 动态复杂表头的编写

目录

  • 前言
  • 一、情景介绍
  • 二、问题分析
  • 三、代码实现
    • 方式一:head 设置
    • 方式二:模板导出
    • 方式三:自定义工具类


前言

Java-easyExcel入门教程:https://blog.csdn.net/xhmico/article/details/134714025

之前有介绍过如何使用 easyExcel,以及写了两个入门的 demo ,这两个 demo 能应付在开发中大多数的导入和导出需求,不过有时候面对一些复杂的表格,就会有点不够用,该篇讲述的是如何实现复杂表头编写


一、情景介绍

在实际的开发过程中可能会遇到需要导出一些带有复杂表头的表格,比如以下案例:

在这里插入图片描述

该表头占了两行,其中 橙色 部分的信息是需要动态生成的


二、问题分析

关于如何实现类似于上述复杂表头,有多种方式均可实现,首先这个表头是一个复杂表头,其次还有动态的部分

查看官方文档,对应复杂表头的实现

官方文档:复杂头写入

在这里插入图片描述

从中可以看出,多行表头就是由 多个纵向 的列表组成,并且表头 相同的部分会自动合并居中对齐

再查阅官方文档关于如何实现动态头的写入

官方文档:动态头、实时生成头写入

在这里插入图片描述

官方给了一个 head() 方法允许我们在代码中自定义表头

    public T head(List<List<String>> head) {
        this.parameter().setHead(head);
        return this.self();
    }

三、代码实现


方式一:head 设置

可以将上述表头看作是以下 6 个集合组成的表头,然后使用 head() 方法去设置

在这里插入图片描述

代码示例:

    /**
     * 复杂表头编写:方式一
     */
    @Test
    public void complexHeadDemo01() {
        // 输出文件路径
        String outFilePath = "D:\\excel-files\\demo01.xlsx";

        // 表格数据
        List<Object> data = new ArrayList<>();

        EasyExcel.write(outFilePath)
                // 动态头
                .head(head())
                .sheet()
                // 表格数据
                .doWrite(data);
    }

    private List<List<String>> head() {
        List<List<String>> list = new ArrayList<List<String>>();
        List<String> head0 = new ArrayList<String>();
        head0.add("部门");
        head0.add("用户名称");
        List<String> head1 = new ArrayList<String>();
        head1.add("运营部");
        head1.add("性别");
        List<String> head2 = new ArrayList<String>();
        head2.add("运营部");
        head2.add("年龄");
        List<String> head3 = new ArrayList<String>();
        head3.add("时间");
        head3.add("出生日期");
        List<String> head4 = new ArrayList<String>();
        head4.add("2024-04-09");
        head4.add("学历");
        List<String> head5 = new ArrayList<String>();
        head5.add("2024-04-09");
        head5.add("电话号码");
        list.add(head0);
        list.add(head1);
        list.add(head2);
        list.add(head3);
        list.add(head4);
        list.add(head5);
        return list;
    }

结果展示:

在这里插入图片描述

可以看到是能够实现这种表头的,不过需要自己定义表头的样式


方式二:模板导出

可以使用模板导出的方式,设置一个模板文件,例如:

在这里插入图片描述

实体类:
DeptUserExcelEntity.java

import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DeptUserExcelEntity {

    @ApiModelProperty(value = "用户名称")
    private String realName;

    @ApiModelProperty(value = "性别")
    private String gender;

    @ApiModelProperty(value = "年龄")
    private Integer age;

    @ApiModelProperty(value = "出生日期")
    private String birthdate;

    @ApiModelProperty(value = "学历")
    private String education;

    @ApiModelProperty(value = "电话号码")
    private String telephone;
}

代码示例:

    /**
     * 复杂表头编写:方式二
     */
    @Test
    public void complexHeadDemo02() {
        // 模板文件路径
        String templateFilePath = "D:\\excel-files\\template.xlsx";
        // 输出文件路径
        String outFilePath = "D:\\excel-files\\demo02.xlsx";

        // 创建 ExcelWriter 实例
        ExcelWriter writer = EasyExcel
                // 写入到
                .write(outFilePath)
                // 指定模板
                .withTemplate(templateFilePath)
                .build();

        WriteSheet sheet = EasyExcel.writerSheet().build();

        Map<String, String> replaceMap = new HashMap<>();
        replaceMap.put("deptName", "运营部");
        replaceMap.put("currentDate", "2024-04-09");

        // 执行填充普通占位符操作
        writer.fill(replaceMap, sheet);

        // 获取员工信息
        List<DeptUserExcelEntity> data = new ArrayList<>();

        FillConfig fillConfig = FillConfig.builder()
                // 开启填充换行
                .forceNewRow(true)
                .build();

        // 执行填充列表操作
        writer.fill(data, fillConfig, sheet);

        // 结束
        writer.finish();
    }

结果展示:

在这里插入图片描述

可以看到效果是比较好的,也不用担心表格样式的问题

关于如何使用 easyexcel 实现按模板导出,可参考:easyExcel - 按模板导出 有较为详细的说明


方式三:自定义工具类

根据官方实现复杂表头的写法,自定义输出对象为

DeptUserExcelEntity.java

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.*;
import com.alibaba.excel.enums.poi.BorderStyleEnum;
import com.alibaba.excel.enums.poi.FillPatternTypeEnum;
import com.alibaba.excel.enums.poi.HorizontalAlignmentEnum;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
// 头背景设置
@HeadStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, horizontalAlignment = HorizontalAlignmentEnum.CENTER, borderLeft = BorderStyleEnum.THIN, borderTop = BorderStyleEnum.THIN, borderRight = BorderStyleEnum.THIN, borderBottom = BorderStyleEnum.THIN)
//标题高度
@HeadRowHeight(40)
//内容高度
@ContentRowHeight(30)
//内容居中,左、上、右、下的边框显示
@ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER, borderLeft = BorderStyleEnum.THIN, borderTop = BorderStyleEnum.THIN, borderRight = BorderStyleEnum.THIN, borderBottom = BorderStyleEnum.THIN)
public class DeptUserExcelEntity {

    @ApiModelProperty(value = "用户名称")
    @ExcelProperty({"部门","用户名称"})
    @ColumnWidth(15)
    private String realName;

    @ApiModelProperty(value = "性别")
    @ExcelProperty({"deptName","性别"})
    @ColumnWidth(15)
    private String gender;

    @ApiModelProperty(value = "年龄")
    @ExcelProperty({"deptName","年龄"})
    @ColumnWidth(15)
    private Integer age;

    @ApiModelProperty(value = "出生日期")
    @ExcelProperty({"时间","出生日期"})
    @ColumnWidth(15)
    private String birthdate;

    @ApiModelProperty(value = "学历")
    @ExcelProperty({"currentDate","学历"})
    @ColumnWidth(20)
    private String education;

    @ApiModelProperty(value = "电话号码")
    @ExcelProperty({"currentDate","电话号码"})
    @ColumnWidth(20)
    private String telephone;
}

如果依照之前的导出案例,代码如下:

    @Test
    public void complexHeadDemo03_test() {

        // 输出文件路径
        String outFilePath = "D:\\excel-files\\demo03.xlsx";

        List<DeptUserExcelEntity> excelEntities = new ArrayList<>();
        EasyExcel.write(outFilePath, DeptUserExcelEntity.class)
                .sheet()
                .doWrite(excelEntities);
    }

最后得到的表格也是根据输出对象定义而来的

在这里插入图片描述

所以如果能让表格在写入的时候,输出对象 DeptUserExcelEntity@ExcelProperty 里面的 deptNamecurrentDate 替换成想要的不就行了,所以我就自定义一个工具类,在需要的时候改变注解的属性值就行了

工具类:

AnnotationUtils.java

import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.CellType;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

/**
 * 注解工具类
 */
public class AnnotationUtils {

    /**
     * 变更注解的属性值再处理业务,处理完业务之后恢复类的属性
     *
     * @param clazz     注解所在的实体类
     * @param tClass    注解类
     * @param attrName 要修改的注解属性名
     * @param attrTypeEnum 要修改的注解属性的类型
     * @param valueMap  要设置的属性值
     */
    public static <A extends Annotation> void changeAnnotationValueToDealProcess(
            Class<?> clazz,
            Class<A> tClass,
            String attrName,
            AttrTypeEnum attrTypeEnum,
            Map<String, String> valueMap,
            DealProcess dealProcess) {
        try {

            Map<String, Object> fieldAnnotationValueMap = new HashMap<>();

            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                A annotation = field.getAnnotation(tClass);
                if (annotation == null) continue;
                Object value = setAnnotationValue(annotation, attrName, attrTypeEnum, valueMap);
                String fieldName = field.getName();
                fieldAnnotationValueMap.put(fieldName, value);
            }

            // 处理业务逻辑
            dealProcess.deal();

            // 恢复
            for (Field field : fields) {
                A annotation = field.getAnnotation(tClass);
                String fieldName = field.getName();
                if (annotation == null) continue;
                Object value = fieldAnnotationValueMap.get(fieldName);

                InvocationHandler handler = Proxy.getInvocationHandler(annotation);
                Field memberValuesField = handler.getClass().getDeclaredField("memberValues");
                memberValuesField.setAccessible(true);
                @SuppressWarnings("all")
                Map<String, Object> memberValues = (Map) memberValuesField.get(handler);
                memberValues.put(attrName, value);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 设置注解中的字段值
     *
     * @param annotation 要修改的注解实例
     * @param attrName  要修改的注解属性名
     * @param attrTypeEnum 要修改的注解属性的类型
     * @param valueMap   替换属性集的map
     */
    @SuppressWarnings("all")
    private static Object setAnnotationValue(Annotation annotation, String attrName,
                                             AttrTypeEnum attrTypeEnum, Map<String, String> valueMap) throws NoSuchFieldException, IllegalAccessException {
        InvocationHandler handler = Proxy.getInvocationHandler(annotation);
        Field field = handler.getClass().getDeclaredField("memberValues");
        field.setAccessible(true);
        Map memberValues = (Map) field.get(handler);
        Object value = memberValues.get(attrName);
        switch (attrTypeEnum) {
            case STRING: {
                String oldValue = (String) value;
                String newValue = valueMap.get(oldValue);
                if (StringUtils.isNotBlank(newValue)) {
                    memberValues.put(attrName, newValue);
                }
            }
            break;
            case STRING_ARR: {
                String[] oldValue = (String[]) value;
                String[] newValue = new String[oldValue.length];
                for (int i = 0; i < oldValue.length; i++) {
                    String replace = valueMap.get(oldValue[i]);
                    newValue[i] = replace != null ? replace : oldValue[i];
                }
                memberValues.put(attrName, newValue);
            }
            break;
        }
        return value;
    }

    public enum AttrTypeEnum {
        STRING,
        STRING_ARR
    }

    public interface DealProcess {

        void deal() throws Exception;

    }

}

代码示例:

    /**
     * 复杂表头编写:方式三
     */
    @Test
    public synchronized void complexHeadDemo03() {

        // 输出文件路径
        String outFilePath = "D:\\excel-files\\demo03.xlsx";

        // 替换注解中的属性值为
        HashMap<String, String> replaceMap = new HashMap<>();
        replaceMap.put("deptName", "运营部");
        replaceMap.put("currentDate", "2024-04-09");

        /*
         * 这里简单的说明一下:
         *      attrName: 对应的是 @ExcelProperty 中的 value 属性
         *      attrTypeEnum: 对应的是 @ExcelProperty 中的 value 属性 的类型
         */
        AnnotationUtils.changeAnnotationValueToDealProcess(
                DeptUserExcelEntity.class, ExcelProperty.class, "value", AnnotationUtils.AttrTypeEnum.STRING_ARR, replaceMap, new AnnotationUtils.DealProcess() {
                    @Override
                    public void deal() {
                        List<DeptUserExcelEntity> excelEntities = new ArrayList<>();
                        EasyExcel.write(outFilePath, DeptUserExcelEntity.class)
                                .sheet().doWrite(excelEntities);
                    }
                }
        );
    }

结果:

在这里插入图片描述

这里要注意的是因为调用该方法时回去修改对应的 class 对象,所以这里最好加锁

这种方式不仅可以处理 easyexcel 的注解动态表头问题,也可以处理传统的 poi 的注解动态表头,目前也是我用得比较多的一种方式

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/530103.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

LeetCode_144(二叉树前序遍历)

1.递归 public List<Integer> preorderTraversal(TreeNode root) {List<Integer> res new ArrayList<>();accessTree(root,res);return res;}public void accessTree(TreeNode root,List<Integer>res){if(root null){return;}res.add(root.val);acce…

Redis 八种常用数据类型常用命令和应用场景

5 种基础数据类型&#xff1a;String&#xff08;字符串&#xff09;、List&#xff08;列表&#xff09;、Set&#xff08;集合&#xff09;、Hash&#xff08;散列&#xff09;、Zset&#xff08;有序集合&#xff09;。 3 种特殊数据类型&#xff1a;HyperLogLog&#xff0…

计算机视觉——Python OpenCV BGR转HSV

这里将介绍如何使用 OpenCV 与 Python 来作彩色影像转HSV(RGB to HSV 或 BGR to HSV)&#xff0c;在写 Python 影像处理程序时常会用到 OpenCV cvtColor 作颜色空间转换的功能&#xff0c;接下来介绍怎么使用 Python 搭配 OpenCV 模块来进行 RGB/BGR 转 HSV 彩色转HSV空间。 H…

03 Php学习:echo 、 print 、EOF

echo 和 print 在 PHP 中有两个基本的输出方式&#xff1a; echo 和 print。 echo 和 print 区别: echo - 可以输出一个或多个字符串print - 只允许输出一个字符串&#xff0c;返回值总为 1 注意&#xff1a;echo 输出的速度比 print 快&#xff0c; echo 没有返回值&…

VS Code开发插件使用 pnpm 打包异常的解决姿势

前言 刚刚准备发一个插件&#xff0c;发现用 pnpm 打出一个本地插件包直接扑街了。 这里只聚焦错误问题的解决&#xff0c;不是发插件的教程。。 聊点背景信息&#xff0c;vscode 的插件命令行的是 vsce 这个模块提供的 cli 能力去做的 环境 pnpm : 8.x 错误截图 本地打…

C++ Virtual详解

Virtual是C OO机制中很重要的一个关键字。只要是学过C的人都知道在类Base中加了Virtual关键字的函数就是虚拟函数&#xff08;例如函数print&#xff09;&#xff0c;于是在Base的派生类Derived中就可以通过重写虚拟函数来实现对基类虚拟函数的覆盖。当基类Base的指针point指向…

LRU算法的实现

目录 一&#xff0c;LRU算法 二&#xff0c;使用场景 三&#xff0c;LRU算法实现 一&#xff0c;LRU算法 LRU-least recently used-最近最少使用算法&#xff0c;是一种内存数据淘汰策略&#xff0c;使用常见是当内存不足时&#xff0c;需要淘汰最近最少使用的数据。LRU常用…

Mac 安装 brew brew cask 遇到的问题以及解决办法

安装Homebrew和Homebrew Cask是在Mac上管理软件包的常用方法。虽然大多数情况下安装这两个工具是比较简单的&#xff0c;但有时候也可能遇到一些问题。下面是一些常见的问题以及解决办法&#xff1a; 问题1&#xff1a;无法安装Homebrew 解决办法&#xff1a; 1.确保你的Mac已连…

4月9日学习记录

[GXYCTF 2019]禁止套娃 涉及知识点&#xff1a;git泄露&#xff0c;无参数RCE 打开环境&#xff0c;源码什么的都没有&#xff0c;扫描后台看看 扫描发现存在git泄露 用githack下载查看得到一串源码 <?php include "flag.php"; echo "flag在哪里呢&#…

REST API实战演练之JavaScript使用Rest API

咱们前面讲了一下如何创建REST API 假期别闲着&#xff1a;REST API实战演练之创建Rest API-CSDN博客 又讲了java客户端如何使用REST API 假期别闲着&#xff1a;REST API实战演练之客户端使用Rest API-CSDN博客 接下来咱们看看JavaScript怎么使用REST API。 一、新建一个…

swiftui macOS实现加载本地html文件

import SwiftUI import WebKitstruct ContentView: View {var body: some View {VStack {Text("测试")HTMLView(htmlFileName: "localfile") // 假设你的本地 HTML 文件名为 index.html.frame(minWidth: 100, minHeight: 100) // 设置 HTMLView 的最小尺寸…

权威报道 | 百分点科技:《突发事件应急预案管理办法》解读

近日&#xff0c;百分点科技CTO刘译璟作为唯一企业界代表&#xff0c;接受应急领域权威期刊——《中国应急管理》杂志邀请&#xff0c;与中国安全生产科学研究院、中央党校、中国政法大学等单位的专家一起&#xff0c;就《突发事件应急预案管理办法》&#xff08;以下简称《办法…

CLI的使用与IOS基本命令

1、实验目的 通过本实验可以掌握&#xff1a; CLI的各种工作模式个CLI各种编辑命令“?” 和【Tab】键使用方法IOS基本命令网络设备访问限制查看设备的相关信息 2、实验拓扑 CLI的使用与IOS基本命令使用拓扑如下图所示。 3、实验步骤 &#xff08;1&#xff09;CLI模式的切…

Vue3 + Vite 构建组件库发布到 npm

你有构建完组件库后&#xff0c;因为不知道如何发布到 npm 的烦恼吗&#xff1f;本教程手把手教你用 Vite 构建组件库发布到 npm 搭建项目 这里我们使用 Vite 初始化项目&#xff0c;执行命令&#xff1a; pnpm create vite my-vue-app --template vue这里以我的项目 vue3-xm…

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单视频处理实战案例 之七 简单指定视频某片段快放效果

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单视频处理实战案例 之七 简单指定视频某片段快放效果 目录 Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单视频处理实战案例 之七 简单指定视频某片段快放效果 一、简单介绍 二、简单指定视频某片段快放效果实现原理…

Docker内更新Jenkins详细讲解

很多小伙伴在Docker中使用Jenkins时更新遇到困难&#xff0c;本次结合自己的实际经验&#xff0c;详细讲解。根据官网Jenkins了解以下内容&#xff1a; 一、Jenkins 是什么? Jenkins是一款开源 CI&CD 软件&#xff0c;用于自动化各种任务&#xff0c;包括构建、测…

python数据分析和可视化【4】星巴克数据分析

有一组关于全球星巴克门店的统计数据directory.csv&#xff0c;分析了在不同国家和地区以及中国不同城市的星巴克门店的数量。 要求&#xff1a; &#xff08;1&#xff09;查看星巴克旗下有哪些品牌。如果我们只关心星巴克咖啡门店&#xff0c;则只需获取星巴克中Brand的数据集…

多层磁介质让HDD容量翻倍,可超过120TB

近些年&#xff0c;尽管HDD市场收到SSD重创&#xff0c;市场占比遭遇滑坡&#xff0c;但SSD在企业存储市场的侵蚀并未显著增加&#xff0c;SSD容量出货占企业存储&#xff08;HDDSSD&#xff09;总量的比例保持在31-32%左右。然而&#xff0c;考虑到AI优化加速计算对全闪存架构…

XUbuntu22.04之Typora添加水印并输出pdf文件(二百二十七)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒体系统工程师系列【原创干货持续更新中……】🚀 优质视频课程:AAOS车载系统+AOSP…

科技动态人工智能应用太空探索生物科技

根据最新的科技资讯&#xff0c;以下是一些值得关注的科技动态&#xff1a; 人工智能领域 智能体热潮 &#xff1a;随着大模型的研发热潮&#xff0c;AI智能体的发展迅速&#xff0c;它们被用作认知核心&#xff0c;具备强大的学习和迁移能力。智能体的架构和交互方式也在不断进…