SpringBoot 集成 EasyExcel 3.x 实现 Excel 导出

news2024/11/17 11:32:20

目录

 

EasyExcel官方文档

EasyExcel是什么?

EasyExcel注解

 springboot集成EasyExcel

简单入门导出 :

实体类

 自定义转换类

测试一下

复杂表头一对多导出 : 

 自定义注解

定义实体类

自定义单元格合并策略

 测试一下

 


 

EasyExcel官方文档

EasyExcel官方文档 - 基于Java的Excel处理工具 | Easy Excel

EasyExcel是什么?

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

EasyExcel注解

  • @ExcelProperty: 核心注解,value属性可用来设置表头名称,converter属性可以用来设置类型转换器

  • @ColumnWidth: 用于设置表格列的宽度

  • @DateTimeFormat: 用于设置日期转换格式

  • @NumberFormat: 用于设置数字转换格式

  • @ExcelIgnore:默认所有字段都会和excel去匹配,加了这个注解会忽略该字段

  • @ExcelIgnoreUnannotated:默认不加 ExcelProperty 的注解的都会参与读写,加了不会参与读写

具体使用请参考:

读Excel:读Excel | Easy Excel

写Excel:写Excel | Easy Excel

 springboot集成EasyExcel

        springboot集成EasyExcel3.x非常简单,只需要引入以下依赖即可。

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

简单入门导出 :

        接下来使用EasyExcel实现简单导出功能。

实体类

       导入数据还是导出数据都可以想象成具体某个对象的集合。

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

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

    @ExcelIgnore
    private String password;

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

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

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

 自定义转换类

        导出excel数据时,可能会遇到XXX状态、XXX类型和性别等需要转换的字段,例如:0->男,1->女。需实现Converter接口来自定义转换器。
 

orElse(null)与orElseGet(null)区别:

orElse(null)表示如果一个都没找到返回null。【orElse()中可以塞默认值。如果找不到就会返回orElse中你自己设置的默认值。】

orElseGet(null)表示如果一个都没找到返回null。【orElseGet()中可以塞默认值。如果找不到就会返回orElseGet中你自己设置的默认值。】

区别就是在使用方法时,即时时没有值 也会执行 orElse 内的方法, 而 orElseGet则不会。

Enum:

@Getter
@AllArgsConstructor
public enum GenderEnum {

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

    /**
     * 女性
     */
    FEMALE(1, "女性"),

    /**
     * 未知
     */
    UNKNOWN(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);
    }
}
Converter:
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());
    }
}

测试一下

File.separator等价于“\”

package com.springwork.high.easyexcel.controller;

/**
 * @author gj
 * @version 1.0.0
 * @date 2023/7/7 15:36
 */

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.springwork.high.easyexcel.vo.StudentDo;
import com.sun.deploy.net.URLEncoder;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.List;

/**
 * EasyExcel导入导出
 *
 * @author william@StarImmortal
 */
@RestController
@RequestMapping("/excel")
public class ExcelController {

    @GetMapping("/export/student")
    public void exportUserExcel(HttpServletResponse response) {
        try {
            this.setExcelResponseProp(response, "学生列表");
            List<StudentDo> studentList = this.getStudentList();
            EasyExcel.write(response.getOutputStream())
                    .head(StudentDo.class)
                    .excelType(ExcelTypeEnum.XLSX)
                    .sheet("学生列表")
                    .doWrite(studentList);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 设置响应结果
     *
     * @param response    响应结果对象
     * @param rawFileName 文件名
     * @throws UnsupportedEncodingException 不支持编码异常
     */
    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", File.separator+"+");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
    }

    /**
     * 读取学生列表数据
     *
     * @return 学生数据
     * @throws IOException IO异常
     */
    private List<StudentDo> getStudentList() throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        ClassPathResource classPathResource = new ClassPathResource("data/students.json");
        InputStream inputStream = classPathResource.getInputStream();
        return objectMapper.readValue(inputStream, new TypeReference<List<StudentDo>>() {
        });
    }
}

json:将JSON数据放在resource/data下即可

[
  {"id": 1,"studentName": "张三","password": "1234","birthday": "2000-01-01","height": "170.5","gender": 0},
  {"id": 2,"studentName": "李四","password": "1234","birthday": "2000-01-02","height": "175.5","gender": 1},
  {"id": 3,"studentName": "王五","password": "1234","birthday": "2000-01-03","height": "180.5","gender": 1},
  {"id": 4,"studentName": "赵六","password": "1234","birthday": "2000-01-04","height": "176.5","gender": 0}
]

