当前位置:首页 > 实用技巧 >

excel文件导入老是失败(excel导入显示格式错误)

来源:原点资讯(www.yd166.com)时间:2023-05-05 11:56:02作者:YD166手机阅读>>

引言

在日常开发过程中,Excel 导入是非常常见的场景,而且也有很多开源的项目是针对Excel的读写的,如Apache 的poi ,最近用的比较好的还是阿里的EasyExcel 开源工具。平时我们只是简单的读取文件并写入数据库持久化即可,但是前段时间,产品搞了个需求,需要将导入失败的数据及原因写入Excel并下载,那这就有得玩了,废话不多说,上才艺。

产品需求
  • 导入Excel数据
  • 数据格式校验
  • 数据合法性校验(校验数据库)
  • 失败数据提供用户下载,并支持再次导入
技术选型
  • https://github.com/alibaba/easyexcel ,Excel 读取/写入
  • https://www.xuxueli.com/xxl-job/ ,做异步处理
需求实现项目依赖(maven)

<!-- easyexcle --> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcle</artifactId> <version>2.2.6</version> </dependency> <!-- xxl job --> <dependency> <groupId>com.xuxueli</groupId> <artifactId>xxl-job-core</artifactId> <version>${xxl-job.version}</version> </dependency>文件解析

解析导入文件,获取文件数据量,用于判定导入是否走异步导入。

public class EasyexcelUtils { /** * * 解析文件,获取最后一行 * @param InputStream 文件流 * @param SheetNum 读取excel表格的sheetNum 索引 * @return 总行数 */ public static Integer lastNum(InputStream inputStream,Integer sheetNum){ Workbook wb = null; sheetNum = sheetNum == null ? 0 : sheetNum; try { wb = WorkbookFactory.create(inputStream); Sheet sheet = wb.getSheetAt(sheetNum); CellReference cellReference = new CellReference("A4"); // 处理空行 for (int i = cellReference.getRow();i <= sheet.getLastRowNum();){ // 省略部分代码 } return sheet.getLastRowNum(); } catch (Exception e){ } return 0; } }

判定导入数据文件是否为空,如果为空,将返回错误信息

@RestController // 省略其他注解 public class ProjectInfoController { /** * 项目信息导入 */ @PostMapping("/import") public R projectInfoImport(MultipartFile file,HttpServletResponse response){ InputStream inputStream = null; int lastNum = 0; try { lastNum = EasyExcelUtils.lastNum(file.getInputStream()); }catch(IOException e){ // 省略部分代码 } if (lastNum <= 0 ){ throw CustomExcetpoin(500,"导入文件数据为空,请重新上传"); } } }

文件解析拿到导入数据的数据量,与系统配置的文件导入上限值进行判定,如果大于上限值将走异步处理(异步导入,请查看异步“异步导入”导入内容)。

@RestController // 省略其他注解 public class ProjectInfoController { @Resource private AsyncExcelService asyncExcelService; /** * 项目信息导入 */ @PostMapping("/import") public R projectInfoImport(MultipartFile file,HttpServletResponse response){ InputStream inputStream = null; int lastNum = 0; try { lastNum = EasyExcelUtils.lastNum(file.getInputStream()); }catch(IOException e){ // 省略部分代码 } if (lastNum <= 0 ){ throw CustomExcetpoin(500,"导入文件数据为空,请重新上传"); } // 获取系统配置的导入上限值 Integer importMax = asyncExcelService.asyncProjectImportMax(); if (lastNum > importMax ){ // 达到上限,走异步 asyncExcelService.asyncProjectImport(file,response); return R.success("数据导入成功,因数据量比较大,已转为异步导入"); } // 省略其他代码 } }

AsyncExcelService 接口实现

/** * 异步导出/导入 service */ public interface AsyncExcelService { /** 默认导入数据上限 **/ Integer DEFAULT_IMPORT_DATA_MAX = 500; /** * 获取最大导入上限值,超过则走异步 */ Integer getImportMax(); /** * 异步导入数据 */ void asyncProjectImport(MultipartFile file,HttpServletResponse response); } @Service // 省略其他注解 public class AsyncExcelServiceImpl implements AsyncExcelService { @Resource private IParamtersClient paramtersClient; @Override public Integer getImportMax(){ Integer value = getParamVaule("paramName",Integer.class); return value == null ? DEFAULT_IMPORT_DATA_MAX : value; } /** * 调用框架接口获取系统参数 * */ private <T> T getParamVaule(String name,Class<T> clazz){ CCBHousingUser user = SecureUtil.getUser(); // 省略部分代码 // 获取系统配置参数 Parameters parameters = paramtersClient.getParamterByCodeAndOrg(name,user.getOrganizationId()); // 省略部分代码 } }

其中,IParamtersClient 属于框架提供的feign 接口,也可以根据自己的实际场景实现相关逻辑。

数据合法校验

导入数据文件解析使用的是alibaba 提供的 EasyExcel 开源工具,我们需要在 EasyExcel 工具的基础上做一些增强处理,如:导入格式校验、导入表头校验、导入数据格式校验等,如果发生校验失败,将错误信息写入错误报告(excel)输出到客户端。

定义easyexcel 导入文件到列与实体映射关系,将使用到 easyexcel 到@ExcleProperty 注解进行关系绑定

@Data // 省略其他注解 public class ProjectInfoExcelDTO { @ExcelProperty(index=0,value="序列号") private String number; @ExcelProperty(index=1,value="项目名称") private String name; // 省略其他字段属性 }

注解 @ExcleProperty 常用属性

