一. 简介
         导出是后台管理系统的常用功能,当数据量特别大的时候会内存溢出和卡顿页面,曾经自己封装过一个导出,POI百万级大数据量EXCEL导出 采用了分批查询数据来避免内存溢出和使用SXSSFWorkbook方式缓存数据到文件上以解决下载大文件EXCEL卡死页面的问题。不过一是存在封装不太友好使用不方便的问题,二是这些poi的操作方式仍然存在内存占用过大的问题,三是存在空循环和整除的时候数据有缺陷的问题,以及存在内存溢出的隐患。有意间查询到阿里开源的EasyExcel框架,发现可以将解析的EXCEL的内存占用控制在KB级别,并且绝对不会内存溢出(内部实现待研究),还有就是速度极快, 大概100W条记录,十几个字段, 只需要70秒即可完成下载。遂抛弃自己封装的,转战研究阿里开源的EasyExcel. 不过 说实话,当时自己封装的那个还是有些技术含量的,例如 外观模式,模板方法模式,以及委托思想,组合思想,可以看看。
————————————————

         EasyExcel的github地址是:https://github.com/alibaba/easyexcel 文档:https://alibaba-easyexcel.github.io/quickstart/write.html

二. 案例
2.1 POM依赖
     

<!-- 阿里开源EXCEL -->
         <dependency>
             <groupId>com.alibaba</groupId>
             <artifactId>easyexcel</artifactId>
             <version>2.1.4</version>
         </dependency>


 

三: 网上基础教程很多这边 仅就“写入技巧” 作下整理 该囊括了大部分我们工作中常用到的excel读写技巧,欢迎收藏查阅
1.7.1. 排除特定字段和只写入特定字段
使用excludeColumnFiledNames来排除特定字段写入,用includeColumnFiledNames表示只写入特定字段
 

/**
     * 根据参数只导出指定列
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link DemoData}
     * <p>
     * 2. 根据自己或者排除自己需要的列
     * <p>
     * 3. 直接写即可
     */
    @Test
    public void excludeOrIncludeWrite() {
        String fileName = TestFileUtil.getPath() + "excludeOrIncludeWrite" + System.currentTimeMillis() + ".xlsx";
 
        // 根据用户传入字段 假设我们要忽略 date
        Set<String> excludeColumnFiledNames = new HashSet<String>();
        excludeColumnFiledNames.add("date");
        // 这里 需要指定写用哪个class去读,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        EasyExcel.write(fileName, DemoData.class).excludeColumnFiledNames(excludeColumnFiledNames).sheet("模板")
            .doWrite(data());
 
        fileName = TestFileUtil.getPath() + "excludeOrIncludeWrite" + System.currentTimeMillis() + ".xlsx";
        // 根据用户传入字段 假设我们只要导出 date
        Set<String> includeColumnFiledNames = new HashSet<String>();
        includeColumnFiledNames.add("date");
        // 这里 需要指定写用哪个class去读,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        EasyExcel.write(fileName, DemoData.class).includeColumnFiledNames(includeColumnFiledNames).sheet("模板")
            .doWrite(data());
    }

1.7.2. 指定写入列
写入列的顺序可以进行指定,在实体类注解上指定index,从小到大,从左到右排列

@Data
 public class IndexData {
     @ExcelProperty(value = "字符串标题", index = 0)
     private String string;
     @ExcelProperty(value = "日期标题", index = 1)
     private Date date;
     /**
      * 这里设置3 会导致第二列空的
      */
     @ExcelProperty(value = "数字标题", index = 3)
     private Double doubleData;
 }


1.7.3. 复杂头写入
如下图这种复杂头

我们可以通过修改实体类注解实现

@Data
 public class ComplexHeadData {
     @ExcelProperty({"主标题", "字符串标题"})
     private String string;
     @ExcelProperty({"主标题", "日期标题"})
     private Date date;
     @ExcelProperty({"主标题", "数字标题"})
     private Double doubleData;
 }


1.7.4. 重复多次写入
分为三种:1. 重复写入同一个sheet;2. 同一个对象写入不同sheet;3. 不同的对象写入不同的sheet
 

