0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

一款解决大文件内存溢出的 Excel 处理工具

jf_ro2CN3Fa 来源:芋道源码 2023-07-03 16:11 次阅读

介绍

快速开始

引入依赖

简单导出

定义实体类

复杂导出

简单导入

参考资料

介绍

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

EasyExcel文档地址:

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

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

项目地址:https://github.com/YunaiV/ruoyi-vue-pro

视频教程:https://doc.iocoder.cn/video/

快速开始

引入依赖


com.alibaba
easyexcel
3.1.3

简单导出

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

定义实体类

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

/**
*用户信息
*
*@authorwilliam@StarImmortal
*/
@Data
publicclassUserDO{
@ExcelProperty("用户编号")
@ColumnWidth(20)
privateLongid;

@ExcelProperty("用户名")
@ColumnWidth(20)
privateStringusername;

@ExcelIgnore
privateStringpassword;

@ExcelProperty("昵称")
@ColumnWidth(20)
privateStringnickname;

@ExcelProperty("生日")
@ColumnWidth(20)
@DateTimeFormat("yyyy-MM-dd")
privateDatebirthday;

@ExcelProperty("手机号")
@ColumnWidth(20)
privateStringphone;

@ExcelProperty("身高(米)")
@NumberFormat("#.##")
@ColumnWidth(20)
privateDoubleheight;

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

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

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

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

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

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

自定义转换器

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

/**
*Excel性别转换器
*
*@authorwilliam@StarImmortal
*/
publicclassGenderConverterimplementsConverter{
@Override
publicClasssupportJavaTypeKey(){
returnInteger.class;
}

@Override
publicCellDataTypeEnumsupportExcelTypeKey(){
returnCellDataTypeEnum.STRING;
}

@Override
publicIntegerconvertToJavaData(ReadConverterContextcontext){
returnGenderEnum.convert(context.getReadCellData().getStringValue()).getValue();
}

@Override
publicWriteCellDataconvertToExcelData(WriteConverterContextcontext){
returnnewWriteCellData<>(GenderEnum.convert(context.getValue()).getDescription());
}
}
/**
*性别枚举
*
*@authorwilliam@StarImmortal
*/
@Getter
@AllArgsConstructor
publicenumGenderEnum{

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

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

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

privatefinalIntegervalue;

@JsonFormat
privatefinalStringdescription;

publicstaticGenderEnumconvert(Integervalue){
returnStream.of(values())
.filter(bean->bean.value.equals(value))
.findAny()
.orElse(UNKNOWN);
}

publicstaticGenderEnumconvert(Stringdescription){
returnStream.of(values())
.filter(bean->bean.description.equals(description))
.findAny()
.orElse(UNKNOWN);
}
}

定义接口

/**
*EasyExcel导入导出
*
*@authorwilliam@StarImmortal
*/
@RestController
@RequestMapping("/excel")
publicclassExcelController{

@GetMapping("/export/user")
publicvoidexportUserExcel(HttpServletResponseresponse){
try{
this.setExcelResponseProp(response,"用户列表");
ListuserList=this.getUserList();
EasyExcel.write(response.getOutputStream())
.head(UserDO.class)
.excelType(ExcelTypeEnum.XLSX)
.sheet("用户列表")
.doWrite(userList);
}catch(IOExceptione){
thrownewRuntimeException(e);
}
}

/**
*设置响应结果
*
*@paramresponse响应结果对象
*@paramrawFileName文件名
*@throwsUnsupportedEncodingException不支持编码异常
*/
privatevoidsetExcelResponseProp(HttpServletResponseresponse,StringrawFileName)throwsUnsupportedEncodingException{
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
StringfileName=URLEncoder.encode(rawFileName,"UTF-8").replaceAll("+","%20");
response.setHeader("Content-disposition","attachment;filename*=utf-8''"+fileName+".xlsx");
}

/**
*读取用户列表数据
*
*@return用户列表数据
*@throwsIOExceptionIO异常
*/
privateListgetUserList()throwsIOException{
ObjectMapperobjectMapper=newObjectMapper();
ClassPathResourceclassPathResource=newClassPathResource("mock/users.json");
InputStreaminputStream=classPathResource.getInputStream();
returnobjectMapper.readValue(inputStream,newTypeReference>(){
});
}
}

测试接口

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

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

接口地址:http://localhost:8080/excel/export/user

d9d6b51c-17b7-11ee-962d-dac502259ad0.pngda05fffc-17b7-11ee-962d-dac502259ad0.png

复杂导出

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

da25636a-17b7-11ee-962d-dac502259ad0.png

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

自定义注解

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

/**
*用于判断是否需要合并以及合并的主键
*
*@authorwilliam@StarImmortal
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public@interfaceExcelMerge{
/**
*是否合并单元格
*
*@returntrue||false
*/
booleanmerge()defaulttrue;

/**
*是否为主键(即该字段相同的行合并)
*
*@returntrue||false
*/
booleanisPrimaryKey()defaultfalse;
}

