1.Apache POI
Apache POI
是Apache软件基金会的开源函式库,提供跨平台的Java API
实现Microsoft Office
格式档案读写。但是存在如下一些问题:
1.1 学习使用成本较高
对POI有过深入了解的才知道原来POI还有SAX模式(Dom解析模式)。但SAX模式相对比较复杂,excel有03和07两种版本,两个版本数据存储方式截然不同,sax解析方式也各不一样。
想要了解清楚这两种解析方式,才去写代码测试,估计两天时间是需要的。再加上即使解析完,要转换到自己业务模型还要很多繁琐的代码。总体下来感觉至少需要三天,由于代码复杂,后续维护成本巨大。
POI的SAX模式的API可以一定程度的解决一些内存溢出的问题,但是POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大,一个3M的Excel用POI的SAX解析,依然需要100M左右内存。
1.2 POI的内存消耗较大
大部分使用POI都是使用他的userModel模式。userModel的好处是上手容易使用简单,随便拷贝个代码跑一下,剩下就是写业务转换了,虽然转换也要写上百行代码,相对比较好理解。然而userModel模式最大的问题是在于非常大的内存消耗,一个几兆的文件解析要用掉上百兆的内存。现在很多应用采用这种模式,之所以还正常在跑一定是并发不大,并发上来后一定会OOM或者频繁的full gc。
总体上来说,简单写法重度依赖内存,复杂写法学习成本高。
1.3 特点
- 功能强大
- 代码书写冗余繁杂
- 读写大文件耗费内存较大,容易OOM
2. 初识EasyExcel
2.1 重写了POI对07版Excel的解析
- EasyExcel重写了POI对07版Excel的解析,可以把内存消耗从100M左右降低到10M以内,并且再大的Excel不会出现内存溢出,03版仍依赖POI的SAX模式。
- 下图为64M内存1分钟内读取75M(46W行25列)的Excel(当然还有急速模式能更快,但是内存占用会在100M多一点)
- 在上层做了模型转换的封装,让使用者更加简单方便
2.2 特点
- 在数据模型层面进行了封装,使用简单
- 重写了07版本的Excel的解析代码,降低内存消耗,能有效避免OOM
- 只能操作Excel
- 不能读取图片
3.快速入门
3.1 导入依赖坐标
<!-- EasyExcel --> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.1.6</version> </dependency> <!-- lombok 优雅编程 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> </dependency> <!-- junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency>
3.2 最简单的读
3.2.1 需求、准备工作
/** * 需求:单实体导入 * 导入Excel学员信息到系统。 * 包含如下列:姓名、出生日期、性别 * 模板:逐浪教育学员信息表.xls文件 */
3.2.2 编写导出数据的实体
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; /** * 学生实体类 * lombok:通过一个插件 + 一个依赖 ,就可以在编译的时候自动帮助生成实体类常用方法 * * @author 狐狸半面添 * @create 2023-02-26 14:56 */ @Data @NoArgsConstructor @AllArgsConstructor public class Student { /** * 学生姓名 */ private String name; /** * 学生出生日期 */ private Date birthday; /** * 学生性别 */ private String gender; /** * id */ private String id; }
3.2.3 读取Excel的监听器,用于处理读取产生的数据
import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.event.AnalysisEventListener; import com.fox.easyexcel.domain.Student; /** * 读取文档的监听器类 * * @author 狐狸半面添 * @create 2023-02-26 15:10 */ public class StudentListener extends AnalysisEventListener<Student> { /** * 读监听器,每读一行内容,都会调用一次invoke,在invoke可以操作使用读取到的数据 * * @param student 每次读取到的数据封装的对象 * @param analysisContext */ public void invoke(Student student, AnalysisContext analysisContext) { System.out.println(student); } /** * 读取完整个文档之后,调用的方法 * * @param analysisContext */ public void doAfterAllAnalysed(AnalysisContext analysisContext) { // todo } }
3.2.4 读取Excel文件
import com.alibaba.excel.EasyExcel; import com.alibaba.excel.read.builder.ExcelReaderBuilder; import com.alibaba.excel.read.builder.ExcelReaderSheetBuilder; import com.fox.easyexcel.domain.Student; import com.fox.easyexcel.listener.StudentListener; import org.junit.Test; /** * @author 狐狸半面添 * @create 2023-02-26 15:03 */ public class ExcelTest { /** * 工作簿:一个excel文件就是一个工作簿 * 工作表:一个工作簿可以有多个工作表(sheet) */ @Test public void test01() { /* 1.获得一个工作簿对象 构建一个读的工作簿对象,参数说明: - pathName:要读的文件的路径 - head:文件中每一行数据要存储到的实体的类型的class - readListener:读监听器,每读一行内容,都会调用一次该对象的invoke,在invoke可以操作使用读取到的数据 */ ExcelReaderBuilder readWorkBook = EasyExcel.read("逐浪教育学员信息表.xlsx", Student.class, new StudentListener()); // 2.获得一个工作表对象,默认读取第一个工作表 ExcelReaderSheetBuilder sheet = readWorkBook.sheet(); // 3.读取工作表中的内容 sheet.doRead(); } }
3.3 最简单的写
3.3.1 需求、准备工作
/** * 需求:单实体导出 * 导出多个学生对象到Excel表格 * 包含如下列:姓名、出生日期、性别 * 模板详见:逐浪教育学员信息表.xlsx */
3.3.2 编写导出数据的实体
import com.alibaba.excel.annotation.ExcelIgnore; import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.write.style.ColumnWidth; import com.alibaba.excel.annotation.write.style.ContentRowHeight; import com.alibaba.excel.annotation.write.style.HeadRowHeight; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; /** * 学生实体类 * lombok:通过一个插件 + 一个依赖 ,就可以在编译的时候自动帮助生成实体类常用方法 * 注解 @ContentRowHeight():内容的行高 * 注解 @HeadRowHeight:表头的行高 * * @author 狐狸半面添 * @create 2023-02-26 14:56 */ @Data @NoArgsConstructor @AllArgsConstructor public class Student { /** * 学生姓名 */ @ExcelProperty("学生姓名") @ColumnWidth(20) private String name; /** * 学生出生日期 */ @ExcelProperty("出生日期") @ColumnWidth(20) private Date birthday; /** * index 从0开始 * 学生性别 */ @ExcelProperty(value = "学生性别", index = 1) private String gender; /** * id */ @ExcelIgnore private String id; }
3.3.3 准备数据并写入到文件
@Test public void test02() { /* 1.构建一个写的工作簿对象 - pathName:要写入的文件路径 - head:封装写入的数据的实体类型 */ ExcelWriterBuilder writeWorkBook = EasyExcel.write("逐浪教育学员信息表.xlsx", Student.class); // 2.获取工作表对象,默认是第一个工作表 ExcelWriterSheetBuilder sheet = writeWorkBook.sheet(); // 3.生成十个测试对象 ArrayList<Student> students = new ArrayList<Student>(); for (int i = 1; i <= 10; i++) { students.add(new Student("逐浪者-" + i, new Date(), "男", null)); } // 4.将数据写入工作表 sheet.doWrite(students); }
3.4 文件上传和下载
基于SpringMVC的文件上传和下载
3.4.1 导入依赖
<!-- EasyExcel --> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.1.6</version> </dependency> <!-- lombok 优雅编程 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> </dependency> <!-- junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.6.3</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
3.4.2 文件上传
🍀 编写excel中每一行对应的实体类
@Data @NoArgsConstructor @AllArgsConstructor public class Student { /** * 学生姓名 */ private String name; /** * 学生性别 */ private String gender; /** * 学生出生日期 */ private Date birthday; /** * id */ private String id; }
🍀 回调监听器StudentReadListener
import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.event.AnalysisEventListener; import com.fox.easyexcel.domain.Student; import com.fox.easyexcel.service.StudentService; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.ArrayList; /** * 读取文档的监听器类 * * @author 狐狸半面添 * @create 2023-02-26 15:10 */ @Component @Scope("prototype") //作者要求每次读取都要使用新的Listener public class StudentListener extends AnalysisEventListener<Student> { @Resource private StudentService studentService; public static final ThreadLocal<ArrayList<Student>> threadLocal = new ThreadLocal<ArrayList<Student>>(); /** * 读监听器,每读一行内容,都会调用一次invoke,在invoke可以操作使用读取到的数据 * * @param student 每次读取到的数据封装的对象 * @param analysisContext */ public void invoke(Student student, AnalysisContext analysisContext) { ArrayList<Student> students = threadLocal.get(); if (students == null) { threadLocal.set(new ArrayList<Student>()); students = threadLocal.get(); } students.add(student); if (students.size() == 5) { studentService.save(students); students.clear(); } } /** * 读取完整个文档之后,调用的方法 * * @param analysisContext */ public void doAfterAllAnalysed(AnalysisContext analysisContext) { // todo } }
🍀 业务代码接口StudentService和实现类StudentServiceImpl
import com.fox.easyexcel.domain.Student; import java.util.ArrayList; /** * @author 狐狸半面添 * @create 2023-02-26 16:27 */ public interface StudentService { /** * 保存学生信息 * * @param students 信息列表 */ void save(ArrayList<Student> students); }
import com.fox.easyexcel.domain.Student; import com.fox.easyexcel.service.StudentService; import org.springframework.stereotype.Service; import java.util.ArrayList; /** * @author 狐狸半面添 * @create 2023-02-26 16:27 */ @Service public class StudentServiceImpl implements StudentService { @Override public void save(ArrayList<Student> students) { System.out.println("save to database = " + students); } }
🍀 读取上传的Excel文件
import com.alibaba.excel.EasyExcel; import com.alibaba.excel.read.builder.ExcelReaderBuilder; import com.alibaba.excel.read.builder.ExcelReaderSheetBuilder; import com.fox.easyexcel.domain.Student; import com.fox.easyexcel.listener.StudentListener; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; /** * @author 狐狸半面添 * @create 2023-02-26 16:29 */ @RestController @RequestMapping("/student") @Slf4j public class StudentController { @Resource private StudentListener studentListener; @PostMapping("/read") public String readExcel(MultipartFile uploadExcel) { try { // 1.获取工作簿 ExcelReaderBuilder readWorkBook = EasyExcel.read(uploadExcel.getInputStream(), Student.class, studentListener); // 2.获取工作表 ExcelReaderSheetBuilder sheet = readWorkBook.sheet(); // 3.读取数据 sheet.doRead(); // 4.释放线程 StudentListener.threadLocal.remove(); return "success"; } catch (Exception e) { log.error("读取文件失败:{}", e.getMessage()); return "fail"; } } }
🍀 ApiFox测试