/**
     * 重复多次写入
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link ComplexHeadData}
     * <p>
     * 2. 使用{@link ExcelProperty}注解指定复杂的头
     * <p>
     * 3. 直接调用二次写入即可
     */
    @Test
    public void repeatedWrite() {
        // 方法1 如果写到同一个sheet
        String fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
        // 这里 需要指定写用哪个class去读
        ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build();
        // 这里注意 如果同一个sheet只要创建一次
        WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
        // 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来
        for (int i = 0; i < 5; i++) {
            // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
            List<DemoData> data = data();
            writeSheet.setSheetName("模板");
            excelWriter.write(data, writeSheet);
        }
        /// 千万别忘记finish 会帮忙关闭流
        excelWriter.finish();
 
        // 方法2 如果写到不同的sheet 同一个对象
        fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
        // 这里 指定文件
        excelWriter = EasyExcel.write(fileName, DemoData.class).build();
        // 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面
        for (int i = 0; i < 5; i++) {
            // 每次都要创建writeSheet 这里注意必须指定sheetNo
            writeSheet = EasyExcel.writerSheet(i, "模板"+i).build();
            // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
            List<DemoData> data = data();
            excelWriter.write(data, writeSheet);
        }
        /// 千万别忘记finish 会帮忙关闭流
        excelWriter.finish();
 
        // 方法3 如果写到不同的sheet 不同的对象
        fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
        // 这里 指定文件
        excelWriter = EasyExcel.write(fileName).build();
        // 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面
        for (int i = 0; i < 5; i++) {
            // 每次都要创建writeSheet 这里注意必须指定sheetNo。这里注意DemoData.class 可以每次都变,我这里为了方便 所以用的同一个class 实际上可以一直变
            writeSheet = EasyExcel.writerSheet(i, "模板"+i).head(DemoData.class).build();
            // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
            List<DemoData> data = data();
            excelWriter.write(data, writeSheet);
        }
        /// 千万别忘记finish 会帮忙关闭流
        excelWriter.finish();
    }

1.7.5. 图片导出
对图片的导出,可能会有这样的需求,它提供了四种数据类型的导出,还是很丰富的
 

@Test
     public void imageWrite() throws Exception {
         String fileName = TestFileUtil.getPath() + "imageWrite" + System.currentTimeMillis() + ".xlsx";
         // 如果使用流 记得关闭
         InputStream inputStream = null;
         try {
             List<ImageData> list = new ArrayList<ImageData>();
             ImageData imageData = new ImageData();
             list.add(imageData);
             String imagePath = TestFileUtil.getPath() + "converter" + File.separator + "img.jpg";
             // 放入四种类型的图片 实际使用只要选一种即可
             imageData.setByteArray(FileUtils.readFileToByteArray(new File(imagePath)));
             imageData.setFile(new File(imagePath));
             imageData.setString(imagePath);
             inputStream = FileUtils.openInputStream(new File(imagePath));
             imageData.setInputStream(inputStream);
             EasyExcel.write(fileName, ImageData.class).sheet().doWrite(list);
         } finally {
             if (inputStream != null) {
                 inputStream.close();
             }
         }
     }


图片类为

@Data
 @ContentRowHeight(100)
 @ColumnWidth(100 / 8)
 public class ImageData {
     private File file;
     private InputStream inputStream;
     /**
      * 如果string类型 必须指定转换器,string默认转换成string
      */
     @ExcelProperty(converter = StringImageConverter.class)
     private String string;
     private byte[] byteArray;
 }


导出结果:两行四列,每列都对应一张图片,四种导出类型均可

其中StringImageConverter自定义转换器为

public class StringImageConverter implements Converter<String> {
     @Override
     public Class supportJavaTypeKey() {
         return String.class;
     }
  
     @Override
     public CellDataTypeEnum supportExcelTypeKey() {
         return CellDataTypeEnum.IMAGE;
     }
  
     @Override
     public String convertToJavaData(CellData cellData, ExcelContentProperty contentProperty,
         GlobalConfiguration globalConfiguration) {
         throw new UnsupportedOperationException("Cannot convert images to string");
     }
  
     @Override
     public CellData convertToExcelData(String value, ExcelContentProperty contentProperty,
         GlobalConfiguration globalConfiguration) throws IOException {
         return new CellData(FileUtils.readFileToByteArray(new File(value)));
     }
  
 }


1.7.6. 字段宽高设置
设置实体类注解属性即可

@Data
 @ContentRowHeight(10)
 @HeadRowHeight(20)
 @ColumnWidth(25)
 public class WidthAndHeightData {
     @ExcelProperty("字符串标题")
     private String string;
     @ExcelProperty("日期标题")
     private Date date;
     /**
      * 宽度为50
      */
     @ColumnWidth(50)
     @ExcelProperty("数字标题")
     private Double doubleData;
 }


1.7.7. 自定义样式
实现会比较复杂,需要做头策略,内容策略,字体大小等
 

