【狂神说Java】POI技术详解

     阅读:62

狂神视频地址

https://www.bilibili.com/video/BV1Ua4y1x7BK


1、常用信息

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

  (2)、将Excel表中的信息录入到网站数据库(习题上传…)

  (3)开发中经常会设计到excel的处理,如导出Excel,导入Excel到数据库中!操作Excel目前比较流行的就是Apache POI和阿里巴巴的easyExcel !

是什么:组件,工具

  Apache POI 是用Java编写的免费开源的跨平台的 Java API,Apache POI提供API给Java程式对Microsoft Office格式档案读和写的功能。


2、Apache POI

官网:

https://poi.apache.org/

在这里插入图片描述


3、easyExcel

官网:

https://www.yuque.com/easyexcel/doc/easyexcel

  Java领域解析,生成Excel比较有名的框架有Apache poi,jxl等,但他们都存在一个严重的问题就是非常的耗内存,如果你的系统并发量不大的话可能还行,但是一旦并发上来后一定会OOM或者JVM频繁的full gc.

  EasyExcel是阿里巴巴开源的一个excel处理框架,以使用简单,节省内存著称,EasyExcel能大大减少占用内存的主要原因是在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析。

  EasyExcel采用一行一行的解析模式,并将一行的解析结果以观察者的模式通知处理(AnalysisEventListener)。

在这里插入图片描述


4、xls和xlsx区别

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

最大行列得数量不同:

  xls最大只有65536行、256列

  xlsx可以有1048576行、16384列

        <!--xls(03)-->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.9</version>
        </dependency>
        
        <!--xlsx(07)-->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>3.9</version>
        </dependency>

poi 操作xls的
poi-ooxml 操作xlsx的

操作的版本不同,使用的工具类也不同

在这里插入图片描述
工作簿:
工作表:
行:
列:


5、POI Excel 写 03和07版本方式

5.1小数据量

package cn.bloghut;

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.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.Date;

/**
 * @Classname ExcelWrite
 * @Description TODO
 * @Date 2022/1/7 12:41
 * @Created by 闲言
 */
public class ExcelWrite {

    String PATH = "G:\\狂\\POIStudy\\xy-poi";

    /**
     * 写工作簿 03版本
     */
    @Test
    public void Write03() throws Exception {
        //1.创建一个工作簿
        Workbook workbook = new HSSFWorkbook();
        //2.创建 一个工作表
        Sheet sheet = workbook.createSheet("闲言粉丝统计表");
        //3.创建一行
        Row row1 = sheet.createRow(0);
        //4.创建一个单元格
        //(1,1)
        Cell cell1 = row1.createCell(0);
        cell1.setCellValue("今日新增观众");
        //(1,2)
        Cell cell2 = row1.createCell(1);
        cell2.setCellValue(666);

        //创建第二行
        Row row2 = sheet.createRow(1);
        //(2,1)
        Cell cell21 = row2.createCell(0);
        cell21.setCellValue("统计时间");
        //(2,2)
        Cell cell22 = row2.createCell(1);
        String datetime = new DateTime().toString("yyyy-MM-dd HH:mm:ss");
        cell22.setCellValue(datetime);

        //生成一张表(IO流),03版本就是使用xls结尾
        FileOutputStream fos = new FileOutputStream(PATH + "闲言观众统计表03.xls");
        //输出
        workbook.write(fos);
        //关闭流
        fos.close();
        System.out.println("文件生成完毕");
    }

    /**
     * 写工作簿 07版本
     */
    @Test
    public void Write07() throws Exception {
        //1.创建一个工作簿
        Workbook workbook = new XSSFWorkbook();
        //2.创建 一个工作表
        Sheet sheet = workbook.createSheet("闲言粉丝统计表");
        //3.创建一行
        Row row1 = sheet.createRow(0);
        //4.创建一个单元格
        //(1,1)
        Cell cell1 = row1.createCell(0);
        cell1.setCellValue("今日新增观众");
        //(1,2)
        Cell cell2 = row1.createCell(1);
        cell2.setCellValue(666);

        //创建第二行
        Row row2 = sheet.createRow(1);
        //(2,1)
        Cell cell21 = row2.createCell(0);
        cell21.setCellValue("统计时间");
        //(2,2)
        Cell cell22 = row2.createCell(1);
        String datetime = new DateTime().toString("yyyy-MM-dd HH:mm:ss");
        cell22.setCellValue(datetime);

        //生成一张表(IO流),03版本就是使用xlsx结尾
        FileOutputStream fos = new FileOutputStream(PATH + "闲言观众统计表07.xlsx");
        //输出
        workbook.write(fos);
        //关闭流
        fos.close();
        System.out.println("文件生成完毕");
    }
}

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


5.2大文件写HSSF(03)

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

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

在这里插入图片描述

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

