EasyExcel

EasyExcel 概述

Java 解析、生成 excel 比较有名的框架有 Apache poi、jxl,但他们都存在一个严重的问题就是非常耗内存,easyExcel 也是用来解析 excel 用的,但是他却解决了这个问题

EasyExcel 的原理

分类管理-后台接口编写_Project

写数据

创建模型,在创建模型之前在 service_video 模型中添加单元测试和热加载相关的依赖,如下

分类管理-后台接口编写_Project_02

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>

然后在引入 easyExcel 相关的依赖

<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>

模型内容如下

分类管理-后台接口编写_监听器_03

@Data
public class StudentData {
@ExcelProperty(value = "学号", index = 0)
private Integer no;
@ExcelProperty(value = "姓名", index = 1)
private String name;
}

写数据,也就是导出 excel 文件

/**
* @author BNTang
* @version S2.3.2Dev
* @program video_parent
* @date Created in 2021/4/3 22:08
* @description
**/
@SpringBootTest
public class ServiceVideoApplicationTests {

@Test
public void writeExcel() {
String fileName = "D:\\01.xlsx";

List<StudentData> studentDataList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
StudentData studentData = new StudentData();

studentData.setNo(i);
studentData.setName("BNTang" + i);

studentDataList.add(studentData);
}

/*
* 1.fileName:文件路径与名称
* 2.StudentData.class:模型的字节码
* 3.sheet:名称
* 4.doWrite:要写的数据,是一个list
*/
EasyExcel.write(fileName, StudentData.class).sheet("学生").doWrite(studentDataList);
}
}

运行测试类,最终结果如下图所示

分类管理-后台接口编写_ide_04

分类管理-后台接口编写_ide_05

读数据

创建监听器

分类管理-后台接口编写_监听器_06

public class ExcelListener extends AnalysisEventListener<StudentData> {
/**
* <p>
* 一行一行读取数据
* </p>
*
* @param studentData 读取到的一行数据模型对象
* @param analysisContext analysisContext
*/
@Override
public void invoke(StudentData studentData, AnalysisContext analysisContext) {
System.out.println("data = " + studentData);
}

@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
System.out.println("表头: " + headMap);
}

/**
* <p>
* 所有的数据读取完毕之后会自动调用
* </p>
*
* @param analysisContext analysisContext
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
System.out.println("读取完毕");
}
}

读数据

分类管理-后台接口编写_监听器_07

@Test
public void ReadExcel() {
String fileName = "D:\\01.xlsx";

// 每读取一行数据的时候, 就会调用监听器当中的方法
EasyExcel.read(fileName, StudentData.class, new ExcelListener()).sheet().doRead();
}

应用到工程当中

建立模型

分类管理-后台接口编写_监听器_08

/**
* @author BNTang
* @version S2.3.2Dev
* @program video_parent
* @date Created in 2021/4/4 2:22
* @description 分类excel上传的模型类
**/
@Data
public class CategoryData {
@ExcelProperty(index = 0)
private String oneCategoryData;

@ExcelProperty(index = 1)
private String twoCategoryData;
}

创建监听器

分类管理-后台接口编写_监听器_09

/**
* @author BNTang
* @version S2.3.2Dev
* @program video_parent
* @date Created in 2021/4/4 2:25
* @description 分类管理上传excel监听器
**/
@Component
public class CategoryExcelListener extends AnalysisEventListener<CategoryData> {
@Override
public void invoke(CategoryData categoryData, AnalysisContext analysisContext) {

}

@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {

}
}

编写控制器,用来接收文件,然后编写对应文件处理的业务

分类管理-后台接口编写_ide_10

/**
* @author BNTang
* @version S2.3.2Dev
* @program video_parent
* @date Created in 2021/4/4 2:27
* @description 分类管理控制器
**/
@RestController
@RequestMapping("/service_video/category")
public class CategoryController {

private final CategoryService categoryService;

public CategoryController(CategoryService categoryService) {
this.categoryService = categoryService;
}

@PostMapping("/addCategory")
public ResponseResult addCategory(MultipartFile file) {
// 调用业务上传excel
categoryService.saveCategory(file);
return ResponseResult.ok();
}
}

如上报错是因为还没有编写对应的接口和实现类所以找不到下面我们就来编写,在继续往下写之前我先来弥补一下我之前的一个错误就是把 mp 的代码生成器给删除了,这个时候要生成分类管理表相关的实体了,所以这个时候把之前的生成器代码拿回来放到测试包下,然后改一下生成的表名,把之前新建的分类管理的 ​​Controller​​ 删除用 mp 生成器生成的

