Java 使用 POI 操作 Excel

Apache POI 基本介绍

Apache POI 是 Apache 软件基金会提供的 100% 开源库。支持 Excel 库的所有基本功能。

Java 使用 POI 操作 Excel

图片来源:易百教程

基本概念

在 POI 中,Workbook 代表着一个 Excel 文件(工作簿),Sheet 代表着 Workbook 中的一个表格 Row 代表 Sheet 中的一行,而 Cell 代表着一个单元格。
__HSSFWorkbook(属于poi) __对应的就是一个 .xls 文件,兼容 Office97-2003 版本。
__XSSFWorkbook(属于 poi-ooxml) __对应的是一个 .xlsx 文件,兼容 Office2007 及以上版本。
在 HSSFWorkbook 中,Sheet接口 的实现类为 HSSFSheet,Row接口 的实现类为HSSFRow,Cell 接口的实现类为 HSSFCell。
XSSFWorkbook 中实现类的命名方式类似,在 Sheet、Row、Cell 前加 XSSF 前缀即可。

引入依赖

<!-- 基本依赖,仅操作 xls 格式只需引入此依赖 -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>3.14</version>
</dependency>
<!-- 使用 xlsx 格式需要额外引入此依赖 -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>3.14</version>
</dependency>

使用 POI

使用 POI 的目的就是为了在 Java 中解析/操作 Excel 表格,实现 Excel 的导入/导出的功能,接下来我们依次来看它们的实现代码及注意事项。

导出

导出操作即使用 Java 写出数据到 Excel 中,常见场景是将页面上的数据(可能是经过条件查询的)导出,这些数据可能是财务数据,也可能是商品数据,生成 Excel 后返回给用户下载文件。
该操作主要涉及 Excel 的创建及使用流输出的操作,在 Excel 创建过程中,可能还涉及到单元格样式的操作。

创建并导出基本数据

进行导出操作的第一步是创建 Excel 文件,我们写一个方法,参数是需要写入 Excel 表格的数据和生成 Excel 方式(HSSF,XSSF),返回一个 Workbook 接口对象。
在方法内部我们采用反射来创建 Workbook 的实例对象。

代码

探索阶段,我们先将数据类型限定为 List,并把列数限定为某个数字,生成一个表格。
代码如下:

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 java.util.List;
/**
 * Excel 工厂类,负责 Workbook 的生成和解析
 *
 * @author calmer
 * @since 2018/12/5 11:19
 */
public class ExcelFactory {
    /**
     * 构造 Workbook 对象,具体实例化哪种对象由 type 参数指定
     * @param data 要导出的数据
     * @param type Excel 生成方式
     * @return 对应 type 的工作簿实例对象
     * @throws Exception 反射生成对象时出现的异常
     * <li>InstantiationException</li>
     * <li>IllegalAccessException</li>
     * <li>InstantiationException</li>
     */
    public static Workbook createExcel(List data,String type) 
        throws Exception{
        //根据 type 参数生成工作簿实例对象
        Workbook workbook = (Workbook) Class.forName(type).newInstance();
        //这里还可以指定 sheet 的名字
        //Sheet sheet = workbook.createSheet("sheetName");
        Sheet sheet = workbook.createSheet();
        // 限定列数
        int cols = 10;
        int rows = data.size() / cols;
        int index = 0;
        for (int rowNum = 0; rowNum < rows; rowNum++) {
            Row row = sheet.createRow(rowNum);
            for (int colNum = 0; colNum < cols; colNum++) {
                Cell cell = row.createCell(colNum);
                cell.setCellValue(data.get(index++).toString());
            }
        }
        return workbook;
    }
}

调用时,我们生成好数据并构造好 Workbook 对象,再调用 Workbook 的 write(OutputStream stream) 方法生成 Excel 文件。

