作者: gh-xiaohe
gh-xiaohe的博客
觉得博主文章写的不错的话,希望大家三连(✌关注,✌点赞,✌评论),多多支持一下!!!
总体上来说,简单写法重度依赖内存,复杂写法学习成本高。
官网:https://easyexcel.opensource.alibaba.com/docs/current/quickstart/read
EasyExcel重写了POI对07版Excel的解析,可以把内存消耗从100M左右降低到10M以内,并且再大的Excel不会出现内存溢出,03版仍依赖POI的SAX模式。
在上层做了模型转换的封装,让使用者更加简单方便
工作簿:一个excel文件就是一个工作簿
工作表:一个工作簿中可以有多个工作表(sheet)
填充数据选用 类 或选用 Map
多组填充 和 单组填充 一起使用的问题 : 组合填充时,因为多组填充的数据量不确定,需要在多组填充完之后另起一行
<dependency>
<groupId>com.alibabagroupId>
<artifactId>easyexcelartifactId>
<version>2.1.6version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.10version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
dependency>
@Data
public class StudentRead {
/**
* 学生姓名
*/
private String name;
/**
* 学生出生日期
*/
private Date birthday;
/**
* 学生性别
*/
private String gender;
/**
* id
*/
private String id;
}
/**
* 读取文档的监听器
*/
public class StudentListenerRead extends AnalysisEventListener<StudentRead> {
/**
* 每读取一行数据会调用该invoke方法一次
*/
@Override
public void invoke(StudentRead student, AnalysisContext analysisContext) {
System.out.println("student="+student);
}
/**
* 读取完整个文档后会调用的内容
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
}
/**
* 读excel文件
*/
@Test
public void test01() {
// 读取文件,读取完之后会自动关闭
/**
* Build excel the read
* 构建excel读取
* @param pathName
* File path to read. 要读取的文件路径。
* @param head
* Annotate the class for configuration information. 为类注释配置信息。
* @param readListener
* Read listener. 读监听器,每读一行就会调用一次该监听器的invoke方法
* @return Excel reader builder. Excel阅读器生成器。
*
* sheet方法参数: 工作表的顺序号(从0开始)或者工作表的名字,不传默认为0
*/
// 获取工作簿对象
ExcelReaderBuilder readWorkBook = EasyExcel.read("杭州黑马在线202003班学员信息表.xlsx", StudentRead.class, new StudentListenerRead());
// 获取工作表对象
ExcelReaderSheetBuilder sheet = readWorkBook.sheet();
// 读取表中的内容
sheet.doRead();
}
@HeadRowHeight(20)//表头高度
@ContentRowHeight(20)//内容的行高
@Data
public class StudentWrite {
// 和excel表中数据一一对应
/**
* 注意:
* 1、生成的excel表格列名顺序,默认是根据类中的字段的顺序一致
* 2、@ExcelProperty(value = "编号",index = 3) 设置列名
* value 列名 index 在excel中的位置
* 如果不从索引0开始给,会把第一行空出来
* 支持多表头 @ExcelProperty(value = {"学生信息表","学生姓名"}) , 当所有的列头,都一致是,自动合并
* 3、@ExcelIgnore 该字段不参与读写
* 4、@ColumnWidth(30) 设置宽度
* 5、@DateTimeFormat("yyyy-MM-dd") 指定日期格式
* 6、@HeadRowHeight(20) 表头的行高 只能使用在类上 高度
* 7、@ContentRowHeight(20) 内容的行高 只能使用在类上
*
*/
/**
* id
*/
@ExcelIgnore
@ExcelProperty(value = "编号",index = 3)
private String id;
/**
* 学生姓名
*/
@ExcelProperty(value = {"学生信息表","学生姓名"}, index = 0)
// @ExcelProperty(value = "学生姓名", index = 0)
@ColumnWidth(30)
private String name;
/**
* 学生性别
*/
@ExcelProperty(value = "学生性别", index = 2)
@ColumnWidth(30)
private String gender;
/**
* 学生出生日期
*/
@ExcelProperty(value = "学生出生日期", index = 1)
@ColumnWidth(30)
@DateTimeFormat("yyyy-MM-dd")//指定日期格式
private Date birthday;
}
/**
* 需求:单实体导出
* 导出多个学生对象到Excel表格
* 包含如下列:姓名、性别、出生日期
* 模板详见:杭州黑马在线202003班学员信息.xlsx
*/
@Test
public void test02(){
/**
* Build excel the write 构建excel
*
* @param file
* File to write 要写入的文件路径
* @param head
* Annotate the class for configuration information
* 为类注释配置信息 封装写入的数据的实体类型
* @return 写的工作表对象
*/
// 获取工作簿对象
ExcelWriterBuilder writeWorkBook = EasyExcel.write("学员信息表.xlsx", StudentWrite.class);
// 获取工作表对象
ExcelWriterSheetBuilder sheet = writeWorkBook.sheet();
// 准备数据
List<StudentRead> students = initData();
// 读取表中的内容
sheet.doWrite(students);
}
/**
* @return 构建10条表数据
*/
private static List<StudentRead> initData() {
ArrayList<StudentRead> students = new ArrayList<>();
for (int i = 0; i < 10; i++) {
StudentRead data = new StudentRead();
data.setName("杭州黑马学号0" + i);
data.setBirthday(new Date());
data.setGender("男");
students.add(data);
}
return students;
}
基于SpringBoot的文件上传和下载
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
@Component
@Scope("prototype") // 需要是多例的
public class WebStudentListener extends AnalysisEventListener<StudentRead> {
@Autowired
StudentService studentService;
List<StudentRead> students = new ArrayList<>();
@Override
public void invoke(StudentRead student, AnalysisContext analysisContext) {
students.add(student);
// 五个操作一次
if (students.size() % 5 == 0) {
studentService.readExcel(students);
students.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
}
@Controller
@RequestMapping("student")
public class StudentController {
@Autowired
WebStudentListener webStudentListener;
/**
* 文件上传
*/
@PostMapping("read")
@ResponseBody
public String readExcel(MultipartFile uploadExcel) {
try {
// 工作薄
ExcelReaderBuilder readBookWork = EasyExcel.read(uploadExcel.getInputStream(), StudentRead.class, webStudentListener);
// 工作表sheet
readBookWork.sheet().doRead();
return "成功";
} catch (IOException e) {
e.printStackTrace();
return "失败";
}
}
}
@Controller
@RequestMapping("student")
public class StudentController {
@Autowired
WebStudentListener webStudentListener;
/**
* 文件导出,访问直接下载文件
*/
@GetMapping("write")
@ResponseBody
public void writeExcel(HttpServletResponse response) throws Exception {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 防止中文乱码
String fileName = URLEncoder.encode("测试", "UTF-8");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName + ".xlsx");
// 工作簿对象
ServletOutputStream outputStream = response.getOutputStream();
ExcelWriterBuilder writeWorkBook = EasyExcel.write(outputStream, StudentWrite.class);
// 工作表对象
ExcelWriterSheetBuilder sheet = writeWorkBook.sheet();
// 写
sheet.doWrite(initData());
}
private static List<StudentWrite> initData() {
List<StudentWrite> students = new ArrayList<>();
for (int i = 0; i < 10; i++) {
StudentWrite data = new StudentWrite();
data.setName("测试" + i + 2);
data.setGender("男");
data.setBirthday(new Date());
students.add(data);
}
System.out.println(students);
return students;
}
}
EasyExcel支持调整行高、列宽、背景色、字体大小等内容,但是控制方式与使用原生POI无异,比较繁琐,不建议使用。
但是可以使用模板填充的方式,向预设样式的表格中直接写入数据,写入数据的时候会保持原有样式。
\{
.\}
Excel表格中用{} 来表示包裹要填充的变量,如果单元格文本中本来就有
{
、}
左右大括号,需要在括号前面使用斜杠转义\{
、\}
。代码中用来填充数据的实体对象的成员变量名或用来填充map集合的key需要和Excel中被0包裹的变量名称一致。
编写封装填充数据的类或选用Map
/**
* 使用实体类封装填充数据
*
* 实体中成员变量名称需要和Excel表各种{}包裹的变量名匹配
*/
@Data
public class FillData {
private String name;
private int age;
}
实体类 和 map 的方式均有测试
/**
* 单组数据的填充
*/
@Test
public void test03(){
// 准备模板
String template = "fill_data_template1.xlsx";
// 获取工作簿对象
ExcelWriterBuilder excelWriterBuilder = EasyExcel.write("Excel单组数据填充.xlsx", FillData.class).withTemplate(template);
// 获取工作表对象
ExcelWriterSheetBuilder sheet = excelWriterBuilder.sheet();
// 准备数据一: 可以从实体里面获取数据
FillData fillData = new FillData();
fillData.setName("张三");
fillData.setAge(12);
// 准备数据二: 可以从map里面获取数据
HashMap<String, String> stringHashMap = new HashMap<>();
stringHashMap.put("name","李四");
stringHashMap.put("age","15");
// 填充数据
// sheet.doFill(fillData);
sheet.doFill(stringHashMap);
}
/**
* 多组数据的填充
*/
@Test
public void test04(){
// 准备模板
String template = "fill_data_template2.xlsx";
// 获取工作簿对象
ExcelWriterBuilder excelWriterBuilder = EasyExcel.write("Excel多组数据填充.xlsx", FillData.class).withTemplate(template);
// 获取工作表对象
ExcelWriterSheetBuilder sheet = excelWriterBuilder.sheet();
// 准备数据
List<FillData> fillData = initFillData();
// 填充数据
sheet.doFill(fillData); // doFill 自动关闭流操作
}
/**
* @return 构建10条表数据
*/
private static List<FillData> initFillData() {
ArrayList<FillData> fillDataList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
FillData fillData = new FillData();
fillData.setName("张三" + i);
fillData.setAge(10 + i);
fillDataList.add(fillData);
}
return fillDataList;
}
即有多组数据填充,又有单一数据填充,为了避免两者数据出现冲突覆盖的情况,在多组填充时需要通过
FillConfig
对象设置换行。
/**
* 组合数据的填充:多组填充 和 单组填充
*/
@Test
public void test05(){
// 准备模板
String template = "fill_data_template3.xlsx";
// 获取工作簿对象 手动关闭流操作 doFill 会自动关闭流操作
ExcelWriter workBook = EasyExcel.write("Excel组合数据填充.xlsx", FillData.class).withTemplate(template).build();
// 获取工作表对象
WriteSheet sheet = EasyExcel.writerSheet().build();
// // 准备数据 会出现问题 数据填充时,多组数据填充 没有进行换行
// List fillData = initFillData();
// HashMap dateAndTotal = new HashMap<>();
// dateAndTotal.put("date","2023-2-26");
// dateAndTotal.put("total","1000");
//
// // 多组数据填充
// workBook.fill(fillData, sheet);
//
// // 单组填充
// workBook.fill(dateAndTotal, sheet);
//
// // 关闭流操作
// workBook.finish();
/**
* 多组填充 和 单组填充 一起使用的问题 :
* 组合填充时,因为多组填充的数据量不确定,需要在多组填充完之后另起一行
*/
// 换行
FillConfig fillConfig = FillConfig.builder().forceNewRow(true).build();
// 准备数据
List<FillData> fillData = initFillData();
HashMap<String, String> dateAndTotal = new HashMap<>();
dateAndTotal.put("date","2023-2-26");
dateAndTotal.put("total","1000");
// 多组数据填充
workBook.fill(fillData, fillConfig,sheet); // 加入换行
// 单组填充
workBook.fill(dateAndTotal, sheet);
// 关闭流操作
workBook.finish();
}
水平填充和多组填充模板一样,不一样的地方在于,填充时需要通过
FillConfig
对象设置水平填充。
/**
* 水平数据的填充 方式一:推荐
*/
@Test
public void test06(){
// 准备模板
String template = "fill_data_template4.xlsx";
// 获取工作簿对象
ExcelWriterBuilder excelWriterBuilder = EasyExcel.write("Excel水平组数据填充1.xlsx", FillData.class).withTemplate(template);
// 获取工作表对象
ExcelWriterSheetBuilder sheet = excelWriterBuilder.sheet();
// 换行 WriteDirectionEnum.HORIZONTAL 枚举 水平 VERTICAL 垂直
FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
// 准备数据
List<FillData> fillData = initFillData();
// 填充数据
sheet.doFill(fillData,fillConfig); // doFill 自动关闭流操作
}
/**
* 水平数据的填充 方式二:
*/
@Test
public void test07(){
// 准备模板
String template = "fill_data_template4.xlsx";
// 获取工作簿对象
ExcelWriter workBook = EasyExcel.write("Excel水平组数据填充.xlsx", FillData.class).withTemplate(template).build();
// 获取工作表对象
WriteSheet sheet = EasyExcel.writerSheet().build();
// 换行 WriteDirectionEnum.HORIZONTAL 枚举 水平 VERTICAL 垂直
FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
// 准备数据
List<FillData> fillData = initFillData();
// 多组数据填充
workBook.fill(fillData, fillConfig,sheet); // 加入换行
// 关闭流操作
workBook.finish();
}
为了节省内存,所以没有采用把整个文档在内存中组织好之后再整体写入到文件的做法,而是采用的是一行一行写入的方式,不能实现删除和移动行,也不支持备注写入。多组数据写入的时候,如果需要新增行,只能在最后一行增加,不能在中间位置添加。
/**
* 综合练习:运行数据统计
*/
@Test
public void test08(){
// 准备模板
String template = "report_template.xlsx";
// 获取工作簿对象 手动关闭流操作 doFill 会自动关闭流操作
ExcelWriter workBook = EasyExcel.write("Excel组合练习.xlsx", FillData.class).withTemplate(template).build();
// 获取工作表对象
WriteSheet sheet = EasyExcel.writerSheet().build();
/**
* 多组填充 和 单组填充 一起使用的问题 :
* 组合填充时,因为多组填充的数据量不确定,需要在多组填充完之后另起一行
*/
// 换行
// FillConfig fillConfig = FillConfig.builder().forceNewRow(true).build();
// 准备数据
// 日期
HashMap<String, String> dateMap = new HashMap<String, String>();
dateMap.put("date", "2020-03-16");
// 总会员数
HashMap<String, String> totalCountMap = new HashMap<String, String>();
dateMap.put("totalCount", "1000");
// 新增员数
HashMap<String, String> increaseCountMap = new HashMap<String, String>();
dateMap.put("increaseCount", "100");
// 本周新增会员数
HashMap<String, String> increaseCountWeekMap = new HashMap<String, String>();
dateMap.put("increaseCountWeek", "50");
// 本月新增会员数
HashMap<String, String> increaseCountMonthMap = new HashMap<String, String>();
dateMap.put("increaseCountMonth", "100");
// 新增会员数据
List<StudentRead> students = initData();
// **** 准备数据结束****
// 写入数据
// 单组填充
workBook.fill(dateMap, sheet);
workBook.fill(totalCountMap, sheet);
workBook.fill(increaseCountMap, sheet);
workBook.fill(increaseCountWeekMap, sheet);
workBook.fill(increaseCountMonthMap, sheet);
// 多组数据填充
workBook.fill(students,sheet); // 加入换行
// 关闭流操作
workBook.finish();
}
使用Mybatis-Plus 操作数据库
/**
* 分类
*/
@Data
@TableName("category")
public class Category1 implements Serializable {
private static final long serialVersionUID = 1L;
@ExcelProperty(value = "id")
private Long id;
// 1 菜品分类 2 套餐分类
@ExcelProperty(value = "类型")
private Integer type;
//分类名称
@ExcelProperty(value = "分类名称")
private String name;
//顺序
@ExcelProperty(value = "顺序")
private Integer sort;
//创建时间
@ExcelProperty(value = "创建时间")
@DateTimeFormat("yyyy-MM-dd")//指定日期格式
@ColumnWidth(50)
private Date createTime;
//更新时间
@ExcelProperty(value = "更新时间")
@DateTimeFormat("yyyy-MM-dd")//指定日期格式
@ColumnWidth(50)
private Date updateTime;
//创建人
@ExcelProperty(value = "创建人")
@ColumnWidth(30)
private Long createUser;
//修改人
@ExcelProperty(value = "修改人")
@ColumnWidth(30)
private Long updateUser;
}
@RestController
@RequestMapping("/category")
public class CategoryController {
@Autowired
private CategoryService1 categoryService;
/**
* 将 Category 分类 表中的数据写入到excel中
*/
@RequestMapping("categoryExcel")
public void categoryExcel() {
// 工作簿
ExcelWriterBuilder categoryExcel = EasyExcel.write("G:/EasyExcel/EasyExecl-demo/瑞吉外卖中的分类表数据导入.xlsx", Category1.class);
// 工作表
ExcelWriterSheetBuilder categorySheet = categoryExcel.sheet();
// 准备数据
List<Category1> list = categoryService.list();
System.out.println("list = " + list);
categorySheet.doWrite(list);
System.out.println("导入数据成功");
}
}
两两一组
使用位置:标准作用在成员变量上,把实体类中属性和excel表中列关联起来
可选属性:
属性名 | 含义 | 说明 |
---|---|---|
index | 对应Excel表中的列数 | 默认-1,建议指定时从0开始 |
value | 对应Excel表中的列头 | |
converter(一般情况不使用) | 成员变量转换器 | 自定义转换器需要实Converter接口 |
使用效果:index属性可以指定当前字段对应excel中的哪一列,可以根据列名value去匹配,也可以不写。
如果不使用@ExcelProperty注解,成员变量从上到下的顺序,对应表格中从左到右的顺序;
使用建议要么全部不写,要么全部用index,要么全部用名字去匹配,尽量不要三个混着用。
标注在成员变量上,默认所有字段都会和excel去匹配,加了这个注解会忽略该字段
标注在成员变量上,日期转换,代码中用String类型的成员变量
去接收excel中日期格式的数据
会调用这个注解。里面的value
参照java.text.SimpleDateFormat
// 5. 按照指定的格式写入Excel内容
标注在成员变量上,数字转换,代码中用String类型的成员变量
去接收excel数字格式的数据
会调用这个注解。里面的value
参照java.text.DecimalFormat
标注在类上。
不标注该注解时,默认类中所有成员变量都会参与读写,无论是否在成员变量上加了@ExcelProperty
的注解。
标注该注解后,类中的成员变量如果没有标注@ExcelProperty
注解将不会参与读写。
ReadWorkbook
,ReadSheet
都会有的参数,如果为空,默认使用上级。
converter
转换器,默认加载了很多转换器。也可以自定义。
readListener
监听器,在读取数据的过程中会不断的调用监听器。
headRowNumber
指定需要读表格的 列头行数。默认有一行头,也就是认为第二行开始起为数据。
head
与clazz
二选一。读取文件头对应的列表,会根据列表匹配数据。建议使用clas,就是文件中每一行数据对应的代码中的实体类型。
clazz
与head
二选一。读取文件的头对应的class,也可以使用注解。如果两个都不指定,则会读取全部数据。
autoTrim
字符串、表头等数据自动trim
password
读的时候是否需要使用密码
excelType
当前excel的类型,读取时会自动判断,无需设置。inputStream
与file
二选一。建议使用file。file
与inputStream
二选一。读取文件的文件。autoCloseStream
自动关闭流。readCache
默认小于5M用 内存,超过5M会使用 EhCache
,不建议使用这个参数。useDefaultListener
@since 2.1.4
默认会加入ModelBuildEventListener
来帮忙转换成传入class
的对象,设置成false
后将不会协助转换对象,自定义的监听器会接收到Map
对象,如果还想继续接听到class
对象,请调用readListener
方法,加入自定义的beforeListener
、 ModelBuildEventListener
、 自定义的afterListener
即可。(一般不使用)sheetNo
需要读取Sheet的编号,建议使用这个来指定读取哪个SheetsheetName
根据名字去匹配Sheet,excel 2003不支持根据名字去匹配使用位置:标准作用在成员变量上
可选属性:
属性名 | 含义 | 说明 |
---|---|---|
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
的注解的都会参与读写,加了不会参与
WriteWorkbook
、WriteSheet
都会有的参数,如果为空,默认使用上级。
converter
转换器,默认加载了很多转换器。也可以自定义。
writeHandler
写的处理器。可以实现WorkbookWriteHandler
,SheetWriteHandler
,RowWriteHandler
,CellWriteHandler
,在写入excel的不同阶段会调用,对使用者透明不可见。
relativeHeadRowIndex
距离多少行后开始。也就是开头空几行
needHead
是否导出头
head
与clazz
二选一。写入文件的头列表,建议使用class。
clazz
与head
二选一。写入文件的头对应的class,也可以使用注解。
autoTrim
字符串、表头等数据自动trim
excelType
当前excel的类型,默认为xlsx
outputStream
与file
二选一。写入文件的流
file
与outputStream
二选一。写入的文件
templateInputStream
模板的文件流
templateFile
模板文件
autoCloseStream
自动关闭流。
password
写的时候是否需要使用密码
useDefaultStyle
写的时候是否是使用默认头
sheetNo
需要写入的编号。默认0
sheetName
需要些的Sheet名称,默认同sheetNo