  • index,与excel文件中,表头列的索引位置对应(从0开始)
  • value,与excel文件中,表头列的名称相对应
  • converter,指定解析数据时,该列需要使用的数据转换器,转换器实现Converter接口

定义校验错误的数据结构类型

@Data // 省略其他注解 public class ExcelChcekErrDTO<T> { private T t; private String errMsg; }

备注:@Data 属于 lombok 工具,简化Bean的封装,感兴趣的同学,可以自行查阅资料。

定义Excel导入校验返回的数据VO

@Data // 省略其他注解 public class ExcelCheckResultVO<T> { /** 校验成功的数据 **/ private List<T> successDatas; /** 校验失败的数据 **/ private List<ExcelChcekErrDTO> errData; }

定义数据解析监听器EasyExcelListener

@Data // 省略部分注解 public class EasyExcelListener<T> extends AnalysisEventListener<T> { // 省略部分代码 }

定义excel 业务校验管理器 ExcelCheckManager,需要做业务校验的(与数据库匹配等)需要实现该接口

public interface ExcelCheckManager<T> { ExcelCheckResultVO checkImportExcle(List<T> datas); }

表头校验

使用EasyExcelListener 用来监听数据解析过程,其中,invokHeadMap 方法将在解析完成excel表头时将被执行

@Data // 省略部分注解 public class EasyExcelListener<T> extends AnalysisEventListener<T> { /** excel 对象的反射类 **/ private Class<T> clazz; private ExcelCheckManager<T> excelCheckManager; public EasyExcelListener(ExcelCheckManager<T> excelCheckManager,Class<T> clazz){ this.clazz = clazz; this.excelCheckManager = excelCheckManager; } @Override public void invokHeadMap(Map<Integer,String> headMap,AnalysisContext context){ super.invokHeadMap(headMap,context); // 反射获取实体到属性值 Map<Integer,String> indexNameMap = getIndexNameMap(clazz); // 将 headMap 与 indexNameMap 进行对比,是否完全匹配 Set<Integer> keySet = indexNameMap.keySet(); for (Integer key : keySet ){ if (StringUtils.isEmpty(headMap.get(key)){ throw ExcelAnalysisExcetpion("数据解析错误,请传入正确的excel格式"); } if (!headMap.get(key).equals(indexNameMap.get(key)){ throw ExcelAnalysisExcetpion("数据解析错误,请传入正确的excel格式"); } } } /** * 反射获取解析数据实体的@ExcleProperty 的value */ public Map<Integer,String> getIndexNameMap(Class clazz){ Map<Integer,String> result = new HashMap<>(); Field field; Field[] fields = clazz.getDeclaredFields(); for (int i = 0; i < fields.length; i ){ field = clazz.getDeclaredField(fields[i].getName()); field.setAccessible(true); ExcelProperty excleProperty = field.getannotation(ExcelProperty.class); if (excelProperty != null){ int index = excleProperty.index(); String[] values = excleProperty.value(); StringBuilder value = new StringBuilder(); for (String v : values ){ value.append(v); } result.put(index,value.toString()); } } return result; } }

数据非空、格式校验

数据非空校验、格式校验,我们将使用hibernate-validator 校验器进行校验格式。

定义validator 工具类

@component public class EasyExcelValidatorHelper { private static Validtor validtor; @Autowired public EasyExcelValidatorHelper(Validtor validtor){ this.EasyExcelValidatroHelper.validtor = validtor; } public static <T> String validateEntity(T obj) throws NoSuchFieldException{ StringBuilder result = new StringBuilder(); // 执行校验 Set<ConstraionViolation<T>> set = validtor.validate(obj,Default.class); // 组装结果 if(set != null && !set.isEmpty()){ for (ConstraionViolation<T> cv : set ){ Field declaredField = obj.getClass.getDeclaredField(cv.getPropertiyPath().toString()); ExcelProperty annotation = declaredField.getAnnotation(ExcelProperty.class); result.append(annotation.value[0] ":" cv.getMessage()).append(";"); } } return result; } }

数据格式校验,使用EasyExcelListener 用来监听数据解析过程,其中,invok 方法将逐行解析excel数据的时候将被调用

@Data // 省略部分注解 public class EasyExcelListener<T> extends AnalysisEventListener<T> { /** 标记是否执行数据解析 **/ private boolean baseMatching = false; /** 解析成功的数据 **/ private List<T> successList = new ArrayList<>(); /** 解析失败的数据 **/ private List<ExcelCheckErrDTO<T>> errList = new ArrayList<>(); /** excel 对象的反射类 **/ private Class<T> clazz; private List<T> list; private ExcelCheckManager<T> excelCheckManager; public EasyExcelListener(ExcelCheckManager<T> excelCheckManager,Class<T> clazz){ this.clazz = clazz; this.excelCheckManager = excelCheckManager; } @Override public void invok(T t,AnalysisContext context){ // 数据解析/转换完成,标记进入到解析起 baseMatching = true; String errMsg; try { // 调用验证器验证数据格式 errMsg = EasyExcelValidatorHelper.validateEntity(t); }catch(Exception e){ errMsg = "解析数据出错"; // 省略部分代码 } // 校验不通过 if (!StringUtils.isEmpty(errMsg){ // 将错误数据放入错误列表中 ExcelChcekErrDTO errDTO = new ExcelChcekErrDTO(t,errMsg); errList.add(errDTO); } else{ // 校验成功 list.add(t); } if (list.size() > 1000){ // 业务校验 ExcelCheckResultVO excelCheckResultVO = excelCheckManager.checkImportExcel(list); successList.addAll(excelCheckResultVO.getSuccessDatas()); errList.addAll(excelCheckResultVO.getErrDatas()); list.clear(); } } /** * 所有数据解析完成后调用此方法 */ @Override public void doAfterAllAnalysed(AnalysisContext context){ ExcelCheckResultVO excelCheckResultVO = excelCheckManager.checkImportExcel(list); successList.addAll(excelCheckResultVO.getSuccessDatas()); errList.addAll(excelCheckResultVO.getErrDatas()); list.clear(); } @Override public void invokHeadMap(Map<Integer,String> headMap,AnalysisContext context){ super.invokHeadMap(headMap,context); // 反射获取实体到属性值 Map<Integer,String> indexNameMap = getIndexNameMap(clazz); // 将 headMap 与 indexNameMap 进行对比,是否完全匹配 Set<Integer> keySet = indexNameMap.keySet(); for (Integer key : keySet ){ if (StringUtils.isEmpty(headMap.get(key)){ throw ExcelAnalysisExcetpion("数据解析错误,请传入正确的excel格式"); } if(!headMap.get(key).equals(indexNameMap.get(key)){ throw ExcelAnalysisExcetpion("数据解析错误,请传入正确的excel格式"); } } } /** * 反射获取解析数据实体的@ExcleProperty 的value */ public Map<Integer,String> getIndexNameMap(Class clazz){ Map<Integer,String> result = new HashMap<>(); Field field; Field[] fields = clazz.getDeclaredFields(); for (int i = 0; i < fields.length; i ){ field = clazz.getDeclaredField(fields[i].getName()); field.setAccessible(true); ExcelProperty excleProperty = field.getAnnotation(ExcelProperty.class); if (excelProperty != null){ int index = excleProperty.index(); String[] values = excleProperty.value(); StringBuilder value = new StringBuilder(); for (String v : values ){ value.append(v); } result.put(index,value.toString()); } } return result; } }

对需要进行校验对字段添加注解

@Data // 省略其他注解 public class ProjectInfoExcelDTO { @ExcelProperty(index=0,value="序列号") private String number; @ExcelProperty(index=1,value="项目名称") @NotBlank(message = "请填写项目名称") private String name; // 省略其他字段属性 }

validator 常用注解传送门(validator 常用注解)。

EasyExcel 读取数据,并调用格式校验

@RestController // 省略其他注解 public class ProjectInfoController { @Resource private AsyncExcelService asyncExcelService; @Resource private ProjectInfoService projectInfoService; /** * 项目信息导入 */ @PostMapping("/import") public R projectInfoImport(MultipartFile file,HttpServletResponse response){ InputStream inputStream = null; int lastNum = 0; try { lastNum = EasyExcelUtils.lastNum(file.getInputStream()); }catch(IOException e){ // 省略部分代码 } if (lastNum <= 0 ){ throw CustomExcetpoin(500,"导入文件数据为空,请重新上传"); } // 获取系统配置的导入上限值 Integer importMax = asyncExcelService.asyncProjectImportMax(); if (lastNum > importMax ){ // 达到上限,走异步 asyncExcelService.asyncProjectImport(file,response); return R.success("数据导入成功,因数据量比较大,已转为异步导入"); } // 省略部分代码 // 实例数据解析监听器 EasyExcelListener<ProjectInfoDTO> easyExcleListener = new EasyExcelListener(projectInfoService,ProjectInfoDTO.class); // 文件读取/解析,并注册监听器 EasyExcle.read(file.getInputStream(),ProjectInfoDTO.class,easyExcleListener).sheet(1).doRead(); // 获取错误数据 List<ExcelCheckErrDTO<ProjectInfoExcelDTO>> errList = easyExcleListener.getErrList(); // 获取解析成功到数据 List<ProjectinfoExcelDTO> successList = easyExcleListener.getSuccessList(); // 如果错误数据不为空,将错误数据写入到excel文件,并输出到浏览器 // 省略代码 // 将成功到数据,批量写入到数据库中 // 省略代码 // 省略其他代码 } }

ProjectInfoService 声明与实现,因为需要做业务数据到校验,因此ProjectInfoService 需要继承 ExcelCheckManager 验证管理器

public interface ProjectInfoService extends ExcelCheckManager{ } @Service // 省略其他注解 public class ProjectInfoServiceImpl implements ProjectInfoService { // 省略部分代码 @Override public ExcelCheckResultVO checkImportExcel(List<ProjectInfoExcelDTO> datas){ // 省略代码 } }

输出错误报告

文件校验完成之后,如果没有完全通过,需要将错误对数据以及错误信息通过easyExcel 输出到客户端。

@RestController // 省略其他注解 public class ProjectInfoController { @Resource private AsyncExcelService asyncExcelService; @Resource private ProjectInfoService projectInfoService; /** * 项目信息导入 */ @PostMapping("/import") public R projectInfoImport(MultipartFile file,HttpServletResponse response){ InputStream inputStream = null; int lastNum = 0; try { lastNum = EasyExcelUtils.lastNum(file.getInputStream()); }catch(IOException e){ // 省略部分代码 } if (lastNum <= 0 ){ throw CustomExcetpoin(500,"导入文件数据为空,请重新上传"); } // 获取系统配置的导入上限值 Integer importMax = asyncExcelService.asyncProjectImportMax(); if (lastNum > importMax ){ // 达到上限,走异步 asyncExcelService.asyncProjectImport(file,response); return R.success("数据导入成功,因数据量比较大,已转为异步导入"); } // 省略部分代码 // 实例数据解析监听器 EasyExcelListener<ProjectInfoDTO> easyExcleListener = new EasyExcelListener(projectInfoService,ProjectInfoDTO.class); // 文件读取/解析,并注册监听器 EasyExcle.read(file.getInputStream(),ProjectInfoDTO.class,easyExcleListener).sheet(1).doRead(); // 获取错误数据 List<ExcelCheckErrDTO<ProjectInfoExcelDTO>> errList = easyExcleListener.getErrList(); // 获取解析成功到数据 List<ProjectinfoExcelDTO> successList = easyExcleListener.getSuccessList(); // 如果错误数据不为空,将错误数据写入到excel文件,并输出到浏览器 if (errList.size() > 0 ){ // 省略部分代码 } // 将成功到数据,批量写入到数据库中 // 省略代码 // 省略其他代码 } }异步导入

异步导入操作,将思考几个问题:

