使用POI和EasyExcel实现Excel导入和导出功能
需求场景
开发中经常会设计到excel的处理,需求场景如下所示:
1、将用户信息导出为excel表格(导出数据)
2、将Excel表中的信息录入到数据库中(导入数据)
操作Excel目前比较流行的就是 Apache POI 和 阿里巴巴开源的 EasyExcel ,当然还有EasyPoi工具。不过EasyPoi的易用性和性能介于前面两者之间,我们这里就暂不进一步深入了。 下面我们对前面两种工具分别进行介绍和实践:
Apache POI介绍
Apache POI 官网:poi.apache.org/


EasyExcel介绍
EasyExcel 官网地址:github.com/alibaba/eas…

EasyExcel 是阿里巴巴开源的一个excel处理框架,以使用简单、节省内存著称。
EasyExcel 能大大减少占用内存的主要原因是在解析 Excel 时没有将文件数据一次性全部加载到内存中, 而是从磁盘上一行行读取数据,逐个解析。
内存问题:POI = 100w数据先加载到内存 OOM,再写入文件。
下图是 EasyExcel 和 POI 在解析Excel时的对比图。
官方文档:www.yuque.com/easyexcel/d…
POI-Excel写
创建项目
1、建立一个空项目,创建普通Maven的module
2、引入pom依赖
<!-- xls(03) --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>3.9</version> </dependency> <!-- xls(03) --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>3.9</version> </dependency> <!-- 日期格式化工具 --> <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.10.5</version> </dependency>
需要注意:2003 版本和 2007 版本存在兼容性的问题!03最多只有 65535 行!
03版本(工作簿、工作表、):