分类管理-后台接口编写_监听器_11

改好了之前点击启动生成即可,这个类以后就保留着,然后把如上 Controller 写的内容复制到 mp 生成器生成的 Controller 中去即可

分类管理-后台接口编写_监听器_12

修改对应的业务类,修改 ​​CategoryService​​ 接口,也就是分类管理的接口

分类管理-后台接口编写_Project_13

/**
* <p>
* 上传excel
* </p>
*
* @param file 上传的文件
*/
void saveCategory(MultipartFile file);

然后就是编写业务类了,内容如下

分类管理-后台接口编写_Project_14

/**
* <p>
* 科目分类 服务实现类
* </p>
*
* @author BNTang
* @since 2021-04-04
*/
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {

/**
* excel监听器
*/
private final CategoryExcelListener categoryExcelListener;

public CategoryServiceImpl(CategoryExcelListener categoryExcelListener) {
this.categoryExcelListener = categoryExcelListener;
}

@Override
public void saveCategory(MultipartFile file) {
// 读取excel
try {
EasyExcel.read(file.getInputStream(), CategoryData.class, categoryExcelListener).sheet().doRead();
} catch (IOException e) {
e.printStackTrace();
}
}
}

剩余的内容就是完善一下我们 excel 上传解析的监听器了,主要就是在监听器当中保存数据,最终监听器代码如下

/**
* @author BNTang
* @version S2.3.2Dev
* @program video_parent
* @date Created in 2021/4/4 2:25
* @description 分类管理上传excel监听器
**/
@Component
@Slf4j
public class CategoryExcelListener extends AnalysisEventListener<CategoryData> {
private final CategoryService categoryService;

public CategoryExcelListener(CategoryService categoryService) {
this.categoryService = categoryService;
}

@Override
public void invoke(CategoryData categoryData, AnalysisContext analysisContext) {
// 写入到数据库当中
if (!Objects.isNull(categoryData)) {
// 如果一级分类不存在,就保存到数据库当中
Category oneCategory = this.isExistOneCategory(categoryData);

if (Objects.isNull(oneCategory)) {
oneCategory = new Category();
oneCategory.setTitle(categoryData.getOneCategoryData());
oneCategory.setParentId("0");

categoryService.save(oneCategory);
}

// 保存2级分类,先判断2级分类是否已经存在
Category twoCategory = this.isExistTwoCategory(categoryData, oneCategory.getId());

if (Objects.isNull(twoCategory)) {
twoCategory = new Category();
twoCategory.setTitle(categoryData.getTwoCategoryData());
twoCategory.setParentId(oneCategory.getId());

categoryService.save(twoCategory);
}
}
}

/**
* <p>
* 判断1级分类是否已经存在
* </p>
*/
private Category isExistOneCategory(CategoryData categoryData) {
QueryWrapper<Category> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("title", categoryData.getOneCategoryData());
queryWrapper.eq("parent_id", "0");
return categoryService.getOne(queryWrapper);
}

/**
* <p>
* 判断2级分类是否已经存在
* </p>
*/
private Category isExistTwoCategory(CategoryData categoryData, String pid) {
QueryWrapper<Category> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("title", categoryData.getTwoCategoryData());
queryWrapper.eq("parent_id", pid);
return categoryService.getOne(queryWrapper);
}

/**
* <p>
* 所有的数据读取完毕之后会自动调用
* </p>
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
log.info("上传分类 excel 解析完毕!");
}
}

如上主要的业务就是先看看数据库存不存在当前的分类有就不添加没有就添加,核心的方法为如下

分类管理-后台接口编写_监听器_15

然后启动工程发现出现了循环依赖注入的问题

分类管理-后台接口编写_Project_16

为啥会出现这个问题呢,可参考:​​https://mrbird.cc/深入理解Spring循环依赖.html​

修改所有注入的地方都改为 ​​@Resource​​ 的方式即可

分类管理-后台接口编写_Project_17

启动工程上传模板发现,报错了,模板的内容如下图所示

分类管理-后台接口编写_Project_18

很明显就是我们少添加了自动填充的实体类配置

分类管理-后台接口编写_ide_19

修改实体类如下

分类管理-后台接口编写_监听器_20

然后在修改我们之前的分类 Controller 因为少加了一些内容加的内容如下

分类管理-后台接口编写_监听器_21

再次重启测试效果如下图所示

分类管理-后台接口编写_监听器_22

分类管理-后台接口编写_监听器_23