一、EasyExcel的了解:
EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。
文档说明地址:阿里EasyExcel
特点:简单,节省内存。
(1)Java领域解析、生成Excel比较有名的框架有Apache poi、jxl等。但他们都存在一个严重的问题就是非常的耗内存。如果你的系统并发量不大的话可能还行,但是一旦并发上来后一定会OOM或者JVM频繁的full gc。
(2)EasyExcel是阿里巴巴开源的一个excel处理框架,以使用简单、节省内存著称。EasyExcel能大大减少占用内存的主要原因是在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析。
(3)EasyExcel采用一行一行的解析模式,并将一行的解析结果以观察者的模式通知处理(AnalysisEventListener)。
前提准备工作:导包坐标
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.1</version>
</dependency>
二、EasyExcel的写操作:
- 首先,需要有一个实体类,来设置表头和数据字段。
public class DemoData {
//设置表头名称
@ExcelProperty("学生编号")
private int sno;
//设置表头名称
@ExcelProperty("学生姓名")
private String sname;
//getter和setter方法省略
- 生成数据集合,可以是从数据库里查询出来的集合。这里简单演示,所以编写一个方法用于数据生成。
//生成一个数据集合
private static List<Student> data() {
List<Student> list = new ArrayList<Student>();
for (int i = 0; i < 10; i++) {
Student student = new Student();
student.setSno(i);
student.setSname("张三"+i);
list.add(student);
}
return list;
}
- 对数据集合进行写入Excel操作。
//要导出的Excel文件名,也可自定义规则。
//如:String fileName=System.currentTimeMillis() + ".xlsx";
String fileName="E:\\11.xlsx";
EasyExcel.write(fileName,Student.class).sheet("学生数据").doWrite(data());
注:使用EasyExcel类的write方法,传入文件名,及实体类(表头名称及数据字段),sheet方法传入字符串,指定工作表名称,最后doWrite方法,传入数据集合进行写入。
- 扩展写法
三、EasyExcel的读操作:
- 创建实体类,用于存数据和设置表头索引。
public class Student2 {
@ExcelProperty(index = 0)
private int sid;
@ExcelProperty(index = 1)
private String sname;
//getter和setter方法省略
}
- 创建监听器
(1)继承AnalysisEventListener<实体类>
(2)创建一个集合,用于封装数据。
(3)执行invoke方法读取excel内容
public class ExcelListener extends AnalysisEventListener<Student2> {
List<Student2> list=new ArrayList<>();
//一行一行去读取excle内容
@Override
public void invoke(Student2 data, AnalysisContext context) {
System.out.println("***"+data);
list.add(data);
}
//读取表头信息
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
System.out.println("表头信息:"+headMap);
}
//读取后执行
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
}
注意:
(1)可以让其每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收。(如下代码)
(2)有个很重要的点Listener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去。如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来。(后面讲具体案例会解释)
//设置常量用于判断
private static final int BATCH_COUNT = 5;
...
...
//如果集合大小等于5时,存入数据到数据库,并清空集合。
if (list.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
list.clear();
}
- 读取文件:与写操作差不多。
(1) read方法传入文件,实体类,及一个监听器实例。由于上面说到的注意项,每次读取都要new一个监听器,所以这里传入new 监听器类。
(2)sheet方法指:读取第一个工作表。
(3)doRead方法:执行读操作。
//指定传入的文件,也可根据上传的文件进行读取
String fileName="E:\\11.xlsx";
EasyExcel.read(fileName, Student2.class,new ExcelListener()).sheet().doRead();
扩展写法:
执行:
四、具体案例:
此时,有这样一个需求,要求通过Excel导入课程类别。一级分类,二级分类为表头,读取课程类别并存入数据库。
数据库表:一级标题parent_id为0,二级标题的parent_id为一级标题的id。如后端开发id:1178214681118568449。那么它的二级标题:(后端)Java的parent_id为:1178214681118568449。
流程步骤:
- 数据实体类:
public class SubjectData {
@ExcelProperty(index = 0)
private String oneSubjectName;
@ExcelProperty(index = 1)
private String twoSubjectName;
//getter和setter方法省略
}
- 创建监听器
(1)注入待会儿需要的插入到数据库的service。通过构造方法进行传入。(回到上面创建监听器时的注意事项)
(2)读取。如果为null,抛出异常。如果一级不为空且不重复,则添加到数据库,获取一级id,为添加二级的父id做设置准备。
public class SubjectExcelListener extends AnalysisEventListener<SubjectData> {
public EduSubjectService eduSubjectService;
public SubjectExcelListener(EduSubjectService eduSubjectService) {
this.eduSubjectService = eduSubjectService;
}
public SubjectExcelListener() {
}
@Override
//一行一行读取,第一个值一级分类,第二个值二级分类
public void invoke(SubjectData subjectData, AnalysisContext analysisContext) {
if(subjectData==null){
throw new GuliException(20001,"文件数据为空!");
}
EduSubject eduOneSubject = this.existOneSubject(subjectData.getOneSubjectName(), eduSubjectService);
if(eduOneSubject==null){
eduOneSubject=new EduSubject();
eduOneSubject.setTitle(subjectData.getOneSubjectName());
eduOneSubject.setParentId("0");
eduSubjectService.save(eduOneSubject);
}
String pid = eduOneSubject.getId();
//二级分类
EduSubject eduTwoSubject = this.existTwoSubject(subjectData.getTwoSubjectName(), eduSubjectService, pid);
if(eduTwoSubject==null){
eduTwoSubject=new EduSubject();
eduTwoSubject.setTitle(subjectData.getTwoSubjectName());
eduTwoSubject.setParentId(pid);
eduSubjectService.save(eduTwoSubject);
}
}
//判断一级分类不能重复添加的方法
private EduSubject existOneSubject(String name,EduSubjectService eduSubjectService){
}
//判断二级分类不能重复添加的方法
private EduSubject existTwoSubject(String name,EduSubjectService eduSubjectService,String pid){
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
}
- 读取文件
(1)Controller层
@ApiOperation(value = "Excel批量导入")
@PostMapping("addSubject")
public R addSubject(MultipartFile file){
eduSubjectService.saveSubject(file,eduSubjectService);
return R.ok();
}
(2)Service层:
public void saveSubject(MultipartFile file, EduSubjectService eduSubjectService) {
//获取文件
try {
//文件输入流
InputStream excelFile = file.getInputStream();
EasyExcel.read(excelFile, SubjectData.class, new SubjectExcelListener(eduSubjectService)).sheet().doRead();
} catch (IOException e) {
e.printStackTrace();
}
}
五、总结
(1)EasyExcel读取文件时对Excel是一行一行进行读取,而非其他整个文件进行读取,所以占用的内存小。
(2)写操作:实体类->文件路径->doWrite()
(3)读操作:实体类->监听器->文件路径->doRead()