SpringBoot整合EasyExcel 3.x

news2024/9/9 7:45:05

文章目录

  • 1 EasyExcel 3.x
    • 1.1 简介
    • 1.2 引入依赖
    • 1.3 简单导出
      • 1.3.1 定义实体类
      • 1.3.2 自定义转换器
      • 1.3.3 定义接口
    • 1.4 简单导入
    • 1.5 复杂导出
      • 1.5.1 引言
      • 1.5.2 自定义注解
      • 1.5.3 定义实体类
      • 1.5.4 数据映射与平铺
      • 1.5.5 自定义单元格合并策略
      • 1.5.6 定义接口

1 EasyExcel 3.x

1.1 简介

EasyExcel 是一个基于 Java 的、快速、简洁、解决大文件内存溢出的 Excel 处理工具。它能让你在不用考虑性能、内存的等因素的情况下,快速完成 Excel 的读、写等功能。

EasyExcel文档地址:https://easyexcel.opensource.alibaba.com/

1.2 引入依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.1.3</version>
</dependency>

1.3 简单导出

1.3.1 定义实体类

EasyExcel中,以面向对象思想来实现导入导出,无论是导入数据还是导出数据都可以想象成具体某个对象的集合,所以为了实现导出用户信息功能,首先创建一个用户对象UserDO实体类,用于封装用户信息:

@Data
public class UserDO {
    @ExcelProperty("用户编号")
    @ColumnWidth(20)
    private Long id;

    @ExcelProperty("用户名")
    @ColumnWidth(20)
    private String username;

    @ExcelIgnore
    private String password;

    @ExcelProperty("昵称")
    @ColumnWidth(20)
    private String nickname;

    @ExcelProperty("生日")
    @ColumnWidth(20)
    @DateTimeFormat("yyyy-MM-dd")
    private Date birthday;

    @ExcelProperty("手机号")
    @ColumnWidth(20)
    private String phone;

    @ExcelProperty("身高(米)")
    @NumberFormat("#.##")
    @ColumnWidth(20)
    private Double height;

    @ExcelProperty(value = "性别", converter = GenderConverter.class)
    @ColumnWidth(10)
    private Integer gender;
}

上面代码中类属性上使用了EasyExcel核心注解:

  • @ExcelProperty:核心注解,value属性可用来设置表头名称,converter属性可以用来设置类型转换器;
  • @ColumnWidth:用于设置表格列的宽度;
  • @DateTimeFormat:用于设置日期转换格式;
  • @NumberFormat:用于设置数字转换格式。

1.3.2 自定义转换器

EasyExcel中,如果想实现枚举类型到字符串类型转换(例如gender属性:1 -> 男,2 -> 女),需实现Converter接口来自定义转换器,下面为自定义GenderConverter性别转换器代码实现:

public class GenderConverter implements Converter<Integer> {
    @Override
    public Class<?> supportJavaTypeKey() {
        return Integer.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }

    @Override
    public Integer convertToJavaData(ReadConverterContext<?> context) {
        return GenderEnum.convert(context.getReadCellData().getStringValue()).getValue();
    }

    @Override
    public WriteCellData<?> convertToExcelData(WriteConverterContext<Integer> context) {
        return new WriteCellData<>(GenderEnum.convert(context.getValue()).getDescription());
    }
}

性别枚举

@Getter
@AllArgsConstructor
public enum GenderEnum {

    /**
     * 未知
     */
    UNKNOWN(0, "未知"),

    /**
     * 男性
     */
    MALE(1, "男性"),

    /**
     * 女性
     */
    FEMALE(2, "女性");

    private final Integer value;

    @JsonFormat
    private final String description;

    public static GenderEnum convert(Integer value) {
        return Stream.of(values())
                .filter(bean -> bean.value.equals(value))
                .findAny()
                .orElse(UNKNOWN);
    }

    public static GenderEnum convert(String description) {
        return Stream.of(values())
                .filter(bean -> bean.description.equals(description))
                .findAny()
                .orElse(UNKNOWN);
    }
}

1.3.3 定义接口

@RestController
@RequestMapping("/excel")
public class ExcelController {

