EasyExcel Java API 使用
一、初识 EasyExcel
1.1、Apache POI
Apache POI
是Apache软件基金会的开源函式库,提供跨平台的Java API
实现Microsoft Office
格式档案读写。但是存在如下一些问题:学习使用成本较高、POI的内存消耗较大,代码书写冗余繁杂,读写大文件耗费内存较大,容易OOM。当然,功能强大。
1.2、EasyExcel
- 在数据模型层面进行了封装,使用简单
- 重写了07版本的Excel的解析代码,降低内存消耗,能有效避免OOM
- 只能操作Excel
- 不能读取图片
二、快速入门
引入坐标依赖
<!-- EasyExcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.10</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
2.1、简单的读
将 excel 中的数据读取出来,包含有姓名、性别、出生日期,excel 数据如下
2.1.1、编写导出数据的实体
基于 Lombok
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
private String name;
private Date birthday;
private String gender;
private String id;
}
2.1.2、读取 Excel 文件
其中,需要自定义监听器,继承官方提供的监听器,实现两个方法
public class ExcelEasyRead {
/**
* 工作薄: 一个 excel 文件就是一个工作薄
* 工作表: 一个工作薄中可以有多个工作表(sheet)
*/
public static void main(String[] args) {
/**
* 构建一个工作簿
* pathName 要读的文件的路径
* head 文件中每一行数据要存储到实体的类型的 class
* readListener 读取监听,每读取一行内容,都会调用该对象的 invoke,在 invoke 可以操作使用读取到的数据
* sheet方法参数: 工作表的顺序号(从0开始)或者工作表的名字,不传默认为0
*/
// 获得一个工作簿对象
ExcelReaderBuilder readerBuilder = EasyExcel.read("read.xlsx", Student.class, new StudentListener());
// 获得一个工作表对象
ExcelReaderSheetBuilder sheet = readerBuilder.sheet();
// 读取工作表中的内容
sheet.doRead();
}
}
public class StudentListener extends AnalysisEventListener<Student> {
/**
* 每读取一行内容,都会调用该对象的 invoke,在 invoke 可以操作使用读取到的数据
* @param data 每次读取到的数据封装的对象
* @param context
*/
@Override
public void invoke(Student data, AnalysisContext context) {
System.out.println("student = " + data);
}
/**
* 全部读完之后,会调用该方法
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
}
运行效果如下
2.2、简单的写
将多个学生的信息导入到 Excel 表格中
2.2.1、直接写入数据
public class ExcelEasyWrite {
public static void main(String[] args) {
/**
* pathName 要写入的文件路径
* head 封装写入的实体的类型
* return 写的工作簿对象
*/
// 工作簿对象
ExcelWriterBuilder writerBuilder = EasyExcel.write("write.xlsx", Student.class);
// 工作表对象
ExcelWriterSheetBuilder sheet = writerBuilder.sheet();
// 准备数据
List<Student> students = initData();
// 写
sheet.doWrite(students);
}
private static List<Student> initData() {
ArrayList<Student> students = new ArrayList<>();
for (int i = 10; i < 20; i++) {
Student student = new Student();
student.setName("测试"+i);
student.setBirthday(new Date());
student.setGender("男");
students.add(student);
}
return students;
}
}
效果如下
自定义列名,列宽等属性
@Data
@NoArgsConstructor
@AllArgsConstructor
// 全局定义列宽
@ColumnWidth(10)
// 内容行高
//@ContentRowHeight(10)
// 表头行高
@HeadRowHeight(20)
public class Student {
/**
* value 字段名
* index 列顺序
*/
@ExcelProperty(value = {"学生信息表","姓名"},index = 0)
private String name;
@ExcelProperty(value = {"学生信息表","出生日期"},index = 2)
@DateTimeFormat("YYYY-MM-dd")
@ColumnWidth(20)
private Date birthday;
@ExcelProperty(value = {"学生信息表","性别"},index = 1)
private String gender;
/**
* 忽略字段
*/
@ExcelIgnore
private String id;
效果如下
三、常用的 API 及 注解
1、常用类
EasyExcel 入口类,用于构建各种对象、开始各种操作;
ExcelReaderBuilder 构建出一个ReadWorkbook对象,即一个工作簿对象,对应的是一个Excel文件;
ExcelWriterBuilder 构建出一个WriteWorkbook对象,即一个工作簿对象,对应的是一个Excel文件;
ExcelReaderSheetBuilder 构建出一个ReadSheet对象,即一个工作表的对象,对应的Excel中的每个sheet,一个工作簿可以有多个工作表;
ExcelWriterSheetBuilder 构建出一WriteSheet对象,即一个工作表的对象,对应的Excel中的每个sheet,一个工作簿可以有多个工作表;
ReadListener 在每一行读取完毕后都会调用ReadListener来处理数据,我们可以把调用service的代码可以写在其invoke方法内部;
WriteHandler 在每一个操作包括创建单元格、创建表格等都会调用WriteHandler来处理数据,对使用者透明不可见;
所有配置都是继承的 Workbook的配置会被Sheet继承。所以在用EasyExcel设置参数的时候,在EasyExcel…sheet()方法之前作用域是整个workBook的所有sheet,之后针对单个sheet。
2、读取时的注解
@ExcelProperty
使用位置:标准作用在成员变量上,把实体类中属性和 excel 表的列关联起来
可选属性:
属性名 | 含义 | 说明 |
---|---|---|
index | 对应Excel表中的列数 | 默认-1,建议指定时从0开始 |
value | 对应Excel表中的列头 | |
converter | 成员变量转换器 | 自定义转换器需要实Converter接口 |
使用效果:index属性可以指定当前字段对应excel中的哪一列,可以根据列名value去匹配,也可以不写。
如果不使用@ExcelProperty注解,成员变量从上到下的顺序,对应表格中从左到右的顺序;
使用建议:要么全部不写,要么全部用index,要么全部用value去匹配,尽量不要三个混着用。
@ExcelIgnore
标注在成员变量上,默认所有字段都会和excel去匹配,加了这个注解会忽略该字段
@DateTimeFormat
标注在成员变量上,日期转换,代码中用String类型的成员变量
去接收excel中日期格式的数据
会调用这个注解。里面的value
参照java.text.SimpleDateFormat
@NumberFormat
标注在成员变量上,数字转换,代码中用String类型的成员变量
去接收excel数字格式的数据
会调用这个注解。里面的value
参照java.text.DecimalFormat
@ExcelIgnoreUnannotated
标注在类上。
不标注该注解时,默认类中所有成员变量都会参与读写,无论是否在成员变量上加了@ExcelProperty
的注解。
标注该注解后,类中的成员变量如果没有标注@ExcelProperty
注解将不会参与读写。
3、 读取时通用参数
ReadWorkbook
,ReadSheet
都会有的参数,如果为空,默认使用上级。
converter
转换器,默认加载了很多转换器。也可以自定义。readListener
监听器,在读取数据的过程中会不断的调用监听器。headRowNumber
指定需要读表格的 列头行数。默认有一行头,也就是认为第二行开始起为数据。head
与clazz
二选一。读取文件头对应的列表,会根据列表匹配数据。建议使用class,就是文件中每一行数据对应的代码中的实体类型。clazz
与head
二选一。读取文件的头对应的class,也可以使用注解。如果两个都不指定,则会读取全部数据。autoTrim
字符串、表头等数据自动trimpassword
读的时候是否需要使用密码
4、ReadWorkbook(工作簿对象)参数
excelType
当前excel的类型,读取时会自动判断,无需设置。inputStream
与file
二选一。建议使用file。file
与inputStream
二选一。读取文件的文件。autoCloseStream
自动关闭流。readCache
默认小于5M用 内存,超过5M会使用EhCache
,不建议使用这个参数。useDefaultListener
@since 2.1.4
默认会加入ModelBuildEventListener
来帮忙转换成传入class
的对象,设置成false
后将不会协助转换对象,自定义的监听器会接收到Map<Integer,CellData>
对象,如果还想继续接听到class
对象,请调用readListener
方法,加入自定义的beforeListener
、ModelBuildEventListener
、 自定义的afterListener
即可。
5、ReadSheet(工作表对象)参数
sheetNo
需要读取Sheet的编号,建议使用这个来指定读取哪个SheetsheetName
根据名字去匹配Sheet,excel 2003不支持根据名字去匹配
6、写入时的注解注解
@ExcelProperty
使用位置:标准作用在成员变量上
可选属性:
属性名 | 含义 | 说明 |
---|---|---|
index | 对应Excel表中的列数 | 默认-1,指定时建议从0开始 |
value | 对应Excel表中的列头 | |
converter | 成员变量转换器 | 自定义转换器需要实Converter接口 |
使用效果:index
指定写到第几列,如果不指定则根据成员变量位置排序;
value
指定写入的列头,如果不指定则使用成员变量的名字作为列头;
如果要设置复杂的头,可以为value指定多个值。
其他注解:
基本和读取时一致
@ContentRowHeight() 标注在类上或属性上,指定内容行高
@HeadRowHeight() 标注在类上或属性上,指定列头行高
@ColumnWidth() 标注在类上或属性上,指定列宽
ExcelIgnore` 默认所有字段都会写入excel,这个注解会忽略这个字段
DateTimeFormat
日期转换,将Date
写到excel会调用这个注解。里面的value
参照java.text.SimpleDateFormat
NumberFormat
数字转换,用Number
写excel会调用这个注解。里面的value
参照java.text.DecimalFormat
ExcelIgnoreUnannotated
默认不加ExcelProperty
的注解的都会参与读写,加了不会参与
7、写入时通用参数
WriteWorkbook
、WriteSheet
都会有的参数,如果为空,默认使用上级。
converter
转换器,默认加载了很多转换器。也可以自定义。writeHandler
写的处理器。可以实现WorkbookWriteHandler
,SheetWriteHandler
,RowWriteHandler
,CellWriteHandler
,在写入excel的不同阶段会调用,对使用者透明不可见。relativeHeadRowIndex
距离多少行后开始。也就是开头空几行needHead
是否导出头head
与clazz
二选一。写入文件的头列表,建议使用class。clazz
与head
二选一。写入文件的头对应的class,也可以使用注解。autoTrim
字符串、表头等数据自动trim
8、WriteWorkbook(工作簿对象)参数
excelType
当前excel的类型,默认为xlsx
outputStream
与file
二选一。写入文件的流file
与outputStream
二选一。写入的文件templateInputStream
模板的文件流templateFile
模板文件autoCloseStream
自动关闭流。password
写的时候是否需要使用密码useDefaultStyle
写的时候是否是使用默认头
9、WriteSheet(工作表对象)参数
sheetNo
需要写入的编号。默认0sheetName
需要些的Sheet名称,默认同sheetNo
四、数据填充
4.1、填充一组数据
4.1.1、准备模板
Excel表格中用{} 来表示包裹要填充的变量,如果单元格文本中本来就有{
、}
左右大括号,需要在括号前面使用斜杠转义\{
、\}
。
代码中被填充数据的实体对象的成员变量名或被填充map集合的key需要和Excel中被{}包裹的变量名称一致。
4.1.2、封装数据
编写封装填充数据的类或选用 Map
/**
* @ClassName FillData
* @Description 实体类封装填充数据,实体类中成员变量名称需要和Excel表各中{包裹的变量名称匹配}
* @Author hudu
* @Date 2021/9/15 2021/9/15
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FillData {
private String name;
private Integer age;
}
4.1.2、填充数据
/**
* 单组数据填充
*/
public static void fillData1() {
// 准备模板
String template = "fill_data_template1.xlsx";
// 创建一个工作簿对象
ExcelWriterBuilder excelWriterBuilder = EasyExcel.write("export/fill_data1.xlsx", FillData.class).withTemplate(template);
// 创建一个工作表对象
ExcelWriterSheetBuilder sheet = excelWriterBuilder.sheet();
// 准备数据
FillData fillData = initData();
// 或者使用map
HashMap<String, Object> map = new HashMap<>();
map.put("name","Alex");
map.put("age",22);
// 填充数据
sheet.doFill(fillData);
}
public static FillData initData() {
return new FillData("Alex",22);
}
效果如下
4.2、填充多组数据
4.2.1、准备模板
Excel表格中用{.}
来表示包裹要填充的变量,如果单元格文本中本来就有{
、}
左右大括号,需要在括号前面使用斜杠转义\{
、\}
。
代码中被填充数据的实体对象的成员变量名或被填充map集合的key需要和Excel中被{}包裹的变量名称一致。
4.2.2、开始填充
/**
* 多组数据填充
*/
public static void fillData2() {
List<FillData> fillData = initDataList();
String template = "fill_data_template2.xlsx";
ExcelWriterBuilder excelWriterBuilder = EasyExcel.write("export/fill_data2.xlsx", FillData.class).withTemplate(template);
ExcelWriterSheetBuilder sheet = excelWriterBuilder.sheet();
sheet.doFill(fillData);
}
public static List<FillData> initDataList() {
ArrayList<FillData> arrayList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
FillData fillData = new FillData("test" + i, i + 20);
arrayList.add(fillData);
}
return arrayList;
}
效果如下
4.3、组合填充
4.3.1、准备模板
既有多组数据填充,又有单一数据填充,为了避免两者数据出现冲突覆盖的情况,在多组填充时需要通过FillConfig
对象设置换行。
4.3.2、数据填充
public static List<FillData> initDataList() {
ArrayList<FillData> arrayList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
FillData fillData = new FillData("test" + i, i + 20);
arrayList.add(fillData);
}
return arrayList;
}
/**
* 组合填充
*/
public static void fillData3() {
// 准备模板
String template = "fill_data_template3.xlsx";
// 目标文件
String target = "fill_data3.xlsx";
// 准备数据
List<FillData> fillData = initDataList();
// 生成工作簿对象
ExcelWriter excelWriter = EasyExcel.write(target).withTemplate(template).build();
// 生成工作表对象
WriteSheet writeSheet = EasyExcel.writerSheet().build();
// 组合填充时,因为多组填充的数据量不确定,需要在多组填充完之后另起一行
FillConfig fillConfig = FillConfig.builder().forceNewRow(true).build();
// 填充并换行
excelWriter.fill(fillData,fillConfig,writeSheet);
HashMap<String, String> map = new HashMap<>();
map.put("date","2021");
map.put("total","10");
excelWriter.fill(map,writeSheet);
// 需要手动进行关闭
excelWriter.finish();
}
效果如下
4.4、水平填充
4.4.1、准备模板
水平填充和多组填充模板一样,不一样的地方在于,填充时需要通过FillConfig
对象设置水平填充。
4.4.2、填充数据
/**
* 水平填充
*/
public static void fillData4() {
// 准备模板
String template = "fill_data_template4.xlsx";
// 目标文件
String target = "export/fill_data4.xlsx";
// 准备数据
List<FillData> fillData = initDataList();
// 生成工作簿对象
ExcelWriter excelWriter = EasyExcel.write(target).withTemplate(template).build();
// 生成工作表对象
WriteSheet writeSheet = EasyExcel.writerSheet().build();
// 组合填充时,因为多组填充的数据量不确定,需要在多组填充完之后另起一行
FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
// 填充并换行
excelWriter.fill(fillData,fillConfig,writeSheet);
// 需要手动进行关闭
excelWriter.finish();
}
效果如下
4.5、注意事项
为了节省内存,所以没有采用把整个文档在内存中组织好之后再整体写入到文件的做法,而是采用的是一行一行写入的方式,不能实现删除和移动行,也不支持备注写入。多组数据写入的时候,如果需要新增行,只能在最后一行增加,不能在中间位置添加。
5、实际应用
5.1 无实体类导出
在我们实际项目开发中,有时候会出现以下应用场景,需要导出数据库中的数据,但是不知道导出哪张表,并且这些表没有具体的实体类对应,所以无法通过实体列注解来映射字段导出,需要自己设置列
模拟生成的数据
@Component
public class ExcelDataInit {
/**
* 模拟数据库字段
*/
public List<String> initExcelHeader() {
List<String> headerList = new ArrayList<>();
for (int i = 1; i <= 16; i++) {
headerList.add("第"+i+"列");
}
return headerList;
}
/**
* 模拟无实体类查询出的 mysql 中的数据
*/
public List<LinkedHashMap<String, Object>> initMysqlData() {
List<LinkedHashMap<String, Object>> maps = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
LinkedHashMap<String, Object> map = new LinkedHashMap<>();
for (int i1 = 1; i1 <= 16; i1++) {
map.put("file"+i1,"第"+i1+"列"+i+"行");
}
maps.add(map);
}
return maps;
}
/**
* 将数据库中查询出来的数据进行转换,注意使用 LinkedHashMap,不然可能会导致导出的数据字段对应不上
*/
public List<List<Object>> initExcelData() {
final List<LinkedHashMap<String, Object>> linkedHashMaps = initMysqlData();
final List<List<Object>> collect = new ArrayList<>();
for (LinkedHashMap<String, Object> linkedHashMap : linkedHashMaps) {
// final List<Object> objectList = linkedHashMap.entrySet().stream().map(Map.Entry::getValue).collect(Collectors.toList());
final List<Object> objectList = new ArrayList<>(linkedHashMap.values());
collect.add(objectList);
}
return collect;
}
}
控制层代码
@RestController
@RequestMapping("/excel")
public class ExcelController {
@Autowired
private ExcelService excelService;
@GetMapping("/export-excel")
public void exportExcel(HttpServletResponse response) {
excelService.exportExcel(response);
}
}
服务层
@Service
public class ExcelServiceImpl implements ExcelService {
@Autowired
private ExcelDataInit excelDataInit;
@Override
public void exportExcel(HttpServletResponse response) {
ExcelWriter excelWriter = null;
OutputStream outputStream = null;
try {
outputStream = response.getOutputStream();
response.setContentType("application/octet-stream");
response.setCharacterEncoding("utf-8");
response.setHeader("content-Disposition", "attachment;filename=" + URLEncoder.encode("test.xlsx", "UTF-8"));
List<List<Object>> hashMaps = excelDataInit.initExcelData();
List<String> list = excelDataInit.initExcelHeader();
List<List<String>> header = new ArrayList<>();
list.forEach(h -> header.add(Collections.singletonList(h)));
excelWriter = EasyExcel.write(outputStream).build();
// 设置 sheet 名称
// 设置自适应列宽,注册一个handler: LongestMatchColumnWidthStyleStrategy
WriteSheet writeSheet = EasyExcel.writerSheet("数据导出").registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build();
// 设置表头
writeSheet.setHead(header);
// 设置写入的数据
excelWriter.write(hashMaps,writeSheet);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (excelWriter != null) {
excelWriter.finish();
}
if (outputStream != null) {
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
最终效果
本作品采用《CC 协议》,转载必须注明作者和本文链接