一、Excel操作背景

1、前言

  日常开发中,excel操作随处可见,然而这些相关操作虽然不是导入就是导出,但是因为需求不一样,所以每添加一个相关操作接口时代码复用率较低,从而大大降低了开发效率;其实excel操作虽然开发中代码几乎都会有一些差异,但是代码逻辑却基本上都是一样的,所以,想要封装excel相关代码以减少代码冗余、提高代码复用率,答案当然是肯定的。

  目前市场上excel相关操作框架有easyExcel、EEC(Excel Export Core)等,easyExcel在数据导出方面有很好的效率,但它对模板自定义配置的支持有很大的针对性,仅仅适用于有规律性的模板;EEC在数据在操作速率上也有比较好的性能,但目前仅支持XLSX格式,而且EEC业务场景主要偏向excel与数据库之间的直接数据交换,相对于日常开发的运用来说,范围相对比较狭隘。

 

2、simple-excel概述

  simple-excel是腾讯云开发平台中开源的一个excel操作框架,尤其在功能方面,可以满足开发者的不同需求,是当前市场中值得推荐使用的一款新型轻量级框架,遗憾的是该工具当前还没有开源在maven中,其功能究竟如何,我们继续往下看。

  我们先看看这几种表头,如果在开发中,你会如何解决?

java excel 流 java excel 流式_java excel 流

图 1-1

java excel 流 java excel 流式_List_02

图 1-2

java excel 流 java excel 流式_java excel 流_03

图 1-3

java excel 流 java excel 流式_List_04

图 1-4

 

  项目中,Excel表头文件一般会有这么几种生成方式:

excel快捷模板生成

excel文件,获取excel文件生成

xml配置中,解析xml生成

  从单个功能实现及其性能上分析,图1-1、图1-2表头比较简单,一般推荐使用方法一;图1-3出现了跨行跨列等特殊操作,一般会选择方法二或者方法三存储表头信息;图1-4是开发中碰到的最蛋疼的操作,虽然很蛋疼,但是确实存在这种情况,其实这种方式仍然还是采用方法二或者方法三,方法二简单易懂这里不做过多的讨论,而simple-excel就是方法三的一种实现方式,具体如何使用将在下文继续讲解。

 

二、Excel导出

1、实体类及其导出模板配置文件

  实体类:

 

public class User {
    /**
     * 编号 	姓名	年龄	性别	出生日期	 爱好
     */
    private Integer id;

    private Integer code;

    private String name;

    private int age;

    private SexEnum sex;

    private Date birthday;

    private Favourite favourite;
    
    //setter and getter
}

 

public enum SexEnum {
    MALE(1, "男"),
    FEMALE(2, "女"),
    OTHER(3, "其他");
    SexEnum(Integer code, String name){
        this.code = code;
        this.name = name;
    }
    private Integer code;
    private String name;

    //..getters and setters..
}

 

public enum Favourite {
    FOOTBALL(1, "足球"),
    BASKETBALL(2, "篮球"),
    PINGPANG(3, "乒乓球");
    private Integer code;
    private String name;

    Favourite(Integer code, String name) {
        this.code = code;
        this.name = name;
    }
    //....       
}

 

  配置文件样板如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE excel PUBLIC "excel" "https://tobiasy.oss-cn-beijing.aliyuncs.com/dtd/simple-excel.dtd">