    @GetMapping("/export/user")
    public void exportUserExcel(HttpServletResponse response) {
        try {
            this.setExcelResponseProp(response, "用户列表");
            List<UserDO> userList = this.getUserList();
            EasyExcel.write(response.getOutputStream())
                    .head(UserDO.class)
                    .excelType(ExcelTypeEnum.XLSX)
                    .sheet("用户列表")
                    .doWrite(userList);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 设置响应结果
     */
    private void setExcelResponseProp(HttpServletResponse response, String rawFileName) throws UnsupportedEncodingException {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String fileName = URLEncoder.encode(rawFileName, "UTF-8").replaceAll("\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
    }
    
    /**
     * 读取用户列表数据
     */
    private List<UserDO> getUserList() throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        ClassPathResource classPathResource = new ClassPathResource("mock/users.json");
        InputStream inputStream = classPathResource.getInputStream();
        return objectMapper.readValue(inputStream, new TypeReference<List<UserDO>>() {
        });
    }
}

1.4 简单导入

@RestController
@RequestMapping("/excel")
@Api(tags = "EasyExcel")
public class ExcelController {
    
    @PostMapping("/import/user")
    public ResponseVO importUserExcel(@RequestPart(value = "file") MultipartFile file) {
        try {
            List<UserDO> userList = EasyExcel.read(file.getInputStream())
                    .head(UserDO.class)
                    .sheet()
                    .doReadSync();
            return ResponseVO.success(userList);
        } catch (IOException e) {
            return ResponseVO.error();
        }
    }
}

1.5 复杂导出

1.5.1 引言

由于 EasyPoi 支持嵌套对象导出,直接使用内置 @ExcelCollection 注解即可实现,遗憾的是 EasyExcel 不支持一对多导出,只能自行实现,通过此issues了解到,项目维护者建议通过自定义合并策略方式来实现一对多导出。
图片
解决思路:只需把订单主键相同的列中需要合并的列给合并了,就可以实现这种一对多嵌套信息的导出

1.5.2 自定义注解

创建一个自定义注解,用于标记哪些属性需要合并单元格,哪个属性是主键,用于判断是否需要合并以及合并的主键

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExcelMerge {
    /**
     * 是否合并单元格
     *
     * @return true || false
     */
    boolean merge() default true;

    /**
     * 是否为主键(即该字段相同的行合并)
     *
     * @return true || false
     */
    boolean isPrimaryKey() default false;
}

1.5.3 定义实体类

在需要合并单元格的属性上设置 @ExcelMerge 注解,二级表头通过设置 @ExcelProperty 注解中 value 值为数组形式来实现该效果:

@Data
public class OrderBO {
    @ExcelProperty(value = "订单主键")
    @ColumnWidth(16)
    @ExcelMerge(merge = true, isPrimaryKey = true)
    private String id;

    @ExcelProperty(value = "订单编号")
    @ColumnWidth(20)
    @ExcelMerge(merge = true)
    private String orderId;

    @ExcelProperty(value = "收货地址")
    @ExcelMerge(merge = true)
    @ColumnWidth(20)
    private String address;

    @ExcelProperty(value = "创建时间")
    @ColumnWidth(20)
    @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
    @ExcelMerge(merge = true)
    private Date createTime;

    @ExcelProperty(value = {"商品信息", "商品编号"})
    @ColumnWidth(20)
    private String productId;

    @ExcelProperty(value = {"商品信息", "商品名称"})
    @ColumnWidth(20)
    private String name;

    @ExcelProperty(value = {"商品信息", "商品标题"})
    @ColumnWidth(30)
    private String subtitle;

    @ExcelProperty(value = {"商品信息", "品牌名称"})
    @ColumnWidth(20)
    private String brandName;

    @ExcelProperty(value = {"商品信息", "商品价格"})
    @ColumnWidth(20)
    private BigDecimal price;

    @ExcelProperty(value = {"商品信息", "商品数量"})
    @ColumnWidth(20)
    private Integer count;
}

1.5.4 数据映射与平铺

导出之前,需要对数据进行处理,将订单数据进行平铺,orderList为平铺前格式,exportData为平铺后格式:
在这里插入图片描述

1.5.5 自定义单元格合并策略

当 Excel 中两列主键相同时,合并被标记需要合并的列:

public class ExcelMergeStrategy implements RowWriteHandler {

    /**
     * 主键下标
     */
    private Integer primaryKeyIndex;

    /**
     * 需要合并的列的下标集合
     */
    private final List<Integer> mergeColumnIndexList = new ArrayList<>();

    /**
     * 数据类型
     */
    private final Class<?> elementType;

    public ExcelMergeStrategy(Class<?> elementType) {
        this.elementType = elementType;
    }

    @Override
    public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
        // 判断是否为标题
        if (isHead) {
            return;
        }
        // 获取当前工作表
        Sheet sheet = writeSheetHolder.getSheet();
        // 初始化主键下标和需要合并字段的下标
        if (primaryKeyIndex == null) {
            this.initPrimaryIndexAndMergeIndex(writeSheetHolder);
        }
        // 判断是否需要和上一行进行合并
        // 不能和标题合并,只能数据行之间合并
        if (row.getRowNum() <= 1) {
            return;
        }
        // 获取上一行数据
        Row lastRow = sheet.getRow(row.getRowNum() - 1);
        // 将本行和上一行是同一类型的数据(通过主键字段进行判断),则需要合并
        if (lastRow.getCell(primaryKeyIndex).getStringCellValue().equalsIgnoreCase(row.getCell(primaryKeyIndex).getStringCellValue())) {
            for (Integer mergeIndex : mergeColumnIndexList) {
                CellRangeAddress cellRangeAddress = new CellRangeAddress(row.getRowNum() - 1, row.getRowNum(), mergeIndex, mergeIndex);
                sheet.addMergedRegionUnsafe(cellRangeAddress);
            }
        }
    }

    /**
     * 初始化主键下标和需要合并字段的下标
     *
     * @param writeSheetHolder WriteSheetHolder
     */
    private void initPrimaryIndexAndMergeIndex(WriteSheetHolder writeSheetHolder) {
        // 获取当前工作表
        Sheet sheet = writeSheetHolder.getSheet();
        // 获取标题行
        Row titleRow = sheet.getRow(0);
        // 获取所有属性字段
        Field[] fields = this.elementType.getDeclaredFields();
        // 遍历所有字段
        for (Field field : fields) {
            // 获取@ExcelProperty注解,用于获取该字段对应列的下标
            ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
            // 判断是否为空
            if (null == excelProperty) {
                continue;
            }
            // 获取自定义注解,用于合并单元格
            ExcelMerge excelMerge = field.getAnnotation(ExcelMerge.class);
            // 判断是否需要合并
            if (null == excelMerge) {
                continue;
            }
            for (int i = 0; i < fields.length; i++) {
                Cell cell = titleRow.getCell(i);
                if (null == cell) {
                    continue;
                }
                // 将字段和表头匹配上
                if (excelProperty.value()[0].equalsIgnoreCase(cell.getStringCellValue())) {
                    if (excelMerge.isPrimaryKey()) {
                        primaryKeyIndex = i;
                    }
                    if (excelMerge.merge()) {
                        mergeColumnIndexList.add(i);
                    }
                }
            }
        }

        // 没有指定主键,则异常
        if (null == this.primaryKeyIndex) {
            throw new IllegalStateException("使用@ExcelMerge注解必须指定主键");
        }
    }
}

1.5.6 定义接口

将自定义合并策略 ExcelMergeStrategy 通过 registerWriteHandler 注册上去

@RestController
@RequestMapping("/excel")
public class ExcelController {

