POI 和 EasyExcel

常用信息

1、将用户信息导出为 excel 表格(导出数据....)

2、将 Excel 表中的信息录入到网站数据库(习题上传....)大大减轻网站录入量!

开发中经常会设计到 excel 的处理,如导出 Excel,导入 Excel 到数据库中!

操作 Excel 目前比较流行的就是 Apache POI 和阿里巴巴的 easyExcel!

Apache POI

Apache POI 官网:Apache POI - the Java API for Microsoft Documents

 easyExcel

easyExcel官网地址:https://github.com/alibaba/easyexcel

EasyExcel 是阿里巴巴开源的一个 excel 处理框架,以使用简单、节省内存著称

EasyExcel 能大大减少占用内存的主要原因是在解析 Excel 时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析。

内存问题:POI = 100w 先加载到内存 OOM。。在写文件 es=1

下图是 EasyExcel 和 POI 在解析 Excel时的对比图。

官方文档:EasyExcel(文档已经迁移) · 语雀 

POI-Excel写

创建项目

1、建立一个空项目,创建普通 Maven 的 Moudle kuang-poi

2、引入 pom 依赖

<!-- 导入依赖 -->
<dependencies>
  <!-- Java 万物皆对象 -->
  <!-- xls(03) -->
  <dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>3.9</version>
  </dependency>

  <!-- xls(07) -->
  <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.1</version>
  </dependency>

  <!-- test -->
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
  </dependency>
</dependencies>

03|07 版本的写,就是对象不同,方法一样的!

需要注意:2003 版本和 2007 版本存在兼容性的问题!03 最多只有 65535 行!

1、工作簿: 2、工作表: 3、行: 4、列:

03 版本:

String PATH = "/Users/zhang/Desktop/gitee/excel/zhang-poi";

@Test
public void testWirate03() throws IOException {
  // 1、创建一个工作簿    03
  Workbook workbook = new HSSFWorkbook();
  // 2、创建一个工作表
  Sheet sheet = workbook.createSheet("狂神观众统计表");
  // 3、创建一个行      0 代表第一行             (1,1)
  Row row1 = sheet.createRow(0);
  // 4、创建一个单元格    0 代表第一个单元格
  Cell cell11 = row1.createCell(0);
  // 去格子里边写入一个数据      第一行的第一列     (1,1)
  cell11.setCellValue("今日新增观众");
  // 5、创建第二个单元格    1 代表第二个单元格
  Cell cell12 = row1.createCell(1);
  // 去格子里边写入一个数据   第一行的第二列        (1,2)
  cell12.setCellValue(666);

  // 第二行
  Row row2 = sheet.createRow(1);
  // 第二行的第一列      (2,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);

  // 生成一张表(IO 流)  03 版本就是使用 xls 结尾!
  FileOutputStream fileOutputStream = new FileOutputStream(PATH + "狂神观众统计表03.xls");
  // 输出
  workbook.write(fileOutputStream);
  // 关闭流
  fileOutputStream.close();

  System.out.println("狂神观众统计表03 生成完毕!");
}

07 版本:

String PATH = "/Users/zhang/Desktop/gitee/excel/zhang-poi";
 
@Test
public void testWirate07() throws IOException {
  // 1、创建一个工作簿    07
  Workbook workbook = new XSSFWorkbook();
  // 2、创建一个工作表
  Sheet sheet = workbook.createSheet("狂神观众统计表");
  // 3、创建一个行      0 代表第一行             (1,1)
  Row row1 = sheet.createRow(0);
  // 4、创建一个单元格    0 代表第一个单元格
  Cell cell11 = row1.createCell(0);
  // 去格子里边写入一个数据      第一行的第一列     (1,1)
  cell11.setCellValue("今日新增观众");
  // 5、创建第二个单元格    1 代表第二个单元格
  Cell cell12 = row1.createCell(1);
  // 去格子里边写入一个数据   第一行的第二列        (1,2)
  cell12.setCellValue(777);

  // 第二行
  Row row2 = sheet.createRow(1);
  // 第二行的第一列      (2,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);

  // 生成一张表(IO 流)  07 版本就是使用 xlsx 结尾!
  FileOutputStream fileOutputStream = new FileOutputStream(PATH + "狂神观众统计表07.xlsx");
  // 输出
  workbook.write(fileOutputStream);
  // 关闭流
  fileOutputStream.close();

  System.out.println("狂神观众统计表07 生成完毕!");

}

注意对象的一个区别,文件后缀

大文件写 HSSF

缺点:最多只能处理 65536 行,否则会抛出异常

java.lang.IllegalArgumentException: Invalid row number (65536) outside allowable range (0..65535)