<excel>
    <sheet name="用户表">
        <colgroup>
            <col index="A" width='17em'/>
            <col index="B" width='17em'/>
            <col index="I" width='17em'/>
        </colgroup>
        <head>
            <tr allalign="center" height="20px" isbold="true" fontsize="12px" fontname="黑体" borderall="hair">
                <td colspan="9">用户信息表</td>
            </tr>
            <tr align="left" height="12px" verticalalign="center">
                <td colspan="9">描述:用户数据包含xxx,出生年月默认格式:2019-01-01</td>
            </tr>
            <tr align="center" height="15px" verticalalign="center">
                <td rowspan="7">序号</td>
                <td>姓名</td>
                <td colspan="2">性别</td>
                <td colspan="2">年龄</td>
                <td colspan="2" rowspan="3">出生年月</td>
                <td rowspan="7">爱好</td>
            </tr>
            <tr align="center" verticalalign="center">
                <td rowspan="6">(真实名字)</td>
                <td rowspan="6">枚举</td>
                <td rowspan="2">男</td>
                <td rowspan="3">最小值</td>
                <td rowspan="3">0</td>
            </tr>
            <tr>
            </tr>
            <tr align="center" verticalalign="center">
                <td rowspan="2">女</td>
                <td rowspan="4">注意事项</td>
                <td rowspan="4">格式</td>
            </tr>
            <tr align="center" verticalalign="center">
                <td rowspan="3">最大值</td>
                <td rowspan="3">100</td>
            </tr>
            <tr align="center" verticalalign="center">
                <td rowspan="2">无</td>
            </tr>
        </head>
        <body>
            <tr firstrow="9" height="10px" allalign="center" color="100">
                <td index="0" property="code"/><!--序号 -->
                <td index="1" property="name"/><!--姓名 -->
                <td index="2" property="sex" columnsize="2" /><!--性别 -->
                <td index="4" property="age" columnsize="2" /><!--年龄 -->
                <td index="6" property="birthday" columnsize="2" /><!--出生年月 -->
                <td index="8" property="favourite" columnsize="1" /><!--爱好 -->
            </tr>
        </body>
    </sheet>
</excel>

 

  由上述配置文件可知,excel单元格是支持跨行跨列操作的,如此一来,我们就可以自定义excel导出文件复杂标题头了(其实这部分配置就是实现图1-4的配置,特点:简洁)。

看到<excel>标签中还有一个<sheet>标签,我们似乎明白了它还支持多个sheet工作表的样式导出。

 

2、定制复杂标题头

将上述xml配置文件直接导出,代码:

public void createExcel() {
        boolean f2 = ExportBuilder.createExcel(
                "simple-user.xml",
                "F:/test/simpleExcelStudent.xls");
        System.out.println(f2);
}

 

处理后的excel文件如图2-1:

 

 

java excel 流 java excel 流式_List_05

  看到这儿,是不是和上图中的样式一模一样呢?

  模板样式文件导出既然这么灵活,那我们又如何在模板中把需要的数据添加上去呢?

3、添加导出数据

(1) 调用时设置导出属性

  我们先看一个数据导出不是很复杂的例子,代码:

public void createExcelList() {
        List<Student> list = ExportBuilderTest.getList();//数据(200条)
        Integer firstRow = 4;//开始行
        Integer firstCol = 0;//开始列
        Integer sheetAt = 0;//所在工作表序号
        String[] attrs = new String[]{"code", "name", "sex", "age", "birthday", "favourite"};//数据集合泛型对象中的属性名称
        ExportBuilder.createExcel(
                "simple-student.xml",
                "F:/test/simpleExcelStudent.xls",
                list, attrs, firstRow, firstCol, sheetAt 
        );
}

 

  我们在集合中放入200条数据(待用),导出后的excel文件样式如图2-2:

java excel 流 java excel 流式_java excel 流_06

  那么如何维护每一列数据的索引顺序呢?其实这个顺序就是由属性数组参数attrs维持的,如果想调整,只需要把参数的顺序调换一下即可。

(2) 通过注解配置导出属性

  我们先看代码:

public void createExcelList() {
        List<Student> list = StudentData.getList();
        boolean f = ExportBuilder.createExcel(
                ClassLoaderUtils.getLoaderFile("simple-user.xml"),
                new File("F:/test/excel/simple-user.xls"),
                ExcelTypeEnum.XLS,
                list
        );
        System.out.println(f);
}

 

  咋一看,怎么只有一个xml配置文件、一个输出文件和需要输出的数据,那集合中的数据是如何对应到excel文件中的呢?

  其实还需要在集合数据对象中设置对应参数:

