初识EasyExcel
Apache POI
Apache POI是Apache软件基金会的开源函式库,提供跨平台的Java API实现Microsoft Office
格式档案读写。
特点
- 功能强大
- 代码书写冗余繁杂 ,学习和使用成本较高
- 读写大文件耗费内存较大,容易OOM
EasyExcel
GitHub地址 官网地址
EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目,在尽可能节约内存的情况下支持读写百M的Excel。
特点
- 在数据模型层面进行了封装,使用简单
- 重写了07版本的Excel的解析代码,降低内存消耗,能有效避免OOM
- 只能操作Excel
- 不能读取图片
性能测试
64M内存20秒读取75M(46W行25列)的Excel(3.0.2+版本),也有极速模式能更快,但是内存占用会在100M多一点。
![large-4261205e1a145b60a6051d57afe137d1.png](./assets/1668427614873-2a21e2a6-e3fb-40d2-a08f-9430f3faa6de.png %}
快速入门
官网地址
快速开始 https://easyexcel.opensource.alibaba.com/docs/current/
读Excel https://easyexcel.opensource.alibaba.com/docs/current/quickstart/read
写Excel https://easyexcel.opensource.alibaba.com/docs/current/quickstart/write
导入依赖坐标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.1.6</version> </dependency>
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> </dependency>
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency>
|
读取Excel
准备工作
编写Excel文件,复制到resources目录下
编写实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Data @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode @ToString
@Builder
public class Student {
private String id;
@ExcelProperty(value = "学生姓名", index = 0) private String name;
@ExcelProperty(value = "学生性别", index = 1) private String gender;
@ExcelProperty(value = "学生出生日期", index = 2) private Date birthDate;
}
|
读取Excel文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class Excel_Read { public static void main(String[] args) { InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("student.xls"); EasyExcel.read( inputStream, Student.class, new StudentListener() ) .sheet() .doRead(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class StudentListener extends AnalysisEventListener<Student> {
@Override public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) { System.out.println("表头信息:" + headMap); }
@Override public void invoke(Student student, AnalysisContext analysisContext) { System.out.println("表体信息:" + student); }
@Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { System.out.println("StudentExcel读取完毕"); } }
|
读取完成
写入Excel
创建写入文件夹
工程路径下/down文件夹
编写实体类
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 26
| @Data @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode @ToString
@Builder
public class Student {
@ExcelIgnore private String id;
@ExcelProperty(value = "学生姓名", index = 0) private String name;
@ExcelProperty(value = "学生性别", index = 1) private String gender;
@ExcelProperty(value = "学生出生日期", index = 2) private Date birthDate;
}
|
写入Excel文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class Excel_Write { public static void main(String[] args) {
ArrayList<Student> students = new ArrayList<>(); for (int i = 0; i < 10; i++) { Date date = Date.from(LocalDate.of(2001, i + 1, 17).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); Student student = Student.builder().name("Ylan" + i).gender("男").birthDate(date).build(); students.add(student); }
EasyExcel.write( "down/student_Write.xls", Student.class ).sheet() .doWrite(students); } }
|
写入完成
写入带有表头的Excel
创建写入文件夹
工程路径下/down文件夹
编写实体类
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 26 27
| @Data @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode @ToString
@Builder
public class Student {
@ExcelIgnore private String id;
@ExcelProperty(value = {"学生信息表", "学生姓名"}, index = 0) private String name;
@ExcelProperty(value = {"学生信息表", "学生性别"}, index = 1) private String gender;
@ExcelProperty(value = {"学生信息表", "学生出生日期"}, index = 2) @DateTimeFormat("yyyy-MM-dd") private Date birthDate;
}
|
写入Excel文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class Excel_Write { public static void main(String[] args) {
ArrayList<Student> students = new ArrayList<>(); for (int i = 0; i < 10; i++) { Date date = Date.from(LocalDate.of(2001, i + 1, 17).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); Student student = Student.builder().name("Ylan" + i).gender("男").birthDate(date).build(); students.add(student); }
EasyExcel.write( "down/student_Write.xls", Student.class ).sheet() .doWrite(students); } }
|
写入完成
写入带有单元格样式的Excel
EasyExcel支持调整行高、列宽、背景色、字体大小等内容,控制方式和使用原生POI无异,比较繁琐,不建议使用。推荐使用模板填充的方式,向预设样式的表格中直接写入数据,写入数据的时候会保持原有样式。
常见注解
1 2 3 4 5 6
| @HeadRowHeight(25)
@ContentRowHeight(15)
@ColumnWidth(20)
|
创建写入文件夹
工程路径下/down文件夹
修改实体类
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 26 27 28 29 30 31 32 33 34 35 36
| @Data @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode @ToString
@Builder
@HeadRowHeight(25)
@ContentRowHeight(15)
@ColumnWidth(20) public class Student {
@ExcelIgnore private String id;
@ExcelProperty(value = "学生姓名", index = 0) @ColumnWidth(20) private String name;
@ExcelProperty(value = "学生性别", index = 1) @ColumnWidth(20) private String gender;
@ExcelProperty(value = "学生出生日期", index = 2) @ColumnWidth(20) private Date birthDate;
}
|
重新写入Excel文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class Excel_Write { public static void main(String[] args) {
ArrayList<Student> students = new ArrayList<>(); for (int i = 0; i < 10; i++) { Date date = Date.from(LocalDate.of(2001, i + 1, 17).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); Student student = Student.builder().name("Ylan" + i).gender("男").birthDate(date).build(); students.add(student); }
EasyExcel.write( "down/student_Write.xls", Student.class ).sheet() .doWrite(students); } }
|
写入完成
文件上传和下载
Excel文件上传
准备Excel文件
编写实体类
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 26 27
| @Data @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode @ToString
@Builder
public class Student {
@ExcelIgnore private String id;
@ExcelProperty(value = "学生姓名", index = 0) private String name;
@ExcelProperty(value = "学生性别", index = 1) private String gender;
@ExcelProperty(value = "学生出生日期", index = 2) @DateTimeFormat("yyyy/MM/dd") private Date birthDate;
}
|
编写数据处理逻辑
1 2 3 4 5 6 7 8 9
| @Service("studentService") public class StudentService { public void readExcel(List<Student> students) { students.forEach(student -> { System.out.println("插入" + student); }); } }
|
编写读监听器
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| @Component @Scope("prototype") public class ExcelListener extends AnalysisEventListener<Student> {
private ArrayList<Student> students = new ArrayList<>();
private final StudentService studentService;
public ExcelListener(StudentService studentService) { this.studentService = studentService; }
@Override public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) { System.out.println("表头信息:" + headMap); }
@Override public void invoke(Student student, AnalysisContext analysisContext) { students.add(student); if (students.size() % 5 == 0) { studentService.readExcel(students); students.clear(); } }
@Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { System.out.println("StudentExcel读取完毕"); } }
|
编写控制器读取Excel
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 26 27 28 29 30
| @Controller("excelController") @RequestMapping("/excel") public class ExcelController {
private final ExcelListener excelListener;
public ExcelController(ExcelListener excelListener) { this.excelListener = excelListener; }
@ResponseBody @PostMapping("/read") public Map<String, String> readExcel(@RequestPart("file") MultipartFile file) { HashMap<String, String> map = new HashMap<>(); try { ExcelReaderBuilder read = EasyExcel.read(file.getInputStream(), Student.class, excelListener); ExcelReaderSheetBuilder sheet = read.sheet(); sheet.doRead(); map.put("message", "success"); } catch (IOException e) { map.put("message", "error"); throw new RuntimeException(e); } return map; } }
|
测试案例
http://localhost:9999/excel/read
类型 Content-Type-multipart/form-data
Excel文件下载
编写实体类
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 26 27
| @Data @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode @ToString
@Builder
public class Student {
@ExcelIgnore private String id;
@ExcelProperty(value = "学生姓名", index = 0) private String name;
@ExcelProperty(value = "学生性别", index = 1) private String gender;
@ExcelProperty(value = "学生出生日期", index = 2) @DateTimeFormat("yyyy/MM/dd") private Date birthDate;
}
|
编写控制器写入Excel
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 26
| @Controller("excelController") @RequestMapping("/excel") public class ExcelController {
@RequestMapping(value = "/write") public void write(HttpServletResponse response) throws IOException { response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20"); response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); EasyExcel.write(response.getOutputStream(), Student.class).sheet().doWrite(data()); }
private List<Student> data() { ArrayList<Student> students = new ArrayList<>(); for (int i = 0; i < 10; i++) { Date date = Date.from(LocalDate.of(2001, i + 1, 17).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); Student student = Student.builder().name("Ylan" + i).gender("男").birthDate(date).build(); students.add(student); } return students; } }
|
测试案例
http://localhost:9999/excel/write
填充模板
官方地址
模板填充 https://easyexcel.opensource.alibaba.com/docs/current/quickstart/fill#since-3
准备工作
创建写入文件夹
工程路径下/down文件夹
填充单组数据
准备模板
Excel表格中用**{}包裹表示要填充的变量,如果单元格文本中本来就有{},需要在括号前面使用斜杠转义{}。
代码中被填充实体对象的成员变量名或被填充map集合的key**需要和Excel中被{}包裹的变量名称一致。
封装数据
1 2 3 4 5 6 7 8 9 10
| @Data @Builder public class FileData { private String name; private int age; }
FileData data = FileData.builder().name("Ylan").age(21).build();
|
1 2 3 4
| HashMap<String, String> data = new HashMap<>(); data.put("name", "pepsi-wyl"); data.put("age", "21");
|
开始填充
准备数据并填充到文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class TemplateTest1 { public static void main(String[] args) { InputStream template = Thread.currentThread().getContextClassLoader().getResourceAsStream("template/fill_data_template1.xlsx"); String targetFileName = "down/Excel_填充_单组数据.xlsx"; FileData data = FileData.builder().name("Ylan").age(21).build();
ExcelWriterBuilder excelWriterBuilder = EasyExcel.write(targetFileName, FileData.class).withTemplate(template); ExcelWriterSheetBuilder sheet = excelWriterBuilder.sheet(); sheet.doFill(data); } }
|
填充效果
填充多组数据
准备模板
Excel表格中用 {.} 包裹表示要填充的变量,如果单元格文本中本来就有**{},需要在括号前面使用斜杠转义{}。
代码中被填充实体对象的成员变量名或被填充map集合的key**需要和Excel中被{}包裹的变量名称一致。
封装数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Data @Builder public class FileData { private String name; private int age; }
private static List<FileData> initFillData() { ArrayList<FileData> fillDatas = new ArrayList<FileData>(); for (int i = 0; i < 10; i++) fillDatas.add(FileData.builder().name("YLan" + i).age(10 + i).build()); return fillDatas; }
|
开始填充
准备数据并填充到文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class TemplateTest2 { public static void main(String[] args) { InputStream template = Thread.currentThread().getContextClassLoader().getResourceAsStream("template/fill_data_template2.xlsx"); String targetFileName = "down/Excel_填充_多组数据.xlsx"; List<FileData> dataList = initFillData();
ExcelWriterBuilder excelWriterBuilder = EasyExcel.write(targetFileName, FileData.class).withTemplate(template); ExcelWriterSheetBuilder sheet = excelWriterBuilder.sheet(); sheet.doFill(dataList); }
private static List<FileData> initFillData() { ArrayList<FileData> fillDatas = new ArrayList<FileData>(); for (int i = 0; i < 10; i++) fillDatas.add(FileData.builder().name("YLan" + i).age(10 + i).build()); return fillDatas; } }
|
填充效果
组合填充
准备模板
即有多组数据填充,又有单一数据填充,为了避免两者数据出现冲突覆盖的情况,在多组填充时需要通过**FillConfig**
对象设置换行。
封装数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Data @Builder public class FileData { private String name; private int age; }
private static List<FileData> initFillData() { ArrayList<FileData> fillDatas = new ArrayList<FileData>(); for (int i = 0; i < 10; i++) fillDatas.add(FileData.builder().name("YLan" + i).age(10 + i).build()); return fillDatas; }
|
1 2 3 4
| HashMap<String, String> dataMap = new HashMap<>(); dataMap.put("date", "2022-03-17"); dataMap.put("total", "21");
|
开始填充
准备数据并填充到文件
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 26 27 28 29 30 31 32 33
| public class TemplateTest3 { public static void main(String[] args) { InputStream template = Thread.currentThread().getContextClassLoader().getResourceAsStream("template/fill_data_template3.xlsx"); String targetFileName = "down/Excel_填充_组合数据.xlsx"; List<FileData> dataList = initFillData(); HashMap<String, String> dataMap = new HashMap<>(); dataMap.put("date", "2022-03-17"); dataMap.put("total", "21");
ExcelWriter workBook = EasyExcel.write(targetFileName, FileData.class).withTemplate(template).build(); WriteSheet sheet = EasyExcel.writerSheet().build(); FillConfig fillConfig = FillConfig.builder().forceNewRow(true).build(); workBook.fill(dataList, fillConfig, sheet); workBook.fill(dataMap, sheet); workBook.finish(); }
private static List<FileData> initFillData() { ArrayList<FileData> fillDatas = new ArrayList<FileData>(); for (int i = 0; i < 10; i++) fillDatas.add(FileData.builder().name("YLan" + i).age(10 + i).build()); return fillDatas; } }
|
填充效果
水平填充
准备模板
水平填充和多组填充模板一样,不一样的地方在于,填充时需要通过**FillConfig**
对象设置水平填充。
封装数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Data @Builder public class FileData { private String name; private int age; }
private static List<FileData> initFillData() { ArrayList<FileData> fillDatas = new ArrayList<FileData>(); for (int i = 0; i < 10; i++) fillDatas.add(FileData.builder().name("YLan" + i).age(10 + i).build()); return fillDatas; }
|
开始填充
准备数据并填充到文件
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 26 27 28
| public class TemplateTest4 { public static void main(String[] args) { InputStream template = Thread.currentThread().getContextClassLoader().getResourceAsStream("template/fill_data_template4.xlsx"); String targetFileName = "down/Excel_填充_水平数据.xlsx"; List<FileData> dataList = initFillData();
ExcelWriter workBook = EasyExcel.write(targetFileName, FileData.class).withTemplate(template).build(); WriteSheet sheet = EasyExcel.writerSheet().build(); FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build(); workBook.fill(dataList, fillConfig, sheet); workBook.finish(); }
private static List<FileData> initFillData() { ArrayList<FileData> fillDatas = new ArrayList<FileData>(); for (int i = 0; i < 10; i++) fillDatas.add(FileData.builder().name("YLan" + i).age(10 + i).build()); return fillDatas; } }
|
填充效果
填充综合练习
准备模板
封装数据
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| @Data @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode @ToString
@Builder public class Members {
@ExcelIgnore private String id;
@ExcelProperty(index = 0) private String name;
@ExcelProperty(index = 1) private String gender;
@ExcelProperty(index = 2) private Date birthday; }
private static List<Members> initData() { ArrayList<Members> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { Date date = Date.from(LocalDate.of(2001, i + 1, 17).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); Members members = Members.builder().name("Ylan" + i).gender("男").birthday(date).build(); list.add(members); } return list; }
HashMap<String, String> data = new HashMap<String, String>();
data.put("date", "2020-03-16");
data.put("totalCount", "1000");
data.put("increaseCount", "100");
data.put("increaseCountWeek", "50");
data.put("increaseCountMonth", "100");
List<Members> members = initData();
|
开始填充
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| public class TemplateTest5 { public static void main(String[] args) {
InputStream template = Thread.currentThread().getContextClassLoader().getResourceAsStream("template/report_template.xlsx"); String targetFileName = "down/Excel_填充_报表导出.xlsx";
HashMap<String, String> data = new HashMap<String, String>(); data.put("date", "2020-03-16"); data.put("totalCount", "1000"); data.put("increaseCount", "100"); data.put("increaseCountWeek", "50"); data.put("increaseCountMonth", "100"); List<Members> members = initData();
ExcelWriter workBook = EasyExcel.write(targetFileName, FileData.class).withTemplate(template).build(); WriteSheet sheet = EasyExcel.writerSheet().build(); workBook.fill(data, sheet); workBook.fill(members, sheet); workBook.finish(); }
private static List<Members> initData() { ArrayList<Members> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { Date date = Date.from(LocalDate.of(2001, i + 1, 17).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); Members members = Members.builder().name("Ylan" + i).gender("男").birthday(date).build(); list.add(members); } return list; } }
|
填充效果
注意事项
为了节省内存,所以没有采用把整个文档在内存中组织好之后再整体写入到文件的做法,而是采用的是一行一行写入的方式,不能实现删除和移动行,也不支持备注写入。多组数据写入的时候,如果需要新增行,只能在最后一行增加,不能在中间位置添加。
API及注解
官方地址
读Excel https://easyexcel.opensource.alibaba.com/docs/current/api/#readsheet
写Excel https://easyexcel.opensource.alibaba.com/docs/current/api/write
填充Excel https://easyexcel.opensource.alibaba.com/docs/current/api/fill
常用类
- EasyExcel 入口类,构建开始的操作
- ExcelReaderBuilder 构建出一个ReadWorkbook对象,即一个工作簿对象,对应的是一个Excel文件
- ExcelReaderSheetBuilder 构建出一个ReadSheet对象,即一个工作表的对象,对应的Excel中的每个sheet,一个工作簿可以有多个工作表
- ReadListener 在每一行读取完毕后调用ReadListener来处理数据,可以把调用service的代码可以写在其invoke方法内部
- ExcelWriterBuilder 构建出一个WriteWorkbook对象,即一个工作簿对象,对应的是一个Excel文件
- ExcelWriterSheetBuilder 构建出一WriteSheet对象,即一个工作表的对象,对应的Excel中的每个sheet,一个工作簿可以有多个工作表
- WriteHandler 创建单元格、创建表格等都会调用WriteHandler来处理数据,对使用者透明不可见
所有配置都是继承的 Workbook的配置会被Sheet继承,即在设置参数的时候,在EasyExcel…sheet()方法之前作用域是整个sheet,之后针对单个sheet。
读取API及注解
注解
@ExcelProperty
使用位置:作用在成员变量上
可选属性:
属性名 |
含义 |
说明 |
index |
对应Excel表中的列数 |
默认-1,指定时从0开始 |
value |
对应Excel表中的列头 |
|
converter |
成员变量转换器 |
自定义转换器需要实Converter接口 |
不使用@ExcelProperty注解时,成员变量从上到下的顺序,对应表格中从左到右的顺序进行解析。index属性和value属性都可以指定当前字段对应excel中的哪一列,推荐让index和vlaue的配置都指向同一列。
@ExcelIgnore
使用位置:标注在成员变量上
默认所有字段都会和Excel表格去匹配,加该注解后会忽略该字段,通常将数据库的主键ID加上该注解。
使用位置:标注在成员变量上
用于日期转换,用String类型的成员变量
去接收excel中日期格式的数据
会调用这个注解,value
参照java.text.SimpleDateFormat
格式。
使用位置:标注在成员变量上
用于数字转换,用String类型的成员变量
去接收excel数字格式的数据
会调用这个注解,value
参照java.text.DecimalFormat
格式。
@ExcelIgnoreUnannotated
使用位置:标注在类上
不标注该注解时,类中所有属性(除@ExcelIgnore的属性)都会参与读写,无论是否在成员变量上加@ExcelProperty
注解。
标注该注解后,只读写类中有标注@ExcelProperty
注解的属性。
读取时通用参数
ReadWorkbook
,ReadSheet
都有的参数,为空时默认使用上级
converter
转换器,默认加载了很多转换器,也可以自定义
readListener
读监听器,在读取数据的过程中会不断的调用监听器
headRowNumber
指定需要读的表格的列头行数。默认有一行头,也就是认为第二行开始起为数据。
head
与clazz
二选一。读取文件头对应的列表,根据列表匹配数据。建议使用class,即文件中每一行数据对应的代码中的实体类型。
clazz
与head
二选一。读取文件头对应的class,根据class匹配数据。如果两个都不指定,则会读取全部数据。
autoTrim
字符串、表头等数据自动trim
password
读的时候是否需要使用密码
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
即可。
ReadSheet(工作表对象)参数
sheetNo
需要读取Sheet的编号,建议使用该参数来指定读取的Sheet
sheetName
需要读取Sheet的名称,excel 2003不支持根据名字去匹配
写入API及注解
注解
@ExcelProperty
使用位置:作用在成员变量上
可选属性:
属性名 |
含义 |
说明 |
index |
对应Excel表中的列数 |
默认-1,指定时建议从0开始 |
value |
对应Excel表中的列头 |
|
converter |
成员变量转换器 |
自定义转换器需要实Converter接口 |
使用效果:index
指定写到第几列,如果不指定则根据成员变量位置排序;value
指定写入的列头,如果不指定则使用成员变量的名字作为列头;如果要设置复杂的头,可以为value指定多个值。
@ExcelIgnore
使用位置:标注在成员变量上
默认所有字段都会和Excel表格去匹配,加该注解后会忽略该字段,通常将数据库的主键ID加上该注解。
使用位置:标注在成员变量上
用于日期转换,用String类型的成员变量
去接收excel中日期格式的数据
会调用这个注解,value
参照java.text.SimpleDateFormat
格式。
使用位置:标注在成员变量上
用于数字转换,用String类型的成员变量
去接收excel数字格式的数据
会调用这个注解,value
参照java.text.DecimalFormat
格式。
@ExcelIgnoreUnannotated
使用位置:标注在类上
不标注该注解时,类中所有属性(除@ExcelIgnore的属性)都会参与读写,无论是否在成员变量上加@ExcelProperty
注解。
标注该注解后,只读写类中有标注@ExcelProperty
注解的属性
@HeadRowHeight(value)
使用位置:标注在类上
指定列头行高
@ContentRowHeight(value)
使用位置:标注在类上上
指定内容行高,统一行高
@ColumnWidth(value)
使用位置:标注在类上或属性上
指定列宽 ,作用于类上时统一列宽,作用于属性上时单独设置列宽,属性上的优先级大于类上。
写入时通用参数
WriteWorkbook
、WriteSheet
都有的参数,为空时使用上级
converter
转换器,默认加载了很多转换器,也可以自定义。
writeHandler
写处理器。可以实现WorkbookWriteHandler
,SheetWriteHandler
,RowWriteHandler
,CellWriteHandler
,在写入excel的不同阶段会调用,对使用者透明不可见。
relativeHeadRowIndex
距离多少行后开始,即开头空几行。
needHead
是否导出头
head
与clazz
二选一。读取文件头对应的列表,根据列表匹配数据。建议使用class,即文件中每一行数据对应的代码中的实体类型。
clazz
与head
二选一。读取文件头对应的class,根据class匹配数据。如果两个都不指定,则会读取全部数据。
autoTrim
字符串、表头等数据自动trim
WriteWorkbook(工作簿对象)参数
excelType
当前excel的类型,默认为xlsx
。
outputStream
与file
二选一,写入文件的流 。
file
与outputStream
二选一,写入的文件 。
templateInputStream
模板的文件流
templateFile
模板文件
autoCloseStream
自动关闭流。
password
写的时候是否需要使用密码
useDefaultStyle
写的时候是否是使用默认头
WriteSheet(工作表对象)参数
sheetNo
需要写入Sheet的编号,建议使用该参数来指定写入的Sheet 。
sheetName
需要写入Sheet的名称,excel 2003不支持根据名字去匹配。
常见问题
官方地址
必读 https://easyexcel.opensource.alibaba.com/qa/
读Excel https://easyexcel.opensource.alibaba.com/qa/read
写Excel https://easyexcel.opensource.alibaba.com/qa/read
填充Excel https://easyexcel.opensource.alibaba.com/qa/fill
必读
我的部分字段为什么没法读取或者写入?
- 读写反射对象用到了Cglib,所以成员变量必须符合驼峰规范,请确认是否符合驼峰规范
- 在3.0.0-beta1 兼容了部分非驼峰,但是还是不建议使用非驼峰字段
- 使用了lombok的@Accessors(chain = true) ,无法被Cglib读取
- 建议使用@Builder来替换@Accessors(chain = true)
我在本地可以,发布到线上环境怎么不可以了?
大概率是缺少字体库导致,2个方案:
- 安装字体(推荐)
- 看下服务器是否安装了字体,jdk8字体需要自己安装请安装字体:dejavu-sans-fonts 和 fontconfig 在dockerfile中增加字体安装命令: RUN yum install dejavu-sans-fonts fontconfig -y
- 普通的线上环境直接运行: yum install dejavu-sans-fonts fontconfig -y
- 开启内存处理模式(不推荐,1W数据以内可以考虑,大了很容易OOM)
1 2 3 4 5 6
| EasyExcel .write(fileName, DemoData.class) .inMemory(Boolean.TRUE) .sheet("模板") .doWrite(data());
|
读Excel
为什么Listener 不能让spring管理?
- 在读取excel的时候,会回调com.alibaba.excel.read.listener.ReadListener#invoke的方法,而spring如果管理Listener会导致Listener 变成了单例,在有并发读取文件的情况下都会回调同一个Listener,就无法区分是哪个文件读取出来的了。
在读的时候Listener里面需要使用spring的@Autowired
方案1:避开创建Listener的类
1 2 3 4 5
| EasyExcel.read(fileName, DemoData.class, new PageReadListener<DemoData>(dataList -> { for (DemoData demoData : dataList) { log.info("读取到一条数据{}", JSON.toJSONString(demoData)); } })).sheet().doRead();
|
方案2
- 给Listener创建成员变量,然后在构造方法里面传进去。必须不让spring管理Listener,每次读取都要new一个。
开启急速模式
急速模式可以自己开启后测试下内存的占用,如果感觉符合预期可以直接开启。
如果最大文件条数也就十几二十万,然后excel也就是十几二十M,而且不会有很高的并发,并且内存也较大。这种情况下可以考虑开启极速模式。
1 2 3 4
|
EasyExcel.read().readCache(new MapCache());
|
写Excel
导出的excel打不开
大概率由以下2个原因导致:
- 前端用各种框架来导出,然后把文件流改了
- 后端各种拦截器,把文件流改了
解决方案:
- 先测试把文件导出到本地,而不是通过浏览器
- 这个核心定位是不是自己代码写错了,通过代表代码没问题
- 不通过自己参照下最简单的写试试
- 再通过浏览器访问的方案去导出
- 这个核心确认是否被后端的各种拦截器或者nginx把流给改坏了,通过代表后端+网络没问题
- 不通过自己试下不用nginx等自己访问行不行,如果还不行那就是后端的各种拦截器导致
- 这里注意下注解是@Controller ,不是RestController
- 返回是void,然后确保后面所有的拦截器之类的不能再修改流
- 接下来就是甩锅给前端
- 后端只要确保浏览器直接访问能下载就行,接下来前端各种框架处理的方案都不一样
填充Excel
模板中的字段未替换
使用EasyExcel生成的模板,去填充,结果填充字段未替换。这个问题原因比较复杂,感兴趣的同学可以了解下:ShardingStrings ,EasyExcel在写入的时候,为了省内存,而未使用ShardingStrings。
解决方案:
在导出模板时,使用参数inMemory=true即可。
1 2 3 4
| // 这里使用inMemory(true) 会触发 写入到ShardingStrings 但是这样所有的数据都会在内存 所以容易OOM // 但是如果我们仅仅是需要导出模板,字段不是很多,所以问题不大。 // 这里注意 导出模板的加入这个参数 实际写数据千万别加,不然容易OOM EasyExcel.write(fileName, DemoData.class).inMemory(true).sheet("模板").doWrite(fillData())
|
SpringBoot读取Resource文件
最近在项目中涉及到Excle的导入功能,通常是我们定义完模板供用户下载,用户按照模板填写完后上传。这里待下载模板位置为resource/excelTemplate/test.xlsx。
方式one
1 2
| ClassPathResource classPathResource = new ClassPathResource("excleTemplate/test.xlsx"); InputStream inputStream =classPathResource.getInputStream();
|
方式two
1
| InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("excleTemplate/test.xlsx");
|
方式three
1
| InputStream inputStream = this.getClass().getResourceAsStream("/excleTemplate/test.xlsx");
|