Springboot项目中使用POI操作Excel(详细教程系列3/3)

文章目录

1、Excel百万数据导出

1.1、概述

Excel可以分为早期的Excel2003版本(使用POI的HSSF对象操作)和Excel2007版本(使用POI的XSSF操作),两者对百万数据的支持如下:

  • Excel 2003:在POI中使用HSSF对象时,excel 2003最多只允许存储65536条数据,一般用来处理较少的数据量。这时对于百万级别数据,Excel肯定容纳不了。

  • Excel 2007:当POI升级到XSSF对象时,它可以直接支持excel2007以上版本,因为它采用ooxml格式。这时excel可以支持1048576条数据,单个sheet表就支持近百万条数据。但实际运行时还可能存在问题,原因是执行POI报表所产生的行对象,单元格对象,字体对象,他们都不会销毁,这就导致OOM的风险。

1.2、解决方案分析

对于百万数据量的Excel导入导出,只讨论基于Excel2007的解决方法。在ApachePoi 官方提供了对操作大数据量的导入导出的工具和解决办法,操作Excel2007使用XSSF对象,可以分为三种模式:

  1. java代码解析xml

  2. dom4j:一次性加载xml文件再解析

  3. SAX:逐行加载,逐行解析

  • 用户模式:用户模式有许多封装好的方法操作简单,但创建太多的对象,非常耗内存(之前使用的方法)

  • 事件模式:基于SAX方式解析XML,SAX全称Simple API for XML,它是一个接口,也是一个软件包。它是一种XML解析的替代方法,不同于DOM解析XML文档时把所有内容一次性加载到内存中的方式,它逐行扫描文档,一边扫描,一边解析。

  • SXSSF对象:是用来生成海量excel数据文件,主要原理是借助临时存储空间生成excel

1.3、原理分析

在实例化SXSSFWorkBook这个对象时,可以指定在内存中所产生的POI导出相关对象的数量(默认100),一旦内存中的对象的个数达到这个指定值时,就将内存中的这些对象的内容写入到磁盘中(XML的文件格式),就可以将这些对象从内存中销毁,以后只要达到这个值,就会以类似的处理方式处理,直至Excel导出完成。

1.4、百万数据的导出

1.4.1、模拟数据
  1. 创建表:
