【SpringBoot整合系列】SpirngBoot整合EasyExcel

目录

背景

需求

在当今信息化社会,数据的导入和导出在各种业务场景中变得越来越重要。

发展

  • 我们知道Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。
  • easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便。
  • 为了满足复杂的导入导出需求,结合Java编程语言、Spring Boot框架以及EasyExcel库,我们可以轻松地构建出强大而灵活的数据处理系统。
  • 当涉及到在Spring Boot 中使用 EasyExcel 实现复杂的导入导出案例时,我们可以结合 Spring Boot 的特性来实现更灵活和集成化的解决方案。

EasyExcel

官网

https://easyexcel.opensource.alibaba.com/

介绍

EasyExcel 是一款基于 Java 的开源库,专门用于处理 Excel 文件的导入和导出操作。它提供了简单易用的 API,使开发人员能够轻松地实现 Excel 数据的读取和写入,同时还支持大数据量的处理,具有较高的性能和灵活性。

优势

EasyExcel 可以在数据迁移、报表生成、数据分析等多个领域发挥作用,尤其适用于需要频繁处理 Excel 数据的场景。无论是个人开发者还是企业开发团队,都可以通过 EasyExcel 更轻松地实现数据导入导出功能,提高开发效率和用户体验。

  1. 简单易用: EasyExcel 提供了简洁的 API 接口,让开发人员能够快速上手。无论是初学者还是有经验的开发者,都能轻松地实现 Excel 文件的导入导出功能。
  2. 支持多种数据格式: EasyExcel 支持导入导出多种数据格式,包括基本的文本、数字、日期等,以及复杂的对象、集合、嵌套结构等数据类型。
  3. 高性能: EasyExcel 在处理大数据量时表现出色,采用了基于流的方式,有效地降低了内存消耗,提升了性能和效率。
  4. 自定义样式: 开发人员可以灵活地自定义单元格样式,包括字体、颜色、对齐方式等,使导出的 Excel 数据更加美观和易读。
  5. 数据转换: EasyExcel 支持自定义数据转换器,可以将原始数据转换为目标格式,满足业务需求。
  6. 异常处理: EasyExcel 提供了丰富的异常处理机制,能够捕获和处理导入导出过程中的异常情况,保障数据的完整性和一致性。
  7. 多平台支持: EasyExcel 可以在各种 Java 开发环境中使用,包括传统的 Java 应用程序、Web 应用程序,甚至是移动应用开发中。
  8. 开源社区: EasyExcel 是一个开源项目,拥有活跃的社区支持,开发人员可以从社区中获取帮助、贡献代码以及分享经验

常用注解

注解 作用
@ExcelProperty 用于标识Excel中的字段,可以指定字段在Excel中的列索引或列名。例如:@ExcelProperty("姓名"),@ExcelProperty(index = 0)。
@ExcelIgnore 用于标识不需要导入或导出的字段。
@ExcelIgnoreUnannotated 用于标识未被 @ExcelProperty 注解标识的字段是否被忽略。
@ColumnWidth 用于设置 Excel 列的宽度。

这些注解可以根据实际需求进行组合使用,以便在 Excel 读写过程中更灵活地控制字段的行为和样式。

SpringBoot整合EaxyExcel

1.引入依赖

xml 复制代码
<!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>easyexcel</artifactId>
			<version>3.1.3</version>
		</dependency>

2.实体类定义

使用@ExcelProperty 注解标记需要在 Excel 中读写的字段,可以指定字段在 Excel 中的列索引或列名。

实体类代码示例

java 复制代码
package com.zjl.model;

import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.zjl.coverter.BillIsPaymentConverter;
import lombok.Data;

/**
 * @author: zjl
 * @datetime: 2024/3/29
 * @desc: 订单实体类
 */
