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

embedded/2024/10/17 13:18:18/

介绍

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

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

快速开始

引入依赖

com.alibaba easyexcel 3.1.3

简单导出

以导出用户信息为例,接下来手把手教大家如何使用EasyExcel实现导出功能!

定义实体类

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

 * 用户信息** @author william@StarImmortal*/
@Data
public class UserDO {@ExcelProperty("用户编号")@ColumnWidth(20)private Long id;@ExcelProperty("用户名")@ColumnWidth(20)private String username;@ExcelIgnoreprivate 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:用于设置数字转换格式。

自定义转换器

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


/*** Excel 性别转换器** @author william@StarImmortal*/
public class GenderConverter implements Converter<Integer> {@Overridepublic Class<?> supportJavaTypeKey() {return Integer.class;}@Overridepublic CellDataTypeEnum supportExcelTypeKey() {return CellDataTypeEnum.STRING;}@Overridepublic Integer convertToJavaData(ReadConverterContext<?> context) {return GenderEnum.convert(context.getReadCellData().getStringValue()).getValue();}@Overridepublic WriteCellData<?> convertToExcelData(WriteConverterContext<Integer> context) {return new WriteCellData<>(GenderEnum.convert(context.getValue()).getDescription());}
}
/*** 性别枚举** @author william@StarImmortal*/
@Getter
@AllArgsConstructor
public enum GenderEnum {/*** 未知*/UNKNOWN(0, "未知"),/*** 男性*/MALE(1, "男性"),/*** 女性*/FEMALE(2, "女性");private final Integer value;@JsonFormatprivate 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);}
}

/*** EasyExcel导入导出** @author william@StarImmortal*/
@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);}}/*** 设置响应结果** @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");response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");}/*** 读取用户列表数据** @return 用户列表数据* @throws IOException IO异常*/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>>() {});}
}

测试接口

运行项目,通过 Postman 或者 Apifox 工具来进行接口测试

注意:在 Apifox 中访问接口后无法直接下载,需要点击返回结果中的下载图标才行,点击之后方可对Excel文件进行保存。

接口地址:http://localhost:8080/excel/export/user
在这里插入图片描述
在这里插入图片描述
复杂导出

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

自定义注解

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

/*** 用于判断是否需要合并以及合并的主键** @author william@StarImmortal*/
@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 值为数组形式来实现该效果:

/*** @author william@StarImmortal*/
@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;
}

数据映射与平铺

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

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


/*** 自定义单元格合并策略** @author william@StarImmortal*/
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;}@Overridepublic 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注解必须指定主键");}}
}

定义接口

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

/*** EasyExcel导入导出** @author william@StarImmortal*/
@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);}}/*** 设置响应结果** @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");response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");}
}

测试接口

运行项目,通过 Postman 或者 Apifox 工具来进行接口测试

注意:在 Apifox 中访问接口后无法直接下载,需要点击返回结果中的下载图标才行,点击之后方可对Excel文件进行保存。

接口地址:http://localhost:8080/excel/export/order
在这里插入图片描述
在这里插入图片描述
简单导入

以导入用户信息为例,接下来手把手教大家如何使用EasyExcel实现导入功能!

/*** EasyExcel导入导出** @author william@StarImmortal*/
@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();}}
}

在这里插入图片描述


http://www.ppmy.cn/embedded/4055.html

相关文章

PySpark预计算ClickHouse Bitmap实践

1. 背景 ClickHouse全称是Click Stream&#xff0c;Data WareHouse&#xff0c;是一款高性能的OLAP数据库&#xff0c;既使用了ROLAP模型&#xff0c;又拥有着比肩MOLAP的性能。我们可以用ClickHouse用来做分析平台快速出数。其中的bitmap结构方便我们对人群进行交并。Bitmap位…

Stable Diffusion 模型分享:MeinaMix(动漫)meinamix_meinaV11

本文收录于《AI绘画从入门到精通》专栏&#xff0c;专栏总目录&#xff1a;点这里&#xff0c;订阅后可阅读专栏内所有文章。 文章目录 模型介绍生成案例案例一案例二案例三案例四案例五案例六案例七案例八 下载地址 模型介绍 MeinaMix 的目标是&#xff1a;能够在很少的提示下…

vue3使用阿里oss上传资源(上传图片、视频、文件、pdf等等),删除oss资源。获取STS token的接口

vue3使用阿里oss上传资源 全部oss.ts代码如下&#xff1a; import OSS from "ali-oss";// 获取STS token export const getSTSToken async () > {const STS_TOKEN_URL "....."; // 获取STS token的接口&#xff0c;后端提供// fetch方式可按需更换成…

2024 极术通讯-Arm 推出新一代 Ethos-U AI 加速器及全新物联网参考设计平台

导读&#xff1a;极术社区推出极术通讯&#xff0c;引入行业媒体和技术社区、咨询机构优质内容&#xff0c;定期分享产业技术趋势与市场应用热点。 芯方向 Neoverse S3 系统 IP 为打造机密计算和多芯粒基础设施 SoC 夯实根基 Arm Neoverse S3 作为针对基础设施的第三代系统 I…

[蓝桥杯 2018 省 A] 航班时间

题目链接&#xff1a;航班时间 显然&#xff1a;去程时间飞行时间时差&#xff0c;回程时间飞行时间-时差 列方程组可知&#xff1a;飞行时间&#xff08;去程时间回程时间&#xff09;/2 本道题目还有一个难点在于如何读入和输出&#xff1a;可以采用scanf&#xff08;&…

记一次 Java 应用内存泄漏的定位过程

问题现象 最近&#xff0c;笔者负责测试的某个算法模块机器出现大量报警&#xff0c;报警表现为机器CPU持续高占用。该算法模块是一个优化算法&#xff0c;本身就是CPU密集型应用&#xff0c;一开始怀疑可能是算法在正常运算&#xff0c;但很快这种猜测就被推翻&#xff1a;同…

fatal: Out of memory, malloc failed

git 切分支&#xff0c;或者clone仓库的时候碰到这个错误&#xff0c;看网上很多配置config文件&#xff0c;都尝试了没效果。 自测了一个可行的方式&#xff1a; 由于本人用的sourcetree 所以解决方式如下&#xff1a; git升级为最新版&#xff0c;选择系统GIt版本&#xf…

项目7-音乐播放器3(删除模块+播放音乐模块设计)

1.播放音乐模块设计 1.1 请求响应设计 请求&#xff1a; { get, /music/get?pathxxx.mp3 } 响应&#xff1a; { 音乐数据本身的字节信息 } 1.2 后端代码 1. Files.readAllBytes(String path) : 读取文件中的所有字节&#xff0c;读入内存 &#xff…