List<String> strings = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
    strings.add(Integer.toString(i+1));
}
FileOutputStream out = new FileOutputStream("F:\\testXSSF.xlsx");
ExcelFactory.createExcel(strings,"org.apache.poi.xssf.usermodel.XSSFWorkbook").write(out);
out.close();

生成结果:

Java 使用 POI 操作 Excel

Java 使用 POI 操作 Excel

问题

以上代码已经完成简单的 Excel 文件生成操作,但其中还有几点问题没有解决

  • 实际场景下,Excel 表格中可能并不会存 Integer、String 这种基本数据结构的数据,更多的可能是对象数据(JSON、List),需要有表头,并将对象对应的属性一行行的显示出来(参考数据库查询语句执行的结果)。并且表头的样式一定是要控制的。
  • 我们并没有对方法中 type 属性进行限制,即外部可以传来任何类似“a”、“b”这样的无效值,届时程序会抛出异常,可以使用静态常量或枚举类来限定,这样可以增强代码可读性和健壮性。这里我并不想用静态常量或枚举类,打算使用注解的方式来控制参数的有效性。

完善

我们已经明确了两个问题:

  1. 之前的程序并不能在实际场景使用,我们需要将其完善到具有处理实际数据的能力。
  2. 利用注解限定参数的有效性。

我们先来解决第二个问题,即参数的问题。

使用注解限定参数

首先创建一个注解类

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 *
 * @author calmer
 * @since 2018/12/5 12:27
 */
@Retention(RetentionPolicy.SOURCE)
public @interface ExcelType {
    String HSSF = "org.apache.poi.hssf.usermodel.HSSFWorkbook";
    String XSSF = "org.apache.poi.xssf.usermodel.XSSFWorkbook";
}

在方法参数上加上注解

public static Workbook createExcel(List data, @ExcelType String type) throws Exception {
    //内容省略

}

调用时

ExcelFactory.createExcel(list,ExcelType.HSSF).write(out);

关于使用注解来限定参数的取值范围这种方式,我也是偶然看到过,可是这种方式在我这里编译器并不会给任何提示,我对注解了解不够,以后有机会要再好好研究一下。

解决实际数据问题

在实际应用中,很常见的情况是我们有很多实体类,比如 Person,Product,Order 等,借助反射,我们可以获取任意实体类的属性列表、getter 方法,所以目前,我打算利用反射,来处理多个对象的 Excel 导出。
首先我们创建一个方法,用来获取某个对象的属性列表(暂时不考虑要获取父类属性的情况)。

/**
 * 获取对象的属性名数组
 * @param clazz Class 对象,用于获取该类的信息
 * @return 该类的所有属性名数组
 */
private static String[] getFieldsName(Class clazz){
    Field[] fields = clazz.getDeclaredFields();
    String[] fieldNames = new String[fields.length];
    for (int i = 0; i < fields.length; i++) {
        fieldNames[i] = fields[i].getName();
    }
    return fieldNames;
}

然后我们完善 createExcel() 方法

public static Workbook createExcel(List data, @ExcelType String type) throws Exception {
    if(data == null || data.size() == 0){
        throw new Exception("数据不能为空");
    }
    //根据类型生成工作簿
    Workbook workbook = (Workbook) Class.forName(type).newInstance();
    //新建表格
    Sheet sheet = workbook.createSheet();
    //生成表头
    Row thead = sheet.createRow(0);
    String[] fieldsName = getFieldsName(data.get(0).getClass());
    for (int i = 0; i < fieldsName.length; i++) {
        Cell cell = thead.createCell(i);
        cell.setCellValue(fieldsName[i]);
    }
    //保存所有属性的getter方法名
    Method[] methods = new Method[fieldsName.length];
    for (int i = 0; i < data.size(); i++) {
        Row row = sheet.createRow(i+1);
        Object obj = data.get(i);
        for (int j = 0; j < fieldsName.length; j++) {
            //加载第一行数据时,初始化所有属性的getter方法
            if(i == 0){
                String fieldName = fieldsName[j];
                //处理布尔值命名 "isXxx" -> "setXxx"
                if (fieldName.contains("is")) {
                    fieldName = fieldName.split("is")[1];
                }
                methods[j] = obj.getClass().getMethod("get" +
                        fieldName.substring(0,1).toUpperCase() +
                        fieldName.substring(1));
            }
            Cell cell = row.createCell(j);
            Object value = methods[j].invoke(obj);
            //注意判断 value 值是否为空
            if(value == null){
                value = "无";
            }
            cell.setCellValue(value.toString());
        }
    }
    return workbook;
}

