- 在说这些实现功能的技术之前,先说好,你不先看看这一篇能行?
- 因为之前碰到一个需求,就是根据系统不同的下位机分布位置,用户可以通过页面的导出功能导出各个地区不同类型的数据,并以excel表格形式导出。当时项目中是以地区名称+设备所在位置+时间为导出表名称+…)表头信息的判断,然后进行一定的处理,而且每一类都有很多数据,咱们做算法题有暴力解法,难道这里if…else…强行堆积嵌套嘛,行可能是行,但是要是把这个代码给后辈去讲解从而传承实验室的项目,这…
- 需求再整理:
- 虽然说刚开始的数据量不是很大,但是后期可以会牵涉到超级大的数据量,使用传统的POI方式来完成导入导出很明显会内存溢出,并且效率会非常低;
数据量大直接使用select * from tableName肯定不行
,一下子查出来比如说300w条数据肯定会很慢,300w 数据导出到Excel时肯定不能都写在一个Sheet中
,这样效率会非常低,估计打开都得几分钟;300w数据导出到Excel中肯定不能一行一行的导出到Excel中
。频繁IO操作绝对不行;导入时300万数据存储到DB如果循环一条条插入也肯定不行
。导入时300w数据如果使用Mybatis的批量插入肯定不行,因为Mybatis的批量插入其实就是SQL的循环
;一样很慢- 其实问题所在就是内存溢出,我们只要使用对上面介绍的POI方式即可
----->
但是呢原生的POI解决起来相当麻烦,所以又因为阿里的一款POI封装工具EasyExcel提供的很方便的使用方法,所以就用POI封装工具EasyExcel了。----->
,上面需求分析已经说了不能一次性查询出全部数据,所以咱们可以用市面上的很多分页插件分批进行查询,只不过是多查询几次的问题
。----->
可以将300w条数据写到不同的Sheet中,每一个Sheet写一百万即可----->
不能一行一行的写入到Excel上,我们可以将分批查询的数据分批写入到Excel中----->
导入到DB时我们可以将Excel中读取的数据存储到集合中,到了一定数量,直接批量插入到DB中----->
不能使用Mybatis的批量插入,我们可以使用JDBC的批量插入,配合事务来完成批量插入到DB。即 Excel读取分批+JDBC分批插入+事务
- 其实问题所在就是内存溢出,我们只要使用对上面介绍的POI方式即可
- 导入与导出通常发生在 Web 环境,对于 Spring MVC 来说,可以将请求信息转换为任意类型的 contoller 方法参数,将 controller 方法返回值转换为客户端支持的内容。
数据的导入就是从Excel中导入到DB中,而数据导出就是从DB中查询数据然后使用POI写到Excel上,然后也可以将Excel导出到磁盘上
。
- 导入与导出 Excel 本质是上将 Excel 文件内容与 Java 对象之间做一个映射,EasyExcel 做的只是在这两者之间转换。
- 由于 poi 占用内存较大,在高并发下很容易发生 OOM 或者频繁 fullgc,阿里基于 poi 开源了 EasyExcel 项目。【除了节约内存,EasyExcel 还简化了 API,通过注解映射 Excel 单元格与对象字段之间的关系】
- 需求再整理:
- 因为之前碰到一个需求,就是根据系统不同的下位机分布位置,用户可以通过页面的导出功能导出各个地区不同类型的数据,并以excel表格形式导出。当时项目中是以地区名称+设备所在位置+时间为导出表名称+…)表头信息的判断,然后进行一定的处理,而且每一类都有很多数据,咱们做算法题有暴力解法,难道这里if…else…强行堆积嵌套嘛,行可能是行,但是要是把这个代码给后辈去讲解从而传承实验室的项目,这…
- Apache POI、阿里巴巴的easyExcel的基本概念:【Apache POI官方文档,很详细】
- 需求分析或者说,咱们项目中什么时候用到这俩组件或者叫工具:
- 将数据库展示在前台界面上的表格数据导出,也就是excel导出嘛
- 人家公司说了,我要对数据手动进行一些改动,然后我再把这个改好的表格导入到服务器上存起来,所以让我们做一个导入功能
- 直接把excel表格存起来,在服务器的linux系统的某个目录下就行(简单粗暴版)
- 把excel中的数据导入到数据库中
- Apache POI:
会先将数据一次性全部加载到内存【apache poi存在的问题就是十分的消耗内存】(所以有可能会内存溢出,量太大的时候也会出现OOM异常)再写入文件
- POI分类:
- POI中的WorkBook接口,一般我们的POI版本也在更新的同时对这个接口的实现类做了更新:【
我们在导入导出操作的时候会用到这样三个实现类
】- HSSFWorkbook :这个实现类是我们早期使用最多的对象,它可以操作Excel2003以前(包含2003)的所有Excel版本。在2003以前Excel的版本后缀还是.xls
- 缺点:
- 最多只能导出 65535行,也就是导出的数据函数超过这个65535行数据就会报错;
- 优点是:
- 不会报内存溢出。(因为数据量还不到7w所以内存一般都够用,首先你得明确知道
HSSFWorkbook 这种方式是将数据先读取到内存中,然后再操作
)
- 不会报内存溢出。(因为数据量还不到7w所以内存一般都够用,首先你得明确知道
- excel03版和07版的区别:
- 03版限制为65536行,07版没有限制
03版会先将数据一次性全部加载到内存(所以有可能会内存溢出)再写入文件
【缺点就是可能会出现内存溢出、优点就是速度快】;【07版写数据速度非常慢,也非常耗内存,也会发生内存溢出,如100万条;优点就是可以写较大的数据量
,如20万条】poi 占用内存较大,在高并发下很容易发生 OOM 或者频繁 fullgc,阿里基于 poi 开源了 EasyExcel 项目
。
- 后缀名不一样,03版本使用xls结尾
- 03版,如果你来个65537行,就会报错
- 缺点:
- XSSFWorkbook :这个实现类现在在很多公司都可以发现还在使用,
XSSFWorkbook是操作的Excel2003--Excel2007之间的版本,Excel的扩展名是.xlsx
- 优点:这种形式的出现是为了突破HSSFWorkbook的65535行局限,是为了针对Excel2007版本的1048576行,16384列,最多可以导出104w条数据;
- 缺点:伴随的问题来了,虽然导出数据行数增加了好多倍,但是随之而来的
内存溢出问题也成了噩梦
。因为你所创建的book,Sheet,row,cell等在写入到Excel之前,都是存放在内存中的(这还没有算Excel的一些样式格式等等),可想而知,内存不溢出就有点不科学了
- SXSSFWorkbook :这个实现类是POI3.8之后的版本才有的,
SXSSFWorkbook 可以操作Excel2007以后的所有版本Excel,扩展名是.xlsx
- 优缺点:从POI 3.8版本开始,提供了一种基于XSSF的低内存占用的SXSSF方式:
- 大文件写SXSSF:
想要实用海量数据的导出自然得使用SXSSFWorkbook这个组件
。
- 优缺点:从POI 3.8版本开始,提供了一种基于XSSF的低内存占用的SXSSF方式:
- HSSFWorkbook :这个实现类是我们早期使用最多的对象,它可以操作Excel2003以前(包含2003)的所有Excel版本。在2003以前Excel的版本后缀还是.xls
- POI中的WorkBook接口,一般我们的POI版本也在更新的同时对这个接口的实现类做了更新:【
- 具体使用三种中的哪种方式【HSSFWorkbook、XSSFWorkbook、SXSSFWorkbook 】还是需要看情况的:
当我们经常导入导出的数据不超过7w的情况下,可以使用 HSSFWorkbook 或者 XSSFWorkbook都行
当数据量查过7w并且导出的Excel中不牵扯对Excel的样式,公式,格式等操作的情况下,推荐使用SXSSFWorkbook
当数据量查过7w,并且我们需要操做Excel中的表头,样式,公式等,这时候我们可以使用 XSSFWorkbook 配合进行分批查询,分批写入Excel的方式来做
- POI分类:
- easyExcel:Java解析excel的工具。
阿里基于 poi 开源了 EasyExcel 项目
- 但是呢,如果咱们要实现百万数据导入到处时,
使用传统的POI方式来完成导入导出很明显会内存溢出,并且效率会非常低
。并且还有以下几点考虑:阿里的一款POI封装工具EasyExcel就是用来解决下面这些问题的
;【对于EasyExcel,作者还专门提供了关流方法,不需要我们手动去关流了,也避免了我们经常忘记关流导致的一系列问题。】- 数据量大直接使用select * from tableName肯定不行,一下子查出来几百万条数据肯定会很慢【不能一次性查询出全部数据,
我们可以分批进行查询,只不过是多查询几次的问题
,况且市面上分页插件很多。】- 导入时几百万条数据存储到DB如果循环一条条插入也肯定不行【导入到DB时我们可以
将Excel中读取的数据存储到集合中,到了一定数量,直接批量插入到DB中
。】
- 导入时几百万条数据存储到DB如果循环一条条插入也肯定不行【导入到DB时我们可以
- 导入时几百万条数据如果使用Mybatis的批量插入肯定不行,
因为Mybatis的批量插入其实就是SQL的循环
;一样很慢。【不能使用Mybatis的批量插入,我们可以使用JDBC的批量插入,配合事务来完成批量插入到DB。即 Excel读取分批+JDBC分批插入+事务
。】Myabtis批量插入和JDBC批量插入性能对比 - 几百万条数据导出到Excel时肯定不能都写在一个Sheet中,这样效率会非常低;估计打开都得几分钟【
可以将几百万条数据写到不同的Sheet中,每一个Sheet写一百万即可
。】 - 几百万条数据导出到Excel中肯定不能一行一行的导出到Excel中。频繁IO操作绝对不行【不能一行一行的写入到Excel上,我们
可以将分批查询的数据分批写入到Excel中
。】
- 数据量大直接使用select * from tableName肯定不行,一下子查出来几百万条数据肯定会很慢【不能一次性查询出全部数据,
- 会通过磁盘一行一行去写,所以不存在内存溢出,也就是避免OOM
- 对apache poi进行了一些优化,并且再大的excel也不会出现内存溢出
- 读、写excel只需要一行代码【
除了节约内存,EasyExcel 还简化了 API,通过注解映射 Excel 单元格与对象字段之间的关系,简单的几行代码就能搞定复杂的导入导出功能了
。】导入与导出 Excel 本质是上将 Excel 文件内容与 Java 对象之间做一个映射,EasyExcel 做的只是在这两者之间转换
。如果项目中的 Excel 导入与导出功能比较多,会产生大量的样板式代码,使用体验类似于 JDBC。咱们JDBC用的是Mybatis来优化,那你这Excel 导入与导出不管能成?- 如果能够使用自定义的 controller 方法参数接收 Excel 文件内容,将 controller 方法返回值转换为 Excel 文件响应,可以直接消除 Excel 导入与导出时的样板式代码。
- 另外,
导入往往还伴随着校验,这是 EasyExcel 没有支持的功能
。如果需要校验,要么写代码手动判断,要么调用 Java Validation 规范 定义的 API 判断
,这又会产生大量样板式代码。- 另外在将请求内容转换为 controller 方法参数时还可以加入自定义的校验逻辑。
- 程序员鱼皮老师关于Excel 在 Java 中的操作,文章,很赞
- 但是呢,如果咱们要实现百万数据导入到处时,
- 需求分析或者说,咱们项目中什么时候用到这俩组件或者叫工具:
- Apache POI、阿里巴巴的easyExcel的实操:
- Apache POI小数据量的写(导入):
- step1:首先不管是SSM还是SpringBoot项目,第一步都是都如MAVEN依赖嘛
- step2:在咱们Java中,一切皆对象,所以首先抽象一下呗:
- 首先,整个excel表格就是一个对象(excel一般最上面那个叫工作簿,下面有三个,叫工作表)
- 也就是有一个工作簿,对象
//1.创建一个工作簿 //new HSSFWorkbook()是生成03版本的excel,换成new XSSFWorkbook()就是生成咱们07版本的excel Workbook workbook = new HSSFWorkbook();
- 还有一个,工作表,对象(工作表是包含在工作簿里面的,所以)
//2.创建一个工作表 Sheet sheet = workbook.createSheet("小区整编表 逐日降雨量")
- 行:行默认是从0开始:Row row1 = sheet.createRow(0);
- 列:创建单元格就相当于创建了列,因为单元格就是行和列的相交嘛:Cell cell1_1 = row.createCell(0);
//这两个就确定了,或者说对应了第一行第一列这个单元格,然后咱们既可以针对这个单元格进行写入值,等操作 Row row1 = sheet.createRow(0);//(1, 1) Cell cell1_1 = row1.createCell(0);//(1, 1) cell1_1.setCellValue("..."); //这两个就确定了,或者说对应了第一行第二列这个单元格,然后咱们既可以针对这个单元格进行写入值,等操作 Cell cell1_2 = row1.createCell(1);//(1, 2) cell1_2.setCellValue("..."); //创建第二行(2, 1) Row row2 = sheet.createRow(1);//(2, 1) Cell cell2_1 = row2.createCell(0); cell2_1.setCellValue("..."); Cell cell2_2 = row2.createCell(1);//(2, 2)
- 也就是有一个工作簿,对象
- 首先,整个excel表格就是一个对象(excel一般最上面那个叫工作簿,下面有三个,叫工作表)
- step3:单独测试或者集成读或者写功能,上面对象,行列等创建完毕,咱们要将这些信息构建成为一张表,也就是生成一张表,本质上生成一张表就是需要通过IO流来进行的IO操作
String outPutPath = "..."; FileOutputStream fileOutputStream = new FileOutputStream(outPutPath + "小区整编表 逐日降雨量03.xls"); //有了IO流之后输出工作簿 workbook.write(fileOutputStream); //用到IO流的地方一定记得,关闭流操作 fileOutputStream.close();
- step1:首先不管是SSM还是SpringBoot项目,第一步都是都如MAVEN依赖嘛
- Apache POI大数据量的写(导入):
... //可以在前后打印一下时间,测一下导入导出时间 long begin = System.currentTimeMillis(); //创建一个工作簿,假设咱用的是03版的 Workbook workbook = new HSSFWorkbook(); //在这个工作簿下创建工作表 Sheet sheet = workbook.createSheet(); //写入数据,因为03版有65536行呀,所以 for(int rowNum = 0; rowNum < 65536; rowNum++){ //通过for循环创建65536行 Row row = sheet.createRow(rowNum); //通过for循环,假设创建20列 for(int cellNum = 0; cellNum < 20; cellNum++){ Cell cell = row.createCell(cellNem);//通过行与列的交叉就创建出了那么多单元格 cell.setCellValue(...); } } FileOutputStream fileOutputStream = new FileOutputStream(outPutPath + "小区整编表 逐日降雨量03.xls"); //有了IO流之后输出工作簿 workbook.write(fileOutputStream); //用到IO流的地方一定记得,关闭流操作 fileOutputStream.close(); long end = System.currentTimeMillis();
- 这里就有三种可以用:03版、07版,07版的加速版:
07版和03版代码的差别就是多态创建工作簿时的子类不一样,然后07版的加速版除了多态那块也要变之外,后面还得多一个清除临时文件
- 这里就有三种可以用:03版、07版,07版的加速版:
- Apache POI大数据量的读:如果表中数据类型很多,那么咱们不仅要循环拿数据,还得判断数据的类型,这是必须要做的数据类型判别
String inputPath = "..."; //把inputPath 路径下对应的表加载进来,也就是获取文件流 FileInputStream inputStream = new FileInputStream(inputPath + "..."); //1.创建一个工作簿.咱们在excel中对工作簿可以操作的这里都可以用workbook操作。然后inputStream输入到workbook中意思就是可以通过文件输入流读取咱们这个工作簿里面的内容,哦,原来文件输入流是这个意思,就是我要读取哪个文件我就把输入流输入给哪个文件,然后相当于一根管道怼到这个文件上,把这个文件中的内容给吸出来,导流出来 Workbook workbook = new HSSFWorkbook(inputStream); //2.得到工作簿中的工作表 Sheet sheet = workbook.getSheetAt(0); //获取表的标题内容 Row rowTitle = sheet.getRow(0); if(rowTitle != null){ //获取,这一行中有数据的有几列。也就是通过一行拿到所有的列 int cellCount = rowTitle.getPhysicalNumberOfCell(); //把所有行都读出来。通过一个列把所有的都遍历出来 for(int cellNum = 0, cellNum < cellCount; cellNum++){ Cell cell = rowTitle.getCell(cellNum); if(cell != null){ int cellType = cell.getCellType(); String stringCellValue = cell.getStringCellValue(); } } } //获取表中的内容,上面咱们把标题行已经获取到了,所以咱们下来就获取除了第一行之外的行 int rowCount = rowTitle.getPhysicalNumberOfRows(); //从1开始,是因为前面把第一行已经获取了,所以只用获取剩下的行就行 for(int rowNum = 1; rowNum < rowCount; rowNum++){ //一行一行的读数据 Row rowData = sheet.getRow(rowNum); if(rowData != null){ //读取非空行的列 int cellCount = rowTitle.getPhysicalNumberOfCells(); //把每一行中的(每一列中)的所有数据都读取出来 for(int cellNum = 0; cellNum < cellCount; cellNum++){ System.out.print(...);//记住要多打印 Cell cell = rowData.getCell(cellNum); //匹配列的数据类型 int cellType = cell.getCellType(); String cellValue = ""; switch(cellType){ case HSSFCell.CELL_TYPE_STRING://字符串 System.out.print(...);//记住要多打印 cellValue = cell.getStringCellValue(); break; case HSSFCell.CELL_TYPE_BOOLEAN:// System.out.print(...);//记住要多打印 cellValue = String.valueOf(cell.getBooleanCellValue()); break; case HSSFCell.CELL_TYPE_BLANK://空 System.out.print(...);//记住要多打印 break; case HSSFCell.CELL_TYPE_NUMERIC://数字(日期和普通数字两种可能性) System.out.print(...);//记住要多打印 if(HSSFDateUtil.isCellDateFormatted(cell)){ System.out.print(...);//记住要多打印 Date date = cell.getDateCellValue(); cellValue = new Date(date).toString("yyyy-MM-dd"); }else{ //不是日期格式,防止数字过长 System.out.print(...);//记住要多打印 cell.setCellType(HSSFCell.CELL_TYPE_STRING); cellValue = cell.toString(); } break; case HSSFCell.CELL_TYPE_ERROR: System.out.print(...);//记住要多打印 break; } } } } /** *获取单行单列形成的单元格 */ Row row = sheet.getRow(0); Cell cell = row.getCell(0);//通过文件输入流得到文件中的工作簿,然后通过工作簿得到工作簿中的工作表,然后通过工作表得到表中的行,通过行与列的相交得到第一个单元格 //通过cell获取单元格中的值时一定要注意值的数据类型, //如果表中数据类型很多,那么咱们不仅要循环拿数据,还得判断数据的类型,这是必须要做的数据类型判别 cell.getXxx...(); inputStream.close();//关闭流操作是一个好习惯
- easyExcel的使用:EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。
github地址:https://github.com/alibaba/easyexcel【GitHub地址上教程和说明很详细,并且附带有读和写的demo代码】
- easyExcel大数据量的导出
- easyExcel大数据量的导入:Myabtis批量插入和JDBC批量插入性能对比
- 芋道源码老师的EasyExcel完成300w数据的导出解决思路,作者还专门提供了关流方法,不需要我们手动去关流了(对于数据量小于300W的数据可以使用在一个Sheet中进行导出)
- 芋道源码老师的300W数据的导入解决思路
- 固定套路:在网上查了很多资料有一个博主测试使用EasyExcel导出102w数据用时105秒
- 1、写入,固定类格式进行写入
- 2、读取,根据监听器设置的规则进行读取!|
- 步骤一:首先找到easyExcel的pom依赖,导入到项目中,然后根据自己的需要进行修改
...... <dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>${revision}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel-core</artifactId> <version>${revision}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel-support</artifactId> <version>${revision}</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>4.1.2</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.1.2</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml-schemas</artifactId> <version>4.1.2</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-csv</artifactId> <version>1.8</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.32</version> </dependency> <dependency> <groupId>org.ehcache</groupId> <artifactId>ehcache</artifactId> <version>3.9.9</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.3.15</version> </dependency> <!--test--> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.5</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.78</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.6.3</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> </dependencies> ......
- 步骤二:写excel
private List<DemoData> data() { List<DemoData> list = ListUtils.newArrayList(); for (int i = 0; i < 10; i++) { DemoData data = new DemoData(); data.setString("字符串" + i); data.setDate(new Date()); data.setDoubleData(0.56); list.add(data); } return list; } /** * 最简单的写 * */ @Test public void simpleWrite() { // 注意 simpleWrite在数据量不大的情况下可以使用(5000以内,具体也要看实际情况),数据量大参照 重复多次写入 // 写法1 JDK8+ // since: 3.0.0-beta1 String fileName = TestFileUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx"; // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭 // 如果这里想使用03 则 传入excelType参数即可 /** *write(fileName, 格式类(也就是写出的excel表是个什么样子)) *sheet(表名) *doWrite(要写入的数据就放到这个地方,你看咱们上面不就是用for循环随机产生了10个随机数,然后把这10个数放入list,在放到这里) */ EasyExcel.write(fileName, DemoData.class) .sheet("模板") .doWrite(() -> { // 分页查询数据 return data(); }); }
- 步骤三:读数据:把excel中的数据读出来
// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去 @Slf4j public class DemoDataListener implements ReadListener<DemoData> { /** * 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收 */ private static final int BATCH_COUNT = 100; /** * 缓存的数据 */ private List<DemoData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT); /** * 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。 */ private DemoDAO demoDAO; public DemoDataListener() { // 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数 demoDAO = new DemoDAO(); } /** * 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来 * * @param demoDAO */ public DemoDataListener(DemoDAO demoDAO) { this.demoDAO = demoDAO; } /** * 这个每一条数据解析都会来调用,也就是每次读取时就会调用这个方法 * * @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()} * @param context *DemoData 数据类型 *AnalysisContext 分析上下文 */ @Override public void invoke(DemoData data, AnalysisContext context) { log.info("解析到一条数据:{}", JSON.toJSONString(data)); cachedDataList.add(data); // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM if (cachedDataList.size() >= BATCH_COUNT) { saveData(); // 存储完成清理 list cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT); } } /** * 所有数据解析完成了 都会来调用 * * @param context */ @Override public void doAfterAllAnalysed(AnalysisContext context) { // 这里也要保存数据,确保最后遗留的数据也存储到数据库 saveData(); log.info("所有数据解析完成!"); } /** * 加上存储数据库 */ private void saveData() { log.info("{}条数据,开始存储数据库!", cachedDataList.size()); demoDAO.save(cachedDataList); log.info("存储数据库成功!"); } } /** * 最简单的读 * <p> * 1. 创建excel对应的实体对象 参照{@link DemoData} * <p> * 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link DemoDataListener} * <p> * 3. 直接读即可 */ @Test public void simpleRead() { // 写法1:JDK8+ ,不用额外写一个DemoDataListener // since: 3.0.0-beta1 String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx"; // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭 // 这里每次会读取3000条数据 然后返回过来 直接调用使用数据就行 EasyExcel.read(fileName, DemoData.class, new PageReadListener<DemoData>(dataList -> { for (DemoData demoData : dataList) { log.info("读取到一条数据{}", JSON.toJSONString(demoData)); } })).sheet().doRead();
- easyExcel大数据量的导出
- 比如咱们现在
对导入数据先进行校验然后再入库
- POI是Java操作Excel的基础库。为了通用性并没有做定制,而且还有一些局限性。经过一番调研决定
采用二次封装库easyexcel来进行业务开发
。 - 使用步骤:
- 先是肯定集成依赖:上面有
- easyexcel将读取Excel的生命周期抽象为了几个阶段,方便我们在各个阶段注入你想要实现的逻辑。这几个阶段包含在ReadListener接口中
- JSR303校验:如果写一个抽象的校验工具,
每一个字段都要编写其具体的校验逻辑
,如果一个Excel的字段量爆炸,这对开发来说就可能是噩梦。业界已经有的规范-JSR303校验规范,它将数据模型(Model)和校验(Validation)各自抽象,非常灵活,而且工作量明显降低
。我们只需要找到和esayexcel生命周期结合的地方就行了。我们只需要引入以下依赖就能在Spring Boot项目中集成JSR303校验
:
- 我们可以在解析每个字段的时候校验,这对应ReadListener的invoke(T data, AnalysisContext context)方法,这种方式可以实现当字段校验触发约束时就停止excel解析的策略;
在编写代码时,尽量职责单一,一个类或者一个方法尽量只干一个事,这样让自己的代码足够清晰
。- 我们
只需要实现Excel抽象解析监听器接口AnalysisEventListener,并将解析字段加入集合,等完全解析完毕后再进行校验。这里如果校验不通过就会抛出携带校验信息的异常,异常经过处理返回前端提示
。
- 另一种可以在Excel解析完毕后执行校验,对应doAfterAllAnalysed(AnalysisContext context)
- 编写校验处理类:把解析和校验分开实现,先编写JSR303校验工具。这里假设已经有了校验器javax.validation.Validator的实现。当Spring Boot集成了JSR303会有一个Validator实现被自动注入Spring IoC,我们可以利用它。【整个校验的逻辑,如果校验通过不提示任何信息,如果校验不通过把校验的约束信息封装返回】
- 我们可以在解析每个字段的时候校验,这对应ReadListener的invoke(T data, AnalysisContext context)方法,这种方式可以实现当字段校验触发约束时就停止excel解析的策略;
- JSR303校验:如果写一个抽象的校验工具,
- POI是Java操作Excel的基础库。为了通用性并没有做定制,而且还有一些局限性。经过一番调研决定
- Apache POI小数据量的写(导入):
上面excel实操完了之后,还有一个挺重要的知识点,就是导出代码实在是冗余太多,所以得稍微优化一下,点这里,瞅瞅
- excel表到处或者说数据导出之番外篇:来了需求是客户让我们优化一个这个excel数据导出过程,实在太慢了,最离谱的快花半分钟到一分钟,页面一直就卡住了,这谁能受得了。
然后我们通过AOP把LOG4J横切进我们某些查询SQL中,打印一些信息检测一下是不是出了什么错或者是什么原因导致查询慢从而导致导出慢
。最后我和师兄分两拨势力,思路如下:- 师兄经过上网查询后发现,把导出的文件后缀改为csv能够快一点,当然,师兄这一条路也成功了
- 我呢,就是把玩redis,瞎玩,虽然最后由于某种原因没有上线,但是有收获。过程是这样的:
- 就是考虑加个redis缓存,
我当时不是写了redis的配置文件和工具类(其实自己做的就是在xml配置文件中配置依赖以及其他插件(比如redis插件),然后写XxxController层代码以及其他工具类代码等)
,然后集成进去之后,但是最后因为有时候查询会查出错误的数据或者说老旧的数据而我们想要的是新的数据,老项目嘛很追求追求稳定,所以就说明查错了嘛,然后那段时间有疫情加上boss让继续重心放在弄其他的上面,就把redis搁置了。 - 使用redis,实现excel导出功能的优化:
用做缓存,将查出来的数据先暂放到redis中,然后,excel表格导出时从redis缓存中拿,相对来说应该能快点,我感觉是因为我提前把要导出的数据准备好了,当要导出时就不用再去数据库中一个一个查了。当时项目中最初也是只在excel表导出那里用到了redis
。过程中我是通过AOP把LOG4J横切进我们某些查询SQL中,打印一些信息检测一下是不是出了什么错或者是什么原因导致查询慢从而导致导出慢。
- 就是考虑加个redis缓存,
- 然后呢,再网上找找,咱们Java的,不考虑一下,面向多线程高并发编程,容易吃亏哟!整起
- 比如,还是上面的
报表导出。整个系统下来,起码超过一百张报表需要导出
。这个时候如何优雅的实现报表导出,释放生产力就显得很重要了【对于每个报表都相同的操作,就得抽取出来呗;那不同的操作咱们也要尽量进行良好的封装,尽可能提高复用性
】。
- 实现的功能点:
- 导出任意类型的数据:.txt、.csv、.xsl、.doc…
- 咱们可以自由设置表头
- 咱们可以自由设置字段的导出格式
- 实现过程:一般呢,比如SSM或者SpringBoot中集成Redis、或者咱们今天说的Excel导出相关的插件,都会自己写一些工具类或者XxxTemplate类,这样调用起来方便一点。
- 比如,咱们这篇中,可以先封装一个
... public class ExcelUtils { //集成日志 private static final Logger LOGGER = LoggerFactory.getLogger(ExcelUtils.class); public static final int ROW_ACCESS_WINDOW_SIZE = 100; public static final int SHEET_MAX_ROW = 100000; private List list;//用来保存待导出的数据 private List<ExcelHeaderInfo> excelHeaderInfos;//主要用来保存表头信息,因为我们需要定义多个表头信息,所以需要使用一个列表来保存 private Map<String, ExcelFormat> formatInfo;//因为我们需要接受多个字段的转换格式,所以定义了一个Map类型来接收,该参数可以省略(默认格式为字符串) ... //构造函数 ... //封装的一些方法 ...
- ExcelHeaderInfo(int firstRow, int lastRow, int firstCol, int lastCol, String title){…}
- firstRow:该表头所占位置的首行
- lastRow:该表头所占位置的尾行
- firstCol:该表头所占位置的首列
- lastCol:该表头所占位置的尾行
- title:该表头的名称
- ExcelFormat:该参数主要用来格式化字段,我们需要预先约定好转换成哪种格式,不能随用户自己定。所以我们定义了一个枚举类型的变量,该枚举类只有一个字符串类型成员变量,用来保存想要转换的格式,例如FORMAT_INTEGER就是转换成整型。
- ExcelHeaderInfo(int firstRow, int lastRow, int firstCol, int lastCol, String title){…}
- 然后,接下来就是能产生表格的内容咯:
- 创建表头:
- 转换数据:
- 赋值正文:
- 多线程查询数据:对50w的数据分别创建20个线程去查询,但是总体的效率并不是50w/20,而是仅仅快了几秒钟
- 因为多个线程之间是同时执行的,你不能够保证哪个线程先执行完毕,但是我们却得保证数据顺序的一致性。在这里我们使用了Callable接口,
通过实现Callable接口的线程可以拥有返回值,我们获取到所有子线程的查询结果,然后合并到一个结果集中即可
。 - 保证合并的顺序:先创建了一个FutureTask类型的List,该FutureTask的类型就是返回的结果集。【
List<FutureTask<List<TtlProductInfoPo>>> tasks = new ArrayList<>()
;】当我们每启动一个线程的时候,就将该线程的FutureTask添加到tasks列表中,这样tasks列表中的元素顺序就是我们启动线程的顺序。
- 顺序塞值:我们按顺序从tasks列表中取出FutureTask,然后执行FutureTask的get()方法,该方法会阻塞调用它的线程,直到拿到返回结果。这样一套循环下来,就完成了所有数据的按顺序存储。
- 因为多个线程之间是同时执行的,你不能够保证哪个线程先执行完毕,但是我们却得保证数据顺序的一致性。在这里我们使用了Callable接口,
- 解决接口超时:
- 如果需要导出海量数据,可能会存在一个问题:接口超时,主要原因就是整个导出过程的时间太长了。
- 接口的响应时间太长,我们缩短响应时间不就可以了嘛。我们使用异步编程解决方案,异步编程的实现方式有很多,这里我们使用最简单的spring中的Async注解,加上了这个注解的方法可以立马返回响应结果。关键的注解的使用方式或者实现步骤:
- 创建表头:
- 比如,咱们这篇中,可以先封装一个
- 假设咱们现在
有一个报表系统,频繁发生内存溢出,在高峰期间使用时,还会频繁的发生拒绝服务
。- 业务场景是由于有些结果集的字段不是太全,因此需要对结果集合进行循环,并通过 HttpClient 调用其他服务的接口进行数据填充。使用 Guava 做了 JVM 内缓存,但是响应时间依然很长。初步排查,JVM 的资源太少。接口 A 每次进行报表计算时,都要涉及几百兆的内存,而且在内存里驻留很长时间,有些计算又非常耗 CPU,特别的“吃”资源。而我们分配给 JVM 的内存只有 3 GB,在多人访问这些接口的时候,内存就不够用了,进而发生了 OOM。在这种情况下,没办法,只有升级机器。把机器配置升级到 4C8G,给 JVM 分配 6GB 的内存,这样 OOM 问题就消失了。但随之而来的是频繁的 GC 问题和超长的 GC 时间,平均 GC 时间竟然有 5 秒多。
- 有些数据需要使用 HttpClient 来获取进行补全。提供数据的服务提供商有的响应时间可能会很长,也有可能会造成服务整体的阻塞。
- 过滤一下 nio 关键字,可以查看 tomcat 相关的线程,足足有 200 个,这和 Spring Boot 默认的 maxThreads 个数不谋而合。更要命的是,有大多数线程,都处于 BLOCKED 状态,说明线程等待资源超时。通过grep fast | wc -l 分析,确实200个中有150个都是blocked的fast的进程。问题找到了,解决方式就顺利成章了。
- 过滤一下 nio 关键字,可以查看 tomcat 相关的线程,足足有 200 个,这和 Spring Boot 默认的 maxThreads 个数不谋而合。更要命的是,有大多数线程,都处于 BLOCKED 状态,说明线程等待资源超时。通过grep fast | wc -l 分析,确实200个中有150个都是blocked的fast的进程。问题找到了,解决方式就顺利成章了。
- 有一个关于服务的某个实例,经常发生服务卡顿。由于服务的并发量是比较高的,每多停顿 1 秒钟,几万用户的请求就会感到延迟。我们统计、类比了此服务其他实例的 CPU、内存、网络、I/O 资源,区别并不是很大,所以一度怀疑是机器硬件的问题。接下来我们对比了节点的 GC 日志,发现无论是 Minor GC,还是 Major GC,这个节点所花费的时间,都比其他实例长得多。
- 有些数据需要使用 HttpClient 来获取进行补全。提供数据的服务提供商有的响应时间可能会很长,也有可能会造成服务整体的阻塞。
- 进一步,由于报表系统和高并发系统不太一样,它的对象,存活时长大得多,并不能仅仅通过增加年轻代来解决;而且,如果增加了年轻代,那么必然减少了老年代的大小,由于 CMS 的碎片和浮动垃圾问题,我们可用的空间就更少了。虽然服务能够满足目前的需求,但还有一些不太确定的风险。
- 优化之后,效果不错,但并不是特别明显。经过评估,针对高峰时期的情况进行调研,我们决定再次提升机器性能,改用 8core16g 的机器。但是,这带来另外一个问题。
- 优化之后,效果不错,但并不是特别明显。经过评估,针对高峰时期的情况进行调研,我们决定再次提升机器性能,改用 8core16g 的机器。但是,这带来另外一个问题。
- 业务场景是由于有些结果集的字段不是太全,因此需要对结果集合进行循环,并通过 HttpClient 调用其他服务的接口进行数据填充。使用 Guava 做了 JVM 内缓存,但是响应时间依然很长。初步排查,JVM 的资源太少。接口 A 每次进行报表计算时,都要涉及几百兆的内存,而且在内存里驻留很长时间,有些计算又非常耗 CPU,特别的“吃”资源。而我们分配给 JVM 的内存只有 3 GB,在多人访问这些接口的时候,内存就不够用了,进而发生了 OOM。在这种情况下,没办法,只有升级机器。把机器配置升级到 4C8G,给 JVM 分配 6GB 的内存,这样 OOM 问题就消失了。但随之而来的是频繁的 GC 问题和超长的 GC 时间,平均 GC 时间竟然有 5 秒多。
- 比如,还是上面的
- 问题描述:转自库森老师的文章。有一次线上遇到故障,
重新启动后,使用 jstat 命令,发现 Old 区一直在增长
。我使用 jmap 命令,导出了一份线上堆栈,然后使用 MAT 进行分析,通过对 GC Roots 的分析,发现了一个非常大的 HashMap 对象
,这个原本是其他同事做缓存用的,但是做了一个无界缓存,没有设置超时时间或者 LRU 策略,在使用上又没有重写key类对象的hashcode和equals方法,对象无法取出也直接造成了堆内存占用一直上升,后来,将这个缓存改成 guava 的 Cache,并设置了弱引用,故障就消失了。
- 关于文件处理器的应用,在读取或者写入一些文件之后,由于发生了一些异常,close 方法又没有放在 finally 块里面,造成了文件句柄的泄漏。由于文件处理十分频繁,产生了严重的内存泄漏问题。【关于文件处理器的应用,在读取或者写入一些文件之后,由于发生了一些异常,close 方法又没有放在 finally 块里面,造成了文件句柄的泄漏。由于文件处理十分频繁,产生了严重的内存泄漏问题。】
巨人的肩膀:
Apache POI:https://poi.apache.org/
官方文档:https://www.yuque.com/easyexcel/doc/easyexcel
github地址:https://github.com/alibaba/easyexcel
SpringForAll前辈的“海量数据下如何使用多线程的导出 Excel”,推荐一看
SpringForAll老师的公众号文章
码农小胖哥的公众号文章
芋道源码老师关于几百万条数据导入导出优化方案方案
Java基基老师的,用 Java 怎么实现生成 Excel + PDF 导出