测试:

使用postman进行测试,选择“Send and Download”选项。

dbf2cb089549448e835b234d113f6e0e.png

 

复杂表头一对多导出 : 

        由于 EasyPoi 支持嵌套对象导出,直接使用内置 @ExcelCollection 注解即可实现,但是 EasyExcel 不支持一对多导出,只能自行实现,可以通过自定义合并策略方式来实现一对多导出。

ba96bd1f013348e48e73a9104da091f0.png

 自定义注解

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

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

定义实体类

        需要合并的字段上标明@ExcelMerge

        二级表头通过@ExcelProperty(value = {"学生信息","学生编号"})设置

@Data
public class SchoolDo {

    @ExcelProperty("学校编号")
    @ColumnWidth(20)
    @ExcelMerge(merge = true,isPrimaryKey = true)
    private String schoolId;

    @ExcelProperty("学校名")
    @ColumnWidth(20)
    @ExcelMerge(merge = true)
    private String schoolName;

    @ExcelProperty(value = {"学生信息","学生编号"})
    @ColumnWidth(20)
    private Long studentId;

    @ExcelProperty(value = {"学生信息","学生名"})
    @ColumnWidth(20)
    private String studentName;

    @ExcelIgnore
    private String password;

    @ExcelProperty(value = {"学生信息","生日"})
    @ColumnWidth(20)
    @DateTimeFormat("yyyy-MM-dd")
    private Date birthday;

    @ExcelProperty(value = {"学生信息","身高(米)"})
    @NumberFormat("#.##")
    @ColumnWidth(20)
    private Double height;

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

自定义单元格合并策略

