1. 准备
导入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
本次示例使用的表格导入模板
2. 后端
2.1 config
2.1.1 自定义异常
抛出该异常后停止解析
public class ExcelAnalysisStopException extends ExcelAnalysisException {
public ExcelAnalysisStopException(String message) {
super(message);
}
}
数据转换异常错误
@Getter
@Setter
public class ExcelDataConvertException extends RuntimeException {
private Integer rowIndex;
private Integer columnIndex;
private CellData cellData;
private ExcelContentProperty excelContentProperty;
public ExcelDataConvertException(Integer rowIndex, Integer columnIndex, CellData cellData) {
super("第" + rowIndex + "行第" + columnIndex + "列,数据:" + cellData + "发生数据类型转换错误");
}
}
2.1.2 监听器
在监听接口中处理异常
public abstract class AnalysisEventListener<T> implements ReadListener<T> {
public AnalysisEventListener() {
}
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
this.invokeHeadMap(ConverterUtils.convertToStringMap(headMap, context), context);
}
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
}
public void extra(CellExtra extra, AnalysisContext context) {
}
public void onException(Exception exception, AnalysisContext context) throws Exception {
throw exception;
} //默认抛出异常
public boolean hasNext(AnalysisContext context) {
return true;
}
}
核心监听器类
@Data
public class ExcelListener extends AnalysisEventListener {
// 在下面进行补充
// 表格数据的解析和导入都是在这里进行
// 要注意的是,该监听器不支持交给 SpringBoot 管理,所以要将需要的类 new 出来,然后传进该监听器中
}
2.2 表格数据对应的实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Departments implements Serializable {
// @ExcelIgnore 表示在进行表格数据导入解析时,忽略该属性
@ExcelIgnore
private Integer id;
@ExcelProperty("院系名称")
private String departmentName;
private static final long serialVersionUID = 1L;
}
2.2 controller
@RestController
@RequestMapping("/excel")
@AllArgsConstructor
public class ExcelImportController {
private ExcelImportService excelImportService;
@PostMapping("/importData")
public String importData(@RequestPart("file") MultipartFile file) {
if (file == null || flag == null) {
throw new Exception("文件为空!");
}
return excelImportService.importData(file);
}
}
2.3 service
/**
* 表格导入 service 接口
*/
public interface ExcelImportService {
String importData(MultipartFile file);
}
表格导入的前提,是要有一个规定的表头模板
在解析时,我们要根据规定的表头去解析。如果提供的表格表头不正确直接抛出异常给前端
@Service
@AllArgsConstructor
public class ExcelImportServiceImpl implements ExcelImportService {
private final DepartmentsMapper departmentMapper;
@Override
public String importData(MultipartFile file) {
String[] headList = {"院系名称"}; // 表头
try {
EasyExcel.read(file.getInputStream(), Departments.class, new ExcelListener(headList, departmentMapper)).sheet().doRead();
} catch (Exception e) {
return "导入失败";
}
return "导入成功";
}
}
2.4 监听器逻辑补充
@Data
public class ExcelListener extends AnalysisEventListener {
private Integer num; // Excel行数
private String message; // 校验规则信息
private String[] headList; // 表头模板数据
private static final int BATCH_COUNT = 30; // 每隔30条存数据库,然后清理list ,方便内存回收
private List datas = new ArrayList<>(); // 数据集合
private DepartmentsMapper departmentMapper;
public ExcelListener(String[] headList, DepartmentsMapper departmentMapper) {
this.num = 0;
this.message = "";
this.headList = headList;
this.departmentMapper = departmentMapper;
}
/**
* 读取表头
* @param headMap 表头Map
* @param context
*/
@Override
public void invokeHeadMap(Map headMap, AnalysisContext context) {
if (context.readRowHolder().getRowIndex() == 0) {
for(int i = 0; i < headList.length; i++) {
try {
if (!headMap.get(i).equals(headList[i])) {
datas.clear();
message = "上传模板与系统模板不匹配";
throw new ExcelAnalysisStopException(message);
}
} catch (Exception e) {
datas.clear();
message = "上传模板与系统模板不匹配";
throw new ExcelAnalysisStopException(message);
}
}
}
}
/**
* 每一条数据解析都会来调用
* @param object 当前行的数据对象
* @param analysisContext
*/
@Override
public void invoke(Object object, AnalysisContext analysisContext) {
if (isNull(object)) {
datas.add(object); // 数据存储到list,供批量处理,或后续自己业务逻辑处理
num++;
}
if (datas.size() >= BATCH_COUNT) {
saveData();
datas.clear(); // 清理存储在内存中的数据
}
}
/**
* 本人自己定义的方法
* 判断导入表格中的数据是否有为空值
* @param object 要进行进行检查的对象
* @return 不为空则为true,反之
*/
private boolean isNull(Object object) {
boolean isNull = true;
// 自己业务的逻辑
return isNull;
}
/**
* 所有数据解析完成都会来调用
* @param analysisContext
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
if (num == 0) {
datas.clear();
message = "上传的表格数据为空或上传的表格数据无效";
throw new ExcelAnalysisStopException(message);
}
saveData(); // 自定义的保存数据方法
datas.clear();
}
/**
* 在转换异常 获取其他异常下会调用本接口,抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行
* @param exception 出现的异常
* @param context
* @throws Exception
*/
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
datas.clear();
ExcelDataConvertException excelDataConvertException = null;
if (exception instanceof ExcelDataConvertException) {
excelDataConvertException = (ExcelDataConvertException) exception;
}
if (null != excelDataConvertException) {
int row = excelDataConvertException.getRowIndex() + 1;
int col = excelDataConvertException.getColumnIndex() + 1;
message = "第" + row + "行,第" + col + "列," + "解析异常";
}
throw new ExcelAnalysisStopException(message);
}
/**
* 当出现模板数据异常时,结束往下解析,抛出异常
* @param context
* @return 布尔值
*/
@Override
public boolean hasNext(AnalysisContext context) {
return true;
}
/**
* 插入数据到数据库
* @return 受影响的行数
*/
private int saveData() {
removeDuplicate(datas); // 去重
return departmentMapper.insertBatch(datas);
}
/**
* 移除List中重复的元素
* @param list 原List集合
* @return 去重后的List集合
*/
private static List removeDuplicate(List list) {
Set h = new HashSet(list);
list.clear();
list.addAll(h);
return list;
}
}
2.5 Mapper
此处使用 Mybatis 来与数据库进行交互
2.5.1 interface
public interface DepartmentsMapper {
int insertBatch(@Param("list") List<Departments> departmentsList);
}
2.5.2 xml
<insert id="insertBatch" useGeneratedKeys="true" keyProperty="id">
insert into departments (department_name)
values
<foreach collection="list" item="item" index="index" separator=",">
(#{item.departmentName, jdbcType=VARCHAR})
</foreach>
</insert>
3. 前端
前端的样例,大家可以参考我下面这一篇文章的第二章节,后续只要更改访问后台的链接即可
SpringBoot + Vue + MinIO 实现文件上传:https://blog.csdn.net/wanzijy/article/details/127601558
4. 不足之处
上面虽然是可以实现表格数据的导入,但如果有人去细心观察过后台的日志就会发现,一些表格中的空白行也会进行解析
虽然本人有对每一行数据进行判空的操作,但是如果可以知道他是空白行,那么就可以省去进行判空的时间,减少资源的损耗
那时本人一直都没有琢磨出来,有懂的博主们,可以私信与我沟通交流下