sql 复制代码
CREATE TABLE `tb_user2` (
  `id` bigint(20) NOT NULL  COMMENT '用户ID',
  `user_name` varchar(100) DEFAULT NULL COMMENT '姓名',
  `phone` varchar(15) DEFAULT NULL COMMENT '手机号',
  `province` varchar(50) DEFAULT NULL COMMENT '省份',
  `city` varchar(50) DEFAULT NULL COMMENT '城市',
  `salary` int(10) DEFAULT NULL,
  `hire_date` datetime DEFAULT NULL COMMENT '入职日期',
  `dept_id` bigint(20) DEFAULT NULL COMMENT '部门编号',
  `birthday` datetime DEFAULT NULL COMMENT '出生日期',
  `photo` varchar(200) DEFAULT NULL COMMENT '照片路径',
  `address` varchar(300) DEFAULT NULL COMMENT '现在住址' 
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  1. 创建存储过程
sql 复制代码
CREATE DEFINER=`root`@`localhost` PROCEDURE `test_insert`()
BEGIN
	DECLARE n int DEFAULT 1;				    -- 定义变量n=1
	SET AUTOCOMMIT=0;						    -- 取消自动提交
	
		while n <= 5000000 do					
			INSERT INTO `tb_user2` VALUES ( n, CONCAT('测试', n), '13800000001', '北京市', '北京市', '11000', '2001-03-01 21:18:29', '1', '1981-03-02 00:00:00', '\\static\\user_photos\\1.jpg', '北京市西城区宣武大街1号院');
			SET n=n+1;
		END while;
		COMMIT;
END
  1. 开始执行
sql 复制代码
CALL test_insert();

插入500W数据大概需要200至300秒左右

sql 复制代码
SELECT COUNT(0)	FROM tb_user2;
1.4.2、思路分析

导出时使用的是SXSSFWorkBook这个类,一个工作表sheet最多只能放1048576行数据, 当我们的业务数据已超过100万了,一个sheet就不够用了,必须拆分到多个工作表。

导出百万数据时有两个弊端:

  1. 不能使用模板

  2. 不能使用太多的样式

也就是说导出的数据太多时必须要放弃一些。

1.4.3、代码实现

UserController代码:

java 复制代码
    @GetMapping(value = "/downLoadMillion",name = "导出用户百万数据的导出")
    public void downLoadMillion(Long id,HttpServletRequest request,HttpServletResponse response) throws Exception{
        userService.downLoadMillion(request,response);
    }

Service方法:

java 复制代码
    void downLoadMillion(HttpServletRequest request, HttpServletResponse response) throws Exception;

实现类方法:

java 复制代码
//    百万数据的导出 1、肯定使用高版本的excel 2、使用sax方式解析Excel(XML)
//     限制:1、不能使用模板 2、不能使用太多的样式
    @Override
    public void downLoadMillion(HttpServletRequest request, HttpServletResponse response) throws Exception {
//        指定使用的是sax方式解析
        Workbook workbook = new SXSSFWorkbook();  //sax方式就是逐行解析
//        Workbook workbook = new XSSFWorkbook(); //dom4j的方式
//        导出500W条数据 不可能放到同一个sheet中 规定:每个sheet不能超过100W条数据
        int page = 1;
        int pageSize=200000;
        int rowIndex = 1; ///记录的是每个sheet的行索引
        int num = 0; // 记录了处理数据的个数
        Row row = null;
        Cell cell = null;
        Sheet sheet = null;
        while (true){  //不停地查询
            List<User> userList = this.findPage(page,pageSize);
            if(CollectionUtils.isEmpty(userList)){  //如果查询不到就不再查询了
                break;
            }
            if(num%1000000==0){  //每100W个就重新创建新的sheet和标题
                rowIndex = 1;
                //        在工作薄中创建一个工作表
                sheet = workbook.createSheet("第"+((num/1000000)+1)+"个工作表");
//        设置列宽
                sheet.setColumnWidth(0,8*256);
                sheet.setColumnWidth(1,12*256);
                sheet.setColumnWidth(2,15*256);
                sheet.setColumnWidth(3,15*256);
                sheet.setColumnWidth(4,30*256);
                //            处理标题
                String[] titles = new String[]{"编号","姓名","手机号","入职日期","现住址"};
                //        创建标题行
                Row titleRow = sheet.createRow(0);

                for (int i = 0; i < titles.length; i++) {
                    cell = titleRow.createCell(i);
                    cell.setCellValue(titles[i]);
                }
            }

//        处理内容
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            for (User user : userList) {
                row = sheet.createRow(rowIndex);
                cell = row.createCell(0);
                cell.setCellValue(user.getId());

                cell = row.createCell(1);
                cell.setCellValue(user.getUserName());

                cell = row.createCell(2);
                cell.setCellValue(user.getPhone());

                cell = row.createCell(3);
                cell.setCellValue(sdf.format(user.getHireDate()));

                cell = row.createCell(4);
                cell.setCellValue(user.getAddress());
                rowIndex++;
                num++;
            }
            page++;// 继续查询下一页
        }
//            导出的文件名称
        String filename="百万数据.xlsx";
//            设置文件的打开方式和mime类型
        ServletOutputStream outputStream = response.getOutputStream();
        response.setHeader( "Content-Disposition", "attachment;filename="  + new String(filename.getBytes(),"ISO8859-1"));
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        workbook.write(outputStream);
    }
1.4.4、测试结果

导出的这个文档大概需要3-5分钟的时间,有105 MB,内容如下

1.5、两种解析excel方式的比较

  • Workbook workbook = new SXSSFWorkbook(); //sax方式就是逐行解析
  • Workbook workbook = new XSSFWorkbook(); //dom4j的方式

使用jvm内存监视:

如果使用xssfworkboot,会出现内存溢出异常

使用SXSSFWorkbook不会出现内存溢出,

java 复制代码
Workbook workbook = new SXSSFWorkbook();  //sax方式就是逐行解析
  • 事件模式:它逐行扫描文档,一边扫描一边解析。由于应用程序只是在读取数据时检查数据,因此不需要将数据存储在内存中,这对于大型文档的解析是个巨大优势。

2、Excel百万数据的导入

2.1、需求分析

使用POI基于事件模式解析案例提供的Excel文件

2.2、思路分析

用户模式:加载并读取Excel时,是通过一次性的将所有数据加载到内存中再去解析每个单元格内容。当Excel数据量较大时,由于不同的运行环境可能会造成内存不足甚至OOM异常。

例如读取我们刚刚导出的百万数据:

java 复制代码
//测试百万数据的导入
public class POIDemo5 {
    public static void main(String[] args) throws Exception {
        XSSFWorkbook workbook = new XSSFWorkbook("C:\\Users\\syl\\Desktop\\百万用户数据的导出.xlsx");
        XSSFSheet sheetAt = workbook.getSheetAt(0);
        String stringCellValue = sheetAt.getRow(0).getCell(0).getStringCellValue();
        System.out.println(stringCellValue);
    }
}

会直接报内存溢出的错误:

事件模式:它逐行扫描文档,一边扫描一边解析。由于应用程序只是在读取数据时检查数据,因此不需要将数据存储在内存中,这对于大型文档的解析是个巨大优势。

2.3、代码实现

2.3.1、步骤分析
  1. 设置POI的事件模式

    根据Excel获取文件流

    根据文件流创建OPCPackage 用来组合读取到的xml 组合出来的数据占用的空间更小

    创建XSSFReader对象

  2. Sax解析

    自定义Sheet处理器

    创建Sax的XmlReader对象

    设置Sheet的事件处理器

    逐行读取

2.3.2、自定义处理器
java 复制代码
package com.tigerhhzz.handler;

import com.tigerhhzz.pojo.User;
import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler;
import org.apache.poi.xssf.usermodel.XSSFComment;

public class SheetHandler implements XSSFSheetXMLHandler.SheetContentsHandler {

//    编号 用户名  手机号  入职日期 现住址
    private User user;

    @Override
    public void startRow(int rowIndex) { //每一行的开始   rowIndex代表的是每一个sheet的行索引
        if(rowIndex==0){
            user = null;
        }else{
            user = new User();
        }
    }
    @Override  //处理每一行的所有单元格
    public void cell(String cellName, String cellValue, XSSFComment comment) {

        if(user!=null){
            String letter = cellName.substring(0, 1);  //每个单元名称的首字母 A  B  C
            switch (letter){
                case "A":{
                    user.setId(Long.parseLong(cellValue));
                    break;
                }
                case "B":{
                    user.setUserName(cellValue);
                    break;
                }
            }
        }
    }
    @Override
    public void endRow(int rowIndex) { //每一行的结束
        if(rowIndex!=0){
            System.out.println(user);
        }

    }
}
2.3.3、自定义解析

通过自定义解析类进行解析:

java 复制代码
package com.tigerhhzz.utils;

import com.tigerhhzz.handler.SheetHandler;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.model.StylesTable;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

import java.io.InputStream;

/**
 * 自定义Excel解析器
 */
public class ExcelParser {

    public void parse (String path) throws Exception {
        //1.根据Excel获取OPCPackage对象
        OPCPackage pkg = OPCPackage.open(path, PackageAccess.READ);
        try {
            //2.创建XSSFReader对象
            XSSFReader reader = new XSSFReader(pkg);
            //3.获取SharedStringsTable对象
            SharedStringsTable sst = reader.getSharedStringsTable();
            //4.获取StylesTable对象
            StylesTable styles = reader.getStylesTable();
            XMLReader parser = XMLReaderFactory.createXMLReader();
            // 处理公共属性:Sheet名,Sheet合并单元格
            parser.setContentHandler(new XSSFSheetXMLHandler(styles,sst, new SheetHandler(), false));
            XSSFReader.SheetIterator sheets = (XSSFReader.SheetIterator) reader.getSheetsData();
            while (sheets.hasNext()) {
                InputStream sheetstream = sheets.next();
                InputSource sheetSource = new InputSource(sheetstream);
                try {
                    parser.parse(sheetSource);
                } finally {
                    sheetstream.close();
                }
            }
        } finally {
            pkg.close();
        }
    }
}

2.4、测试

  • 用户模式下读取测试Excel文件直接内存溢出,测试Excel文件映射到内存中还是占用了不少内存;
  • 事件模式下可以流畅的运行。

使用事件模型解析:

java 复制代码
package com.tigerhhzz;

import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

//测试百万数据的导入
public class POIDemo5 {
    public static void main(String[] args) throws Exception {
        XSSFWorkbook workbook = new XSSFWorkbook("C:\\Users\\syl\\Desktop\\百万用户数据的导出.xlsx");
        XSSFSheet sheetAt = workbook.getSheetAt(0);
        String stringCellValue = sheetAt.getRow(0).getCell(0).getStringCellValue();
        System.out.println(stringCellValue);
    }
}

3、opencsv操作CSV文件

3.1 CSV文件简介

现在好多的网站中导出的文件会出现一种csv文件,我们接下来学习一下csv文件的导出方式。

CSV文件:Comma-Separated Values,中文叫逗号分隔值或者字符分割值,其文件以纯文本的形式存储表格数据。该文件是一个字符序列,可以由任意数目的记录组成,记录间以某种换行符分割。每条记录由字段组成,字段间的分隔符是其他字符或者字符串。所有的记录都有完全相同的字段序列,相当于一个结构化表的纯文本形式。

用文本文件、excel或者类似与文本文件的编辑器都可以打开CSV文件。

为了简化开发,我们可以使用opencsv类库来导出csv文件

需要的依赖:

xml 复制代码
		<dependency>
			<groupId>com.opencsv</groupId>
			<artifactId>opencsv</artifactId>
			<version>4.5</version>
		</dependency>

2.2 opencsv常用API

  1. 写入到csv文件会用到CSVWriter对象,创建此对象常见API如下

  2. 使用CSVWriter对象写入数据常用的方法如下:

  3. 读取csv文件会用到CSVReader对象,创建此对象常见API如下

构造器涉及到的三个参数:

  • reader:读取文件的流对象,常有的是BufferedReader,InputStreamReader。
  • separator:用于定义前面提到的分割符,默认为逗号CSVWriter.DEFAULT_SEPARATOR用于分割各列。
  • quotechar:用于定义各个列的引号,有时候csv文件中会用引号或者其它符号将一个列引起来,例如一行可能是:"1","2","3",如果想读出的字符不包含引号,就可以把参数设为:"CSVWriter.NO_QUOTE_CHARACTER "

read方法:

3.3 导出CSV文件

3.3.1 需求

我们还是以需求作为学习的驱动:把用户的列表数据导出到csv文件中

3.3.2 代码实现

UserController代码

java 复制代码
    @GetMapping(value = "/downLoadCSV",name = "导出用户数据到CSV文件中")
    public void downLoadCSV(HttpServletResponse response){
        userService.downLoadCSV(response);
    }

Service层

java 复制代码
 void downLoadCSV(HttpServletResponse response);

实现类

java 复制代码
@Override
    public void downLoadCSV(HttpServletResponse response) {
        try {
            //            准备输出流
            ServletOutputStream outputStream = response.getOutputStream();
            //            文件名
            String filename="百万数据.csv";
            //            设置两个头 一个是文件的打开方式 一个是mime类型
            response.setHeader( "Content-Disposition", "attachment;filename="  + new String(filename.getBytes(),"ISO8859-1"));
            response.setContentType("text/csv");
            OutputStreamWriter osw = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
            osw.write(0xFEFF);
            //            创建一个用来写入到csv文件中的writer
            CSVWriter writer = new CSVWriter(osw);
            //            先写头信息
            writer.writeNext(new String[]{"编号","姓名","手机号","入职日期","现住址"});

            //            如果文件数据量非常大的时候,我们可以循环查询写入
            int page = 1;
            int pageSize=200000;
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
            while (true) {  //不停地查询
                List<User> userList = this.findPage(page, pageSize);
                if (CollectionUtils.isEmpty(userList)) {  //如果查询不到就不再查询了
                    break;
                }
                //                把查询到的数据转成数组放入到csv文件中
                for (User user : userList) {
                    writer.writeNext(new String[]{user.getId().toString(),user.getUserName(),user.getPhone(),simpleDateFormat.format(user.getHireDate()),user.getAddress()});
                }
                writer.flush();
                page++;
            }
            writer.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

3.4 读取CSV文件

读取刚才导出的CSV文件

java 复制代码
package com.tigerhhzz;

import com.opencsv.CSVReader;
import com.tigerhhzz.pojo.User;

import java.io.FileReader;
import java.text.SimpleDateFormat;

//读取百万级数据的csv文件
public class CsvDemo {

    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");

    public static void main(String[] args) throws Exception {
        CSVReader csvReader = new CSVReader(new FileReader("C:\\Users\\Administrator\\Downloads\\百万数据.csv"));
        String[] titles = csvReader.readNext(); //读取到第一行 是小标题
//        "编号","姓名","手机号","入职日期","现住址"
        User user = null;
        while (true){
            user = new User();
            String[] content = csvReader.readNext();
            if(content==null){
                break;
            }
            user.setId(Long.parseLong(content[0]));
            user.setUserName(content[1]);
            user.setPhone(content[2]);
            user.setHireDate(simpleDateFormat.parse(content[3]));
            user.setAddress(content[4]);
            System.out.println(user);
        }
    }
}

"人的一生会经历很多痛苦,但回头想想,都是传奇"。


相关推荐
TaiKuLaHa2 小时前
Spring Bean的生命周期
java·后端·spring
刀法如飞3 小时前
开箱即用的 DDD(领域驱动设计)工程脚手架,基于 Spring Boot 4.0.1 和 Java 21
java·spring boot·mysql·spring·设计模式·intellij-idea
JavaGuide3 小时前
SpringBoot 官宣停止维护 3.2.x~3.4.x!
java·后端
Victor3563 小时前
Hibernate(39)Hibernate中如何使用拦截器?
后端
Victor3563 小时前
Hibernate(40)Hibernate的命名策略是什么?
后端
Knight_AL4 小时前
Spring 事务管理:为什么内部方法调用事务不生效以及如何解决
java·后端·spring
bcbnb4 小时前
iOS代码混淆技术深度实践:从基础到高级全面解析
后端
加洛斯4 小时前
SpringSecurity入门篇(2):替换登录页与config配置
前端·后端