    @GetMapping("/export/order")
    public void exportOrderExcel(HttpServletResponse response) {
        try {
            this.setExcelResponseProp(response, "订单列表");
            List<OrderDO> orderList = this.getOrderList();
            List<OrderBO> exportData = this.convert(orderList);
            EasyExcel.write(response.getOutputStream())
                    .head(OrderBO.class)
                    .registerWriteHandler(new ExcelMergeStrategy(OrderBO.class))
                    .excelType(ExcelTypeEnum.XLSX)
                    .sheet("订单列表")
                    .doWrite(exportData);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 设置响应结果
     */
    private void setExcelResponseProp(HttpServletResponse response, String rawFileName) throws UnsupportedEncodingException {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String fileName = URLEncoder.encode(rawFileName, "UTF-8").replaceAll("\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

string容器语法

文章目录 string容器string基本概念本质&#xff1a;string和char * 区别&#xff1a;特点&#xff1a; string构造函数示例 string赋值操作示例&#xff1a; string字符串拼接示例&#xff1a; string查找和替换示例&#xff1a; string字符串比较示例&#xff1a; string字符…

第六章 rabbitmq高可用集群

在服务之间会采用mq进行消息通信,而rabbitmq本身也如同consul一样,如果只有一个节点那么就可能出现宕机的问题,并且基于mq的特点我们是可以在多个服务之间使用同一个mq来相互通信,因此高可用的架构设计就必不可少 1、rabbitmq集群方案 主备 远程 镜像 多活 构建 2、…

[uni-app]设置运行到微信小程序

1、设置微信小程序开发工具路径 2、检查微信小程序开发工具是否开启了服务端口 服务端口要是没有开启&#xff0c;会报 initialize。 3、在uni-app开发工具中点击运行微信开发者工具&#xff0c;微信开发工具运行成功。

Spark架构体系

StandAlone模式是spark自带的集群运行模式&#xff0c;不依赖其他的资源调度框架&#xff0c;部署起来简单。 StandAlone模式又分为client模式和cluster模式&#xff0c;本质区别是Driver运行在哪里&#xff0c;如果Driver运行在SparkSubmit进程中就是Client模式&#xff0c;如…

谷歌Med-PaLM 2霸榜医学问答领域

谷歌IO大会上&#xff0c;谷歌CEO桑达尔・皮查伊&#xff08;Sundar Pichai&#xff09;向全世界AI开发者发布了谷歌最新的大型语言模型&#xff08;LLMs&#xff09;PaLM 2&#xff0c;作为对标OpenAI最新大模型GPT-4的竞品&#xff0c;PaLM 2展现出了强大的多语言和推理能力。…

概率论与数理统计发展历史简介

概率论与数理统计发展历史简介 1 介绍1.1 概述1.2 概率论发展历史1.3 统计学发展历史1.4 概率论演化 2 在线学习在线 概率与统计 视觉化学习 -- 布朗大学何志坚老师的数理统计讲义鸢尾花书--统计至简 参考 1 介绍 1.1 概述 概率论是与概率有关的数学分支。虽然有几种不同的概…

Linux内核模块开发 第 8 章

The Linux Kernel Module Programming Guide Peter Jay Salzman, Michael Burian, Ori Pomerantz, Bob Mottram, Jim Huang译 断水客&#xff08;WaterCutter&#xff09;源 LKMPG 8 sysfs: 与模块交互 sysfs 允许用户通过读写模块中的变量实现与内核模块的交互。这个特性在…

leetcode907. 子数组的最小值之和(单调栈-java)

子数组的最小值之和 leetcode907. 子数组的最小值之和题目描述单调栈解法一代码演示单调栈解法二 单调栈专题 leetcode907. 子数组的最小值之和 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;https://leetcode.cn/problems/sum-of-subarray-minimums…

如何快速学习一门计算机语言

如何快速学习一门计算机语言 掌握一门语言的基本数据类型和基本语法。掌握语言里数组和集合工具类的使用掌握循环分支控制掌握一下该计算机语言面向对象或者函数式编程的特征对异常或者错误的处理文件读写&#xff0c;输入输出流字符串的处理日志的打印运行时module或者librar…

【C语言基础】函数

C语言中的函数是模块化编程的基础&#xff0c;通过函数的定义、实参与形参的传递以及函数的调用流程&#xff0c;我们可以实现代码的重用和逻辑的封装。本文将深入探讨C语言函数的定义方式、实参与形参的传递机制&#xff0c;以及函数的调用流程和局部变量与栈内存的关系。 一、…

企业快递管理制定教程

在经济飞速发展的助力之下&#xff0c;现代企业接触到的制度越来越多&#xff0c;除了我们熟知的CRM、OA等等&#xff0c;管理制度进一步细分。企业寄件在企业内部运转中的地位越发重要&#xff0c;随之也产生了快递管理制度。不少人就会问&#xff1a;有必要这么细分吗&#x…

跨应用连接同一个redis,从redis取缓存,对象属性值都为null

本地idea部署和docker部署问题&#xff0c;连接同一个redis&#xff0c;idea项目的redis缓存&#xff0c;docker中取不到&#xff0c;docker中缓存的redis本地取不到 ✅ 原因&#xff1a;idea本地代码实体类未进行代码混淆&#xff0c;docker代码实体类进行了混淆&#xff0c;…

Caused by: java.io.IOException: CreateProcess error=206, 文件名或扩展名太长

java.io.IOException: Cannot run program "D:\javaAPP\jdk\bin\java.exe" (in directory "D:\java\demo"): CreateProcess error206, 文件名或扩展名太长。 Caused by: java.io.IOException: CreateProcess error206, 文件名或扩展名太长。 删除项目.ide…

Vue -- 生命周期 数据共享

1 组件的生命周期 1.1 生命周期 & 生命周期函数 生命周期&#xff08;Life Cycle&#xff09;是指一个组件从创建 -> 运行 -> 销毁的整个阶段&#xff0c;强调的是一个时间段。 生命周期函数&#xff1a;是由 vue 框架提供的内置函数&#xff0c;会伴随着组件的生命…

leetcode极速复习版-第一章数组

目录 数组 数组理论基础 704二分查找 27移除元素 977.有序数组的平方 209.长度最小的子数组 59.螺旋矩阵II 数组部分总结 数组 数组理论基础 数组的元素是不能删的&#xff0c;只能覆盖。 二维数组&#xff1a; 704二分查找 二分法 middle int(left right)的int 直接对着一个…

SSM学习笔记-------SpringMVC(一)

SSM学习笔记-------SpringMVC_day01 SpringMVC_day011、SpringMVC简介1.1 SpringMVC概述 2、SpringMVC入门案例2.1 需求分析2.2 案例制作步骤1:创建Maven项目&#xff0c;并导入对应的jar包步骤2:创建控制器类步骤3:创建配置类步骤4:创建Tomcat的Servlet容器配置类步骤5:配置To…

【2022吴恩达机器学习课程实验翻译笔记】 Python 和 Jupyter Notebook 简介

为了看着比较连贯&#xff0c;我直接翻译了&#xff0c;不放英文原文对照了 选修实验课: Python 和 Jupyter Notebook 简介 欢迎来到第一节选修实验课 选修实验课的目的是&#xff1a; 提供信息&#xff0c;就像这个notebook一样通过实例加深对课程的理解展示在课程中使用的…

【Unity实战】制作类元气骑士、挺进地牢——俯视角射击游戏多种射击效果(二)(附源码)

文章目录 前言一、火箭筒1. 编写火箭筒脚本2. 创建火箭弹和新爆炸特效的预制体3. 编写火箭弹脚本4. 设置好火箭弹和火箭筒的脚本和参数5. 运行效果 二、激光枪1. 编写激光枪脚本2. 先运行游戏&#xff0c;看看效果3. 美化射线4. 完善代码5. 再次运行游戏6. 升级URP项目7. 后处理…

剑指offer13.机器人的运动范围

一开始没看清题目&#xff0c;没看到要一步一步移动&#xff0c;我以为是看所有格子中有几个格子符合条件&#xff0c;就直接遍历所有格子&#xff0c;把每个格子的i&#xff0c;j每个位数上的数相加看看是否小于k&#xff0c;是就给counts加一最后返回couts&#xff0c;我还说…

OSPF小实验

OSPF小实验 要求&#xff1a; 1、地址配置 R1&#xff1a; R2&#xff1a; R3&#xff1a; R4&#xff1a; R5&#xff1a; R6&#xff1a; 2、启用R1-R3的ospf&#xff0c;划分为区域0 R1&#xff1a; R2&#xff1a; R3&#xff1a; 3、R1-R2之间采用ppp的pap单向认证 R1为…