目录
- 背景
- 需求
- 发展
- EasyExcel
- 官网
- 介绍
- 优势
- 常用注解
- SpringBoot整合EaxyExcel
- 1.引入依赖
- 2.实体类定义
- 实体类代码示例
- 注解解释
- 3.自定义转换器
- 转换器代码示例
- 涉及的枚举类型
- 4.Excel工具类
- 5.简单导出
- 接口
- SQL
- 6.简单导入
- 接口
- SQL
- 7.复杂的导出(合并行、合并列)
- 解决思路
- 自定义注解
- 定义实体类
- SQL
- 自定义单元格合并策略
- 接口
背景
需求
在当今信息化社会,数据的导入和导出在各种业务场景中变得越来越重要。
发展
- 我们知道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 更轻松地实现数据导入导出功能,提高开发效率和用户体验。
- 简单易用: EasyExcel 提供了简洁的 API 接口,让开发人员能够快速上手。无论是初学者还是有经验的开发者,都能轻松地实现 Excel 文件的导入导出功能。
- 支持多种数据格式: EasyExcel 支持导入导出多种数据格式,包括基本的文本、数字、日期等,以及复杂的对象、集合、嵌套结构等数据类型。
- 高性能: EasyExcel 在处理大数据量时表现出色,采用了基于流的方式,有效地降低了内存消耗,提升了性能和效率。
- 自定义样式: 开发人员可以灵活地自定义单元格样式,包括字体、颜色、对齐方式等,使导出的 Excel 数据更加美观和易读。
- 数据转换: EasyExcel 支持自定义数据转换器,可以将原始数据转换为目标格式,满足业务需求。
- 异常处理: EasyExcel 提供了丰富的异常处理机制,能够捕获和处理导入导出过程中的异常情况,保障数据的完整性和一致性。
- 多平台支持: EasyExcel 可以在各种 Java 开发环境中使用,包括传统的 Java 应用程序、Web 应用程序,甚至是移动应用开发中。
- 开源社区: EasyExcel 是一个开源项目,拥有活跃的社区支持,开发人员可以从社区中获取帮助、贡献代码以及分享经验
常用注解
注解 | 作用 |
---|---|
@ExcelProperty | 用于标识Excel中的字段,可以指定字段在Excel中的列索引或列名。例如:@ExcelProperty(“姓名”),@ExcelProperty(index = 0)。 |
@ExcelIgnore | 用于标识不需要导入或导出的字段。 |
@ExcelIgnoreUnannotated | 用于标识未被 @ExcelProperty 注解标识的字段是否被忽略。 |
@ColumnWidth | 用于设置 Excel 列的宽度。 |
这些注解可以根据实际需求进行组合使用,以便在 Excel 读写过程中更灵活地控制字段的行为和样式。
SpringBoot整合EaxyExcel
1.引入依赖
<!-- 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 中的列索引或列名。
实体类代码示例
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;//供应商名称
}
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支付状态转换器代码实现:
转换器代码示例
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());
}
}
涉及的枚举类型
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工具类
设置相应结果
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.简单导出
接口
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
<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.简单导入
接口
@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
<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了解到,项目维护者建议通过自定义合并策略方式来实现一对多导出。
解决思路
只需把订单主键相同的列中需要合并的列给合并了,就可以实现这种一对多嵌套信息的导出
自定义注解
创建一个自定义注解,用于标记哪些属性需要合并单元格,哪个属性是主键,用于判断是否需要合并以及合并的主键
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 值为数组形式来实现该效果:
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
<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>
自定义单元格合并策略
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注解必须指定主键");
}
}
}
接口
@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);
}
}