优点:过程写入缓存,不操作磁盘,最后一次性写入磁盘,速度快

String PATH = "/Users/zhang/Desktop/gitee/excel/zhang-poi";

@Test
public void testWirate03BigData() throws IOException {
  // 获取当前时间
  long begin = System.currentTimeMillis();

  // 创建工作簿
  Workbook workook = new HSSFWorkbook();
  // 创建工作表
  Sheet sheet = workook.createSheet();
  // 写入数据                超过 65536 行就会报错   java.lang.IllegalArgumentException: Invalid row number (65536) outside allowable range (0..65535)
  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");

  // 生成一张表(io 流)      03  版本就是使用  xls 结尾!
  FileOutputStream fileOutputStream = new FileOutputStream(PATH + "testWirate03BigData.xls");
  // 输出
  workook.write(fileOutputStream);
  // 关闭流
  fileOutputStream.close();
  long end = System.currentTimeMillis();
  double time = (double) (end - begin) / 1000;
  System.out.println("时间差为:" + time);
}

大文件写 XSSF

缺点:写数据时速度非常慢,非常耗内存,也会发生内存溢出,如 100 万条

优点:可以写较大的数据量,如 20 万条

String PATH = "/Users/zhang/Desktop/gitee/excel/zhang-poi";

// 耗时较长!    优化,缓存
@Test
public void testWirate07BigData() throws IOException {
  // 获取当前时间
  long begin = System.currentTimeMillis();

  // 创建工作簿
  Workbook workook = new XSSFWorkbook();
  // 创建工作表
  Sheet sheet = workook.createSheet();
  // 写入数据
  for (int rowNum = 0; rowNum < 100000; 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");

  // 生成一张表(io 流)      07  版本就是使用  xlsx 结尾!
  FileOutputStream fileOutputStream = new FileOutputStream(PATH + "testWirate07BigData2.xlsx");
  // 输出
  workook.write(fileOutputStream);
  // 关闭流
  fileOutputStream.close();
  long end = System.currentTimeMillis();
  double time = (double) (end - begin) / 1000;
  System.out.println("时间差为:" + time);
}

大文件写 SXSSF

优点:可以写非常大的数据量,如 100 万条甚至更多条,写数据速度更快,占用更少内存

注意:

过程中会产生临时文件,需要清理临时文件

默认由 100 条记录被保存在内存中,如果超过这数量,则最前面的数据被写入临时文件

如果想自定义内存中的数量,可以使用 new SXSSFWorkbook(数量)

String PATH = "/Users/zhang/Desktop/gitee/excel/zhang-poi";

@Test
public void testWirate07BigDataS() throws IOException {
  // 获取当前时间
  long begin = System.currentTimeMillis();

  // 创建工作簿
  Workbook workbook = new SXSSFWorkbook();
  // 创建工作表
  Sheet sheet = workbook.createSheet();
  // 写入数据
  for (int rowNum = 0; rowNum < 1000000; 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");

  // 生成一张表(io 流)      07  版本就是使用  xlsx 结尾!
  FileOutputStream fileOutputStream = new FileOutputStream(PATH + "testWirate07BigDataS2.xlsx");
  // 输出
  workbook.write(fileOutputStream);
  // 关闭流
  fileOutputStream.close();
  // 清除临时文件!
  ((SXSSFWorkbook) workbook).dispose();
  long end = System.currentTimeMillis();
  double time = (double) (end - begin) / 1000;
  System.out.println("时间差为:" + time);
}

SXSSFWorkbook 来自官方的解释:实现"BigGridDemo"策略的流式 XSSFWorkbook 版本。这允许写入非常大的文件而不会耗尽内存,因为任何时候只有可配置的行部分被保存在内存中。

请注意:仍然可能会消耗大量内存,这些内存基于您正在使用的功能,例如合并区域,注释......仍然只存储在内存中,因此如果广泛使用,可能需要大量内存。

再使用 POI 的时候!内存问题 Jprofile

POI-Excel读

03|07

03 版本

String PATH = "/Users/zhang/Desktop/gitee/excel/";   //  zhang-poi

@Test
public void testRead03() throws IOException {

  // 获取文件流
  FileInputStream fileInputStream = new FileInputStream(PATH + "zhang-poi狂神观众统计表03.xls");
  // 1、   创建一个工作簿   将文件流放到工作簿里        使用 excel 能操作的这边也都可以操作
  Workbook workbook = new HSSFWorkbook(fileInputStream);
  // 2、得到表    获取第 1 个表
  Sheet sheetAt = workbook.getSheetAt(0);
  // 3、得到行    获取第 1 行
  Row row = sheetAt.getRow(0);
  // 4、得到列    获取第一个单元格里边的数据
  Cell cell = row.getCell(1);

  // 读取值的时候一定要注意类型,否则就会失败

  // 取出 cell 的值   getStringCellValue()  获取字符串类型
  //        System.out.println("cell.getStringCellValue() = " + cell.getStringCellValue());
  System.out.println("cell.getNumericCellValue() = " + cell.getNumericCellValue());
  fileInputStream.close();

}

07 版本

String PATH = "/Users/zhang/Desktop/gitee/excel/";   //  zhang-poi

@Test
public void testRead07() throws IOException {

  // 获取文件流
  FileInputStream fileInputStream = new FileInputStream(PATH + "zhang-poi狂神观众统计表07.xlsx");
  // 1、   创建一个工作簿   将文件流放到工作簿里        使用 excel 能操作的这边也都可以操作
  Workbook workbook = new XSSFWorkbook(fileInputStream);
  // 2、得到表    获取第 1 个表
  Sheet sheetAt = workbook.getSheetAt(0);
  // 3、得到行    获取第 1 行
  Row row = sheetAt.getRow(0);
  // 4、得到列    获取第一个单元格里边的数据
  Cell cell = row.getCell(1);

  // 读取值的时候一定要注意类型,否则就会失败

  // 取出 cell 的值   getStringCellValue()  获取字符串类型
  //        System.out.println("cell.getStringCellValue() = " + cell.getStringCellValue());
  System.out.println("cell.getNumericCellValue() = " + cell.getNumericCellValue());
  fileInputStream.close();

}

注意获取值得类型即可

读不同的数据类型(最麻烦的就是这里)

String PATH = "/Users/zhang/Desktop/gitee/excel/";   //  zhang-poi

@Test
public void testCellType() throws IOException {
  // 获取文件流
  FileInputStream fileInputStream = new FileInputStream(PATH + "明细表.xls");
  // 1、   创建一个工作簿   将文件流放到工作簿里        使用 excel 能操作的这边也都可以操作
  Workbook workbook = new HSSFWorkbook(fileInputStream);
  // 2、得到表    获取第 1 个表
  Sheet sheetAt = workbook.getSheetAt(0);
  // 获取标题内容
  Row rowTitle = sheetAt.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 = sheetAt.getPhysicalNumberOfRows();
  for (int rowNum = 1; rowNum < rowCount; rowNum++) {
    // 获取行里边的内容
    Row rowData = sheetAt.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);
        // 匹配列的数据类型
        if (cell != null) {
          int cellType = cell.getCellType();
          String cellValue = "";
          switch (cellType){
            case Cell.CELL_TYPE_STRING:    // 字符串
              System.out.print("【String】");
              cellValue = cell.getStringCellValue();
              break;

            case Cell.CELL_TYPE_BOOLEAN:    // 布尔类型
              System.out.print("【Boolean】");
              cellValue = String.valueOf(cell.getBooleanCellValue());
              break;

            case Cell.CELL_TYPE_BLANK:    // 为空
              System.out.print("【Blank】");
              break;

            case Cell.CELL_TYPE_NUMERIC:    // 数字(日期、普通数字)类型
              System.out.print("【Numeric】");
              // 判断当前列是不是时间日期
              if (HSSFDateUtil.isCellDateFormatted(cell)) {
                System.out.print("【日期】");
                Date date = cell.getDateCellValue();
                cellValue = new DateTime(date).toString("yyyy-MM-dd");
              }else {
                // 不是日期格式,防止数字过长
                System.out.print("【转换为字符串输出】");
                cell.setCellType(Cell.CELL_TYPE_STRING);
                cellValue = cell.toString();
              }
              break;

            case Cell.CELL_TYPE_ERROR:    // 布尔类型
              System.out.print("【数据类型错误】");
              cellValue = String.valueOf(cell.getErrorCellValue());
              break;

            case Cell.CELL_TYPE_FORMULA:  // 公式类型,代表这个cell存的是一个公式
              System.out.print("【公式】");
              // 6、获取计算公式
              FormulaEvaluator formulaEvaluator = new HSSFFormulaEvaluator((HSSFWorkbook) workbook);
              // 8、得到第 5 行第 1 列       计算公式的内容
              String formula = cell.getCellFormula();
              // formula = SUM(A2:A4)
              System.out.println("formula = " + formula);

              // 9、计算得到值  把获取到第 5 行,第 1 列的内容放入到
              CellValue evaluate = formulaEvaluator.evaluate(cell);
              // 10、将的到的内容转化为字符串
              cellValue = evaluate.formatAsString();
              // cellValue = 600.0
              // System.out.println("cellValue = " + cellValue);
              break;
          }
          System.out.println(cellValue);
        }
      }
    }
  }
  fileInputStream.close();
}