@Data
public class BillDO {
    @ExcelIgnore
    private Long id;
    @ExcelProperty(index = 0, value = "订单编码")   // 列索引为1
    @ColumnWidth(15)
    private String billCode;//订单编码
    @ExcelProperty(index = 1,value = "商品名称")   // 列索引为2
    @ColumnWidth(30)
    private String productName;//商品名称
    @ExcelProperty(index = 2,value = "商品描述")   // 列索引为3
    @ColumnWidth(30)
    private String productDesc;//商品描述
    @ExcelProperty(index = 3,value = "商品单位")   // 列索引为4
    @ColumnWidth(15)
    private String productUnit;//商品单位
    @ExcelProperty(index = 4,value = "商品数量")   // 列索引为5
    @ColumnWidth(15)
    private String productCount;//商品数量
    @ExcelProperty(index = 5,value = "商品总金额")   // 列索引为6
    @ColumnWidth(15)
    private String totalPrice;//商品总金额
    @ExcelProperty(index = 6,value = "支付状态",converter = BillIsPaymentConverter.class)   // 列索引为7
    @ColumnWidth(15)
    private int isPayment;//支付状态:是否支付(1:未支付 2:已支付)
    @ExcelProperty(index = 7,value = "创建人")   // 列索引为8
    @ColumnWidth(15)
    private String createdBy;//创建人
    @ExcelProperty(index = 8,value = "创建时间")   // 列索引为9
    @ColumnWidth(25)
    @DateTimeFormat("yyyy-MM-dd")
    private String creationDate;//创建时间
    @ExcelProperty(index = 9,value = "供应商名称")   // 列索引为10
    @ColumnWidth(30)
    private String providerName;//供应商名称
}
java 复制代码
package com.zjl.model;

import com.alibaba.excel.annotation.ExcelProperty;
import com.zjl.coverter.BillIsPaymentConverter;
import lombok.Data;

/**
 * @author: zjl
 * @datetime: 2024/3/30
 * @desc:
 */
@Data
public class BillImportBO {
    @ExcelProperty(index = 0, value = "订单编码")   // 列索引为1
    private String billCode;//订单编码
    @ExcelProperty(index = 1,value = "商品名称")   // 列索引为2
    private String productName;//商品名称
    @ExcelProperty(index = 2,value = "商品描述")   // 列索引为3
    private String productDesc;//商品描述
    @ExcelProperty(index = 3,value = "商品单位")   // 列索引为4
    private String productUnit;//商品单位
    @ExcelProperty(index = 4,value = "商品数量")   // 列索引为5
    private String productCount;//商品数量
    @ExcelProperty(index = 5,value = "商品总金额")   // 列索引为6
    private String totalPrice;//商品总金额
    @ExcelProperty(index = 6,value = "支付状态",converter = BillIsPaymentConverter.class)   // 列索引为7
    private int isPayment;//支付状态:是否支付(1:未支付 2:已支付)
    private String creationDate;//创建时间
    @ExcelProperty(index = 7,value = "供应商ID")   // 列索引为10
    private String providerId;//供应商名称
}

注解解释

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

3.自定义转换器

在EasyExcel中,如果想实现枚举类型到字符串类型转换(例如isPayment属性:1 -> 未支付,2 -> 已支付),需实现Converter接口来自定义转换器,下面为自定义BillIsPaymentConverter支付状态转换器代码实现:

转换器代码示例

java 复制代码
package com.zjl.coverter;

import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.converters.ReadConverterContext;
import com.alibaba.excel.converters.WriteConverterContext;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.zjl.enums.IsPaymentEnum;

/**
 * @author: zjl
 * @datetime: 2024/3/29
 * @desc:
 */
public class BillIsPaymentConverter implements Converter<Integer> {
    @Override
    public Class<?> supportJavaTypeKey() {
        return Integer.class;
    }

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

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

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

涉及的枚举类型

java 复制代码
package com.zjl.enums;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;

import java.util.stream.Stream;

/**
 * @author: zjl
 * @datetime: 2024/3/30
 * @desc:
 */
@Getter
@AllArgsConstructor
public enum IsPaymentEnum {
    UNKNOWN(0,"其他"),
    ISNOTPAY(1, "未支付"),
    ISPAY(2, "已支付");

    private final Integer value;

    @JsonFormat
    private final String description;

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

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

4.Excel工具类

设置相应结果

java 复制代码
package com.zjl.utils;

import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

/**
 * @author: zjl
 * @datetime: 2024/3/30
 * @desc:
 */
public class ExcelUtil {