@Test
    public void Write03BigData() throws Exception{

        //时间
        long begin = System.currentTimeMillis();
        //1.创建一个工作簿
       Workbook workbook = new HSSFWorkbook();
        //2.创建一个表
        Sheet sheet = workbook.createSheet("第一页");
        //写入数据
        for (int rowNum = 0;rowNum<65536;rowNum++){
            //3.创建行
            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流
        FileOutputStream fos = new FileOutputStream(PATH+"Write03BigData.xls");
        //生成一张表
        workbook.write(fos);
        fos.close();
        long end = System.currentTimeMillis();
        System.out.println("耗时:"+(end-begin));
    }

结果:
在这里插入图片描述


5.3大文件写XSSF(07)

缺点:写数据时速度非常慢,非常耗内存,也会发生内存溢出,如100万条。
优点:可以写较大数据量,如20万条

    @Test
    public void Write07BigData() throws Exception{

        //时间
        long begin = System.currentTimeMillis();
        //1.创建一个工作簿
        Workbook workbook = new XSSFWorkbook();
        //2.创建一个表
        Sheet sheet = workbook.createSheet("第一页");
        //写入数据
        for (int rowNum = 0;rowNum<65537;rowNum++){
            //3.创建行
            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流
        FileOutputStream fos = new FileOutputStream(PATH+"Write03BigData.xlsx");
        //生成一张表
        workbook.write(fos);
        fos.close();
        long end = System.currentTimeMillis();
        System.out.println("耗时:"+(end-begin));
    }

结果:
在这里插入图片描述


5.4大文件写SXSSF

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

注意:

  1. 过程中会产生临时文件,需要清理临时文件
  2. 默认由100条记录被保存在内存中,如果超过这数量,则最前面的数据被写入临时文件
  3. 如果想自定义内存中数据的数量,可以使用new SXSSFWorkbook(数量)
@Test
    public void Write07BigDataS() throws Exception{

        //时间
        long begin = System.currentTimeMillis();
        //1.创建一个工作簿
        Workbook workbook = new SXSSFWorkbook();
        //2.创建一个表
        Sheet sheet = workbook.createSheet("第一页");
        //写入数据
        for (int rowNum = 0;rowNum<100000;rowNum++){
            //3.创建行
            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流
        FileOutputStream fos = new FileOutputStream(PATH+"Write03BigDataS.xlsx");
        //生成一张表
        workbook.write(fos);
        fos.close();
        //清除临时文件
        ((SXSSFWorkbook) workbook).dispose();
        long end = System.currentTimeMillis();
        System.out.println("耗时:"+(end-begin));
    }

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

  请注意,仍然可能会消耗大量内存,这些内存基于您正在使用的功能,例如合并区域,注.…….当然只存储在内存中,因此如果广泛使用,可能需要大量内存。
再使用POI的时候!内存问题Jprofile !


6、POI Excel读 03和07版本方式

6.1 (03版本)

 @Test
    public  void Read03() throws Exception{
        //1.获取文件流
        FileInputStream fis = new FileInputStream(PATH+"xy-poi闲言观众统计表03.xls");
        //2.创建一个工作簿。使用excel能操作的这边都可以操作!
        Workbook workbook = new HSSFWorkbook(fis);
        //3.获取表
        Sheet sheet = workbook.getSheetAt(0);
        //4.获取第一行
        Row row1 = sheet.getRow(0);
        //5.获取第一列
        Cell cell1 = row1.getCell(0);
        //6.获取第一行第一列的值
        String stringCellValue = cell1.getStringCellValue();
        //获取第二列
        Cell cell2 = row1.getCell(1);
        //获取第一行第二列的值
        double numericCellValue = cell2.getNumericCellValue();
        System.out.println(stringCellValue+" | "+numericCellValue);

        fis.close();
    }

  注意获取值的类型即可


6.2(07版本)

@Test
    public  void Read07() throws Exception{
        //1.获取文件流
        FileInputStream fis = new FileInputStream(PATH+"xy-poi闲言观众统计表07.xlsx");
        //2.创建一个工作簿。使用excel能操作的这边都可以操作!
        Workbook workbook = new XSSFWorkbook(fis);
        //3.获取表
        Sheet sheet = workbook.getSheetAt(0);
        //4.获取第一行
        Row row1 = sheet.getRow(0);
        //5.获取第一列
        Cell cell1 = row1.getCell(0);
        //6.获取第一行第一列的值
        String stringCellValue = cell1.getStringCellValue();
        //获取第二列
        Cell cell2 = row1.getCell(1);
        //获取第一行第二列的值
        double numericCellValue = cell2.getNumericCellValue();
        System.out.println(stringCellValue+" | "+numericCellValue);
        fis.close();
    }

  注意获取值的类型即可


6.3读取不同的数据类型

    @Test
    public void CellType03() throws Exception{
        //获取文件流
        FileInputStream fis = new FileInputStream(PATH+"明显表.xls");
        //获取一个工作簿
        Workbook workbook = new HSSFWorkbook(fis);
        //获取一个工作表
        Sheet sheet = workbook.getSheetAt(0);
        //获取第一行内容
        Row row = sheet.getRow(0);
        if (row != null){
            //获取所有的列
            int Cells = row.getPhysicalNumberOfCells();
            for (int col = 0;col < Cells;col++){
                //获取当前列
                Cell cell = row.getCell(col);
                if (cell != null){
                    //获取当前行的第 col 列的值
                    String cellValue = cell.getStringCellValue();
                    System.out.print(cellValue+" | ");
                }
            }
        }
        //获取标准的内容
        //获取有多少行
        int rowCount = sheet.getPhysicalNumberOfRows();
        //从1开始,第一行是标题
        for (int rowNum = 1;rowNum < rowCount;rowNum++){
            Row rowData = sheet.getRow(rowNum);
            if (rowData != null){
                //获取当前行的列数
                int cellCount = rowData.getPhysicalNumberOfCells();
                System.out.println();
                for (int col = 0;col < cellCount;col++){
                    //获取当前列的值
                    Cell cellData = rowData.getCell(col);
                    //打印当前行当前列的值
                    System.out.print("["+(rowNum+1)+"-"+(col+1)+"]");
                    //匹配列的类型
                    if (cellData != null){
                        //获取列的类型
                        int cellType = cellData.getCellType();
                        String cellValue = "";
                        switch (cellType){
                            case  Cell.CELL_TYPE_STRING://字符串
                                System.out.print("[string]");
                                cellValue = cellData.getStringCellValue();
                                break;
                            case  Cell.CELL_TYPE_BOOLEAN://布尔
                                System.out.print("[boolean]");
                                cellValue = String.valueOf(cellData.getBooleanCellValue());
                                break;
                            case  Cell.CELL_TYPE_BLANK://System.out.print("[blank]");
                                break;
                            case  Cell.CELL_TYPE_NUMERIC://数字(日期、普通数字)
                                System.out.print("[numeric]");
                                if (HSSFDateUtil.isCellDateFormatted(cellData)){
                                    //如果是日期
                                    System.out.print("[日期] ");
                                    Date date = cellData.getDateCellValue();
                                    cellValue = new  DateTime(date).toString("yyyy-MM-dd HH:mm:ss");
                                }else {
                                    //不是日期格式,防止数字过长
                                    System.out.print("[转换字符串输出] ");
                                    //转为字符串
                                    cellData.setCellType(HSSFCell.CELL_TYPE_STRING);
                                    cellValue = cellData.toString();

                                }
                                break;
                            case  Cell.CELL_TYPE_ERROR://错误
                                System.out.print("[error]");
                                break;
                        }
                        System.out.print("["+cellValue+"]\n");
                    }
                }
            }
        }

        System.out.println();
        System.out.println("over");
        fis.close();
    }

  如果是07版本的Excel ,只需要将HSSFWorkbook类修改为XSSFWorkbook类。将xls文件修改为xlsx文件即可


测试:读取以下表格内容

在这里插入图片描述


结果:
在这里插入图片描述


7 EasyExcel操作

7.1导入依赖

        <!--easyExcel-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.2.0-beta2</version>
        </dependency>

7.2写入测试

1.格式类

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

2.写的方法

@Test
    public  void simpleWrite(){
        // 写法1
        String fileName = PATH+"EasyTest.xlsx";
        //write(fileName,格式类)
        //sheet(表名)
        //doWrite(数据)
        EasyExcel.write(fileName,DemoData.class).sheet("模板").doWrite(data());

    }

结果

在这里插入图片描述


固定套路:
1、写入:固定类格式进行写入
2、读取:根据监听器设置的规则进行读取

7.3读测试

演示读取以下excel表格数据
在这里插入图片描述

1.格式类

@Getter
@Setter
@EqualsAndHashCode
public class DemoData {
    private String string;
    private Date date;
    private Double doubleData;
}

2.监听器

package cn.bloghut.esay;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.fastjson.JSON;

import java.util.ArrayList;
import java.util.List;

// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
public class DemoDataListener extends AnalysisEventListener<DemoData> {

    private static final int BATCH_COUNT = 100;
    private List<DemoData> cachedDataList = new ArrayList<>(BATCH_COUNT);

    private DemoDAO demoDAO;

    public DemoDataListener() {
        // 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数
        demoDAO = new DemoDAO();
    }

    public DemoDataListener(DemoDAO demoDAO) {
        this.demoDAO = demoDAO;
    }

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

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

    /**
     * 加上存储数据库
     */
    private void saveData() {
        System.out.println("{}条数据,开始存储数据库!"+cachedDataList.size());
        demoDAO.save(cachedDataList);
        System.out.println("存储数据库成功");
    }

}

DAO类(不操作数据库,用不到)

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

测试

@Test
    public void simpleRead() throws Exception{
        String fileName = PATH+"EasyTest.xlsx";
        EasyExcel.read(fileName,DemoData.class,new DemoDataListener()).sheet().doRead();

    }

结果
在这里插入图片描述