定义实体类

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

/**
*@authorwilliam@StarImmortal
*/
@Data
publicclassOrderBO{
@ExcelProperty(value="订单主键")
@ColumnWidth(16)
@ExcelMerge(merge=true,isPrimaryKey=true)
privateStringid;

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

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

@ExcelProperty(value="创建时间")
@ColumnWidth(20)
@DateTimeFormat("yyyy-MM-ddHHss")
@ExcelMerge(merge=true)
privateDatecreateTime;

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

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

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

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

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

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

数据映射与平铺

导出之前,需要对数据进行处理,将订单数据进行平铺,orderList为平铺前格式,exportData为平铺后格式:

da5a96a2-17b7-11ee-962d-dac502259ad0.png

自定义单元格合并策略

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

/**
*自定义单元格合并策略
*
*@authorwilliam@StarImmortal
*/
publicclassExcelMergeStrategyimplementsRowWriteHandler{

/**
*主键下标
*/
privateIntegerprimaryKeyIndex;

/**
*需要合并的列的下标集合
*/
privatefinalListmergeColumnIndexList=newArrayList<>();

/**
*数据类型
*/
privatefinalClasselementType;

publicExcelMergeStrategy(ClasselementType){
this.elementType=elementType;
}

@Override
publicvoidafterRowDispose(WriteSheetHolderwriteSheetHolder,WriteTableHolderwriteTableHolder,Rowrow,IntegerrelativeRowIndex,BooleanisHead){
//判断是否为标题
if(isHead){
return;
}
//获取当前工作表
Sheetsheet=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导入导出
*
*@authorwilliam@StarImmortal
*/
@RestController
@RequestMapping("/excel")
publicclassExcelController{

@GetMapping("/export/order")
publicvoidexportOrderExcel(HttpServletResponseresponse){
try{
this.setExcelResponseProp(response,"订单列表");
ListorderList=this.getOrderList();
ListexportData=this.convert(orderList);
EasyExcel.write(response.getOutputStream())
.head(OrderBO.class)
.registerWriteHandler(newExcelMergeStrategy(OrderBO.class))
.excelType(ExcelTypeEnum.XLSX)
.sheet("订单列表")
.doWrite(exportData);
}catch(IOExceptione){
thrownewRuntimeException(e);
}
}

/**
*设置响应结果
*
*@paramresponse响应结果对象
*@paramrawFileName文件名
*@throwsUnsupportedEncodingException不支持编码异常
*/
privatevoidsetExcelResponseProp(HttpServletResponseresponse,StringrawFileName)throwsUnsupportedEncodingException{
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
StringfileName=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

da8b135e-17b7-11ee-962d-dac502259ad0.pngdac00fe6-17b7-11ee-962d-dac502259ad0.png

简单导入

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

/**
*EasyExcel导入导出
*
*@authorwilliam@StarImmortal
*/
@RestController
@RequestMapping("/excel")
@Api(tags="EasyExcel")
publicclassExcelController{

@PostMapping("/import/user")
publicResponseVOimportUserExcel(@RequestPart(value="file")MultipartFilefile){
try{
ListuserList=EasyExcel.read(file.getInputStream())
.head(UserDO.class)
.sheet()
.doReadSync();
returnResponseVO.success(userList);
}catch(IOExceptione){
returnResponseVO.error();
}
}
}
dafb5a4c-17b7-11ee-962d-dac502259ad0.png

责任编辑:彭菁

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 内存
    +关注

    关注

    8

    文章

    2767

    浏览量

    72777
  • 文件
    +关注

    关注

    1

    文章

    540

    浏览量

    24402
  • Excel
    +关注

    关注

    4

    文章

    212

    浏览量

    55185

原文标题:SpringBoot 集成 EasyExcel 3.x 优雅实现 Excel 导入导出

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    Linux平台大文件生成和处理方法

    在日常工作中,为了验证某些场景下的功能,经常需要人为构造一些大文件进行测试,有时需要用大文件来测试下载速度,有时需要用大文件来覆盖磁盘空间;偶尔会看到一些网络博文会教大家如何构造大文件
    发表于 07-14 16:38 3454次阅读

    文件自我销毁------一款值得收藏备用的小工具

    文件自我销毁------一款值得收藏备用的小工具  
    发表于 10-15 12:23

    ReqMan需求提取和协同处理工具怎么样看了就知道

    ReqMan是由德国engineering method AG公司开发的一款高效的、可自由定制的需求提取和协同处理工具。ReqMan 能够将PDF、Word、Excel等格式的文档提取出来并将需求条目化,同时提供了多种文档格式之
    发表于 03-08 07:52

    介绍一款苹果操作系统的电源管理工具

    Power Manager for Mac是苹果操作系统上的一款笔记本电源管理工具,该工具支持苹果系列的笔记本,可以有效地优化苹果系统,结束不必要的系统任务,同时还可以提高笔记本电池的使用量
    发表于 01-03 07:42

    基于PHP大文件上传的研究和设计

    基于PHP大文件上传的研究和设计,感兴趣的可以看看。
    发表于 02-22 18:15 6次下载

    迷你图片处理工具

    图片处理工具,可以修改图片编码格式,能够处理JAVA不能识别的图片。
    发表于 03-24 14:52 4次下载

    TXT大文件切割软体应用程序免费下载

    本文档的主要内容详细是TXT大文件切割软体应用程序免费下载,可以切割任意大小的txt文件,可以根据大小,数量,标题等类别进行切割,绝对是大文件切割的必备工具,该软体绿色免安装,谁用谁知
    发表于 11-07 08:00 5次下载
    TXT<b class='flag-5'>大文件</b>切割软体应用程序免费下载

    JAVA中NIO通过MappedByteBuffer操作大文件

    java io操作中通常采用BufferedReader,BufferedInputStream等带缓冲的IO类处理大文件,不过java nio中引入了一种基于MappedByteBuffer操作大文件的方式,其读写性能极高,本
    的头像 发表于 05-05 23:42 3254次阅读

    内存溢出内存泄露的区别_内存溢出的原因以及解决方法

    内存溢出内存泄露的区别是什么?内存溢出怎么解决?内存溢出
    发表于 06-01 10:27 2736次阅读

    EXCEL大文件Vlookup工具”使用步骤资料下载

    电子发烧友网为你提供EXCEL大文件Vlookup工具”使用步骤资料下载的电子资料下载,更有其他相关的电路图、源代码、课件教程、中文资料、英文资料、参考设计、用户指南、解决方案等资料,希望可以帮助到广大的电子工程师们。
    发表于 04-28 08:55 16次下载
    <b class='flag-5'>EXCEL</b>“<b class='flag-5'>大文件</b>Vlookup<b class='flag-5'>工具</b>”使用步骤资料下载

    如何通过python轻松处理大文件

    众所周知,python除了以简洁著称,其成熟的第三方库功能也是很强大的,今天浩道带大家看看如何通过python轻松处理大文件,真让人直呼yyds 。
    的头像 发表于 04-27 10:54 503次阅读

    java内存溢出排查方法

    Java内存溢出(Memory overflow)是指Java虚拟机(JVM)中的堆内存无法满足对象分配的需求,导致程序抛出OutOfMemoryError异常。内存
    的头像 发表于 11-23 14:46 702次阅读

    jvm内存溢出故障排查

    JVM内存溢出是常见且令人头疼的问题,特别是在运行大型Java应用程序或长时间运行的应用程序时。当JVM分配给应用程序的内存不足以处理应用程序所需的数据时,就会发生
    的头像 发表于 12-05 11:04 356次阅读

    jvm内存溢出该如何定位解决

    超出限制和堆空间不足。 定位JVM内存溢出问题是一个比较复杂的任务,需要结合工具和技术来进行分析和解决。本文将介绍一些常用的调试和解决内存溢出
    的头像 发表于 12-05 11:05 632次阅读

    内存溢出内存泄漏:定义、区别与解决方案

    内存溢出内存泄漏:定义、区别与解决方案  内存溢出内存泄漏是计算机科学中常见的问题,在开发和
    的头像 发表于 12-19 14:10 1276次阅读