    /**
     * 设置响应结果
     */
    public static 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");
    }

}

5.简单导出

接口

java 复制代码
package com.zjl.controller;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.zjl.model.BillDO;
import com.zjl.service.BillService;
import com.zjl.utils.ExcelUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

/**
 * @author: zjl
 * @datetime: 2024/3/30
 * @desc:
 */
@RestController
@RequestMapping("bill")
public class BillController {
    @Resource
    private BillService billService;
    /**
     * 导出
     * @param response
     */
    @GetMapping("/export")
    public void exportExcel(HttpServletResponse response) {
        try {
            ExcelUtil.setExcelResponseProp(response, "订单列表");
            List<BillDO> billList = billService.getBillList();
            EasyExcel.write(response.getOutputStream())
                    .head(BillDO.class)
                    .excelType(ExcelTypeEnum.XLSX)
                    .sheet("订单列表")
                    .doWrite(billList);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

...省略service、mapper方法定义和实现,没有复杂业务,就返回一个数据集

SQL

xml 复制代码
<select id="selectBillList" resultType="com.zjl.model.BillDO">
        select
            billCode,
            productName,
            productDesc,
            productUnit,
            productCount,
            totalPrice,
            isPayment,
            userName as createdBy,
            b.creationDate,
            proName as providerName
        from smbms_bill b join smbms_provider p
        on b.providerId = p.id
        join smbms_user u on b.createdBy = u.id
    </select>

6.简单导入

接口

java 复制代码
@PostMapping("/import")
    public String importExcel(@RequestPart(value = "file") MultipartFile file) {
        try {
            List<BillImportBO> billImportBOList = EasyExcel.read(file.getInputStream())
                    .head(BillImportBO.class)
                    .sheet()
                    .doReadSync();
            return billService.saveBillBath(billImportBOList);
        } catch (IOException e) {
            return "导入失败!";
        }
    }

省略service层

SQL

xml 复制代码
<insert id="insertBillBatch" parameterType="com.zjl.model.BillImportBO">
        INSERT INTO smbms_bill
            (billCode,productName, productDesc, productUnit,
             productCount,totalPrice, isPayment, createdBy,
             creationDate, providerId)
        VALUES
        <foreach collection="list" item="bill" separator=",">
            (#{bill.billCode},#{bill.productName},#{bill.productDesc},#{bill.productUnit},
             #{bill.productCount},#{bill.totalPrice},#{bill.isPayment},1,
             now(),#{bill.providerId})
        </foreach>
    </insert>

7.复杂的导出(合并行、合并列)

由于 EasyPoi 支持嵌套对象导出,直接使用内置 @ExcelCollection 注解即可实现,遗憾的是 EasyExcel 不支持一对多导出,只能自行实现,通过此issues了解到,项目维护者建议通过自定义合并策略方式来实现一对多导出。

解决思路

只需把订单主键相同的列中需要合并的列给合并了,就可以实现这种一对多嵌套信息的导出

自定义注解

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

java 复制代码
package com.zjl.annotate;

import java.lang.annotation.*;

/**
 * @author: zjl
 * @datetime: 2024/3/30
 * @desc:
 */
@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 值为数组形式来实现该效果:

java 复制代码
package com.zjl.model;

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.zjl.annotate.ExcelMerge;
import com.zjl.coverter.BillIsPaymentConverter;
import lombok.Data;

/**
 * @author: zjl
 * @datetime: 2024/3/30
 * @desc:
 */
@Data
public class ProviderDO {
    @ExcelProperty(value = "供应商ID")
    @ExcelMerge(merge = true, isPrimaryKey = true)
    @ColumnWidth(15)
    private String id;

    @ExcelProperty(value = "供应商编码")
    @ExcelMerge(merge = true)
    @ColumnWidth(15)
    private String proCode;
    @ExcelProperty(value = "供应商名称")
    @ExcelMerge(merge = true)
    @ColumnWidth(30)
    private String proName;//供应商名称
    @ExcelProperty(value = "合作联系人")
    @ExcelMerge(merge = true)
    @ColumnWidth(20)
    private String proContact;
    @ExcelProperty(value = "联系电话")
    @ExcelMerge(merge = true)
    @ColumnWidth(25)
    private String proPhone;
    @ExcelProperty(value = "供应商地址")
    @ExcelMerge(merge = true)
    @ColumnWidth(30)
    private String proAddress;
    @ExcelProperty(value = "供应商座机")
    @ExcelMerge(merge = true)
    @ColumnWidth(20)
    private String proFax;


    @ExcelProperty(value = {"订单信息","订单编码"})
    @ColumnWidth(15)
    private String billCode;//订单编码
    @ExcelProperty(value = {"订单信息","商品名称"})
    @ColumnWidth(30)
    private String productName;//商品名称
    @ExcelProperty(value = {"订单信息","商品描述"})
    @ColumnWidth(30)
    private String productDesc;//商品描述
    @ExcelProperty(value = {"订单信息","商品单位"})
    @ColumnWidth(15)
    private String productUnit;//商品单位
    @ExcelProperty(value = {"订单信息","商品数量"})
    @ColumnWidth(15)
    private String productCount;//商品数量
    @ExcelProperty(value = {"订单信息","商品总金额"})
    @ColumnWidth(15)
    private String totalPrice;//商品总金额
    @ExcelProperty(value = {"订单信息","支付状态"},converter = BillIsPaymentConverter.class)   // 列索引为7
    @ColumnWidth(15)
    private int isPayment;//支付状态:是否支付(1:未支付 2:已支付)
    @ExcelProperty(value = {"订单信息","创建人"})
    @ColumnWidth(15)
    private String createdBy;//创建人
    @ExcelProperty(value = {"订单信息","创建时间"})
    @ColumnWidth(25)
    @DateTimeFormat("yyyy-MM-dd")
    private String creationDate;//创建时间
}

SQL

xml 复制代码
<select id="selectProviderListAndBill" resultType="com.zjl.model.ProviderDO">
        select
            p.id,
            billCode,
            productName,
            productDesc,
            productUnit,
            productCount,
            totalPrice,
            isPayment,
            userName as createdBy,
            b.creationDate,
            proName,
            proCode,
            proContact,
            proPhone,
            proAddress,
            proFax
        from smbms_bill b join smbms_provider p
        on b.providerId = p.id
        join smbms_user u on b.createdBy = u.id
    </select>

自定义单元格合并策略

java 复制代码
package com.zjl.utils;

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.write.handler.RowWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import com.zjl.annotate.ExcelMerge;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

/**
 * @author: zjl
 * @datetime: 2024/3/30
 * @desc:
 */
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注解必须指定主键");
        }
    }
}

接口

java 复制代码
@GetMapping("/mergeExport")
    public void mergeExport(HttpServletResponse response) {
        try {
            ExcelUtil.setExcelResponseProp(response, "供应商列表-合并");
            List<ProviderDO> providerList = providerService.getProviderList();
            EasyExcel.write(response.getOutputStream())
                    .head(ProviderDO.class)
                    .registerWriteHandler(new ExcelMergeStrategy(ProviderDO.class))
                    .excelType(ExcelTypeEnum.XLSX)
                    .sheet("供应商列表")
                    .doWrite(providerList);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
相关推荐
码农周21 分钟前
告别大体积PDF!基于PDFBox的Java压缩工具
java·spring boot
devilnumber31 分钟前
java中Redisson ,jedis,Lettuce和Spring Data Redis的四种深度对比和优缺点详解
java·redis·spring
摇滚侠32 分钟前
Java 进阶教程,全面剖析 Java 多线程编程
java·开发语言
yaaakaaang32 分钟前
十四、命令模式
java·命令模式
大佬,救命!!!35 分钟前
etp中未运行用例顺序的定位及补齐脚本自动化生成
python·学习笔记·excel·自动化脚本·用例整理清洗
小锋java12341 小时前
【技术专题】Matplotlib3 Python 数据可视化 - Matplotlib3 绘制饼状图(Pie)
java
wuminyu1 小时前
专家视角看JVM_StartThread
java·linux·c语言·jvm·c++
专职1 小时前
cursor中与vim插件冲突时的配置
编辑器·vim·excel
吕永强1 小时前
基于SpringBoot+Vue小区报修系统的设计与实现(源码+论文+部署)
spring boot·毕业设计·毕业论文·报修系统·小区报修