        当 Excel 中两列主键相同时,合并被标记需要合并的列,将自定义合并策略 ExcelMergeStrategy 通过 registerWriteHandler 注册上去:


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注解必须指定主键");
        }
    }
}

 测试一下

 @GetMapping("/export/school")
    public void exportOrderExcel(HttpServletResponse response) {
        try {
            this.setExcelResponseProp(response, "学校列表");
            List<SchoolDo> exportData = this.getSchoolList();
            EasyExcel.write(response.getOutputStream())
                    .head(SchoolDo.class)
                    .registerWriteHandler(new ExcelMergeStrategy(SchoolDo.class))
                    .excelType(ExcelTypeEnum.XLSX)
                    .sheet("学校列表")
                    .doWrite(exportData);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
/**
     * 读取学校列表数据
     *
     * @return 列表数据
     * @throws IOException IO异常
     */
    private List<SchoolDo> getSchoolList() throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        ClassPathResource classPathResource = new ClassPathResource("data/schools.json");
        InputStream inputStream = classPathResource.getInputStream();
        return objectMapper.readValue(inputStream, new TypeReference<List<SchoolDo>>() {
        });
    }

json数据:

[
  {"schoolId":"1","schoolName":"北京大学","studentId": 1,"studentName": "张三","password": "1234","birthday": "2000-01-01","height": "170.5","gender": 0},
  {"schoolId":"1","schoolName":"北京大学","studentId": 2,"studentName": "李四","password": "1234","birthday": "2000-01-02","height": "175.5","gender": 1},
  {"schoolId":"1","schoolName":"北京大学","studentId": 3,"studentName": "王五","password": "1234","birthday": "2000-01-03","height": "180.5","gender": 1},
  {"schoolId":"1","schoolName":"北京大学","studentId": 4,"studentName": "赵六","password": "1234","birthday": "2000-01-04","height": "176.5","gender": 0},
  {"schoolId":"2","schoolName":"河北大学","studentId": 1,"studentName": "张三","password": "1234","birthday": "2000-01-01","height": "170.5","gender": 0},
  {"schoolId":"2","schoolName":"河北大学","studentId": 2,"studentName": "李四","password": "1234","birthday": "2000-01-02","height": "175.5","gender": 1},
  {"schoolId":"2","schoolName":"河北大学","studentId": 3,"studentName": "王五","password": "1234","birthday": "2000-01-03","height": "180.5","gender": 1},
  {"schoolId":"2","schoolName":"河北大学","studentId": 4,"studentName": "赵六","password": "1234","birthday": "2000-01-04","height": "176.5","gender": 0}
]

测试:

使用postman进行测试,选择“Send and Download”选项。

c0439545ac76411cb6ee32a948c107d1.png

参考文献

官方文档:

 

https://www.yuque.com/easyexcel/doc/easyexcel

一对多导出优雅方案:

 

https://github.com/alibaba/easyexcel/issues

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

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

相关文章

The Sandbox 展示泰国 2023 年元宇宙生态系统

The Sandbox 举办了 2023 年泰国合作伙伴日活动&#xff0c;宣布创建泰国元宇宙生态系统&#xff0c;并对泰国创客社区的巨大合作和发展表示认可。 The Sandbox 联合创始人兼首席运营官 Sebastien BORGET 说&#xff1a;“我们很高兴见证 The Sandbox 泰国生态系统的发展&#…

第七章 网络安全【计算机网络】

第七章 网络安全【计算机网络】 前言推荐第7章 网络安全7.1网络安全问题概述7.1.1计算机网络面临的安全性威胁7.1.2安全的计算机网络7.1.3数据加密模型 7.2两类密码体制7.2.1对称密钥密码体制7.2.2公钥密码体制 7.3鉴别7.3.1报文鉴别7.3.2实体鉴别 7.4密钥分配7.4.1 对称密钥的…

银行软开能干到退休吗?

大家好&#xff0c;我是熊哥。 21世纪了好像不躺平对不起自己&#xff1f;很多读者都关心哪些企业适合躺平&#xff0c;做程序员是不是在银行可以舒舒服服干一辈子&#xff1f;银行招软开&#xff08;软件开发&#xff09;有哪些要求&#xff1f; 现在就来详细讲一讲。 擦亮…

数据结构--二叉树的线索化

数据结构–二叉树的线索化 用土办法找到中序前驱 typedef struct BiTNode {ElemType data; //数据域struct BiTNode *lchild, *rchild; //左、右孩子指针struct BiTnode *parent; //父节点指针 }BiTNode, *BiTree;BiTNode *p; // p指向目标结点 BiTNode *pre NULL; //指向当前…

用Postman和jmeter做接口测试有什么区别吗?

目录 1.创建接口用例集&#xff08;没区别&#xff09; 2.步骤的实现&#xff08;有区别&#xff09; 3数据用例的实现 4断言的实现 5执行 6其他 总结&#xff1a; 1.创建接口用例集&#xff08;没区别&#xff09; Postman是Collections&#xff0c;Jmeter是线程组&am…

web安全php基础_php之string对象详解

PHP 字符串 字符串变量用于包含有字符的值。 在创建字符串之后&#xff0c;我们就可以对它进行操作了。您可以直接在函数中使用字符串&#xff0c;或者把它存储在变量中。 在下面的实例中&#xff0c;我们创建一个名为 txt 的字符串变量&#xff0c;并赋值为 “Hello world!…

MySQL数据库:数据库管理系统与安装MySQL数据库

目录 一、理论 1.数据库管理系统 2.关系型数据库 3.数据库 4.MySQL数据库 5.MySQL部署 二、实验 1.yum安装MySQL 2.配置MySQL数据库的Tab补全 三、问题 1.数据库登录报错 2.数据库密码复杂度报错 四、总结 一、理论 1.数据库管理系统 &#xff08;1&#xff09…

深入浅出fromCharCode和charCodeAt

先来看一个简单的输入输出 console.log(String.fromCharCode(30328)); //癸console.log(癸.charCodeAt()) //30328来回答一个问题&#xff0c;30328是什么&#xff1f; ascii? unicode? utf-8? utf-16? 来让我们看看fromCharCode 在MDN上面的解释 UTF-16代码单元序列…

【数据分析 - 基础入门之NumPy③】日常难题解决

知识目录 前言一、启动Jupyter Notebook报错没有这样的目录结语# 往期文章&相关导读 前言 本篇文章用于整理在学习 NumPy 过程中遇到的错误&#xff0c;以此做个记录&#xff0c;希望能帮助到大家&#xff0c;让大家少走弯路。 一、启动Jupyter Notebook报错没有这样的目…

改动最小,最简洁的 tomcat catalina.out日志切割(按天)

tomcat日志切割 环境:实现步骤具体操作 环境: 系统: linux操作系统 centOStomcat 8.5* 实现步骤 安装cronolog工具修改tomcat中的bin目录下的 catalina.sh 文件配置重启tomcat 具体操作 安装cronolog 工具输入命令: yum install cronolog 中间 停顿 填个 y 同意修改配置…

什么是cookies,session,token(面试必问)

目录 前言 http无状态 cookies cookie的格式显示 cookie存在问题 利用cookies可以做什么 session session的格式显示 session存在的问题 利用session可以做什么 token token的格式显示 token存在的问题 利用token可以做什么 总结 前言 一般在面试的时候&#xf…

内网隧道代理技术(九)之应用层代理技术介绍

应用层代理技术介绍 前面我们介绍了相关的一些概念和隧道的技术,主要还是停留在单个端口的隧道 1、反弹shell2、端口转发3、端口映射这些都是利用端口建立隧道,但是并不能满足我们日常生活中的要求 加入我们需要对网络中的机器进行扫描,我们就不能使用端口转发或者端口映射…

【计算机视觉 | 目标检测 | 图像分割】arxiv 计算机视觉关于目标检测和图像分割的学术速递(7 月 7 日论文合集)

文章目录 一、检测相关(5篇)1.1 Contextual Affinity Distillation for Image Anomaly Detection1.2 Noise-to-Norm Reconstruction for Industrial Anomaly Detection and Localization1.3 MMNet: Multi-Collaboration and Multi-Supervision Network for Sequential Deepfake…

postman接口测试之postman常用的快捷键

作为一名IT程序猿&#xff0c;不懂一些工具的快捷方式&#xff0c;应该会被鄙视的吧。收集了一些Postman的快捷方式&#xff0c;大家一起动手操作~ 简单操作 操作mac系统windows系统 打开新标签 ⌘TCtrl T关闭标签⌘WCtrl W强制关闭标签Cmd Alt WCtrl Alt W切换到下一个…

【JavaEE】前后端综合项目-博客系统(上)

【JavaEE】前后端综合项目-博客系统&#xff08;上&#xff09; 文章目录 【JavaEE】前后端综合项目-博客系统&#xff08;上&#xff09;1. 创建项目2. 数据库设计3. 数据库操作的封装3.1 DataSource&#xff08;单例&#xff09;3.1 连接操作3.2 关闭操作3.3 创建实体类3.4 封…

“AI无界·智链全球”!壹沓科技2023世界人工智能大会论坛成功举办,构建数智供应链新范式

“AI无界智链全球”论坛成功举办 构建数智供应链新范式 7月6日&#xff0c;由世界人工智能大会组委会办公室指导&#xff0c;壹沓科技主办&#xff0c;上海交通大学安泰经济与管理学院、上海现代服务业联合会物流与供应链专委会、中国航务周刊协办的“AI无界智链全球”高端论坛…

前端vue入门(纯代码)23_多组件共享

我不喜欢去银行&#xff0c;就好像太监不喜欢去妓院一样&#xff0c;有些东西你没有&#xff0c;去了也是难受。 【24.Vuex中的多组件状态共享】 页面展示&#xff1a; vuex数据共享完整代码&#xff1a; App.vue和main.js&#xff1a;就不展示了。 store/index.js //该文…

Verilog学习笔记1:D触发器

代码1&#xff1a; timescale 1ns/10psmodule d_trigger(d,clk,q);input d; input clk; output q;assign q~d;endmodule//testbench module d_trigger_tb; reg clk; reg d; wire q;d_trigger d_trigger(.d(d),.clk(clk),.q(q));initial begind<0;clk<0;#1000 $stop;end…

异步电路后端实现流程(cdc signOff 后端做什么)

目录 1.一种后端异步电路的signOff流程 2.cdc maxDelay的原因及relax 2.1为什么要做maxDelay检查 2.2 如果有不同名字的clk 原本是同步时钟域 该怎么办 2.3 如果有异步电路不能满足0.7倍的fastclk maxDelay检查该怎么办&#xff1f; 1.一种后端异步电路的signOff流程 同步…

Openpyxl给同个单元格的不同文字“上色”

文章目录 0.背景1. 原文代码2. 我的代码3. 总结 0.背景 openpyxl一直在用&#xff0c;今天看到一直关注的公众号又发出了奇技淫巧&#xff08;想要看原文或者要关注原作者的可以点击这里&#xff09;&#xff0c;就直接收录到主页好了。免得以后找不到了。 1. 原文代码 原来…