计算公式(了解即可)

// 计算公式
@Test
public void testFormula() throws IOException {
  // 1、获取文件流        通过文件路径 加 文件名
  FileInputStream fileInputStream = new FileInputStream( PATH + "公式表.xls" );
  // 2、创建一个工作簿,并把文件流放入工作簿当中
  Workbook workbook = new HSSFWorkbook(fileInputStream);
  // 3、得到表    获取第一个表
  Sheet sheetAt = workbook.getSheetAt(0);

  // 4、获取到第 5 行的内容
  Row row = sheetAt.getRow(4);
  // 5、获取第 5 行,第 1 列的内容
  Cell cell = row.getCell(0);

  // 6、获取计算公式
  FormulaEvaluator formulaEvaluator = new HSSFFormulaEvaluator((HSSFWorkbook) workbook);
  // 7、匹配列的数据类型   输出单元格的内容
  int cellType = cell.getCellType();
  switch (cellType){
    case Cell.CELL_TYPE_FORMULA:    // 公式
      // 8、得到第 5 行第 1 列       计算公式的内容
      String formula = cell.getCellFormula();
      // formula = SUM(A2:A4)
      System.out.println("formula = " + formula);

      // 9、计算得到值  把获取到第 5 行,第 1 列的内容放入到
      CellValue evaluate = formulaEvaluator.evaluate(cell);
      // 10、将的到的内容转化为字符串
      String cellValue = evaluate.formatAsString();
      // cellValue = 600.0
      System.out.println("cellValue = " + cellValue);
      break;
  }

}