//String[] attrs = new String[]{"code", "name", "sex", "age", "birthday", "favourite"};

@ExportUseAnnotation(firstRow = 9) //指定用注解方式导出
public class User {

    @ExcelField() //index默认为0
    private Integer code;

    @ExcelField(index = 1)
    private String name;

    @ExcelField(index = 3)
    private int age;

    @ExcelField(index = 2)
    private SexEnum sex;

    @ExcelField(index = 4)
    private Date birthday;

    @ExcelField(index = 5)
    private Favourite favourite;

 

  看到这儿突然恍然大悟了,原来这是通过注解标识字段并且设置好其索引数据,这样自然省去了属性参数的调用申明。导出文件依旧和图2-2一致。

(3) 导出时的数据跨列问题

  通过上面案例发现,如果模板就像导出模板图2-3(即上文图2-1)这样的呢?

java excel 流 java excel 流式_List_07

  注意途中红色标注部分均跨了两个列,如果按照我们上面的逻辑处理,导出数据后会是这样的,详见图2-4:

 

java excel 流 java excel 流式_java excel 流_08

  这时我们会发现,数据因为跨列的问题错位了,那么如何解决呢?

  其实解决办法也很简单,只要我们配置好每一个属性对应的列数就好了(缺省值为1):

 

@ExcelField(index = 3, columnSize = 2)
    private int age;

    @ExcelField(index = 2, columnSize = 2)
    private SexEnum sex;

    @ExcelField(index = 4, columnSize = 2)
    private Date birthday;

 

此时我们继续操作,导出后发现这个问题已经完美的解决了,如图2-5所示:

 

java excel 流 java excel 流式_System_09

 

导出的模板几乎很完美,至于如何设置导出中的时间格式,后面章节将会仔细说明。

到这儿excel导出基本讲解完了。

 

 

三、Excel导入

1、返回List<Map>数据集合

  我们选择图2-2的excel导出结果文件,数据条数是200条,代码:

   

public void getDataTest() {
        File file = new File("F:/test/simple-user.xls");//excel文件
        Integer firstRow = 4;//开始行
        List<Map<Integer, String>> list = ImportBuilder.getData(file, firstRow);
        System.out.println(list.size());
        list.forEach(System.out::println);
}

 

  控制台打印如下:

200

{0=0, 1=admin0, 2=男, 3=18, 4=2019-07-16, 5=篮球}

{0=1, 1=admin1, 2=女, 3=19, 4=2019-07-16, 5=足球}

{0=2, 1=admin2, 2=男, 3=20, 4=2019-07-16, 5=篮球}

{0=3, 1=admin3, 2=女, 3=21, 4=2019-07-16, 5=乒乓球}

{0=4, 1=admin4, 2=男, 3=22, 4=2019-07-16, 5=篮球}

{0=5, 1=admin5, 2=女, 3=23, 4=2019-07-16, 5=足球}

......

  数据条数确实是200条,而且单元格中的数据都获取到了,但是数据类型均是String,我们要用数据时,一般情况下都会将数据放置到对象中,然后再处理对象,那么如何将数据直接装配到对象集合中呢?且看下文。

2、返回List<[对象]>数据集合

(1) 调用时属性声明

  即导入的属性等对应信息在导入函数调用时指定;代码:

public void getListTest() {
        File file = new File("F:/test/simpleExcelStudent.xls");//excel文件
        Integer firstRow = 4;//当前行
        String[] attrs = new String[]{"code", "name", "sex", "age", "birthday", "favourite"};//列对应属性
        Function<String, Integer> f1 = Integer::parseInt;
        Function<String, String> f2 = (s) -> s;
        Function<String, Enum> f3 = (s) -> TO_ENUM.toEnum(s, SexEnum.class);
        Function<String, Enum> f5 = (s) -> TO_ENUM.toEnum(s, Favourite.class);
        Function<String, Date> f4 = TO_DATE;
        Function[] functions = new Function[]{f1, f2, f3, f1, f4, f5};//每种属性的转换函数
        List<Student> list = ImportBuilder.getList(
                file, firstRow,
                attrs, functions,
                Student::new);
        System.out.println(list.size());
        list.forEach(System.out::println);
}

 

