导入导出可说是表单必备功能,目前系统中已有好几个工具类,但这些工具类考虑的都不够全面(尤其是在导出方面),它们或者不能满足于某些特定场景,或者不具备可扩展性,又或者实现得不够优雅。
导出相对复杂一些,需要满足以下更多需求:
值转换 
特殊值标识 
批量 
主子表 
海量数据 
并行 
分页查询 
打包 
复杂表头 
自定义样式 
基于模板 
 
为此我开发了一套类库:ijz-export ,它基本实现了以上各类需求。下面将依次介绍如何利用该库来做导入/导出。
导入 简单的 1 2 3 4 5 6 7 List<People> dataList = ExcelImportUtils.imports(new  FileInputStream("E:\\人员表.xlsx" ),         new  ImportDefinition.Builder<People>()                                                   .modelClass(People.class ) // 接收导入数据的类                  .simpleColumns(Arrays.asList("name", "birthday", "sex", "isSingle", "isMarried", "ratio")) // excel中每列依次对应的属性名                 .build()); 
最简单的导入只需要设置modelClass和simpleColumns。如果需要读取指定sheet,则调用sheetName;如果需要从指定行开始读取数据,则调用startIndex。
值转换 场景:People类中有int类型字段sex(1表示男,2表示女),但在excel中的sex对应列的值就是字符串“男”、“女”,那么如何自动给sex字段赋相应的值?
1 2 3 4 5 6 7 8 9 10 11 12 List<People> dataList = ExcelImportUtils.imports(new  FileInputStream("E:\\人员表.xlsx" ),         new  ImportDefinition.Builder<People>()                 .modelClass(People.class )                  .columns (Arrays .asList (                         new ColumnDefinition.Builder().name("name").build(), // 不需要转换                         new  ColumnDefinition.Builder().name("birthday" ).valueFunction(new  DateImportFunction()).build(),                          new  ColumnDefinition.Builder().name("sex" ).valueFunction((value) -> value.equals("男" ) ? 1  : 2 ).build(),                          new  ColumnDefinition.Builder().name("isSingle" ).valueFunction(new  BooleanIntegerImportFunction()).build(),                          new  ColumnDefinition.Builder().name("isMarried" ).valueFunction(new  BooleanImportFunction()).build(),                           new  ColumnDefinition.Builder().name("ratio" ).valueFunction(new  PercentageImportFunction()).build()                  ))                 .build()); 
目前预置了DateImportFunction、BooleanImportFunction、BooleanIntegerImportFunction和PercentageImportFunction四个值导入转换类,对应的也预置了DateExportFunction、BooleanExportFunction、BooleanIntegerExportFunction和PercentageExportFunction这四个值导出转换类。
必填校验 1 2 3 4 5 6 7 8 9 10 11 12 List<People> dataList = ExcelImportUtils.imports(new  FileInputStream("E:\\人员表.xlsx" ),         new  ImportDefinition.Builder<People>()                 .modelClass(People.class )                  .columns (Arrays .asList (                         new ColumnDefinition.Builder().name("name").required(true).build(),                         new  ColumnDefinition.Builder().name("birthday" ).required(true ).build(),                         new  ColumnDefinition.Builder().name("sex" ).required(true ).build(),                         new  ColumnDefinition.Builder().name("isSingle" ).build(),                         new  ColumnDefinition.Builder().name("isMarried" ).required(false ).build(),                         new  ColumnDefinition.Builder().name("ratio" ).required(false ).build()                 ))                 .build()); 
默认是非必填,设置必填后如果excel中有值为空,就会抛出ImportException,并提示哪行哪列值设置了必填却为空。
并行 1 2 3 4 5 6 7 List<People> dataList = ExcelImportUtils.imports(new  FileInputStream("E:\\人员表.xlsx" ),        new  ImportDefinition.Builder<People>()                 .modelClass(People.class )                  .simpleColumns(Arrays.asList("name", "birthday", "sex", "isSingle", "isMarried"))                .parallel(true )                                 .build()); 
默认是串行导入,如果设置为并行导入,建议线程数根据实际情况设置大些(导入是io密集型任务)。
批量 1 2 3 4 5 6 7 8 9 10 11 12 13 List<List<?>> dataList = ExcelImportUtils.batchImports(new  FileInputStream("E:\\人员表.xlsx" ),         Arrays.asList(                 new  ImportDefinition.Builder<People>()                                                  .modelClass(People.class )                          .simpleColumns(Arrays.asList("name", "birthday", "sex", "isSingle", "isMarried"))                         .build(),                 new  ImportDefinition.Builder<People>()                                                  .modelClass(People.class )                          .simpleColumns(Arrays.asList("name", "birthday", "sex", "isSingle", "isMarried"))                         .build()         )); 
未指定sheetName时,将按顺序读取sheet数据。
看到这里,相信大家也发现了,要定制各种导入,重点在于如何构建ImportDefinition,而通过调用ImportDefinition.Builder类,可以很方面的构建ImportDefinition。不止是ImportDefinition,类库中各类Definition都有对应的Builder。(对,正是运用了建造者设计模式。)
导出 简单的 1 2 3 4 5 6 7 8 9 ExcelExportUtils.export(new  FileOutputStream("E:\\人员表.xlsx" ),         buildData(100 ),          new  ExportDefinition.Builder()                                                                                     .simpleProperties(Arrays.asList("name" , "birthday" , "sex" , "isSingle" , "isMarried" , "ratio" ))                 .build()); 
最简单的导出只需设置simpleProperties。标题默认高度为一行,如果要指定标题高度,就调用title方法。如果要导出labels,注意labels要与properties一致。
值转换 1 2 3 4 5 6 7 8 9 10 11 12 ExcelExportUtils.export(new  FileOutputStream("E:\\人员表.xlsx" ),         buildData(100 ),         new  ExportDefinition.Builder()                 .properties(Arrays.asList(                         new  PropertyDefinition.Builder().name("name" ).build(),                          new  PropertyDefinition.Builder().name("birthday" ).valueFunction(new  DateExportFunction()).build(),                          new  PropertyDefinition.Builder().name("sex" ).valueFunction((value) -> (int ) value == 1  ? "男"  : "女" ).build(),                          new  PropertyDefinition.Builder().name("isSingle" ).valueFunction(new  BooleanIntegerExportFunction()).build(),                          new  PropertyDefinition.Builder().name("isMarried" ).valueFunction(new  BooleanExportFunction()).build(),                          new  PropertyDefinition.Builder().name("ratio" ).valueFunction(new  PercentageExportFunction()).build()                 ))                 .build()); 
特殊值标识 场景:将人员数据中已经结婚的用红色字体突出显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ExcelExportUtils.export(new  FileOutputStream("E:\\人员表.xlsx" ),         buildData(100 ),         new  ExportDefinition.Builder()                 .properties(Arrays.asList(                         new  PropertyDefinition.Builder().name("name" ).build(),                         new  PropertyDefinition.Builder().name("birthday" ).build(),                         new  PropertyDefinition.Builder().name("sex" ).build(),                         new  PropertyDefinition.Builder().name("isSingle" ).build(),                         new  PropertyDefinition.Builder().name("isMarried" ).valueFunction(new  BooleanExportFunction())                                 .styleFunction((wb, value) -> {                                     if  ((boolean ) value) {                                         CellStyle style = wb.createCellStyle();                                         Font dataFont = wb.createFont();                                         dataFont.setColor(HSSFColor.HSSFColorPredefined.RED.getIndex());                                         style.setFont(dataFont);                                         return  style;                                     }                                     return  new  DefaultStyleFunctionFactory().getDataStyleFunction().apply(wb);                                 })                                 .build(),                         new  PropertyDefinition.Builder().name("ratio" ).valueFunction(new  PercentageExportFunction()).build()                 ))                 .build()); 
批量 1 2 3 4 5 6 7 8 9 10 11 12 13 ExcelExportUtils.batchExport(new  FileOutputStream("E:\\人员表.xlsx" ),         Arrays.asList(                 buildData(100 ),                 buildData(100 )         ),         Arrays.asList(                 new  ExportDefinition.Builder()                         .simpleProperties(Arrays.asList("name" , "birthday" , "sex" , "isSingle" , "isMarried" , "ratio" ))                         .build(),                 new  ExportDefinition.Builder()                         .simpleProperties(Arrays.asList("name" , "birthday" , "sex" , "isSingle" , "isMarried" , "ratio" ))                         .build()         )); 
主子表 首先要确定主子表导出到excel中的展现形式,我的想法是做成类似EasyUI中SubGrid这样的:
海量数据 1 2 3 4 5 6 7 ExcelExportUtils.export(new  FileOutputStream("E:\\人员表.xlsx" ),         buildData(2000000 ),         new  ExportDefinition.Builder()                 .simpleProperties(Arrays.asList("name" , "birthday" , "sex" , "isSingle" , "isMarried" , "ratio" ))                 .windowSize(100 )                  .compress(false )                  .build()); 
使用poi导出海量数据,需要注意两点:
使用SXSSFWorkbook 
每张sheet中最多只能导出1048576行数据,数据量超出该范围时,将数据导出到多个sheet里 
 
以上两点在内部实现中已经考虑到了,另外根据实际情况,可以设置内存中数据量达到多少时就刷新到磁盘、以及是否需要压缩临时文件。
并行 上面说过,导出使用了SXSSFWorkbook,因此当内存中数据达到一定数量时,这些数据就会刷新到磁盘,当后面的数据刷新到磁盘,就不能回头去写前面的数据了,这样就不能使用多线程来同时往多个行写数据。当然可以设置不刷新数据到磁盘,但这样又会大大降低导出性能。因此目前未实现并行导出。
分页查询 场景:要导出1000w条数据,如果一次性都查出来放内存中,很可能会导出内存溢出,此时就需要考虑分页查询,数据一部分一部分的写到磁盘。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ExcelExportUtils.export(new  FileOutputStream("E:\\人员表.xlsx" ),         new  QueryModel(                 new  PageQuery(                         0 ,                          "id" ,                          1000 ,                          null                   ),                  pageQuery -> {                                          List<People> dataList = buildData(1000 );                     dataList.get(dataList.size() - 1 ).setId(pageQuery.getId() + pageQuery.getPageSize());                     if  (pageQuery.getId() == 2000000 ) {                         dataList.remove(dataList.size() - 1 );                         dataList.get(dataList.size() - 1 ).setId(pageQuery.getId() + pageQuery.getPageSize() - 1 );                     }                     return  dataList;                 }          ),          new  ExportDefinition.Builder()                 .simpleProperties(Arrays.asList("name" , "birthday" , "sex" , "isSingle" , "isMarried" , "ratio" ))                 .build()); 
此时就不是传递dataList而是改为传递QueryModel了。PageQuery对象里之所以是id而不是pageIndex,是因为海量数据分页查询时,主键应设置为long且保持自增,不然查询会有性能问题,且可能会重复查询数据。
打包 场景:当sheet中数据量超过100w时,打开excel是很慢的,可以考虑把数据分批导出到多个excel,最后压缩成zip导出。
1 2 3 4 5 6 ExcelExportUtils.packageExport(new  FileOutputStream("E:\\人员表.zip" ),         buildData(1000000 ),         new  ExportDefinition.Builder()                 .simpleProperties(Arrays.asList("name" , "birthday" , "sex" , "isSingle" , "isMarried" , "ratio" ))                 .packageSize(200000 )                  .build()); 
复杂表头 场景:很多时候我们导出的表头是这种简单的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ExcelExportUtils.export(new  FileOutputStream("E:\\人员表.xlsx" ),          buildData(100 ),         new  ExportDefinition.Builder()                 .labels(Arrays.asList(                         new  LabelDefinition.Builder().name("a" ).children(                                 Arrays.asList(                                         new  LabelDefinition.Builder().name("b" ).children(                                                 Arrays.asList(                                                         new  LabelDefinition.Builder().name("姓名" ).build(),                                                         new  LabelDefinition.Builder().name("出生年月" ).build()                                                 )                                         ).build(),                                         new  LabelDefinition.Builder().name("性别" ).build()                                 )                         ).build(),                         new  LabelDefinition.Builder().name("c" ).children(                                 Arrays.asList(                                         new  LabelDefinition.Builder().name("是否单身" ).build(),                                         new  LabelDefinition.Builder().name("是否已婚" ).build(),                                         new  LabelDefinition.Builder().name("百分比" ).build()                                 )                         ).build()                 ))                 .simpleProperties(Arrays.asList("name" , "birthday" , "sex" , "isSingle" , "isMarried" , "ratio" ))                 .build()); 
唯一需要注意的是,最底层的labels应与properties一一对应。
自定义样式 1 2 3 4 5 6 ExcelExportUtils.export(new  FileOutputStream("E:\\人员表.xlsx" ),         buildData(100 ),         new  ExportDefinition.Builder()                 .simpleProperties(Arrays.asList("name" , "birthday" , "sex" , "isSingle" , "isMarried" , "ratio" ))                 .styleFunctionFactory(new  DefaultStyleFunctionFactory())                 .build()); 
DefaultStyleFunctionFactory实现了StyleFunctionFactory接口,类结构如下:
基于模板 场景:实际业务中,可能需要导出各种复杂的excel,当封装的库不能实现时,就可以考虑使用jxls 基于模板来导出excel。
1 2 3 4 5 ExcelExportUtils.exportByTemplate(         new  FileOutputStream("E:\\template.xlsx" ),          "template.xlsx" ,           null   ); 
jxls的缺点在于,当数据量很大时性能会有问题,且每处导出都需要创建模板略显麻烦。