Java 使用 POI 操作 Excel

19,013 阅读16分钟

Apache POI 基本介绍

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

image.png | left | 629x536

图片来源:易百教程

基本概念

在 POI 中,Workbook代表着一个 Excel 文件(工作簿),Sheet代表着 Workbook 中的一个表格,Row 代表 Sheet 中的一行,而 Cell 代表着一个单元格。 HSSFWorkbook对应的就是一个 .xls 文件,兼容 Office97-2003 版本。 XSSFWorkbook对应的是一个 .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();

生成结果:

image.png | left | 747x402

image.png | left | 747x223

问题

以上代码已经完成简单的 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();
    

    生成的结果如下

    image.png | left | 747x402

    image.png | left | 747x402

    其他

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

    image.png | left | 387x113

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

    image.png | left | 405x114

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

    样式

    通常,我们需要控制的样式有两个部分,一个是表头部分的样式,另一个是普通单元格的样式。这次我们就仅创建两个方法演示样式的设置方式。 在 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;
    }
    

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

    image.png | left | 747x402

    注意

    这里我运行的出了一个问题,在此记录。 注意上面代码的第 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();
    

    输出结果如下

    image.png | left | 638x223

    image.png | left | 581x210

    数据已经拿到,接下来的问题是解析为对象,毕竟我们平时向数据库保存数据使用的 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);
        }
    }
    

    执行结果如下:

    image.png | left | 747x113

    image.png | left | 747x135

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

    image.png | left | 516x170

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

    使用 Sax 事件驱动解析

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

    感悟

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

    须知