  控制台打印如下:

200

Student{code=0, name='admin0', age=18, sex='MALE', birthday=Tue Jul 16 00:00:00 CST 2019, favourite='BASKETBALL'}

Student{code=1, name='admin1', age=19, sex='FEMALE', birthday=Tue Jul 16 00:00:00 CST 2019, favourite='FOOTBALL'}

Student{code=2, name='admin2', age=20, sex='MALE', birthday=Tue Jul 16 00:00:00 CST 2019, favourite='BASKETBALL'}

Student{code=3, name='admin3', age=21, sex='FEMALE', birthday=Tue Jul 16 00:00:00 CST 2019, favourite='PINGPANG'}

Student{code=4, name='admin4', age=22, sex='MALE', birthday=Tue Jul 16 00:00:00 CST 2019, favourite='BASKETBALL'}

Student{code=5, name='admin5', age=23, sex='FEMALE', birthday=Tue Jul 16 00:00:00 CST 2019, favourite='FOOTBALL'}

......

  从打印结果看来,所有的数据都已经获取到了,而且实现了自定义转换。

  上述调用操作虽然灵活,但是每一种操作函数都是自定义未免有点不太友好,所以ExcelFunction类中还提供了一些基本的操作函数:

TO_INTEGER, TO_STRING, TO_DATE, TO_BIGDECIMAL, TO_DOUBLE, TO_FLOAT, TO_LONG, TO_BOOLEAN, TO_ENUM, TO_DATE_FORMAT 等

如此,上述属性转换函数即可这样优化:

        Function<String, Enum> f3 = (s) -> TO_ENUM.toEnum(s, SexEnum.class);

        Function<String, Enum> f5 = (s) -> TO_ENUM.toEnum(s, Favourite.class);

        Function<String, Object>[] functions = new Function[]{

                TO_INTEGER, TO_STRING, f3, TO_INTEGER, TO_DATE, f5

        };

  这里因为性别和爱好两个字段均是枚举类型,只能自定义导入函数,除了用户自定义数据类型之外,其他很多通用类型均可以使用内置转换函数。

  总而言之,用户只需要按照属性一一匹配属性的转换函数即可完成。

(2) 注解的属性配置

  导入的基本配置信息用注解方式实现;代码:

 

@FunctionTarget(value = SubExcelFunction.class)①
@ImportUseAnnotation(firstRow = 9)
public class Student {
    private Integer id;

    @ExcelField(function = "TO_INTEGER")
    private Integer code;

    @ExcelField(index = 1)
    private String name;

    @ExcelField(index = 4, function = "TO_INTEGER")
    private int age;

    @ExcelField(index = 2, function = "TO_SEX")②
    private SexEnum sex;

    @ExcelField(index = 6, function = "TO_DATE")
    private Date birthday;

    @ExcelField(index = 8, function = "TO_FAVOURITE")③
    private Favourite favourite;

 

  由上述配置代码我们看明白了添加@ImportUseAnnotation注解表示用注解方式导入,在属性上配置其转换函数得到自定义的类型值,但是②③均为自定义属性,这两个函数系统自然不知道,那如何将这两个函数告诉系统呢?

  解决办法是:写一个自定义类继承ExcelFunction(例子中的这个类就是SubExcelFunction),然后将自定函数放置到其中,在操作对象中添加@FuctionTarget注解,指定其value值为刚创建的对象即可。那么①的作用就很明显了,即告诉系统去value对应的类中去扫描对应函数。

  在该自定义类中还可以重写getDefaultPattern()方法实现时间格式的自定义,这就是上文提到的自定义导出时间格式。