一、初识EasyExcel*
1. Apache POI
先说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。
总体上来说,简单写法重度依赖内存,复杂写法学习成本高。
特点
-
功能强大
-
代码书写冗余繁杂
-
读写大文件耗费内存较大,容易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多一点)
- 在上层做了模型转换的封装,让使用者更加简单方便
特点
- 在数据模型层面进行了封装,使用简单
- 重写了07版本的Excel的解析代码,降低内存消耗,能有效避免OOM
- 只能操作Excel
- 不能读取图片
二、快速入门–QuickStart
0、导入依赖坐标
<!-- 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.16.20</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
导入
easyexcel-2.1.6
坐标的时候,已依赖传递导入poi-3.17
的POI。
1、最简单的读
1.1、需求、准备工作
/**
* 需求:单实体导入(从磁盘将文件导入到应用程序)
* 导入Excel学员信息到系统。
* 包含如下列:姓名、性别、出生日期
* 模板详见:学员信息.xlsx
*/
学员信息.xlsx
1.2、编写导出数据的实体类
package com.taotie.test;
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.HeadRowHeight;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc
*/
// 基于lombok
@Data
@NoArgsConstructor
@AllArgsConstructor
@HeadRowHeight(20) // 指定列头行高
@ColumnWidth(20) // 指定列宽
public class Student {
/**
* 学生姓名
*/
@ExcelProperty(value = "学生姓名", index = 0)
private String name;
/**
* 学生性别
*/
@ExcelProperty(value = "学生性别", index = 2)
private String gender;
/**
* 学生出生日期
*/
@ExcelProperty(value = "学生出生日期", index = 1)
private Date birthday;
/**
* id
*/
// @ExcelProperty(value = "编号",index = 3)
@ExcelIgnore // 忽略,不读取
private String id;
}
注解: 文章后面有详解
1.3、 读取Excel
文件(上传)
调用EasyExcel
的API
读取的Excel
文件的测试类StudentReadDemo
package com.taotie.test;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.read.builder.ExcelReaderBuilder;
import com.alibaba.excel.read.builder.ExcelReaderSheetBuilder;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc
*/
@SpringBootTest
public class TestEasyExcel {
/**
* 测试读取数据
*/
@Test
public void testRead() {
// 读取文件,读取完之后会自动关闭
/**
* 参数1:pathName 文件路径;"d:\\学员信息.xls"
* 参数2:head 每行数据对应的实体;Student.class
* 参数3:readListener 读监听器,每读一样就会调用一次该监听器的invoke方法
* 参数4:sheet方法参数: 工作表的顺序号(从0开始)或者工作表的名字,不传默认为0
*/
// // 封装工作簿对象
// ExcelReaderBuilder workBook = EasyExcel.read
// ("E:\\学员信息.xlsx",
// Student.class,
// new StudentReadListener( ));
// // 封装工作表
// ExcelReaderSheetBuilder sheet1 = workBook.sheet( );
// // 读取
// sheet1.doRead( );
// 最简单的写法
EasyExcel.read("E:\\学员信息.xlsx",Student.class,new StudentReadListener()).sheet().doRead();
}
}
读取Excel的监听器,用于处理读取产生的数据
package com.taotie.listener;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.taotie.domain.Student;
/**
* @Author Vsunks.v
* @Description:
*/
public class StudentReadListener extends AnalysisEventListener<Student> {
// 每读一行,会调用该invoke方法一次
@Override
public void invoke(Student data, AnalysisContext context) {
System.out.println("data = " + data);
}
// 全部读完之后,会调用该方法
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// TODO......
}
}
2、最简单的写(导出)
2.1 需求、准备工作
/**
* 需求:单实体导出(从应用程序将文件导入到磁盘)
* 导出多个学生对象到Excel表格
* 包含如下列:姓名、性别、出生日期
* 模板详见:学员信息.xlsx
*/
2.2、编写导出数据的实体
// 还是上个实体类...
2.3、 准备数据并写入到文件
/**
* 测试写出数据
*/
@Test
public void simpleWrite() {
// 创造数据
ArrayList<Student> list = new ArrayList<>( );
list.add(new Student("张三","男",new Date(),"1001"));
list.add(new Student("李四","女",new Date(),"1002"));
list.add(new Student("王五","男",new Date(),"1003"));
list.add(new Student("赵六","女",new Date(),"1004"));
list.add(new Student("周期","男",new Date(),"1005"));
list.add(new Student("茅十八","女",new Date(),"1006"));
// 写出的文件路径,当前项目下
String fileName = "student.xlsx";
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
// 如果这里想使用03 则 传入excelType参数即可
EasyExcel.write(fileName, Student.class).sheet("学生信息").doWrite(list);
System.out.println("导出OK" );
}
三、vue文件上传和下载[重点]
基于springboot的文件上传和下载
0. 导入依赖
<!-- EasyExcel -->
<!-- lombok -->
<!-- junit -->
3.1 文件上传
注意: 本地Excel表格,列要和数据库一致
需求: 批量插入计量单位(导入excel数据到项目中)
思路:
- 前端设计文件上传组件 ,点击开始文件上传
- 后端接收文件,使用工具解析数据
- 插入数据库
3.1.1 前端
在页面设计上传的组件
1.设置上传按钮,显示上传的对话框
2.设置对话框+上传组件
<!-- 上传excel的对话框 -->
<el-dialog title="上传计量单位Excel" :visible.sync="dialogExcelVisible" width="40%">
<el-upload
class="upload-demo"
drag
action="http://localhost:8888/md/unit/upload/excel"
accept=".xlsx,.xls"
:on-success="uploadExcelSuccess"
:on-error="uploadExcelError"
multiple>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">只能上传.xlsx,.xls文件,且不超过500kb</div>
</el-upload>
</el-dialog>
uploadExcelSuccess(){
this.$message({
type:"success",
message:"上传成功"
})
this.dialogExcelVisible = false;
this.fetchData();
},
uploadExcelError(err){
this.$message({
type:"error",
message:err
})
}
3.1.2 后端-实体类注解
3.1.3 后端-监听器
package com.qf.config;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.qf.entity.SysUser;
import com.qf.entity.UnitMeasure;
import com.qf.service.SysUserService;
import com.qf.service.UnitMeasureService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc
*/
@Component
@Scope("prototype") // 作者要求每次读取都要使用新的Listener
public class MdUnitMeasureReadListener extends AnalysisEventListener<UnitMeasure> {
// 有个很重要的点 DemoDataListener 不能被spring管理,
// 要每次读取excel都要new,然后里面用到spring可以构造方法传进去
private UnitMeasureService service;
public MdUnitMeasureReadListener(){} // 空参构造
// 有参构造
public MdUnitMeasureReadListener(UnitMeasureService service){
this.service = service;
}
// 每隔5条存储数据库,实际项目中使用时可以100条,然后清理list ,方便内存回收
private final int BATCH_COUNT = 5;
private ArrayList<UnitMeasure> list = new ArrayList<>( );
// 每读一行,会调用该invoke方法一次
@Override
public void invoke(UnitMeasure data, AnalysisContext context) {
list.add(data);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (list.size( ) >= BATCH_COUNT) {
// 调用方法,执行插入
saveData( );
// 存储完成清理 list
list.clear();
}
}
// 全部读完之后,会调用该方法
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
if(list.size() > 0) {
saveData( );
}
System.out.println("所有数据解析完成!");
}
// 向数据库插入数据
private void saveData() {
System.out.println("开始存储数据库!");
// 调用业务层存数据库
service.saveBatch(list);
System.out.println("存储数据库成功!");
}
}
3.1.4 后端-Controller接收
/**
* 上传Excel文件
*/
@PostMapping("/upload/excel")
public R uploadExcel(MultipartFile file) {
try {
// 重点,此处第三个参数需要传入Service对象
EasyExcel.read(file.getInputStream( ),
UnitMeasure.class,
new MdUnitMeasureReadListener(service))
.sheet( ).doRead( );
}catch (Exception e){
e.printStackTrace();
return R.fail(e.getMessage());
}
return R.ok( );
}
3.1.5 后端-Service
// 需要添加批量插入的方法
3.1.6 后端-Mapper
<!-- 批量插入 -->
<insert id="saveBatch">
insert into
md_unit_measure (measure_code,measure_name,primary_flag,primary_id,change_rate,enable_flag)
values
<foreach collection="list" item="unit" separator=",">
(#{unit.measureCode},
#{unit.measureName},
#{unit.primaryFlag},
#{unit.primaryId},
#{unit.changeRate},
#{unit.enableFlag})
</foreach>
3.1.7 测试
本地创建一个excel表格,按照实体类中定义的列名填充数据
点击上传
3.2 文件导出
需求: 将项目中的数据导出到本地excel表格
思路:
- 前端设计按钮,点击开始导出
- 后端请求,查询出数据,
- 使用工具导出
实体类于之前一样
// 略
Controller层
/**
* 下载Excel文件
* 注意,返回值是void
*/
@GetMapping("/download/excel")
public void downloadExcel(HttpServletResponse response) throws IOException {
// 查询数据库全部数据
List<UnitMeasure> list = service.findAll(null);
// 下面这个注释是前端使用方式1,即a标签发请求时采用
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码
String fileName = URLEncoder.encode("计量单位信息", "UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
// easyexcel导出工具
EasyExcel.write(response.getOutputStream(), UnitMeasure.class).autoCloseStream(Boolean.FALSE).sheet("计量单位信息")
.doWrite(list);
}
// todo servicce+mapper实现查询全部
前端
1.导出按钮触发函数
2.函数内确认导出,发出请求到后端
3.3、自定义单元格样式
EasyExcel支持调整行高、列宽、背景色、字体大小等内容,但是控制方式与使用原生POI无异,比较繁琐,不建议使用。
但是可以使用模板填充的方式,向预设样式的表格中直接写入数据,写入数据的时候会保持原有样式。