  • 导入文件存到什么地方?当一个同步请求结束之后,后续我们想再次拿到该请求到数据,我们应该考虑将文件放到某一个单独到地方,提供我们二次使用,比如:自己到文件服务器、oss 存储等,这里我们使用自己的文件服务器。
  • 怎么异步执行?我们可以使用新启用一个本地线程去执行我们的操作,不影响当前请求主线程的操作,也是可以的,但是考虑到执行重试问题,我们将使用(#xxl-job)分布式调度系统,进行调度执行任务。
  • 客户如何查看任务执行状态?我们需要提供一个任务执行日志列表,让用户可以清晰的看到本次导出的任务是否执行完成/是否存在导入错误。
  • 怎么将错误报告输出给到客户?我们需要将导入到错误报告文件(excel)上传至文件服务器,提供用户二次或多次下载使用;同时,需要将文件信息保存至任务执行日志信息中,为用户提供下载入口。

定义通用的job handler 父类 AsyncTaskHandler ,所有需要使用xxl-job 发起异步任务和给xxl-job 发起回调,都需要继承AsyncTaskHandler ,并实现execute 抽象方法。

public abstract class AsyncTaskHandler <T extends AsyncTaskPramsDTO> { /** xxl-job server 端提供的创建任务接口 uri **/ private final static String JOB_ADMIN_URI = "/outapi/asyn/"; /** 与xxl-job server 通讯的加密密钥对 **/ @Setter protected String publicKey; /** * xxl-job server 回调对方法 */ public abstract ReturnT<String> execute(String params); /** * 向xxl-job 发起调度任务 */ public JobResponseDTO sendTask(T prams){ prams.setUser(null); // 省略部分代码,相关内容,请查询xxl-job server 端所提供的接口文档 // 将 params 中的 user 对象保存至redis 中,xxl-job 接口有长度限制 } public abstract RedisUtil getRedisUtil(); public abstract JobProperties getJobProperties(); /** 回调方法名称 **/ public abstract String getHandlerName(); } 定义 AsyncTaskPramsDTO 异步参数实体 @Data // 省略其他注解 public class AsyncTaskPramsDTO { private String requestId; }数据导出

数据导出功能常指,客户想将系统中的相关(按照查询条件筛选)数据通过excel形式保存到自己本地。在数据导出过程中,需要通过数据筛选条件将数据从系统数据库中筛选出来,然后通过一定格式(excel导出模版格式)写入到excel中,最后输出到客户端(浏览器)提供客户下载保存到本地。

栏目热文

excel导出不完整(excel表导出怎么会不全了)

excel导出不完整(excel表导出怎么会不全了)

在我们日常导出订单数据的数据时,经常会碰到显示不全的问题,这就造成了我们见到的未必是“真实的”,这就需要我们掌握一些方法...

2023-05-05 12:21:41查看全文 >>

导出数据excel打开失败(为什么无法导出excel)

导出数据excel打开失败(为什么无法导出excel)

responseType: "arraybuffer" 是关键export function fileP...

2023-05-05 12:27:30查看全文 >>

怎么导出excel文件是空白的(怎么导出excel里的内容)

怎么导出excel文件是空白的(怎么导出excel里的内容)

大家好,我是皮皮。一、前言前几天在Python铂金交流群【Jethro Shen】问了一个Python自动化处理的问题,...

2023-05-05 12:03:23查看全文 >>

为什么导出的excel文件没内容(导出excel为什么只有表格没有数据)

为什么导出的excel文件没内容(导出excel为什么只有表格没有数据)

最近在使用EasyPoi模板导出Excel功能时,遇到个问题:第一列数据显示不出来。反复检查模板文件和程序数据,对照官方...

2023-05-05 11:51:42查看全文 >>

excel导出文件打开是空白的怎么办(excel文件打开是空白怎么解决)

excel导出文件打开是空白的怎么办(excel文件打开是空白怎么解决)

故障现象:鼠标双击打开Excel表格后是空白状态不显示内容。(版本为office 2016)解决方案:修改注册表有风险,...

2023-05-05 12:15:40查看全文 >>

excel文件导出找不到(导出excel成功但是找不到文件)

excel文件导出找不到(导出excel成功但是找不到文件)

相信大家都遇到过当你正在做Excel表的时候,电脑突然断电或者死机,辛辛苦苦编辑的Excel表还没来得及保存怎么办?难道...

2023-05-05 12:00:34查看全文 >>

导出文件excel格式显示不出(excel导出后显示文件格式无效)

导出文件excel格式显示不出(excel导出后显示文件格式无效)

财工贸软件使用过程中,有部分客户经常会将报表数据导出在EXCEL中统计做账的情况;但是导出之后会遇到数据显示不对或求和的...

2023-05-05 12:15:40查看全文 >>

猕猴桃种植培训方案(猕猴桃种苗的培养方法)

猕猴桃种植培训方案(猕猴桃种苗的培养方法)

猕猴桃,也被称为奇异果,因其营养价值高,又有一定的抗癌作用,配合上酸酸甜甜的独特口感,深受人们喜爱。近年来,种植面积也在...

2023-05-05 11:54:20查看全文 >>

猕猴桃栽培理论与生产技术(猕猴桃种植方法和技术)

猕猴桃栽培理论与生产技术(猕猴桃种植方法和技术)

作者介绍:段眉会,高级农艺师。创建了中国猕猴桃技术集成网,主编出版了《猕猴桃产业实用技术450问》等三本专著。西安市农业...

2023-05-05 11:52:25查看全文 >>

猕猴桃种植十大技术(猕猴桃种植技巧和方法)

猕猴桃种植十大技术(猕猴桃种植技巧和方法)

夏季猕猴桃,进入了果树二次梢生长和果实膨大期,同时也进入了高温期,如果果园管理不当,易造成日灼、肥害、药害等高温危害。是...

2023-05-05 11:45:23查看全文 >>

文档排行