一. 简介
导出是后台管理系统的常用功能,当数据量特别大的时候会内存溢出和卡顿页面,曾经自己封装过一个导出,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;
}