03版本
package com.deepbluet.poimyself.controller; import org.apache.poi.hssf.usermodel.HSSFWorkbook; 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.usermodel.Workbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.joda.time.DateTime; import org.junit.jupiter.api.Test; import java.io.FileOutputStream; /** * @author 南街北巷 * @data 2020/8/13 4:50 */ public class ExcelWriteTest { private static String PATH = "E:\ideaProject\poi-myself\excel\"; @Test public void testWrite03() throws Exception { // 1.创建一个工作簿 Workbook workbook = new HSSFWorkbook(); // 2.创建一个工作表 Sheet sheet = workbook.createSheet("南街北巷粉丝统计表"); // 3.创建一行(1,1) Row row1 = sheet.createRow(0); // 4.创建一个单元格 // 第一行 // 1-1 Cell cell11 = row1.createCell(0); cell11.setCellValue("今日新增人数"); // 1-2 Cell cell12 = row1.createCell(1); cell12.setCellValue(520); // 第二行 // 2-1 Row row2 = sheet.createRow(1); Cell cell21 = row2.createCell(0); cell21.setCellValue("统计时间"); // 2-2 Cell cell22 = row2.createCell(1); String time = new DateTime().toString("yyyy-MM-dd HH:mm:ss"); cell22.setCellValue(time); //5\. 生成工作表(IO流) 03版本就是使用.xls结尾 FileOutputStream fileOutputStream = new FileOutputStream(PATH + "南街北巷粉丝统计表03.xls"); // 输出流 workbook.write(fileOutputStream); //关闭流 fileOutputStream.close(); System.out.println("南街北巷粉丝统计表03 生成完毕!"); } }
07版本:
@Test public void testWrite07() throws Exception { // 1.创建一个工作簿 Workbook workbook = new XSSFWorkbook(); // 2.创建一个工作表 Sheet sheet = workbook.createSheet("南街北巷粉丝统计表"); // 3.创建一行(1,1) Row row1 = sheet.createRow(0); // 4.创建一个单元格 // 第一行 // 1-1 Cell cell11 = row1.createCell(0); cell11.setCellValue("今日新增人数"); // 1-2 Cell cell12 = row1.createCell(1); cell12.setCellValue(520); // 第二行 // 2-1 Row row2 = sheet.createRow(1); Cell cell21 = row2.createCell(0); cell21.setCellValue("统计时间"); // 2-2 Cell cell22 = row2.createCell(1); String time = new DateTime().toString("yyyy-MM-dd HH:mm:ss"); cell22.setCellValue(time); //5\. 生成工作表(IO流) 07版本就是使用.xlsx结尾 FileOutputStream fileOutputStream = new FileOutputStream(PATH + "南街北巷粉丝统计表07.xlsx"); // 输出流 workbook.write(fileOutputStream); //关闭流 fileOutputStream.close(); System.out.println("南街北巷粉丝统计表07 生成完毕!"); }
注意对象的一个区别,文件后缀!
数据批量导入!
大文件写HSSF
缺点:最多只能处理65535行,否则会抛出异常!
java.lang.IllegalArgumentException: Invalid row number (65536) outside allowable range (0..65535)
优点:过程中写入缓存,不操作磁盘,最后一次写入磁盘,速度快
@Test public void testWriteBigData03() throws IOException { // 时间 Long begin = System.currentTimeMillis(); // 创建一个工作簿 Workbook workbook = new HSSFWorkbook(); // 创建表 Sheet sheet = workbook.createSheet(); // 写入数据 for (int rowNum = 0; rowNum < 65536; rowNum++) { Row row = sheet.createRow(rowNum); for (int cellNum = 0; cellNum < 10; cellNum++) { Cell cell = row.createCell(cellNum); cell.setCellValue(cellNum); } } System.out.println("over"); FileOutputStream outputStream = new FileOutputStream(PATH + "testWriteBigData03.xls"); workbook.write(outputStream); outputStream.close(); Long end = System.currentTimeMillis(); System.out.println((double) (end-begin)/1000); }
大文件写XSSF
缺点:写数据时速度非常慢,非常耗内存,也会发生内存溢出,如100万条以上
优点:可以写较大的数据量,如20万条
@Test public void testWriteBigData07() throws IOException { // 时间 Long begin = System.currentTimeMillis(); // 创建一个工作簿 Workbook workbook = new XSSFWorkbook(); // 创建表 Sheet sheet = workbook.createSheet(); // 写入数据 for (int rowNum = 0; rowNum < 65537; rowNum++) { Row row = sheet.createRow(rowNum); for (int cellNum = 0; cellNum < 10; cellNum++) { Cell cell = row.createCell(cellNum); cell.setCellValue(cellNum); } } System.out.println("over"); FileOutputStream outputStream = new FileOutputStream(PATH + "testWriteBigData07.xlsx"); workbook.write(outputStream); outputStream.close(); Long end = System.currentTimeMillis(); System.out.println((double) (end-begin)/1000); }
大文件写SXSSF
优点:可以写非常大的数据量,如100万条甚至更多条,写数据速度快,占用更少的内存
@Test public void testWriteBigDataS07() throws IOException { // 时间 Long begin = System.currentTimeMillis(); // 创建一个工作簿 Workbook workbook = new SXSSFWorkbook(); // 创建表 Sheet sheet = workbook.createSheet(); // 写入数据 for (int rowNum = 0; rowNum < 65537; rowNum++) { Row row = sheet.createRow(rowNum); for (int cellNum = 0; cellNum < 10; cellNum++) { Cell cell = row.createCell(cellNum); cell.setCellValue(cellNum); } } System.out.println("over"); FileOutputStream outputStream = new FileOutputStream(PATH + "testWriteBigDataS07.xlsx"); workbook.write(outputStream); outputStream.close(); // 清除临时文件(父类调用子类才能使用这个方法) ((SXSSFWorkbook) workbook).dispose(); Long end = System.currentTimeMillis(); System.out.println((double) (end-begin)/1000); }
注意:
过程中会产生临时文件,需要清理临时文件
默认由100条记录被保存在内存中,如果超过这数量,则最前面的数据被写入临时文件 如果想自定义内存中数据的数量,可以使用new SXSSFWorkbook ( 数量 )
SXSSFWorkbook-来至官方的解释:实现“BigGridDemo”策略的流式XSSFWorkbook版本。这允许写入 非常大的文件而不会耗尽内存,因为任何时候只有可配置的行部分被保存在内存中。
请注意,仍然可能会消耗大量内存,这些内存基于您正在使用的功能,例如合并区域,注释......... 仍然只存
储在内存中,因此如果广泛使用,可能需要大量内存。
POI-Excel读
03版本
package com.deepbluet.poimyself; import org.apache.poi.hssf.usermodel.HSSFWorkbook; 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.usermodel.Workbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.junit.jupiter.api.Test; import java.io.FileInputStream; /** * @author 南街北巷 * @data 2020/8/13 19:22 */ public class ExcelReadTest { private static String PATH = "E:\ideaProject\poi-myself\excel\"; @Test public void testRead03() throws Exception { // 获取文件流 FileInputStream inputStream = new FileInputStream(PATH + "南街北巷粉丝统计表03.xls"); // 1.创建一个工作簿,将流放入工作簿中(能够进行Excel的所有操作) Workbook workbook = new HSSFWorkbook(inputStream); // 2.获取表 Sheet sheet = workbook.getSheetAt(0); // 3.获取表中的行 Row row = sheet.getRow(0); // 4.获取表中的单元格 Cell cell = row.getCell(1); // System.out.println(cell.getStringCellValue()); // 读取时要考虑数据类型 System.out.println(cell.getNumericCellValue()); inputStream.close(); } }
07版本
@Test public void testRead07() throws Exception { // 获取文件流 FileInputStream inputStream = new FileInputStream(PATH + "南街北巷粉丝统计表07.xlsx"); // 1.创建一个工作簿,将流放入工作簿中(能够进行Excel的所有操作) Workbook workbook = new XSSFWorkbook(inputStream); // 2.获取表 Sheet sheet = workbook.getSheetAt(0); // 3.获取表中的行 Row row = sheet.getRow(0); // 4.获取表中的单元格 Cell cell = row.getCell(1); System.out.println(cell.getNumericCellValue()); inputStream.close(); }
注意:获取值的类型
读取不同的数据类型
@Test public void testCellType() throws Exception { // 获取文件流 FileInputStream inputStream = new FileInputStream(PATH + "明细表.xls"); // 1.创建一个工作簿,将流放入工作簿中(能够进行Excel的所有操作) Workbook workbook = new HSSFWorkbook(inputStream); Sheet sheet = workbook.getSheetAt(0); // 获取标题内容 Row rowTitle = sheet.getRow(0); // 首先判断标题(表头)是否为空 if (rowTitle!=null){ int cellCount = rowTitle.getPhysicalNumberOfCells(); //获取列数 for (int cellNum = 0; cellNum < cellCount; cellNum++) { Cell cell = rowTitle.getCell(cellNum); // 其次对单元格进行非空判断 if (cell!=null){ int cellType = cell.getCellType(); //获取单元格数据类型 String cellValue = cell.getStringCellValue(); System.out.print(cellValue + " | "); } } System.out.println(); } // 获取表中的内容 int rowCount = sheet.getPhysicalNumberOfRows(); 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("[" + (rowNum+1) + "-" +(cellNum+1 + "]")); Cell cell = rowData.getCell(cellNum); //可以直接转为String类型显示 // String cellString = cell.toString(); // System.out.print(cellString); // 匹配列的数据类型 if (cell!=null){ int cellType = cell.getCellType(); String cellValue = ""; switch (cellType) { case HSSFCell.CELL_TYPE_STRING: // 字符串 System.out.print("[String]"); cellValue = cell.getStringCellValue(); break; case HSSFCell.CELL_TYPE_BOOLEAN: // 布尔 System.out.print("[Boolean]"); cellValue = String.valueOf(cell.getBooleanCellValue()); break; case HSSFCell.CELL_TYPE_BLANK: // 空值 System.out.println("[Blank]"); break; case HSSFCell.CELL_TYPE_NUMERIC: // 数字(日期、普通数字) System.out.print("[Numeric]"); // POI日期时间工具类,判断是不是日期类型 if (HSSFDateUtil.isCellDateFormatted(cell)) { // 日期 System.out.print("[日期]"); Date date = cell.getDateCellValue(); // JDK自带时间工具类 cellValue = new DateTime(date).toString("yyyy-MM-dd");// Joda时间工具类 break; }else { // 不是日期格式,防止数字过长 System.out.print("[转换为字符串输出]"); cell.setCellType(HSSFCell.CELL_TYPE_STRING); cellValue = cell.toString(); } break; case HSSFCell.CELL_TYPE_ERROR: // 错误 System.out.print("[数据类型错误]"); break; } System.out.println(cellValue); } } } } inputStream.close(); }
注意:代码转换问题
计算公式
@Test public void testForMula() throws Exception{ FileInputStream inputStream =new FileInputStream(PATH + "公式.xls"); Workbook workbook = new HSSFWorkbook(inputStream); Sheet sheet = workbook.getSheetAt(0); Row row = sheet.getRow(4); Cell cell = row.getCell(0); // 拿到计算公式eval,这里直接使用HSSFFormulaEvaluator的子接口,然后参数直接使用子类的参数即可(面向接口编程) FormulaEvaluator formulaEvaluator = new HSSFFormulaEvaluator((HSSFWorkbook) workbook); // 输出单元格的内容 int cellType = cell.getCellType(); switch (cellType){ case Cell.CELL_TYPE_FORMULA: // 公式 String formula = cell.getCellFormula(); System.out.println(formula ); // 打印公式内容 // 计算 CellValue evaluate = formulaEvaluator.evaluate(cell); String cellValue = evaluate.formatAsString(); System.out.println(cellValue); // 打印公式计算出的值 break; } }
EasyExcel操作
导入依赖
<dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.2.6</version> </dependency>
写入测试
1、DemoData.java
package com.deepbluet.poimyself.easyExcel; import com.alibaba.excel.annotation.ExcelIgnore; import com.alibaba.excel.annotation.ExcelProperty; import lombok.Data; import java.util.Date; /** * @author 南街北巷 * @data 2020/8/22 5:02 */ @Data public class DemoData { @ExcelProperty("字符串标题") private String string; @ExcelProperty("日期标题") private Date date; @ExcelProperty("数字标题") private Double doubleData; /** * 忽略这个字段 */ @ExcelIgnore private String ignore; }
2、测试写入数据
package com.deepbluet.poimyself.easyExcel; import com.alibaba.excel.EasyExcel; import com.alibaba.excel.ExcelReader; import com.alibaba.excel.ExcelWriter; import com.alibaba.excel.read.metadata.ReadSheet; import com.alibaba.excel.write.metadata.WriteSheet; import org.junit.Test; import java.io.File; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * @author 南街北巷 * @data 2020/8/22 5:32 */ public class easyTest { private static String PATH = "E:\ideaProject\poi-myself\excel\"; private List<DemoData> data() { List<DemoData> list = new ArrayList<DemoData>(); 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; } // 根据list,写入Excel @Test public void simpleWrite() { String fileName = PATH + "EasyTest.xlsx"; // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭 // 如果这里想使用03 则 传入excelType参数即可 // 这里需要指定用哪个class去写,然后写到第一个sheet,名字为模板,然后文件流会自动关闭 // write(fileName,格式类) // sheet(表名) // doWrite(数据) -- 一般为list数据 // 需要传递两个参数:文件名(包括路径和后缀)和实体类 EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data()); } }
2、测试写入数据(监听器和读取操作)
监听器
package com.deepbluet.poimyself.easyExcel; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.event.AnalysisEventListener; import com.alibaba.fastjson.JSON; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; /** * @author 南街北巷 * @data 2020/8/24 3:30 */ // 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去 public class DemoDataListener extends AnalysisEventListener<DemoData> { private static final Logger LOGGER = LoggerFactory.getLogger(DemoDataListener.class); /** * 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收 */ private static final int BATCH_COUNT = 5; List<DemoData> list = new ArrayList<DemoData>(); private DemoDAO demoDAO; public DemoDataListener() { // 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数 demoDAO = new DemoDAO(); } public DemoDataListener(DemoDAO demoDAO) { this.demoDAO = demoDAO; } /** * 读取每一条数据都会来调用invoke(反射)方法 * DemoData 类型(实体类) * AnalysisContext 分析器(分析上下文) */ @Override public void invoke(DemoData data, AnalysisContext context) { LOGGER.info("解析到一条数据:{}", JSON.toJSONString(data)); // System.out.println( JSON.toJSONString(data)); list.add(data); // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM if (list.size() >= BATCH_COUNT) { saveData(); // 持久化逻辑 // 存储完成清理 list list.clear(); } } /** * 所有数据解析完成了 都会来调用 * * @param context */ @Override public void doAfterAllAnalysed(AnalysisContext context) { // 这里也要保存数据,确保最后遗留的数据也存储到数据库 saveData(); LOGGER.info("所有数据解析完成!"); } /** * 加上存储数据库 */ private void saveData() { LOGGER.info("{}条数据,开始存储数据库!", list.size()); demoDAO.save(list); LOGGER.info("存储数据库成功!"); } }
Dao层
package com.deepbluet.poimyself.easyExcel; import java.util.List; /** * @author 南街北巷 * @data 2020/8/24 3:33 * 假设这个是你的DAO存储。当然还要这个类让spring管理,当然你不用需要存储,也不需要这个类。 **/ public class DemoDAO { public void save(List<DemoData> list) { // 持久化操作 // 如果是mybatis,尽量别直接调用多次insert,自己写一个mapper里面新增一个方法batchInsert,所有数据一次性插入 } }
EasyTest
package com.deepbluet.poimyself.easyExcel; import com.alibaba.excel.EasyExcel; import com.alibaba.excel.ExcelReader; import com.alibaba.excel.ExcelWriter; import com.alibaba.excel.read.metadata.ReadSheet; import com.alibaba.excel.write.metadata.WriteSheet; import org.junit.Test; import java.io.File; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * @author 南街北巷 * @data 2020/8/22 5:32 */ public class EasyTest { private static String PATH = "E:\ideaProject\poi-myself\excel\"; private List<DemoData> data() { List<DemoData> list = new ArrayList<DemoData>(); 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; } // 根据list,写入Excel @Test public void simpleWrite() { String fileName = PATH + "EasyTest.xlsx"; // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭 // 如果这里想使用03 则 传入excelType参数即可 // 这里需要指定用哪个class去写,然后写到第一个sheet,名字为模板,然后文件流会自动关闭 // write(fileName,格式类) // sheet(表名) // doWrite(数据) -- 一般为list数据 // 需要传递两个参数:文件名(包括路径和后缀)和实体类 EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data()); } // 读取监听器监听到的Excel内容 @Test public void simpleRead() { // 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去 // 写法1: String fileName = PATH + "EasyTest.xlsx"; // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭 EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead(); } }
总结
这两种框架,操作Excel方式的共同点为:
- 写入:固定格式进行写入;
- 读取:根据监听器设置的规则进行读取。