测试

以上代码基本满足一开始的需求,即以类的属性名为表头并生成表格。接下来我们生成一定量的数据,并测试导出效果。
实体类代码

/**
 *
 * @author calmer
 * @since 2018/12/5 14:50
 */
public class Person {
    private Integer id;
    private String name;
    private Integer age;
    private String hobby;
    private String job;
    private String address;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getHobby() {
        return hobby;
    }

    public void setHobby(String hobby) {
        this.hobby = hobby;
    }

    public String getJob() {
        return job;
    }

    public void setJob(String job) {
        this.job = job;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

测试类代码

List<Person> list = new ArrayList<>();
for (int i = 0; i < 60000; i++) {
    int num = i + 1;
    Person person = new Person();
    person.setId(num);
    person.setName("张三-"+(num));
    person.setAddress("花园路"+num+"号"+(int)Math.ceil(Math.random()*10)+"号楼");
    person.setAge(i+18);
    person.setHobby("洗脸刷牙打DOTA");
    person.setJob("程序员");
    list.add(person);
}
FileOutputStream out = new FileOutputStream("F:\\testXSSF.xlsx");
ExcelFactory.createExcel(list,ExcelType.XSSF).write(out);
out.close();

生成的结果如下

Java 使用 POI 操作 Excel

Java 使用 POI 操作 Excel

其他

这里测试的时候我使用6W的数据,所以程序进行的比较慢,用时如下:

Java 使用 POI 操作 Excel

像这种大数据量的导出,我们可以使用 SXSSF 的方式,网上也有很多例子,官网的对比。使用 SXSSF 方式导出用时如下:

Java 使用 POI 操作 Excel

可以看到时间缩短了很多。接下来我们单独来了解一下如何控制表格的样式。

样式

通常,我们需要控制的样式有两个部分,一个是表头部分的样式,另一个是普通单元格的样式。这次我们就仅创建两个方法演示样式的设置方式。
在 POI 中,控制单元格样式的对象是 CellStyle 接口,可以通过 Workbook 的createStyle 方法获得实例对象,这里我们写一个方法设置表头的样式。

private static CellStyle getTheadStyle(Workbook workbook){
    CellStyle style = workbook.createCellStyle();
    //设置填充色
    style.setFillForegroundColor(IndexedColors.LIGHT_BLUE.index);
    style.setFillPattern(CellStyle.SOLID_FOREGROUND);
    //设置对齐方式
    style.setAlignment(CellStyle.ALIGN_CENTER);
    //字体样式
    Font font = workbook.createFont();
    //设置字体名称
    font.setFontName("华文隶书");
    //斜体
    font.setItalic(true);
    //字体颜色
    font.setColor(IndexedColors.YELLOW.index);
    //字体大小
    font.setFontHeightInPoints((short)12);
    //不要忘记这句
    style.setFont(font);
    return style;
}

调用

Row thead = sheet.createRow(0);
//设置行高
thead.setHeight((short) 500);
//仅使用 setRowStyle 方法会对除有值的表头设置样式
thead.setRowStyle(style);
String[] fieldsName = getFieldsName(data.get(0));
for (int i = 0; i < fieldsName.length; i++) {
    Cell cell = thead.createCell(i);
    cell.setCellValue(fieldsName[i]);
    //在这里循环为每个有值的表头设置样式。
    //结合上面的 setRowStyle 会将表头行全部设置样式
    cell.setCellStyle(style);
}

接下来我们写获取普通单元格样式的方法

private static CellStyle getCommonStyle(Workbook workbook){
    CellStyle style = workbook.createCellStyle();
    //设置填充色
    style.setFillForegroundColor(IndexedColors.GREEN.index);
    style.setFillPattern(CellStyle.SOLID_FOREGROUND);
    //设置居中对齐
    style.setAlignment(CellStyle.ALIGN_CENTER);
    Font font = workbook.createFont();
    font.setFontName("华文彩云");
    //不要忘记这句
    style.setFont(font);
    return style;
}

完整调用

public static Workbook createExcel(List data, @ExcelType String type) throws Exception {
    if(data == null || data.size() == 0){
        throw new Exception("数据不能为空");
    }
    //根据类型生成工作簿
    Workbook workbook = (Workbook) Class.forName(type).newInstance();
    //生成样式
    CellStyle style = getTheadStyle(workbook);
    //新建表格
    Sheet sheet = workbook.createSheet();
    //生成表头
    Row thead = sheet.createRow(0);
    //设置行高
    thead.setHeight((short) 500);
    //仅使用 setRowStyle 方法会对除有值的表头设置样式
    thead.setRowStyle(style);
    String[] fieldsName = getFieldsName(data.get(0));
    for (int i = 0; i < fieldsName.length; i++) {
        Cell cell = thead.createCell(i);
        cell.setCellValue(fieldsName[i]);
        //在这里循环为每个有值的表头设置样式。
        //结合上面的 setRowStyle 会将表头行全部设置样式
        cell.setCellStyle(style);
    }
    //保存所有属性的getter方法名
    Method[] methods = new Method[fieldsName.length];
    //获取普通单元格样式
    style = getCommonStyle(workbook);
    for (int i = 0; i < data.size(); i++) {
        Row row = sheet.createRow(i+1);
        Object obj = data.get(i);
        for (int j = 0; j < fieldsName.length; j++) {
            //加载第一行数据时,初始化所有属性的getter方法
            if(i == 0){
                String fieldName = fieldsName[j];
                methods[j] = obj.getClass().getMethod("get" +
                        fieldName.substring(0,1).toUpperCase() +
                        fieldName.substring(1));
            }
            Cell cell = row.createCell(j);
            Object value = methods[j].invoke(obj);
            //注意判断 value 值是否为空
            if(value == null){
                value = "无";
            }
            cell.setCellValue(value.toString());
            //设置单元格样式
            cell.setCellStyle(style);
        }
    }
    return workbook;
}

生成结果如下(忽视颜色搭配与美观程度)

Java 使用 POI 操作 Excel

注意

这里我运行的出了一个问题,在此记录。
注意上面代码的第 28 行和第 48 行,这里我们在 for 循环外面获取 Style 对象,在 for 循环中循环设置单元格样式的时候,始终使用的是__同一个__ Style。而最开始我测试的时候,并不是这样写,而是像下面这样:

for (int i = 0; i < data.size(); i++) {
    Row row = sheet.createRow(i+1);
    Object obj = data.get(i);
    for (int j = 0; j < fieldsName.length; j++) {
        //加载第一行数据时,初始化所有属性的getter方法
        if(i == 0){
            String fieldName = fieldsName[j];
            methods[j] = obj.getClass().getMethod("get" +
                    fieldName.substring(0,1).toUpperCase() +
                    fieldName.substring(1));
        }
        Cell cell = row.createCell(j);
        Object value = methods[j].invoke(obj);
        //注意判断 value 值是否为空
        if(value == null){
            value = "无";
        }
        cell.setCellValue(value.toString());
        //设置单元格样式
        cell.setCellStyle(getCommonStyle(workbook));
    }
}

注意 20 行,在 getCommonStyle 方法中,我们每次调用都会使用 Workbook 对象创建一个 Style 对象,而我们的数据一共有 6W 条,没条数据又有 6 个属性,我们一共要渲染 36W 个单元格,也就是要生成 36W 个 Style 对象。于是,在我运行代码时便出现了如下报错。

F:\java\jdk1.8.0_151\bin\java.exe 
Exception in thread "main" java.lang.IllegalStateException: The maximum number of Cell Styles was exceeded. You can define up to 64000 style in a .xlsx Workbook
	at org.apache.poi.xssf.model.StylesTable.createCellStyle(StylesTable.java:789)
	at org.apache.poi.xssf.usermodel.XSSFWorkbook.createCellStyle(XSSFWorkbook.java:682)
	at org.apache.poi.xssf.streaming.SXSSFWorkbook.createCellStyle(SXSSFWorkbook.java:869)
	at com.xhc.study.util.poi.ExcelFactory.getCommonStyle(ExcelFactory.java:114)
	at com.xhc.study.util.poi.ExcelFactory.createExcel(ExcelFactory.java:73)
	at Test.main(Test.java:62)

Process finished with exit code 1

这里提示我们最多让一个 Workbook 对象生成 64000 个 Style 对象。
以后一些危险的操作还是少做????

导入

导入操作即使用 Java 读取 Excel 中的数据,常见场景是在页面上点击导入按钮,用户选择 Excel 文件,其中可能是多条商品数据(包含编号、名称、参数等信息),通过文件上传功能将 Excel 读取到我们的程序中,解析其中的数据并存入数据库中。

读取数据并打印

导入操作主要依靠 Workbook 的一个构造函数,源码如下

/**
 * Constructs a XSSFWorkbook object, by buffering the whole stream into memory
 *  and then opening an {@link OPCPackage} object for it.
 * 
 * <p>Using an {@link InputStream} requires more memory than using a File, so
 *  if a {@link File} is available then you should instead do something like
 *   <pre><code>
 *       OPCPackage pkg = OPCPackage.open(path);
 *       XSSFWorkbook wb = new XSSFWorkbook(pkg);
 *       // work with the wb object
 *       ......
 *       pkg.close(); // gracefully closes the underlying zip file
 *   </code></pre>
 */
public XSSFWorkbook(InputStream is) throws IOException {
    super(PackageHelper.open(is));

    beforeDocumentRead();
    
    // Build a tree of POIXMLDocumentParts, this workbook being the root
    load(XSSFFactory.getInstance());

    // some broken Workbooks miss this...
    if(!workbook.isSetBookViews()) {
        CTBookViews bvs = workbook.addNewBookViews();
        CTBookView bv = bvs.addNewWorkbookView();
        bv.setActiveTab(0);
    }
}

从这个构造函数来看,我们只需提供一个输入流,便能构造一个 Workbook 对象出来,接下来我们首先写一个处理 Workbook 的方法,参数为一个 Workbook 对象,我们在方法内部遍历表格并输出数据,这里我们默认该文件是一个规则的表格,即符合我们之前生成的 Excel 那样的格式。代码如下

/**
 * 读取 Excel 数据并处理
 * @param workbook 完整的 Workbook 对象
 */
public static void readExcel(Workbook workbook) {
    Sheet sheet = workbook.getSheetAt(0);
    //获取总行数
    int rows = sheet.getPhysicalNumberOfRows();
    //去除表头,从第 1 行开始打印
    for (int i = 0; i < rows; i++) {
        Row row = sheet.getRow(i);
        //获取总列数
        int cols = row.getPhysicalNumberOfCells();
        for (int j = 0; j < cols; j++) {
            System.out.print(row.getCell(j) + "\t");
        }
        System.out.println();
    }
}

为了输出方便,我已将 Excel 中的数据降为 100 条。调用代码如下

FileInputStream in = new FileInputStream("F:\\testXSSF.xlsx");
XSSFWorkbook workbook = new XSSFWorkbook(in);
ExcelFactory.readExcel(workbook);
in.close();

输出结果如下

Java 使用 POI 操作 Excel

Java 使用 POI 操作 Excel

数据已经拿到,接下来的问题是解析为对象,毕竟我们平时向数据库保存数据使用的 ORM 框架一般都使用了传输对象。这里我们再次利用反射,完善代码,使 readExcel 方法有读取 Excel 中的数据并将其映射为对象的能力。

完善

这里需要明确几个问题

  1. 如何确定对象?
  2. 如何将未知对象的每个字段的数据类型与 Excel 表格中的字符串数据进行转换?
  3. 出现空值我们如何解决?
  4. 日期格式的数据我们如何转换?有几种日期格式?

接下来我们开始完善 readExcel 方法,代码如下

/**
 * 读取 Excel 数据并处理
 *
 * @param workbook 完整的 Workbook 对象
 * @param clazz    Excel 中存储的数据的类的 Class 对象
 * @param <T>      泛型
 * @return 解析之后的对象列表,与泛型一致
 * @throws Exception
 */
public static <T> List<T> readExcel(Workbook workbook, Class<T> clazz) throws Exception {
    List<T> list = new ArrayList<>();
    Sheet sheet = workbook.getSheetAt(0);
    //获取总行数
    int rows = sheet.getPhysicalNumberOfRows();
    //获取所有字段名
    String[] fieldsName = getFieldsName(clazz);
    Method[] methods = new Method[fieldsName.length];
    //去除表头,从第 1 行开始打印
    for (int i = 1; i < rows; i++) {
        T obj = clazz.newInstance();
        Row row = sheet.getRow(i);
        //获取总列数
        int cols = row.getPhysicalNumberOfCells();
        //获取所有属性
        Field[] fields = clazz.getDeclaredFields();
        //处理对象的每一个属性
        for (int j = 0; j < cols; j++) {
            //第一次循环时初始化所有 setter 方法名
            if (i == 1) {
                String fieldName = fieldsName[j];
                //处理布尔值命名 "isXxx" -> "setXxx"
                if (fieldName.contains("is")) {
                    fieldName = fieldName.split("is")[1];
                }
                methods[j] = obj.getClass().getMethod("set" +
                        fieldName.substring(0, 1).toUpperCase() +
                        fieldName.substring(1), fields[j].getType());
            }
            //先将单元格中的值按 String 保存
            String param = row.getCell(j).getStringCellValue();
            //属性的类型
            String typeName = fields[j].getType().getSimpleName();
            //set 方法
            Method method = methods[j];
            //排除空值
            if (param == null || "".equals(param)) {
                continue;
            }
            //根据对象的不同属性字段转换单元格中的数据类型并调用 set 方法赋值
            if ("Integer".equals(typeName) || "int".equals(typeName)) {
                method.invoke(obj, Integer.parseInt(param));
            } else if ("Date".equals(typeName)) {
                String pattern;
                if (param.contains("CST")) {
                    //java.util.Date 的默认格式
                    pattern = "EEE MMM dd HH:mm:ss zzz yyyy";
                } else if (param.contains(":")) {
                    //带有时分秒的格式
                    pattern = "yyyy-MM-dd HH:mm:ss";
                } else {
                    //简单格式
                    pattern = "yyyy-MM-dd";
                }
                method.invoke(obj, new SimpleDateFormat(pattern, Locale.UK).parse(param));
            } else if ("Long".equalsIgnoreCase(typeName)) {
                method.invoke(obj, Long.parseLong(param));
            } else if ("Double".equalsIgnoreCase(typeName)) {
                method.invoke(obj, Double.parseDouble(param));
            } else if ("Boolean".equalsIgnoreCase(typeName)) {
                method.invoke(obj, Boolean.parseBoolean(param));
            } else if ("Short".equalsIgnoreCase(typeName)) {
                method.invoke(obj, Short.parseShort(param));
            } else if ("Character".equals(typeName) || "char".equals(typeName)) {
                method.invoke(obj, param.toCharArray()[0]);
            } else {
                //若数据格式为 String 则不必转换
                method.invoke(obj, param);
            }
        }
        //不要忘记这句
        list.add(obj);
    }
    return list;
}

接下来我们改造 Person 类,添加几个不同类型的数据,并加入 toString() 方法,供我们测试使用。

import java.util.Date;

/**
 *
 * @author calmer
 * @since 2018/12/5 14:50
 */
public class Person {
    private Integer id;
    private String name;
    private Integer age;
    private String hobby;
    private String job;
    private String address;
    private Date birthday;
    private Character sex;
    private Long phone;
    private Boolean isWorked;

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", hobby='" + hobby + '\'' +
                ", job='" + job + '\'' +
                ", address='" + address + '\'' +
                ", birthday=" + birthday +
                ", sex=" + sex +
                ", phone=" + phone +
                ", isWorked=" + isWorked +
                '}';
    }


    public Long getPhone() {
        return phone;
    }

    public void setPhone(Long phone) {
        this.phone = phone;
    }

    public Boolean getWorked() {
        return isWorked;
    }

    public void setWorked(Boolean worked) {
        isWorked = worked;
    }

    public Character getSex() {
        return sex;
    }

    public void setSex(Character sex) {
        this.sex = sex;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getHobby() {
        return hobby;
    }

    public void setHobby(String hobby) {
        this.hobby = hobby;
    }

    public String getJob() {
        return job;
    }

    public void setJob(String job) {
        this.job = job;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

接下来是测试调用的代码,我们直接将导出与导入两段代码一起执行。

public static void main(String[] args) throws Exception {
    //生成数据
    List<Person> list = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
        int num = i + 1;
        Person person = new Person();
        person.setId(num);
        person.setName("张三-"+(num));
        person.setAddress("花园路"+num+"号"+(int)Math.ceil(Math.random()*10)+"号楼");
        person.setAge(i+18);
        person.setHobby("洗脸刷牙打DOTA");
        person.setJob("程序员");
        person.setBirthday(new Date());
        person.setSex('男');
        person.setPhone(4536456498778789123L);
        person.setWorked(true);
        list.add(person);
    }
    //导出 Excel
    FileOutputStream out = new FileOutputStream("F:\\testXSSF.xlsx");
    ExcelFactory.createExcel(list,ExcelType.SXSSF).write(out);
    out.close();
    //导入 Excel
    FileInputStream in = new FileInputStream("F:\\testXSSF.xlsx");
    XSSFWorkbook workbook = new XSSFWorkbook(in);
    List<Person> personList = ExcelFactory.readExcel(workbook,Person.class);
    in.close();
    //遍历结果
    for (Person person : personList) {
        System.out.println(person);
    }
}

执行结果如下:

Java 使用 POI 操作 Excel

Java 使用 POI 操作 Excel

功能已经基本实现,我们这次再试一下在大数据导入的情景下,程序的耗时如何。我们这次同样适用 6W 条数据。结果如下

Java 使用 POI 操作 Excel

这里我们可以看到,导入操作占用的内存和耗时,都比导出操作多很多。在导出的时候我们知道 POI 在导出大数据量的时候提供了 SXSSF 的方式解决耗时和内存溢出问题,那么在导入时是不是也会有某种方式可以解决这个问题呢?

使用 Sax 事件驱动解析

关于这部分的代码,可以在网上找到许多,本次暂不讨论。另外听说有一个 EasyExcel 挺好用的,有时间试一下。

感悟

通过这次探索,深知自己不足的地方还很多,原来写代码的时候考虑的太少,有关效率,内存使用等方面的问题在自己测试的时候是看不出来的,真正使用的时候这些问题才会暴露出来,比如某项操作可能会导致用户几十秒甚至几分钟的等待,或者程序直接崩掉。
所以以后还是要小心谨慎,对工具类的使用不能会用就够,要尽量的深入研究。
道可顿悟,事需渐修。

须知