@Test
     public void styleWrite() {
         String fileName = TestFileUtil.getPath() + "styleWrite" + System.currentTimeMillis() + ".xlsx";
         // 头的策略
         WriteCellStyle headWriteCellStyle = new WriteCellStyle();
         // 背景设置为红色
         headWriteCellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
         WriteFont headWriteFont = new WriteFont();
         headWriteFont.setFontHeightInPoints((short)20);
         headWriteCellStyle.setWriteFont(headWriteFont);
         // 内容的策略
         WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
         // 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.头默认了 FillPatternType所以可以不指定
         contentWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
         // 背景绿色
         contentWriteCellStyle.setFillForegroundColor(IndexedColors.GREEN.getIndex());
         WriteFont contentWriteFont = new WriteFont();
         // 字体大小
         contentWriteFont.setFontHeightInPoints((short)20);
         contentWriteCellStyle.setWriteFont(contentWriteFont);
         // 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现
         HorizontalCellStyleStrategy horizontalCellStyleStrategy =
             new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
  
         // 这里 需要指定写用哪个class去读,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
         EasyExcel.write(fileName, DemoData.class).registerWriteHandler(horizontalCellStyleStrategy).sheet("模板")
             .doWrite(data());
     }


效果如下

1.7.8. 单元格合并
 

@Test
     public void mergeWrite() {
         String fileName = TestFileUtil.getPath() + "mergeWrite" + System.currentTimeMillis() + ".xlsx";
         // 每隔2行会合并。当然其他合并策略也可以自己写
         LoopMergeStrategy loopMergeStrategy = new LoopMergeStrategy(2, 0);
         // 这里 需要指定写用哪个class去读,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
         EasyExcel.write(fileName, DemoData.class).registerWriteHandler(loopMergeStrategy).sheet("模板").doWrite(data());
     }


效果如下,第一列单元格数据,2,3两行合并

1.7.9. 自动列宽
根据作者描述,POI对中文的自动列宽适配不友好,easyexcel对数字也不能准确适配列宽,他提供的适配策略可以用,但不能精确适配,可以自己重写
想用就注册处理器LongestMatchColumnWidthStyleStrategy
   

@Test
     public void longestMatchColumnWidthWrite() {
         String fileName =
             TestFileUtil.getPath() + "longestMatchColumnWidthWrite" + System.currentTimeMillis() + ".xlsx";
         // 这里 需要指定写用哪个class去读,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
         EasyExcel.write(fileName, LongestMatchColumnWidthData.class)
             .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).sheet("模板").doWrite(dataLong());
     }


1.7.10. 下拉,超链接
下拉,超链接等功能需要自定义实现
   

@Test
     public void customHandlerWrite() {
         String fileName = TestFileUtil.getPath() + "customHandlerWrite" + System.currentTimeMillis() + ".xlsx";
         // 这里 需要指定写用哪个class去读,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
         EasyExcel.write(fileName, DemoData.class).registerWriteHandler(new CustomSheetWriteHandler())
             .registerWriteHandler(new CustomCellWriteHandler()).sheet("模板").doWrite(data());
     }


其中主要为处理器CustomCellWriteHandler类,其实现CellWriteHandler接口,我们在后处理方法afterCellDispose做处理

public class CustomCellWriteHandler implements CellWriteHandler {
  
     private static final Logger LOGGER = LoggerFactory.getLogger(CustomCellWriteHandler.class);
  
     @Override
     public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row,
         Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {
  
     }
  
     @Override
     public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell,
         Head head, Integer relativeRowIndex, Boolean isHead) {
  
     }
  
     @Override
     public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
         List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
         // 这里可以对cell进行任何操作
         LOGGER.info("第{}行,第{}列写入完成。", cell.getRowIndex(), cell.getColumnIndex());
         if (isHead && cell.getColumnIndex() == 0) {
             CreationHelper createHelper = writeSheetHolder.getSheet().getWorkbook().getCreationHelper();
             Hyperlink hyperlink = createHelper.createHyperlink(HyperlinkType.URL);
             hyperlink.setAddress("https://github.com/alibaba/easyexcel");
             cell.setHyperlink(hyperlink);
         }
     }
  
 }


1.7.11. 不创建对象的写
在设置write的时候不设置对象类,在head里添加List<List<String>>的对象头
   

@Test
     public void noModleWrite() {
         // 写法1
         String fileName = TestFileUtil.getPath() + "noModleWrite" + System.currentTimeMillis() + ".xlsx";
         // 这里 需要指定写用哪个class去读,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
         EasyExcel.write(fileName).head(head()).sheet("模板").doWrite(dataList());
     }
     
     private List<List<String>> head() {
         List<List<String>> list = new ArrayList<List<String>>();
         List<String> head0 = new ArrayList<String>();
         head0.add("字符串" + System.currentTimeMillis());
         List<String> head1 = new ArrayList<String>();
         head1.add("数字" + System.currentTimeMillis());
         List<String> head2 = new ArrayList<String>();
         head2.add("日期" + System.currentTimeMillis());
         list.add(head0);
         list.add(head1);
         list.add(head2);
         return list;
     }