EasyExcel操作

导入依赖

<!-- easyexcel -->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>easyexcel</artifactId>
  <version>3.1.1</version>
</dependency>

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>1.2.79</version>
</dependency>

写入测试

1、DemoData.java

package com.zhang.easyexcelpojo;

import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

import java.util.Date;

@Data
public class DemoData {
    @ExcelProperty("字符串标题")
    private String string;
    @ExcelProperty("日期标题")
    private Date date;
    @ExcelProperty("数字标题")
    private Double doubleData;
    /**
     * 忽略这个字段
     */
    @ExcelIgnore
    private String ignore;
}

2、测试写入数据:

String PATH = "/Users/zhang/Desktop/gitee/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参数即可
  //write(fileName,格式类)
  //sheet(表名)
  //doWrite(数据),真实的就是从数据库中读取的或前端传过来的
  EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());
}

3、最终的结果:

读取测试

测试读取:

1、创建类 DemoDataListener 监听器:

package com.zhang.easyexcelpojo;

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;

// 有个很重要的点 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() {
        demoDAO = new DemoDAO();
    }
    public DemoDataListener(DemoDAO demoDAO) {
        this.demoDAO = demoDAO;
    }

    // 读取数据 会执行 invoke 方法    这个每一条数据解析都会来调用
    // DemoData 类型
    // AnalysisContext 分析上下文
    @Override
    public void invoke(DemoData data, AnalysisContext context) {
        System.out.println(JSON.toJSONString(data));
        list.add(data);
        System.out.println(list);
        // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
        if (list.size() >= BATCH_COUNT) {
            saveData();     //
            // 存储完成清理 list
            list.clear();
        }
    }

    // 所有数据解析完成了 都会来调用
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 这里也要保存数据,确保最后遗留的数据也存储到数据库
        saveData();
        LOGGER.info("所有数据解析完成!");
    }

    // 加上存储数据库
    private void saveData() {
        demoDAO.save(list);
    }
}

具体操作:

2、持久层DemoDAO:

package com.zhang.easyexcelpojo;

import java.util.List;

/**
 * 假设这个是你的DAO存储。当然还要这个类让spring管理,当然你不用需要存储,也不需要这个类。
 **/
public class DemoDAO {

    public void save(List<DemoData> list) {
        // 持久化操作
        // 如果是mybatis,尽量别直接调用多次insert,自己写一个mapper里面新增一个方法batchInsert,所有数据一次性插入
    }

}

 3、测试:

// 最简单的读
@Test
public void simpleRead() {
  String fileName = PATH + "EasyTest.xlsx";
  // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
  EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();
}

4、结果: 

固定套路:

1、写入、固定类格式进行写入

2、读取、根据监听器设置的规则进行读取!

学习方式

了解,面向对象的思想,学会面向接口编程!

理解使用测试 API!

可以把EasyExcel的所有 api 测试一下(2-3小时!)

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