sprngboot导入基于Easyexcel经典导入案例
一、框架环境介绍
1、springboot 2.2.6.RELEASE
2、easyexcel 3.1.1
二、demo编写流程
1、引入依赖包
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.1</version>
</dependency>
2、相关编码
@RestController
@RequestMapping("consumerHandler")
public class ExcelController {
@Autowired
private ScoreImportService scoreImportService;
@PostMapping("/import-scores")
public ImportResDTO importScores(@RequestParam("file") MultipartFile file) throws IOException {
EasyExcelListener listener = scoreImportService.importScores(file);
return listener.getRes();
}
}
此处接口层入参设计为:文本对象,每条栏目的对应解析实体,导入新的数据插入集合,导入更新时需要更新的数据集合。
package com.jiuzhou.desgin.excel;
import com.alibaba.excel.context.AnalysisContext;
import java.util.List;
/**
* 接口
* github地址 http://www.github.com/wanyushu
* gitee地址 http://www.gitee.com/wanyushu
*
* @author yushu
* @email 921784721@qq.com
* @date 2025/1/7 11:13
*/
public interface ImportService<T,R> {
/**
* 处理数据接口
* @param context
* @param t
* @param r
* @param importList
* @param updateList
*/
String importCheck(AnalysisContext context, T t, R r, List<T> importList, List<T> updateList);
/**
* 返回元数据相关信息
*/
String importCheck(AnalysisContext context, T t, R r, List<T> importList, List<T> updateList, List<T> sList);
/**
* 保存导入的信息
* @param importList 导入的数据对象
*/
void saveData( List<T> importList);
/**
* 需要更新的data
*/
void updateData( List<T> importList);
/**
* 数据解析完成时
*/
String uploadMeta(List<T> sList);
}
此类封装了通常导入所需要的基础方法,并将通用的导入需要用到的数据判空处理,数据是否符合预期要求进行了转换,并做了通常导入过程中最后一条全空的判断,如需求有要求导入的元数据如果异常,需要连同一起添加批注信息的,此处也进行了集成。
package com.jiuzhou.desgin.excel.Impl;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.fastjson.JSON;
import com.jiuzhou.desgin.excel.CommonImportService;
import com.jiuzhou.desgin.excel.dto.ImportResDTO;
import com.jiuzhou.desgin.excel.fun.CommentCellWriteHandler;
import com.jiuzhou.desgin.excel.vo.StudentScore;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;
/**
* github地址 http://www.github.com/wanyushu
* gitee地址 http://www.gitee.com/wanyushu
* @author yushu
* @email 921784721@qq.com
* @date 2025/1/7 11:26
*/
@Slf4j
@Service
public class ScoreImportServiceImpl extends CommonImportService<StudentScore> {
@Override
public String importCheck(AnalysisContext context, StudentScore studentScore,
ImportResDTO importResDTO, List<StudentScore> importList,
List<StudentScore> updateList, List<StudentScore> sList) {
String s = this.importCheck(context, studentScore, importResDTO, importList, updateList);
studentScore.setComment(s);
sList.add(studentScore);
return s;
}
@Override
public String importCheck(AnalysisContext context, StudentScore studentScore, ImportResDTO importResDTO,
List<StudentScore> importList, List<StudentScore> updateList) {
int row = context.getCurrentRowNum()+1;
//检查是否都是空
if(this.checkLastLine(studentScore)){
return null;
}
String s = this.checkData(studentScore);
importResDTO.setTotal(importResDTO.getTotal()+1);
if(StringUtils.isEmpty(s)){
//todo 判断数据是否存在
boolean exit= false;
if(exit){
updateList.add(studentScore);
}else{
importList.add(studentScore);
}
importResDTO.setSucc(importResDTO.getSucc()+1);
}else{
importResDTO.getErrorMap().put("第"+row+"行",s);
int fail = importResDTO.getFail();
importResDTO.setFail(fail+1);
}
return s;
}
@Override
public void saveData(List<StudentScore> importList) {
log.info("开始新增数据:{}", JSON.toJSONString(importList));
//todo 实际入库操作
}
@Override
public void updateData(List<StudentScore> importList) {
log.info("开始更新数据:{}", JSON.toJSONString(importList));
//todo 实际入库操作
}
@Override
public String uploadMeta(List<StudentScore> sList) {
log.info("开始上传元数据相关信息到文件服务器以便后续下载异常信息");
try {
// 重新写入文件并添加批注样式
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
EasyExcel.write(outputStream,StudentScore.class)
.registerWriteHandler(getCellStyleStrategy())
.registerWriteHandler(new CommentCellWriteHandler())
.sheet("data")
.doWrite(sList);
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
// todo 将文件存入文件服务器
String replace = UUID.randomUUID().toString().replace("-", "");
return replace;
} catch (Exception e) {
log.error("异常文件上传至服务器发生异常");
e.printStackTrace();
}
return null;
}
}
此处将保存和更新数据的方法进行了抽象并集成通用检查方法。
package com.jiuzhou.desgin.excel;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.jiuzhou.desgin.excel.dto.ImportResDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import java.util.List;
/**
* github地址 http://www.github.com/wanyushu
* gitee地址 http://www.gitee.com/wanyushu
* @author yushu
* @email 921784721@qq.com
* @date 2025/1/7 11:30
*/
@Slf4j
public abstract class CommonImportService<T > extends BaseCommon implements ImportService<T, ImportResDTO> {
public abstract void saveData(List<T> importList);
public abstract void updateData(List<T> importList);
public String uploadMeta(List<T> sList){
return null;
}
protected HorizontalCellStyleStrategy getCellStyleStrategy() {
// 表头样式
WriteCellStyle headCellStyle = new WriteCellStyle();
WriteFont headFont = new WriteFont();
headFont.setFontHeightInPoints((short) 12);
headCellStyle.setWriteFont(headFont);
// 内容样式
WriteCellStyle contentCellStyle = new WriteCellStyle();
contentCellStyle.setWrapped(true);
return new HorizontalCellStyleStrategy(headCellStyle, contentCellStyle);
}
}
该类通过在导入的实体对象上面加注解,判断对应的栏目的取值是否为空,如果不是空则判断,该值有没有需要查询的字典值,如果符合预期则,替换为数据字典的value
package com.jiuzhou.desgin.excel;
import com.jiuzhou.desgin.excel.annotation.CheckData;
import org.apache.commons.lang.StringUtils;
import java.lang.reflect.Field;
/**
* github地址 http://www.github.com/wanyushu
* gitee地址 http://www.gitee.com/wanyushu
*
* @author yushu
* @email 921784721@qq.com
* @date 2025/1/7 15:09
*/
public class BaseCommon<T> {
protected static final String NO_EMPTY= "不能为空,";
public String checkData(T t){
String info = null;
StringBuilder sb = new StringBuilder();
for (Field field : t.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(CheckData.class)) {
CheckData apiModelProperty = field.getAnnotation(CheckData.class);
if (apiModelProperty.required()) { // 判断是否必填
field.setAccessible(true); // 允许访问私有属性
try {
Object value = field.get(t);
if (value == null) {
// 使用 value 属性描述字段名称,添加错误信息到 importResDTO
sb.append(apiModelProperty.value());
sb.append(NO_EMPTY);
info=sb.toString();
//值是否符合字典要求
}else if(StringUtils.isNotBlank(apiModelProperty.dictName())){
sb = this.searchDictList(sb,apiModelProperty.dictName(),value,apiModelProperty.value(),field,t);
info = sb.toString();
}
} catch (IllegalAccessException e) {
e.printStackTrace(); // 处理异常
}
}else if(StringUtils.isNotBlank(apiModelProperty.dictName())){//如果非必填,但是填了
field.setAccessible(true);
try {
Object value = field.get(t);
if(value!=null){
sb = this.searchDictList(sb,apiModelProperty.dictName(),value,apiModelProperty.value(),field,t);
info = sb.toString();
}
}catch (Exception e){
e.printStackTrace(); // 处理异常
}
}
}
}
return info;
}
//检查某字典类型的名称是否符合预期要求
private StringBuilder searchDictList(StringBuilder sb, String dictType, Object value, String apiValue, Field field, Object targetObject) {
// List<SysDictDTO> sysDictDTOS = AeyeCacheManager.getList(CachePrexConstants.common_dict_list,
// AeyeReflectionUtil.getFieldName(SysDictDTO::getDicType) + AbstractCacheBOImpl.MERGE_KEY_SPLIT_MARK + dictType,
// SysDictDTO.class);
// // 判断 value 是否包含在范围内
// boolean isValid = false;
// String dicValue = null; // 用于存储字典值
// for (SysDictDTO sysDictDTO : sysDictDTOS) {
// if (sysDictDTO.getDicName().equals(value)) {
// // 字典名称如果符合预期,将该字段 field 值设置为 sysDictDTO.getDicValue;
// isValid = true;
// dicValue = sysDictDTO.getDicValue(); // 获取对应的字典值
// break;
// }
// }
// if (isValid) {
// // 设置字段的值为字典值
// try {
// field.setAccessible(true); // 允许访问私有属性
// field.set(targetObject, dicValue); // 设置字段值为字典值
// } catch (IllegalAccessException e) {
// e.printStackTrace(); // 处理异常
// }
// } else {
// // 添加错误信息到 importResDTO
// sb.append(apiValue);
// sb.append("如");
// // 列举符合要求的值
// Set<String> collect = sysDictDTOS.stream().map(SysDictDTO::getDicName).collect(Collectors.toSet());
// sb.append(collect);
// }
return sb;
}
public boolean checkLastLine(T t){
for (Field field : t.getClass().getDeclaredFields()) {
// 检查是否有 @ApiModelProperty 注解
if (field.isAnnotationPresent(CheckData.class)) {
CheckData apiModelProperty = field.getAnnotation(CheckData.class);
if (apiModelProperty.required()) { // 判断是否必填
field.setAccessible(true); // 允许访问私有属性
try {
Object value = field.get(t);
if (value != null) {
// 使用 value 属性描述字段名称,添加错误信息到 importResDTO
return false;
}
} catch (IllegalAccessException e) {
e.printStackTrace(); // 处理异常
}
}
}
}
return true;
}
}
实体对象
package com.jiuzhou.desgin.excel.vo;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import com.jiuzhou.desgin.excel.annotation.CheckData;
import lombok.Data;
@Data
@ContentRowHeight(15)
@HeadRowHeight(20)
@ColumnWidth(20)
public class StudentScore {
@CheckData(value = "学生姓名",dictName = "",required = true)
@ExcelProperty("学生姓名")
private String studentName;
@CheckData(value = "学科",dictName = "",required = true)
@ExcelProperty("学科")
private String subject;
@CheckData(value = "成绩",dictName = "",required = true)
@ExcelProperty("成绩")
private Integer score;
// 用于存放批注信息
@ExcelProperty("批注")
private String comment;
}
导入结果信息对象
package com.jiuzhou.desgin.excel.dto;
import lombok.Data;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
*/
@Data
public class ImportResDTO implements Serializable {
private static final long serialVersionUID = -4577699748290750945L;
private int total = 0;
private int fail = 0;
private int succ= 0;
private String serviceId;
private Map<Object,Object> errorMap = new HashMap<>();
}
通过构造方法传入对应的bean,然后利用bean调用对应的函数
package com.jiuzhou.desgin.excel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.jiuzhou.desgin.excel.dto.ImportResDTO;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
/**
*/
@Slf4j
public class EasyExcelListener<T> extends AnalysisEventListener<T> {
/**
* 读取的头行
*/
private int sheetNo;
/**
* 保存的数据
*/
private List<T> saveData;
/**
* 更新的数据
*/
private List<T> updateData;
/**
* 记录导入的异常信息
*/
private ImportResDTO importResDTO;
/**
* 导入的接口实例
*/
private ImportService importService;
/**
* 元数据信息
*/
private List<T> sList;
/**
* 插入一次量
*/
private int BATCH_COUNT = 1000;
public EasyExcelListener(Integer sheetNo, ImportService importService,List<T> sList) {
this.sheetNo = sheetNo;
this.saveData = new ArrayList<>();
this.updateData = new ArrayList<>();
this.importResDTO = new ImportResDTO();
this.importService = importService;
this.sList = sList;
}
@Override
public void invoke(T t, AnalysisContext context) {
if(context.readSheetHolder().getSheetNo()!=sheetNo)return;
if(null==this.sList){
importService.importCheck(context,this.importResDTO,t,this.saveData,updateData);
}else{
importService.importCheck(context,t,importResDTO,this.saveData,this.updateData,this.sList);
}
if (saveData.size() >= BATCH_COUNT) {
importService.saveData(saveData);
saveData.clear();
}
if (updateData.size() >= BATCH_COUNT) {
importService.updateData(updateData);
updateData.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
if(context.readSheetHolder().getSheetNo()!=sheetNo)return;
//数据入库逻辑
if(saveData.size()>0){
importService.saveData(saveData);
}
if(updateData.size()>0){
importService.updateData(updateData);
}
//处理数据解析完成时
if(importResDTO.getFail()>0){
String s = importService.uploadMeta(sList);
this.getRes().setServiceId(s);
}
}
public ImportResDTO getRes() {
return importResDTO;
}
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
super.onException(exception, context);
log.error(exception.getMessage());
}
}
二、单元测试
1、正常案例
导入的demo数据信息,通过postman测试导入结果
{
"total": 1,
"fail": 0,
"succ": 1,
"serviceId": null,
"errorMap": {}
}
2、异常案例
postman测试结果
{
"total": 1,
"fail": 1,
"succ": 0,
"serviceId": "b96612e0cbec437fabbe895884adeac9",
"errorMap": {
"第2行": "成绩不能为空,"
}
}
三、总结
通常我们在导入的过程中需要对导入的数据进行数据检查并判断对应的明文值和数据库存的值是否一致,这个案例通过查询对应的数据库字典,获取该字段的字典值来判断用户的导入数据是否符合预期,如果我们在不同的案例中根据业务不同,进行导入,我们只需要实现importService接口,并通过spring的指定注入策略来进行导入导出操作,效率会大大提高