Spring Boot 一个接口实现任意表的 Excel 导入导出

Java的web开发需要excel的导入导出工具,所以需要一定的工具类实现,如果是使用easypoi、Hutool导入导出excel,会非常的损耗内存,因此可以尝试使用easyexcel解决大数据量的数据的导入导出,且可以通过Java8的函数式编程解决该问题。

使用easyexcel,虽然不太会出现OOM的问题,但是如果是大数据量的情况下也会有一定量的内存溢出的风险,所以我打算从以下几个方面优化这个问题:

  • 使用Java8的函数式编程实现低代码量的数据导入

  • 使用反射等特性实现单个接口导入任意excel

  • 使用线程池实现大数据量的excel导入

  • 通过泛型实现数据导出

maven导入

<!--EasyExcel相关依赖-->
<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.0.5</version>
</dependency>

使用泛型实现对象的单个Sheet导入

先实现一个类,用来指代导入的特定的对象

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("stu_info")
@ApiModel("学生信息")
//@ExcelIgnoreUnannotated 没有注解的字段都不转换
publicclass StuInfo {privatestaticfinallong serialVersionUID = 1L;/*** 姓名*/// 设置字体,此处代表使用斜体
//    @ContentFontStyle(italic = BooleanEnum.TRUE)// 设置列宽度的注解,注解中只有一个参数value,value的单位是字符长度,最大可以设置255个字符@ColumnWidth(10)// @ExcelProperty 注解中有三个参数value,index,converter分别代表表名,列序号,数据转换方式@ApiModelProperty("姓名")@ExcelProperty(value = "姓名",order = 0)@ExportHeader(value = "姓名",index = 1)private String name;/*** 年龄*/
//    @ExcelIgnore不将该字段转换成Excel@ExcelProperty(value = "年龄",order = 1)@ApiModelProperty("年龄")@ExportHeader(value = "年龄",index = 2)private Integer age;/*** 身高*///自定义格式-位数
//    @NumberFormat("#.##%")@ExcelProperty(value = "身高",order = 2)@ApiModelProperty("身高")@ExportHeader(value = "身高",index = 4)private Double tall;/*** 自我介绍*/@ExcelProperty(value = "自我介绍",order = 3)@ApiModelProperty("自我介绍")@ExportHeader(value = "自我介绍",index = 3,ignore = true)private String selfIntroduce;/*** 图片信息*/@ExcelProperty(value = "图片信息",order = 4)@ApiModelProperty("图片信息")@ExportHeader(value = "图片信息",ignore = true)private Blob picture;/*** 性别*/@ExcelProperty(value = "性别",order = 5)@ApiModelProperty("性别")private Integer gender;/*** 入学时间*///自定义格式-时间格式@DateTimeFormat("yyyy-MM-dd HH:mm:ss:")@ExcelProperty(value = "入学时间",order = 6)@ApiModelProperty("入学时间")private String intake;/*** 出生日期*/@ExcelProperty(value = "出生日期",order = 7)@ApiModelProperty("出生日期")private String birthday;}

重写ReadListener接口

@Slf4j
publicclass UploadDataListener<T> implements ReadListener<T> {/*** 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收*/privatestaticfinalint BATCH_COUNT = 100;/*** 缓存的数据*/private List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);/*** Predicate用于过滤数据*/private Predicate<T> predicate;/*** 调用持久层批量保存*/private Consumer<Collection<T>> consumer;public UploadDataListener(Predicate<T> predicate, Consumer<Collection<T>> consumer) {this.predicate = predicate;this.consumer = consumer;}public UploadDataListener(Consumer<Collection<T>> consumer) {this.consumer = consumer;}/*** 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来** @param demoDAO*//*** 这个每一条数据解析都会来调用** @param data    one row value. Is is same as {@link AnalysisContext#readRowHolder()}* @param context*/@Overridepublic void invoke(T data, AnalysisContext context) {if (predicate != null && !predicate.test(data)) {return;}cachedDataList.add(data);// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOMif (cachedDataList.size() >= BATCH_COUNT) {try {// 执行具体消费逻辑consumer.accept(cachedDataList);} catch (Exception e) {log.error("Failed to upload data!data={}", cachedDataList);thrownew BizException("导入失败");}// 存储完成清理 listcachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);}}/*** 所有数据解析完成了 都会来调用** @param context*/@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {// 这里也要保存数据,确保最后遗留的数据也存储到数据库if (CollUtil.isNotEmpty(cachedDataList)) {try {// 执行具体消费逻辑consumer.accept(cachedDataList);log.info("所有数据解析完成!");} catch (Exception e) {log.error("Failed to upload data!data={}", cachedDataList);// 抛出自定义的提示信息if (e instanceof BizException) {throw e;}thrownew BizException("导入失败");}}}
}

Controller层的实现

@ApiOperation("只需要一个readListener,解决全部的问题")
@PostMapping("/update")
@ResponseBody
public R<String> aListener4AllExcel(MultipartFile file) throws IOException {try {EasyExcel.read(file.getInputStream(),StuInfo.class,new UploadDataListener<StuInfo>(list -> {// 校验数据ValidationUtils.validate(list);// dao 保存···//最好是手写一个,不要使用mybatis-plus的一条条新增的逻辑service.saveBatch(list);log.info("从Excel导入数据一共 {} 行 ", list.size());})).sheet().doRead();} catch (IOException e) {log.error("导入失败", e);thrownew BizException("导入失败");}return R.success("SUCCESS");
}

但是这种方式只能实现已存对象的功能实现,如果要新增一种数据的导入,那我们需要怎么做呢?关注公众号:码猿技术专栏,回复关键词:1111 获取阿里内部java性能调优手册!

可以通过读取成Map,根据顺序导入到数据库中。

通过实现单个Sheet中任意一种数据的导入

Controller层的实现

@ApiOperation("只需要一个readListener,解决全部的问题")
@PostMapping("/listenMapDara")
@ResponseBody
public R<String> listenMapDara(@ApiParam(value = "表编码", required = true)@NotBlank(message = "表编码不能为空")@RequestParam("tableCode") String tableCode,@ApiParam(value = "上传的文件", required = true)@NotNull(message = "上传文件不能为空") MultipartFile file) throws IOException {try {//根据tableCode获取这张表的字段,可以作为insert与剧中的信息EasyExcel.read(file.getInputStream(),new NonClazzOrientedListener(list -> {// 校验数据
//                                        ValidationUtils.validate(list);// dao 保存···log.info("从Excel导入数据一共 {} 行 ", list.size());})).sheet().doRead();} catch (IOException e) {log.error("导入失败", e);thrownew BizException("导入失败");}return R.success("SUCCESS");
}

重写ReadListener接口

@Slf4j
publicclass NonClazzOrientedListener implements ReadListener<Map<Integer, String>> {/*** 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收*/privatestaticfinalint BATCH_COUNT = 100;private List<List<Object>> rowsList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);private List<Object> rowList = new ArrayList<>();/*** Predicate用于过滤数据*/private Predicate<Map<Integer, String>> predicate;/*** 调用持久层批量保存*/private Consumer<List> consumer;public NonClazzOrientedListener(Predicate<Map<Integer, String>> predicate, Consumer<List> consumer) {this.predicate = predicate;this.consumer = consumer;}public NonClazzOrientedListener(Consumer<List> consumer) {this.consumer = consumer;}/*** 添加deviceName标识*/privateboolean flag = false;@Overridepublic void invoke(Map<Integer, String> row, AnalysisContext analysisContext) {consumer.accept(rowsList);rowList.clear();row.forEach((k, v) -> {log.debug("key is {},value is {}", k, v);rowList.add(v == null ? "" : v);});rowsList.add(rowList);if (rowsList.size() > BATCH_COUNT) {log.debug("执行存储程序");log.info("rowsList is {}", rowsList);rowsList.clear();}}@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {consumer.accept(rowsList);if (CollUtil.isNotEmpty(rowsList)) {try {log.debug("执行最后的程序");log.info("rowsList is {}", rowsList);} catch (Exception e) {log.error("Failed to upload data!data={}", rowsList);// 抛出自定义的提示信息if (e instanceof BizException) {throw e;}thrownew BizException("导入失败");} finally {rowsList.clear();}}}

这种方式可以通过把表中的字段顺序存储起来,通过配置数据和字段的位置实现数据的新增,那么如果出现了导出数据模板/手写excel的时候顺序和导入的时候顺序不一样怎么办?

可以通过读取header进行实现,通过表头读取到的字段,和数据库中表的字段进行比对,只取其中存在的数据进行排序添加

/*** 这里会一行行的返回头** @param headMap* @param context*/
@Override
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {//该方法必然会在读取数据之前进行Map<Integer, String> columMap = ConverterUtils.convertToStringMap(headMap, context);//通过数据交互拿到这个表的表头
//        Map<String,String> columnList=dao.xxxx();Map<String, String> columnList = new HashMap();columMap.forEach((key, value) -> {if (columnList.containsKey(value)) {filterList.add(key);}});//过滤到了只存在表里面的数据,顺序就不用担心了,可以直接把filterList的数据用于排序,可以根据mybatis做一个动态sql进行应用log.info("解析到一条头数据:{}", JSON.toJSONString(columMap));// 如果想转成成 Map<Integer,String>// 方案1: 不要implements ReadListener 而是 extends AnalysisEventListener// 方案2: 调用 ConverterUtils.convertToStringMap(headMap, context) 自动会转换
}

那么这些问题都解决了,如果出现大数据量的情况,如果要极大的使用到cpu,该怎么做呢?

可以尝试使用线程池进行实现

使用线程池进行多线程导入大量数据

Java中线程池的开发与使用与原理我可以单独写一篇文章进行讲解,但是在这边为了进行好的开发我先给出一套固定一点的方法。

由于ReadListener不能被注册到IOC容器里面,所以需要在外面开启

详情可见

https://juejin.cn/post/7251566038524133436

通过泛型实现对象类型的导出

public <T> void commonExport(String fileName, List<T> data, Class<T> clazz, HttpServletResponse response) throws IOException {if (CollectionUtil.isEmpty(data)) {data = new ArrayList<>();}//设置标题fileName = URLEncoder.encode(fileName, "UTF-8");response.setContentType("application/vnd.ms-excel");response.setCharacterEncoding("utf-8");response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");EasyExcel.write(response.getOutputStream()).head(clazz).sheet("sheet1").doWrite(data);
}

直接使用该方法可以作为公共的数据的导出接口

如果想要动态的下载任意一组数据怎么办呢?可以使用这个方法

public void exportFreely(String fileName, List<List<Object>> data, List<List<String>> head, HttpServletResponse response) throws IOException {if (CollectionUtil.isEmpty(data)) {data = new ArrayList<>();}//设置标题fileName = URLEncoder.encode(fileName, "UTF-8");response.setContentType("application/vnd.ms-excel");response.setCharacterEncoding("utf-8");response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");EasyExcel.write(response.getOutputStream()).head(head).sheet("sheet1").doWrite(data);}

什么?不仅想一个接口展示全部的数据与信息,还要增加筛选条件?这个后期可以单独解决这个问题。

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

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

相关文章

QT原子变量:QAtomicInteger、QAtomicPointer、QAtomicFlag

引言&#xff1a;原子变量为何重要&#xff1f; 在多线程编程中&#xff0c;共享数据的原子性访问是保证线程安全的核心。传统互斥锁虽然有效&#xff0c;但会带来性能损耗和死锁风险。QT提供的原子类型&#xff08;QAtomicInteger、QAtomicPointer、QAtomicFlag&#xff09;通…

大模型金融企业场景落地应用

一、商业银行体系 1. 江苏银行 企业背景&#xff1a;江苏银行是总部位于江苏南京的全国性股份制商业银行&#xff0c;在城商行中资产规模位居前列&#xff0c;积极拥抱金融科技&#xff0c;将数字化转型作为核心战略之一。近年来&#xff0c;江苏银行持续加大在人工智能、大数…

卡特兰数在数据结构上面的运用

原理 Catalan数是一个数列&#xff0c;其第n项表示n个不同结点可以构成的二叉排序树的数量。Catalan数的第n项公式为&#xff1a; &#xfffc; 其中&#xff0c;&#xfffc;是组合数&#xff0c;表示从2n个元素中选择n个元素的组合数。 Catalan数的原理可以通过以下方式理解&…

影视后期工具学习之PR(中)

pr剪辑之旅----声音设计 第五课 镜头语言和绿幕抠像 超级键效果(超级键通过简单的吸管取色和参数调整,即可实现专业级抠像与合成效果。无论是绿幕替换背景,还是创意双重曝光,都能轻松驾驭。建议结合「Alpha 通道」视图观察透明区域,逐步优化细节,最终导出高质量视频。)…

使用BootStrap 3的原创的模态框组件,没法弹出!估计是原创的bug

最近在给客户开发一个CRM系统&#xff0c;其中用到了BOOTSTRAP的模态框。版本是3。由于是刚开始用该框架。所以在正式部署到项目中前&#xff0c;需要测试一下&#xff0c;找到框架中的如下部分。需要说明的是。我用的asp.net mvc框架开发。测试也是在asp.net mvc环境下。 复制…

Camera2 与 CameraX 闲谈

目录 &#x1f4c2; 前言 1. &#x1f531; Camera2 2. &#x1f531; CameraX 3. &#x1f531; Camera2 与 CameraX 1&#xff09;使用复杂度与开发效率 2&#xff09;控制能力与应用场景 3&#xff09;设备兼容性与稳定性 4&#xff09;更新与维护 4. &#x1f4a0…

【大语言模型_8】vllm启动的模型通过fastapi封装增加api-key验证

背景&#xff1a; vllm推理框架启动模型不具备api-key验证。需借助fastapi可以实现该功能 代码实现&#xff1a; rom fastapi import FastAPI, Header, HTTPException, Request,Response import httpx import logging# 创建 FastAPI 应用 app FastAPI() logging.basicConfig(…

基于SpringBoot的名著阅读网站

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

Langchain 自定义工具和内置工具

使用介绍 自定义工具时的元素概念介绍 在Langchain中&#xff0c;工具&#xff08;Tool&#xff09;是与语言模型交互的基本单元。以下是自定义工具时的关键元素&#xff1a; name 定义&#xff1a;工具的名称&#xff0c;用于唯一标识该工具。作用&#xff1a;当工具被集成…

Gitee上库常用git命令

Gitee上库常用git命令 1、Fork 项目2、个人仓库修改3、追加提交4、创建PR5、多笔commit合一 1、Fork 项目 2、个人仓库修改 git add . // -s 表示自动添加邮箱签名信息&#xff0c;-m表示其后跟随commit描述 git commit -sm “add transition freeze” git push origin [目标…

Java 大视界 -- Java 大数据在智慧农业精准灌溉与施肥决策中的应用(144)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

Redux,React-redux。基础

状态管理库&#xff0c;集中式存储状态&#xff0c;管理状态 ✅ redux //简单实现 redux源码 export function createStore(reducer) {// reducer由用户编写&#xff0c; 必须是一个函数&#xff0c;dispatch的时候&#xff0c;reducer要执行if (typeof reducer ! function) t…

5.2 位运算专题:LeetCode 268. 丢失的数字

1. 题目链接 LeetCode 268. 丢失的数字 2. 题目描述 给定一个包含 [0, n] 范围内 n 个不同整数的数组 nums&#xff08;实际长度为 n&#xff09;&#xff0c;找出数组中缺失的那个数字。 示例&#xff1a; 输入&#xff1a;nums [3,0,1] → 输出&#xff1a;2&#xff08;…

基于第三方库的人脸识别系统的设计与实现

标题:基于第三方库的人脸识别系统的设计与实现 内容:1.摘要 本文针对传统人脸识别系统开发复杂、效率低的问题&#xff0c;旨在设计并实现基于第三方库的人脸识别系统。通过选用合适的第三方人脸识别库&#xff0c;利用其成熟的算法和接口&#xff0c;简化系统开发流程。对收集…

【Android】VehiclePropertyAccess引起CarService崩溃

VehiclePropertyAccess引起CarService崩溃 VehiclePropertyAccess VehiclePropertyAccess属性&#xff0c;用于定义车辆属性的访问权限。权限包括 读&#xff1a;READ&#xff0c;只可以读取&#xff0c;不能写入。 VehiclePropertyAccess:READ写&#xff1a;WRITE&#xf…

【Go】Go语言并发模型:MPG

Go 语言并发模型&#xff1a;MPG Go 的并发模型主要由三个部分构成&#xff1a; M (Machine) 系统线程&#xff0c;用于实际执行任务。 P (Processor) 逻辑处理器&#xff0c;负责管理和调度 goroutine。每个 P 拥有一个本地队列和关联的全局 G 队列。 G (Goroutine) Go 语言…

SpringCloud配置中心:Config Server与配置刷新机制

文章目录 引言一、Config Server基础架构1.1 Server端配置1.2 配置文件命名规则 二、Config Client配置2.1 Client端配置2.2 配置注入与使用 三、配置刷新机制3.1 手动刷新配置3.2 使用Spring Cloud Bus实现自动刷新3.3 配置仓库Webhook自动触发刷新 四、高级配置管理策略4.1 配…

PyTorch生成式人工智能实战:从零打造创意引擎

PyTorch生成式人工智能实战&#xff1a;从零打造创意引擎 0. 前言1. 生成式人工智能1.1 生成式人工智能简介1.2 生成式人工智能技术 2. Python 与 PyTorch2.1 Python 编程语言2.2 PyTorch 深度学习库 3. 生成对抗网络3.1 生成对抗网络概述3.2 生成对抗网络应用 4. Transformer4…

allure结合pytest生成测试报告

结合 pytest 和 Allure 可以生成详细而美观的测试报告&#xff0c;帮助测试人员和开发者更好地理解测试结果。这包括测试的执行情况、步骤、附件&#xff08;如截图&#xff09;、分类以及优先级标记。下面是如何在 pytest 中使用 Allure 生成测试报告的步骤&#xff1a; 安装…

STM32标准库开发中断流程

在STM32标准外设库&#xff08;SPL&#xff09;开发中&#xff0c;外设中断的处理流程通常如下&#xff1a; 一、标准库外设中断处理流程 &#xff08;1&#xff09;使能外设时钟 在使用任何外设之前&#xff0c;都必须打开外设的时钟。例如&#xff0c;使用USART1的中断&…