文章目录
一、 EasyExcel概述
Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便。
二、 EasyExcel核心类简述
2.1 EasyExcel
EasyExcel
继承自EasyExcelFactory
工厂类,是EasyExcel的入口类,用于构建读和写的方法,通过不同的参数可以实现不同方式读写文件。EasyFactory
用于实现具体的方法。
调用EasyExcel
类的静态方法read()
,通过传入不同的参数实现不同方式读取数据,较为常用的是read(String pathName, Class head, ReadListener readListener)
方法,参数pathName
为要读取文件的文件路径,参数head
为参照该实体类读取数据,参数readListener
为在读取数据时所执行的监听器。返回值为ExcelReaderBuilder
类。
其他读取Excel的方法:
方法 | 参数 | 返回值 |
---|---|---|
read() | ExcelReaderBuilder | |
read(File file) | file 待读取文件 | ExcelReaderBuilder |
read(File file, ReadListener readListener) | file 待读取文件readListener 监听器 | ExcelReaderBuilder |
read(File file, Class head, ReadListener readListener) | file 读取文件head 按照实体类读取数据readListener 监听器 | ExcelReaderBuilder |
read(String pathName) | pathName 待读取文件路径 | ExcelReaderBuilder |
read(String pathName, ReadListener readListener) | pathName 待读取文件路径readListener 监听器 | ExcelReaderBuilder |
read(String pathName, Class head, ReadListener readListener) | pathName 待读取文件路径head 按照实体类读取数据readListener 监听器 | ExcelReaderBuilder |
read(InputStream inputStream) | inputStream 待读取文件文件流 | ExcelReaderBuilder |
read(InputStream inputStream, ReadListener readListener) | inputStream 待读取文件文件流readListener 监听器 | ExcelReaderBuilder |
read(InputStream inputStream, Class head, ReadListener readListener) | inputStream 待读取文件文件流head 按照实体类读取数据readListener 监听器 | ExcelReaderBuilder |
调用EasyExcel
类的静态方法write()
,通过传入不同的参数实现不同方式读取数据,较为常用的是write(String pathName, Class head)
方法,参数pathName
为要读取文件的文件路径,参数head
为参考该实体类导出数据。返回值为ExcelWriterBuilder
类。
其他写Excel方法:
方法 | 参数 | 返回值 |
---|---|---|
write() | ExcelWriterBuilder | |
write(File file) | file 待输出文件 | ExcelWriterBuilder |
write(File file, Class head) | file 待输出文件head 按照实体类输出数据 | ExcelWriterBuilder |
write(String pathName) | pathName 待输出文件路径 | ExcelWriterBuilder |
write(String pathName, Class head) | pathName 待输出文件路径head 按照实体类输出数据 | ExcelWriterBuilder |
write(OutputStream outputStream) | inputStream 待输出文件文件流 | ExcelWriterBuilder |
write(OutputStream outputStream, Class head) | inputStream 待输出文件文件流head 按照实体类输出数据 | ExcelWriterBuilder |
ExcelReaderBuilder
提供读取Excel文件数据的读取方式以及一些工作簿属性的设置。
2.2 ExcelReaderBuilder
与ExcelWriterBuilder
ExcelReaderBuilder
与ExcelWriterBuilder
用于创建ReadWorkbook
与WriteWorkbook
,可以理解为用于构建Excel的文件。
2.2.1 ExcelReaderBuilder
- 设置
ReadWorkbook
属性
方法 | 参数 | 返回值 | 描述 |
---|---|---|---|
ExcelReaderBuilder() | / | / | ExcelReaderBuilder类的构造方法,同时创建ReadWorkbook的实体类 |
excelType(ExcelTypeEnum excelType) | excelType Excel类型(枚举类型) | ExcelReaderBuilder | 设置Excel的文件类型,明确读取的是xls、xlsx还是cvs文件 |
autoCloseStream(Boolean autoCloseStream) | autoCloseStream 是否自动关闭流(布尔类型) | ExcelReaderBuilder | 设置读取完毕后是否自动关闭流,默认为自动关闭 |
ignoreEmptyRow(Boolean ignoreEmptyRow) | ignoreEmptyRow 是否忽略空行(布尔类型) | ExcelReaderBuilder | 设置是否忽略空行,默认自动忽略 |
extraRead(CellExtraTypeEnum extraType) | extraType 单元格属性类型(枚举类型) | ExcelReaderBuilder | 读取单元格的批注、超链接和合并单元格属性 |
- 创建ExcelReader
ExcelReader
作为EasyExcel的核心实现类,用于实现文件的读取,调用build()
方法会返回一个ExcelReader
对象,并传入设置好的readWorkbook
属性。
public ExcelReader build() {
return new ExcelReader(readWorkbook);
}
- 创建
ExcelReaderSheetBuilder
如果是指定读取某个工作簿的内容,可以先调用sheet()
方法设置当前需要读取的是第几个工作簿或指定工作簿的名称。不指定参数则获取所有工作簿的内容。在调用ExcelReaderSheetBuilder
类的doRead()
和doReadSync()
方法读取数据。
方法 | 参数 | 返回值 |
---|---|---|
sheet() | / | ExcelReaderSheetBuilder |
sheet(Integer sheetNo) | sheetNo 工作簿序号(从0开始) | ExcelReaderSheetBuilder |
sheet(String sheetName) | sheetName 工作簿名称 | ExcelReaderSheetBuilder |
sheet(Integer sheetNo, String sheetName) | sheetNo 工作簿序号(从0开始)sheetName 工作簿名称 | ExcelReaderSheetBuilder |
- 读取数据
若需要读取全部工作簿的数据,可以直接调用doReadAll()
方法。
public void doReadAll() {
ExcelReader excelReader = build();
excelReader.readAll();
excelReader.finish();
}
如果需要将结果拿出来再处理,可以调用doReadAllSync()
方法,可以返回List<T>
对应的泛型集合。
public <T> List<T> doReadAllSync() {
SyncReadListener syncReadListener = new SyncReadListener();
registerReadListener(syncReadListener);
ExcelReader excelReader = build();
excelReader.readAll();
excelReader.finish();
return (List<T>)syncReadListener.getList();
}
2.2.2 ExcelWriterBuilder
- 设置
WriteWorkbook
属性
方法 | 参数 | 返回值 | 描述 |
---|---|---|---|
ExcelWriterBuilder() | / | / | ExcelWriterBuilder 类的构造方法,同时创建WriteWorkbook 的实体类 |
excelType(ExcelTypeEnum excelType) | excelType Excel类型(枚举类型) | ExcelWriterBuilder | 设置Excel的文件类型,明确读取的是xls、xlsx还是cvs文件 |
autoCloseStream(Boolean autoCloseStream) | autoCloseStream 是否自动关闭流(布尔类型) | ExcelWriterBuilder | 设置读取完毕后是否自动关闭流,默认为自动关闭 |
inMemory(Boolean inMemory) | inMemory 是否在内存中运行 | ExcelWriterBuilder | 设置是否将数据读取到内存中,默认为false |
- 创建ExcelWriter
ExcalWriter
作为EasyExcel
写文件的核心类,用于实现输出Excel,可以调用ExcelWriterBuilder
类的build()
方法创建ExcalWriter
类,并将设置好的writeWorkbook
。示例:
public ExcelWriter build() {
return new ExcelWriter(writeWorkbook);
}
- 创建
ExcelWriterSheetBuilder
调用ExcelWriterBuilder
类的sheet()
方法,用于设置将该数据流写入文件的那个工作簿,并设置工作簿的名称
方法 | 参数 | 返回值 |
---|---|---|
sheet() | / | ExcelWriterSheetBuilder |
sheet(Integer sheetNo) | sheetNo 工作簿序号(从0开始) | ExcelWriterSheetBuilder |
sheet(String sheetName) | sheetName 工作簿名称 | ExcelWriterSheetBuilder |
sheet(Integer sheetNo, String sheetName) | sheetNo 工作簿序号(从0开始)sheetName 工作簿名称 | ExcelWriterSheetBuilder |
如果不设置工作簿名称,则输出在第一个工作簿并且工作簿名称为0
- 输出文件
调用ExcelWriter
类的write(Collection<?> data, WriteSheet writeSheet, WriteTable writeTable)
方法,参数data
为需要输出的数据集合,参数writeSheet
为需要写入的工作簿信息,参数writeTable
为写入的table(若不同的几组数据写入同一个工作簿可以创建多个WriteTable
,可以为空)。
public ExcelWriter write(Collection<?> data, WriteSheet writeSheet, WriteTable writeTable) {
excelBuilder.addContent(data, writeSheet, writeTable);
return this;
}
2.3 ExcelReaderSheetBuilder
与ExcelWriterSheetBuilder
ExcelReaderSheetBuilder
与ExcelWriterSheetBuilder
用于创建ExcelReader
和ExcelWriter
类,可以理解为创建Excel的工作表。
2.3.1 ExcelReaderSheetBuilder
- 设置
ReadSheet
属性
方法 | 参数 | 返回值 | 描述 |
---|---|---|---|
ExcelReaderSheetBuilder() | \ | \ | 构造函数,用于创建ReadSheet |
ExcelReaderSheetBuilder(ExcelReader excelReader) | ExcelReader excelReader | \ | 构造函数,用于创建ReadSheet 和ExcelReader |
sheetNo(Integer sheetNo) | Integer sheetNo 工作表序号,从0开始 | ExcelReaderSheetBuilder | 用于设置读取哪个工作表 |
sheetName(String sheetName) | String sheetName 工作表名称 | ExcelReaderSheetBuilder | 用于设置读取哪个工作表 |
- 获取
ReadSheet
对象
调用build()
方法,可以返回ReadSheet
属性。
public ReadSheet build() {
return readSheet;
}
- 读取文件
/**
* Sax read
*/
public void doRead() {
if (excelReader == null) {
throw new ExcelGenerateException("Must use 'EasyExcelFactory.read().sheet()' to call this method");
}
excelReader.read(build());
excelReader.finish();
}
/**
* Synchronous reads return results
*
* @return
*/
public <T> List<T> doReadSync() {
if (excelReader == null) {
throw new ExcelAnalysisException("Must use 'EasyExcelFactory.read().sheet()' to call this method");
}
SyncReadListener syncReadListener = new SyncReadListener();
registerReadListener(syncReadListener);
excelReader.read(build());
excelReader.finish();
return (List<T>)syncReadListener.getList();
}
2.3.2 ExcelWriterSheetBuilder
- 设置
WriteSheet
属性
方法 | 参数 | 返回值 | 描述 |
---|---|---|---|
ExcelWriterSheetBuilder() | \ | \ | 构造函数,用于创建WriteSheet |
ExcelWriterSheetBuilder(ExcelWriter excelWriter) | ExcelWriter excelWriter | \ | 构造函数,用于创建WriteSheet 和ExcelWriter |
sheetNo(Integer sheetNo) | Integer sheetNo 工作表序号 | ExcelWriterSheetBuilder | 设置工作表 |
sheetName(String sheetName) | String sheetName 工作表名称 | ExcelWriterSheetBuilder | 设置工作表 |
- 创建
WriteSheet
对象
调用build()
方法,可以返回WriteSheet
属性。
public WriteSheet build() {
return writeSheet;
}
- 创建文件
使用doWrite()
方法可以向文件直接填充数据
public void doWrite(Collection<?> data) {
if (excelWriter == null) {
throw new ExcelGenerateException("Must use 'EasyExcelFactory.write().sheet()' to call this method");
}
excelWriter.write(data, build());
excelWriter.finish();
}
public void doWrite(Supplier<Collection<?>> supplier) {
doWrite(supplier.get());
}
使用doFill()
方法可以根据模板向文件填充数据
public void doFill(Object data) {
doFill(data, null);
}
public void doFill(Object data, FillConfig fillConfig) {
if (excelWriter == null) {
throw new ExcelGenerateException("Must use 'EasyExcelFactory.write().sheet()' to call this method");
}
excelWriter.fill(data, fillConfig, build());
excelWriter.finish();
}
public void doFill(Supplier<Object> supplier) {
doFill(supplier.get());
}
public void doFill(Supplier<Object> supplier, FillConfig fillConfig) {
doFill(supplier.get(), fillConfig);
}
2.4 ExcelReader
与ExcelWriter
该类用于提供读和写Excel的实现方法
2.4.1 ExcelReader
方法 | 参数 | 描述 |
---|---|---|
ExcelReader(ReadWorkbook readWorkbook) | ReadWorkbook readWorkbook | 构造函数,用于创建ExcelAnalyser 类 |
readAll() | \ | 读取所有工作表的数据 |
read(ReadSheet... readSheet) | ReadSheet... readSheet | 读取指定的工作表 |
read(List<ReadSheet> readSheetList) | List<ReadSheet> readSheetList | 读取指定的工作表 |
2.4.2 ExcelWriter
方法 | 参数 | 描述 |
---|---|---|
ExcelWriter(WriteWorkbook writeWorkbook) | WriteWorkbook writeWorkbook | 构造函数,用于创建ExcelBuilder 类 |
write(Collection<?> data, WriteSheet writeSheet) | Collection<?> data 数据集WriteSheet writeSheet 写工作表类 | 导出Excel文件 |
write(Collection<?> data, WriteSheet writeSheet, WriteTable writeTable) | Collection<?> data 数据集WriteSheet writeSheet 写工作表类WriteTable writeTable 若为多个数据集写入一个表可以创建WriteTable 分区导出 | 导出Excel文件 |
fill(Object data, WriteSheet writeSheet) | Object data 数据集WriteSheet writeSheet | 使用模板填充数据 |
fill(Object data, FillConfig fillConfig, WriteSheet writeSheet) | Object data 数据集FillConfig fillConfig 填充配置WriteSheet writeSheet | 使用模板填充数据 |
三、示例
3.1 读
3.1.1 简单读取数据
为避免一次性读取过量数据导致内存崩溃,可以自己实现ReadListener
接口分批读取数据,并在读取过程中进行数据处理(存储数据库等)操作。
实现示例:
@Test
public void simpleRead() {
// 写法1:JDK8+ ,不用额外写一个DemoDataListener
// since: 3.0.0-beta1
String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
// 这里每次会读取3000条数据 然后返回过来 直接调用使用数据就行
EasyExcel.read(fileName, DemoData.class, new DemoDataListener<DemoData>()).sheet().doRead();
}
如果是自己实现
ReadListener
类需要注意该实现类无法被Spring管理,同时也无法使用注解创建对象,所以可以定义一个私有属性通过构造函数将Spring管理的类传入
package com.alibaba.easyexcel.test.demo.read;
import java.util.ArrayList;
import java.util.List;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 模板的读取类
*
* @author Jiaju Zhuang
*/
// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
public class DemoDataListener extends AnalysisEventListener<DemoData> {
private static final Logger LOGGER = LoggerFactory.getLogger(DemoDataListener.class);
/**
* 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 3000;
/**
* 缓存的数据
*/
private List<DemoData> list = new ArrayList<>(BATCH_COUNT);
/**
* 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
*/
private DemoDAO demoDAO;
/**
* 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
*
* @param demoDAO
*/
public DemoDataListener(DemoDAO demoDAO) {
this.demoDAO = demoDAO;
}
/**
* 这个每一条数据解析都会来调用
*
* @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}
* @param context
*/
@Override
public void invoke(DemoData data, AnalysisContext context) {
LOGGER.info("解析到一条数据:{}", JSON.toJSONString(data));
list.add(data);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (list.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
list = new ArrayList<>(BATCH_COUNT);
}
}
/**
* 所有数据解析完成了 都会来调用
*
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
saveData();
LOGGER.info("所有数据解析完成!");
}
/**
* 加上存储数据库
*/
private void saveData() {
LOGGER.info("{}条数据,开始存储数据库!", list.size());
demoDAO.save(list);
LOGGER.info("存储数据库成功!");
}
}
对于Excel读取出来对应的实体类,如果不加注解会按照顺序对应实体类的属性,即第一列数据对应第一个属性依次类推。如果需要进行数据表与属性的绑定则需在实体类的属性上添加注解@ExcelProperty
。该注解有两个参数,@value
为属性与数据表的列名绑定,@index
将属性与数据表的列数绑定,对于其他不需要绑定数据的属性可以添加@ExcelIgnore
注解避免将错误的至赋给实体类。
对于一些需要进行数据类型转换或格式化的属性可以使用@ExcelProperty
的converter
指定转换的实体类,对于时间格式可以使用@DateTimeFormat
注解对时间类型数据进行格式化,对于数值型可以使用@NumberFormat
注解格式化数值型,例如@NumberFormat("#.##%")
将值转为百分比。
也可以重写
ExcelReaderBuilder
继承自AbstractExcelReaderParameterBuilder
类的registerReadListener
方法,使用该方法后会对实体类虽有符合的属性进行转化,不如注解的形式灵活。
3.1.2 读取单元格的额外属性(超链接、批注、合并单元格信息)
除了对简单数据表读取功能外,EasyExcel还支持读取合并单元格、超链接、批注的信息读取。调用ExcelReaderBuilder
的extraRead()
方法设置要获取的属性。通过调用DemoExtraListener
类重写的extra()
获取读取的额外属性。示例:
@Test
public void extraRead() {
String fileName = TestFileUtil.getPath() + "demo" + File.separator + "extra.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet
EasyExcel.read(fileName, DemoExtraData.class, new DemoExtraListener())
// 需要读取批注 默认不读取
.extraRead(CellExtraTypeEnum.COMMENT)
// 需要读取超链接 默认不读取
.extraRead(CellExtraTypeEnum.HYPERLINK)
// 需要读取合并单元格信息 默认不读取
.extraRead(CellExtraTypeEnum.MERGE).sheet().doRead();
}
3.1.3 读取文件中的异常处理
在实际应用过程中,我们无法保证用户填写的文件百分百准确。所以在读取到无法解析的数据时需要做容错处理,对异常数据进行抛出。可以在监听器中调用onException()
方法处理无法处理的数据。示例:
@Test
public void exceptionRead() {
String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet
EasyExcel.read(fileName, ExceptionDemoData.class, new DemoExceptionListener()).sheet().doRead();
}
@Data
public class ExceptionDemoData {
/**
* 用日期去接字符串 肯定报错
*/
private Date date;
}
package com.alibaba.easyexcel.test.demo.read;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.exception.ExcelDataConvertException;
import com.alibaba.fastjson.JSON;
/**
* 读取转换异常
*
* @author Jiaju Zhuang
*/
public class DemoExceptionListener extends AnalysisEventListener<ExceptionDemoData> {
private static final Logger LOGGER = LoggerFactory.getLogger(DemoExceptionListener.class);
/**
* 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 5;
List<ExceptionDemoData> list = new ArrayList<ExceptionDemoData>();
/**
* 在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。
*
* @param exception
* @param context
* @throws Exception
*/
@Override
public void onException(Exception exception, AnalysisContext context) {
LOGGER.error("解析失败,但是继续解析下一行:{}", exception.getMessage());
// 如果是某一个单元格的转换异常 能获取到具体行号
// 如果要获取头的信息 配合invokeHeadMap使用
if (exception instanceof ExcelDataConvertException) {
ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception;
LOGGER.error("第{}行,第{}列解析异常,数据为:{}", excelDataConvertException.getRowIndex(),
excelDataConvertException.getColumnIndex(), excelDataConvertException.getCellData());
}
}
/**
* 这里会一行行的返回头
*
* @param headMap
* @param context
*/
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
LOGGER.info("解析到一条头数据:{}", JSON.toJSONString(headMap));
}
@Override
public void invoke(ExceptionDemoData data, AnalysisContext context) {
LOGGER.info("解析到一条数据:{}", JSON.toJSONString(data));
if (list.size() >= BATCH_COUNT) {
saveData();
list.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
saveData();
LOGGER.info("所有数据解析完成!");
}
/**
* 加上存储数据库
*/
private void saveData() {
LOGGER.info("{}条数据,开始存储数据库!", list.size());
LOGGER.info("存储数据库成功!");
}
}
3.1.4 返回读取数据
如果需要将该数据再提取出来二次处理的话,可以使用doReadSync()
方法,可以返回实体类的集合数据。
3.2 写
3.2.1 原数据输出
@Test
public void simpleWrite() {
// 写法1 JDK8+
// since: 3.0.0-beta1
String fileName = TestFileUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
// 如果这里想使用03 则 传入excelType参数即可
EasyExcel.write(fileName, DemoData.class)
.sheet("模板")
.doWrite(() -> {
// 分页查询数据
return data();
});
}
如果需要输出为其他格式的文件,可以通过调用
ExcelWriteBuilder
的excelType(ExcelTypeEnum excelType)
设置输出的文件类型。
3.2.2 指定字段输出
若有些数据不需要输出或者需要单独输出某几列的内容,可以调用ExcelWriterBuilder
继承自AbstractExcelWriterParameterBuilder
的excludeColumnFiledNames(Collection<String> excludeColumnFiledNames)
方法去除指定的列,将需要去除的列写成Set
集合作为参数传入。调用includeColumnFiledNames(Collection<String> includeColumnFiledNames)
方法可以指定需要导出的列,同样可以写成Set
集合。
示例:
@Test
public void excludeOrIncludeWrite() {
String fileName = TestFileUtil.getPath() + "excludeOrIncludeWrite" + System.currentTimeMillis() + ".xlsx";
// 这里需要注意 在使用ExcelProperty注解的使用,如果想不空列则需要加入order字段,而不是index,order会忽略空列,然后继续往后,而index,不会忽略空列,在第几列就是第几列。
// 根据用户传入字段 假设我们要忽略 date
Set<String> excludeColumnFiledNames = new HashSet<>();
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<>();
includeColumnFiledNames.add("date");
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
EasyExcel.write(fileName, DemoData.class).includeColumnFiledNames(includeColumnFiledNames).sheet("模板")
.doWrite(data());
}
也可以直接使用注解规定需要输出的属性和位置。@ExcelProperty
可以设置导出后列的标题(通过设置value
属性)和导出在第几列中(通过设置Index属性,从0开始)。
示例:
package com.alibaba.easyexcel.test.demo.write;
import java.util.Date;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
/**
* 基础数据类
*
* @author Jiaju Zhuang
**/
@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;
}
3.2.3 格式化数据输出
当需要输出的字段存在格式要求时(时间、金额等)可以通过设置格式转换类对属性进行格式化,再输出为Excel时就能够按照既定格式输出。方法一:定义转换器,实现Convert
接口对数据进行单独处理,在使用@ExcelProperty
的convert
属性添加到实体类的属性上;方法二:对于简单的时间格式或数值格式,可以直接使用@DateFormat
和@NumberFormat
注解(与读取格式化数据用法相同)。
示例:
package com.alibaba.easyexcel.test.demo.write;
import java.util.Date;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.format.NumberFormat;
import lombok.Data;
/**
* 基础数据类.这里的排序和excel里面的排序一致
*
* @author Jiaju Zhuang
**/
@Data
public class ConverterData {
/**
* 我想所有的 字符串起前面加上"自定义:"三个字
*/
@ExcelProperty(value = "字符串标题", converter = CustomStringStringConverter.class)
private String string;
/**
* 我想写到excel 用年月日的格式
*/
@DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
@ExcelProperty("日期标题")
private Date date;
/**
* 我想写到excel 用百分比表示
*/
@NumberFormat("#.##%")
@ExcelProperty(value = "数字标题")
private Double doubleData;
}
package com.alibaba.easyexcel.test.demo.write;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
/**
* String and string converter
*
* @author Jiaju Zhuang
*/
public class CustomStringStringConverter implements Converter<String> {
@Override
public Class<?> supportJavaTypeKey() {
return String.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
/**
* 这里是读的时候会调用 不用管
*
* @param cellData NotNull
* @param contentProperty Nullable
* @param globalConfiguration NotNull
* @return
*/
@Override
public String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
return cellData.getStringValue();
}
/**
* 这里是写的时候会调用 不用管
*
* @param value NotNull
* @param contentProperty Nullable
* @param globalConfiguration NotNull
* @return
*/
@Override
public WriteCellData<?> convertToExcelData(String value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
return new WriteCellData<>("自定义:" + value);
}
}
@Test
public void converterWrite() {
String fileName = TestFileUtil.getPath() + "converterWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
EasyExcel.write(fileName, ConverterData.class).sheet("模板").doWrite(data());
}
3.2.4 设置单元格样式、超链接、备注、公式
先根据WriteCellData
实现一个待输出的数据类类似于WriteCellDemoData
,超链接、备注、公式、样式的类型都为WriteCellData
类型。对于需要设置样式的属性,可以通过创建对象将属性赋给各自对象即可。
Class | Property | 描述 |
---|---|---|
HyperlinkData | String address 超链接的访问地址HyperlinkType hyperlinkType 超链接的类型 | 超链接 |
CommentData | String author 备注作者RichTextStringData richTextStringData 备注内容 | 备注 |
FormulaData | String formulaValue 表达式 | 公式 |
WriteCellStyle | 见源码 | 单元格样式 |
RichTextStringData | 见源码 | 单元格多样式 |
实现:
package com.alibaba.easyexcel.test.demo.write;
import com.alibaba.excel.metadata.data.WriteCellData;
import lombok.Data;
/**
* 根据WriteCellData写
*
* @author Jiaju Zhuang
*/
@Data
public class WriteCellDemoData {
/**
* 超链接
*
* @since 3.0.0-beta1
*/
private WriteCellData<String> hyperlink;
/**
* 备注
*
* @since 3.0.0-beta1
*/
private WriteCellData<String> commentData;
/**
* 公式
*
* @since 3.0.0-beta1
*/
private WriteCellData<String> formulaData;
/**
* 指定单元格的样式。当然样式 也可以用注解等方式。
*
* @since 3.0.0-beta1
*/
private WriteCellData<String> writeCellStyle;
/**
* 指定一个单元格有多个样式
*
* @since 3.0.0-beta1
*/
private WriteCellData<String> richText;
}
@Test
public void writeCellDataWrite() {
String fileName = TestFileUtil.getPath() + "writeCellDataWrite" + System.currentTimeMillis() + ".xlsx";
WriteCellDemoData writeCellDemoData = new WriteCellDemoData();
// 设置超链接
WriteCellData<String> hyperlink = new WriteCellData<>("官方网站");
writeCellDemoData.setHyperlink(hyperlink);
HyperlinkData hyperlinkData = new HyperlinkData();
hyperlink.setHyperlinkData(hyperlinkData);
hyperlinkData.setAddress("https://github.com/alibaba/easyexcel");
hyperlinkData.setHyperlinkType(HyperlinkType.URL);
// 设置备注
WriteCellData<String> comment = new WriteCellData<>("备注的单元格信息");
writeCellDemoData.setCommentData(comment);
CommentData commentData = new CommentData();
comment.setCommentData(commentData);
commentData.setAuthor("Jiaju Zhuang");
commentData.setRichTextStringData(new RichTextStringData("这是一个备注"));
// 备注的默认大小是按照单元格的大小 这里想调整到4个单元格那么大 所以向后 向下 各额外占用了一个单元格
commentData.setRelativeLastColumnIndex(1);
commentData.setRelativeLastRowIndex(1);
// 设置公式
WriteCellData<String> formula = new WriteCellData<>();
writeCellDemoData.setFormulaData(formula);
FormulaData formulaData = new FormulaData();
formula.setFormulaData(formulaData);
// 将 123456789 中的第一个数字替换成 2
// 这里只是例子 如果真的涉及到公式 能内存算好尽量内存算好 公式能不用尽量不用
formulaData.setFormulaValue("REPLACE(123456789,1,1,2)");
// 设置单个单元格的样式 当然样式 很多的话 也可以用注解等方式。
WriteCellData<String> writeCellStyle = new WriteCellData<>("单元格样式");
writeCellStyle.setType(CellDataTypeEnum.STRING);
writeCellDemoData.setWriteCellStyle(writeCellStyle);
WriteCellStyle writeCellStyleData = new WriteCellStyle();
writeCellStyle.setWriteCellStyle(writeCellStyleData);
// 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.
writeCellStyleData.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
// 背景绿色
writeCellStyleData.setFillForegroundColor(IndexedColors.GREEN.getIndex());
// 设置单个单元格多种样式
WriteCellData<String> richTest = new WriteCellData<>();
richTest.setType(CellDataTypeEnum.RICH_TEXT_STRING);
writeCellDemoData.setRichText(richTest);
RichTextStringData richTextStringData = new RichTextStringData();
richTest.setRichTextStringDataValue(richTextStringData);
richTextStringData.setTextString("红色绿色默认");
// 前2个字红色
WriteFont writeFont = new WriteFont();
writeFont.setColor(IndexedColors.RED.getIndex());
richTextStringData.applyFont(0, 2, writeFont);
// 接下来2个字绿色
writeFont = new WriteFont();
writeFont.setColor(IndexedColors.GREEN.getIndex());
richTextStringData.applyFont(2, 4, writeFont);
List<WriteCellDemoData> data = new ArrayList<>();
data.add(writeCellDemoData);
EasyExcel.write(fileName, WriteCellDemoData.class).inMemory(true).sheet("模板").doWrite(data);
}
3.2.5 合并单元格
方法一:
使用注解@ContentLoopMerge
用于循环扩展单元格作用于实体类的属性上,使用两个参数eachRow
用于设置跨行合并单元格,columnExtend
用于设置跨列合并单元格。注解@OnceAbsoluteMerge
作用于实体类上,用于设置合并的单元格的范围,示例:
package com.alibaba.easyexcel.test.demo.write;
import java.util.Date;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ContentLoopMerge;
import lombok.Data;
/**
* 样式的数据类
*
* @author Jiaju Zhuang
**/
@Data
// 将第6-7行的2-3列合并成一个单元格
// @OnceAbsoluteMerge(firstRowIndex = 5, lastRowIndex = 6, firstColumnIndex = 1, lastColumnIndex = 2)
public class DemoMergeData {
// 这一列 每隔2行 合并单元格
@ContentLoopMerge(eachRow = 2)
@ExcelProperty("字符串标题")
private String string;
@ExcelProperty("日期标题")
private Date date;
@ExcelProperty("数字标题")
private Double doubleData;
}
方法二:
单独设置合并单元格策略,调用ExcelWriterBuilder
的registerWriteHandler
方法,传入LoopMergeStrategy
和OnceAbsoluteMergeStrategy
类自定义合并的策略(参数与使用注解的属性相同),示例:
@Test
public void mergeWrite() {
// 方法1 注解
String fileName = TestFileUtil.getPath() + "mergeWrite" + System.currentTimeMillis() + ".xlsx";
// 在DemoStyleData里面加上ContentLoopMerge注解
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
EasyExcel.write(fileName, DemoMergeData.class).sheet("模板").doWrite(data());
// 方法2 自定义合并单元格策略
fileName = TestFileUtil.getPath() + "mergeWrite" + System.currentTimeMillis() + ".xlsx";
// 每隔2行会合并 把eachColumn 设置成 3 也就是我们数据的长度,所以就第一列会合并。当然其他合并策略也可以自己写
LoopMergeStrategy loopMergeStrategy = new LoopMergeStrategy(2, 0);
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
EasyExcel.write(fileName, DemoData.class).registerWriteHandler(loopMergeStrategy).sheet("模板").doWrite(data());
}
3.2.6 使用table输出
若需要向同一个工作簿中添加数据,可以设置不同的WriteTable
。
@Test
public void tableWrite() {
String fileName = TestFileUtil.getPath() + "tableWrite" + System.currentTimeMillis() + ".xlsx";
// 这里直接写多个table的案例了,如果只有一个 也可以直一行代码搞定,参照其他案例
// 这里 需要指定写用哪个class去写
ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcel.write(fileName, DemoData.class).build();
// 把sheet设置为不需要头 不然会输出sheet的头 这样看起来第一个table 就有2个头了
WriteSheet writeSheet = EasyExcel.writerSheet("模板").needHead(Boolean.FALSE).build();
// 这里必须指定需要头,table 会继承sheet的配置,sheet配置了不需要,table 默认也是不需要
WriteTable writeTable0 = EasyExcel.writerTable(0).needHead(Boolean.TRUE).build();
WriteTable writeTable1 = EasyExcel.writerTable(1).needHead(Boolean.TRUE).build();
// 第一次写入会创建头
excelWriter.write(data(), writeSheet, writeTable0);
// 第二次写如也会创建头,然后在第一次的后面写入数据
excelWriter.write(data(), writeSheet, writeTable1);
} finally {
// 千万别忘记finish 会帮忙关闭流
if (excelWriter != null) {
excelWriter.finish();
}
}
}
3.3 填充
3.3.1 简单填充
使用对象填充,创建需要导出的Excel模板,在对应位置上填写导出数据对象的属性名称,并在doFill
中传入实体类对象。或者是创建一个Map对象将属性和值作为键值对传入doFill
方法中。
示例:
@Test
public void simpleFill() {
// 模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"\{","\}"代替
String templateFileName =
TestFileUtil.getPath() + "demo" + File.separator + "fill" + File.separator + "simple.xlsx";
// 方案1 根据对象填充
String fileName = TestFileUtil.getPath() + "simpleFill" + System.currentTimeMillis() + ".xlsx";
// 这里 会填充到第一个sheet, 然后文件流会自动关闭
FillData fillData = new FillData();
fillData.setName("张三");
fillData.setNumber(5.2);
EasyExcel.write(fileName).withTemplate(templateFileName).sheet().doFill(fillData);
// 方案2 根据Map填充
fileName = TestFileUtil.getPath() + "simpleFill" + System.currentTimeMillis() + ".xlsx";
// 这里 会填充到第一个sheet, 然后文件流会自动关闭
Map<String, Object> map = new HashMap<String, Object>();
map.put("name", "张三");
map.put("number", 5.2);
EasyExcel.write(fileName).withTemplate(templateFileName).sheet().doFill(map);
}
3.3.2 填充列表
可以通过List集合输出数据,将所有数据填充进对应单元格中。
@Test
public void listFill() {
// 模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"\{","\}"代替
// 填充list 的时候还要注意 模板中{.} 多了个点 表示list
String templateFileName =
TestFileUtil.getPath() + "demo" + File.separator + "fill" + File.separator + "list.xlsx";
// 方案1 一下子全部放到内存里面 并填充
String fileName = TestFileUtil.getPath() + "listFill" + System.currentTimeMillis() + ".xlsx";
// 这里 会填充到第一个sheet, 然后文件流会自动关闭
EasyExcel.write(fileName).withTemplate(templateFileName).sheet().doFill(data());
// 方案2 分多次 填充 会使用文件缓存(省内存) jdk8
// since: 3.0.0-beta1
fileName = TestFileUtil.getPath() + "listFill" + System.currentTimeMillis() + ".xlsx";
EasyExcel.write(fileName)
.withTemplate(templateFileName)
.sheet()
.doFill(() -> {
// 分页查询数据
return data();
});
// 方案3 分多次 填充 会使用文件缓存(省内存)
fileName = TestFileUtil.getPath() + "listFill" + System.currentTimeMillis() + ".xlsx";
ExcelWriter excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build();
WriteSheet writeSheet = EasyExcel.writerSheet().build();
excelWriter.fill(data(), writeSheet);
excelWriter.fill(data(), writeSheet);
// 千万别忘记关闭流
excelWriter.finish();
}
3.3.3 复杂填充
填充功能可以事先在Excel中设计好格式和排版,再在后台将需要导出的数据传入fill
方法中。
@Test
public void complexFill() {
// 模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"\{","\}"代替
// {} 代表普通变量 {.} 代表是list的变量
String templateFileName =
TestFileUtil.getPath() + "demo" + File.separator + "fill" + File.separator + "complex.xlsx";
String fileName = TestFileUtil.getPath() + "complexFill" + System.currentTimeMillis() + ".xlsx";
ExcelWriter excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build();
WriteSheet writeSheet = EasyExcel.writerSheet().build();
// 这里注意 入参用了forceNewRow 代表在写入list的时候不管list下面有没有空行 都会创建一行,然后下面的数据往后移动。默认 是false,会直接使用下一行,如果没有则创建。
// forceNewRow 如果设置了true,有个缺点 就是他会把所有的数据都放到内存了,所以慎用
// 简单的说 如果你的模板有list,且list不是最后一行,下面还有数据需要填充 就必须设置 forceNewRow=true 但是这个就会把所有数据放到内存 会很耗内存
// 如果数据量大 list不是最后一行 参照下一个
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
excelWriter.fill(data(), fillConfig, writeSheet);
excelWriter.fill(data(), fillConfig, writeSheet);
Map<String, Object> map = new HashMap<String, Object>();
map.put("date", "2019年10月9日13:28:28");
map.put("total", 1000);
excelWriter.fill(map, writeSheet);
excelWriter.finish();
}
注意中间注释内容,如果是使用List填充表格,会在最后添加一个空行,这里使用的是设置
forceNewRow
,但是会将所有数据加载到内存中。所以如果是大容量数据,可以利用ExcelWriter
的write()
方法,利用3.2.6的方法将最后一行的内容追加在工作簿中。
示例:
@Test
public void complexFillWithTable() {
// 模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"\{","\}"代替
// {} 代表普通变量 {.} 代表是list的变量
// 这里模板 删除了list以后的数据,也就是统计的这一行
String templateFileName =
TestFileUtil.getPath() + "demo" + File.separator + "fill" + File.separator + "complexFillWithTable.xlsx";
String fileName = TestFileUtil.getPath() + "complexFillWithTable" + System.currentTimeMillis() + ".xlsx";
ExcelWriter excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build();
WriteSheet writeSheet = EasyExcel.writerSheet().build();
// 直接写入数据
excelWriter.fill(data(), writeSheet);
excelWriter.fill(data(), writeSheet);
// 写入list之前的数据
Map<String, Object> map = new HashMap<String, Object>();
map.put("date", "2019年10月9日13:28:28");
excelWriter.fill(map, writeSheet);
// list 后面还有个统计 想办法手动写入
// 这里偷懒直接用list 也可以用对象
List<List<String>> totalListList = new ArrayList<List<String>>();
List<String> totalList = new ArrayList<String>();
totalListList.add(totalList);
totalList.add(null);
totalList.add(null);
totalList.add(null);
// 第四列
totalList.add("统计:1000");
// 这里是write 别和fill 搞错了
excelWriter.write(totalListList, writeSheet);
excelWriter.finish();
// 总体上写法比较复杂 但是也没有想到好的版本 异步的去写入excel 不支持行的删除和移动,也不支持备注这种的写入,所以也排除了可以
// 新建一个 然后一点点复制过来的方案,最后导致list需要新增行的时候,后面的列的数据没法后移,后续会继续想想解决方案
}
这里最后的一行不再使用模板填充,直接写入sheet
除了纵向填充,还可以设置横向填充,示例:
@Test
public void horizontalFill() {
// 模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"\{","\}"代替
// {} 代表普通变量 {.} 代表是list的变量
String templateFileName =
TestFileUtil.getPath() + "demo" + File.separator + "fill" + File.separator + "horizontal.xlsx";
String fileName = TestFileUtil.getPath() + "horizontalFill" + System.currentTimeMillis() + ".xlsx";
ExcelWriter excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName).build();
WriteSheet writeSheet = EasyExcel.writerSheet().build();
// 设置文件填充方向
FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
excelWriter.fill(data(), fillConfig, writeSheet);
excelWriter.fill(data(), fillConfig, writeSheet);
Map<String, Object> map = new HashMap<String, Object>();
map.put("date", "2019年10月9日13:28:28");
excelWriter.fill(map, writeSheet);
// 别忘记关闭流
excelWriter.finish();
}