前段时间刚刚在一个项目中集成EasyExcel实现了数据导出到Excel表中,“产品经理”突然又加了要求:“那个谁,这个项目好像还缺一个批量数据导入的功能,你去写一下”。我:“……”。生活所迫,只能“自愿”的去完成了这个功能。
EasyExcel官网 – > https://www.yuque.com/easyexcel/doc/easyexcel
这里用一个简单的学生Student类进行演示
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
@ExcelProperty(value = "学号")
private String studentId;
@ExcelProperty(value = "姓名")
private String studentName;
@ExcelProperty(value = "年级")
private String studentGrade;
@ExcelProperty(value = "专业")
private String studentMajor;
@ExcelProperty(value = "班级")
private String studentClass;
}
根据白嫖来的官网教学,我们还需要自定义一个Excel读的监听器。这里设定了每隔100条存储数据库,然后清理list,这是为了防止数据几万条数据在内存,从而造成OOM(OutOfMemory,内存溢出)。
这里使用到了泛型提高了这个监听器的复用性,也可以将下面的
T
换成自己指定的一个实体类
@Slf4j
@Setter
@EqualsAndHashCode(callSuper = true)
public class DataListener<T> extends AnalysisEventListener<T> {
/** 每隔100条存储数据库,然后清理list ,方便内存回收*/
private static final int BATCH_COUNT = 100;
/** 需要传递一个service进来对业务逻辑进行操作,即存放数据进数据库中 */
private IService<T> service;
/** 每次创建Listener的时候把service传进来 */
public DataListener(IService<T> service){
this.service = service;
}
/** 缓存的数据 */
List<T> list = new ArrayList<T>();
/**
* @description 读取数据所执行的逻辑
* @author xBaozi
* @date 17:14 2022/5/3
**/
@Override
public void invoke(T instance, AnalysisContext analysisContext) {
log.info("读取到了数据{}", instance);
list.add(instance);
// 达到BATCH_COUNT存储一次数据库,防止数据几万条数据在内存造成OOM
if (list.size() >= BATCH_COUNT){
this.saveData();
list.clear();
}
}
/**
* @description 所有数据读取完毕之后将数据存放到数据库中
* @author xBaozi
* @date 17:15 2022/5/3
**/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
// 确保最后遗留的数据也存储到数据库
this.saveData();
log.info("所有数据导入成功");
}
/**
* @description 将数据存放到数据库中
* @author xBaozi
* @date 17:09 2022/5/3
**/
private void saveData() {
for (T data : list) {
log.info("{}开始存放数据库……", data);
try {
this.service.save(data);
} catch (Exception e) {
log.info(e.getMessage());
}
}
}
}
在上面的两个准备工作弄好之后,就可以编写控制器对其进行使用实现我们的需求了
/**
* @description 实现数据批量导入
* @author xBaozi
* @date 19:27 2022/5/3
* @param file 需要导入的文件
**/
@PostMapping("/upload")
public Result<String> upload(@RequestParam("files") MultipartFile file) throws IOException {
// 获得上传文件的名称
String filename = file.getOriginalFilename();
// 获取源文件的后缀名
int index = filename != null ? filename.lastIndexOf(".") : -1;
// 对没有后缀名的文件进行判断
if (index < 0) {
return new Result.error("请选择正确的文件");
}
// 截取.点后面的内容,即后缀名
String suffix = filename.substring(index + 1);
log.info("获取后缀名为:{}", suffix);
//判断是否为excel文档
if (!"xlsx".equals(suffix) && !"xls".equals(suffix)) {
return new Result.error("只支持Excel文档导入");
}
try {
EasyExcel.read(file.getInputStream(), Student.class, new DataListener<Student>(studentService)).sheet().doRead();
} catch (Exception e) {
return new Result.error("导入失败,请重试");
}
return new Result.success("批量导入成功");
}
根据实体类创建导入数据。
这里可能有些伙伴有疑问,这里为什么会有一个序号呢?实体类里面没有需要这个属性啊,这是因为在编写实体类的时候是按照value进行匹配,而不是根据index进行匹配的,一次在导入时只会匹配实体类中存在的value。
狗子我在写这一个功能时遇到了一个bug,就是在读取Excel文件时,读取到的数据都是null,在排查了好一段时间的问题之后可算是给我找到了问题所在,原来是EasyExcel本身的问题,它与我使用的Lombok产生了冲突。
EasyExcel和Lombok会有一个冲突:当你尝试在用于接收Excel解析数据的Bean上面加上
@Accessors(chain = true)
注解时,你会发现该Bean接收不到数据,体现在String类型的字段总是为null,int类型的字段总是为0。
因此目前的解决办法就是,把@Accessors(chain = true)
这个注解先给删掉,鱼与熊掌不可兼得,只能放弃其一了。