更多使用参考:easyExcel官网
业务需求:1、导入用户账号(手机号)进行修改用户名称,2、用户如果存在,判断用户姓名是否与库中一致,不一致修改库中用户姓名,3、记录导入失败数据(前端显示),以备修改后再次导入。
代码实现:
一、controller层:
/*** 修改名称导入* @return*/@ApiOperation(value = "修改名称导入", notes = "修改名称导入")@PostMapping("/updateName")public R importCustomerExcel(@RequestParam("file") MultipartFile file) {String fileName = file.getOriginalFilename();String suffixName = "";if (StrUtil.isNotBlank(fileName)) {suffixName = fileName.substring(fileName.lastIndexOf("."));}if (!".xlsx".equals(suffixName) && !".xls".equals(suffixName)){return R.failed("文件格式不对,请导入excel文件!");}cpCustomerService.importCustomerExcel(file);// 导入失败的数据List<ImportCustomerExcelModel> failData = ImportCustomerExcelListener.failList();return R.ok(failData);}
二、service:
/*** 用户名称修改导入* @param file* @return*/void importCustomerExcel(MultipartFile file);
@Overridepublic void importCustomerExcel(MultipartFile file) {try (InputStream inputStream = file.getInputStream()){EasyExcel.read(inputStream, CpCustomer.class, new ImportCustomerExcelListener(cpCustomerService)).doReadAll();} catch (IOException e) {LOGGER.error("读取文件出现错误,请重新导入!{}", file);}}
EasyExcel.read()解释:
EasyExcel.read(inputStream, CpCustomer.class,new ImportCustomerExcelListener(cpCustomerService)).doReadAll();
1、inputStream:需要读取的文件流。
2、CpCustomer.class:excel导入的字段对应的实体。
3、ImportCustomerExcelListener(cpCustomerService):数据处理监听器,主要逻辑处理都在这里面;由于我需要service去查库判断,所以我这里传了一个cpCustomerService。
4、doReadAll():读取全部sheet。
三、ImportCustomerExcelListener监听器:
/*** 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去*/public class ImportCustomerExcelListener extends AnalysisEventListener<CpCustomer> {private static final Logger LOGGER = LoggerFactory.getLogger(ImportCustomerExcelListener.class);/*** 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收*/private static final int BATCH_COUNT = 5;List<CpCustomer> list = new ArrayList<>();/*** 错误数据*/private static List<ImportCustomerExcelModel> errList = new ArrayList<>();/***添加失败集合数据*/public static List<ImportCustomerExcelModel> failList(){List<ImportCustomerExcelModel> listerror = new ArrayList<>();listerror.addAll(errList);errList.clear();return listerror;}/*** 头信息*/Map<Integer, String> headMap = new HashMap<>();/*** 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。*/private CpCustomerService cpCustomerService;public ImportCustomerExcelListener() {// 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数}/*** 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来** @param cpCustomerService*/public ImportCustomerExcelListener(CpCustomerService cpCustomerService) {this.cpCustomerService = cpCustomerService;}/*** 这个每一条数据解析都会来调用** @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}* @param context*/@Overridepublic void invoke(CpCustomer data, AnalysisContext context) {updateCustomerName(data);}/*** 修改用户名称* @param data*/public void updateCustomerName(CpCustomer data) {if (!Validator.isMobile(data.getPhoneNum())){failDataOperation(data, "手机号格式错误");throw new ExcelAnalysisStopException(data.getCustomerName()+"的手机号格式错误,错误手机号:"+data.getPhoneNum());}// if (!LegalUtils.isNameLegal(data.getCustomerName())) {// failDataOperation(data, "姓名格式错误(中文2-4位)");// throw new ExcelAnalysisStopException(data.getCustomerName()+":姓名应该是中文2-4位,请重新输入!");// }CpCustomer cpCustomer = cpCustomerService.getOne(Wrappers.<CpCustomer>query().eq("phone_num", data.getPhoneNum()));Optional.ofNullable(cpCustomer).ifPresent(customer -> {LOGGER.info("解析到一条数据:{}", JSON.toJSONString(data));// 避免名字中间存在空格String newName = "";String oldName = "";if (StrUtil.isNotBlank(data.getCustomerName())) {newName = data.getCustomerName().replace(" ", "");}if (StrUtil.isNotBlank(cpCustomer.getCustomerName())) {oldName = cpCustomer.getCustomerName().replace(" ", "");}if (!newName.equals(oldName)) {data.setId(cpCustomer.getId());list.add(data);}LOGGER.info("需要修改的数据:{}", list);});// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOMif (list.size() >= BATCH_COUNT) {// 添加到数据库//saveData();// 修改数据库数据updateData();// 存储完成清理 listlist.clear();}}/*** 所有数据解析完成了 都会来调用** @param context*/@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {// 这里也要保存/修改数据,确保最后遗留的数据也存储到数据库//saveData();if (CollUtil.isNotEmpty(list)) {// 修改数据updateData();LOGGER.info("所有数据解析完成!");}else {LOGGER.info("没有需要修改的数据");}}/*** 加上存储数据库(判断数据库中是否存在该数据)*/private void saveData() {LOGGER.info("{}条数据,开始存储数据库!", list.size());cpCustomerService.saveBatch(list);LOGGER.info("存储数据库成功!");}@Overridepublic void onException(Exception exception, AnalysisContext context) {LOGGER.error("解析失败:{}", exception.getMessage());// 以下为导入内容格式转换检测:如:时间格式“yyyy-MM-dd HH:mm:ss”// 如果是某一个单元格的转换异常 能获取到具体行号;如果要获取头的信息 配合invokeHeadMap使用int row = 0;int col = 0;String title = "";if (exception instanceof ExcelDataConvertException) {ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception;col = excelDataConvertException.getColumnIndex();row = excelDataConvertException.getRowIndex();LOGGER.error("第{}行,第{}列解析异常,数据为:{}", row, col , excelDataConvertException.getCellData());title = this.headMap.get(col);LOGGER.info("出错标题列名称:{}", title);}// 此处抛出异常则--停止导入 不抛出异常则--继续往下导入// throw new ExcelAnalysisStopException(exception.getMessage()+(row==0?"":", 出错行:"+row)+("".equals(title)?"": "出错列:"+title));}@Overridepublic void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {this.headMap = headMap;}/*** 修改数据库*/public void updateData() {LOGGER.info("{}条数据,开始修改数据库信息!", list.size());try {cpCustomerService.updateBatchById(list);}catch (Exception e){LOGGER.error("以下数据修改过程中出现错误,请重新导入:{}", list);list.forEach(data -> failDataOperation(data, "修改过程中出现错误,请重新导入!"));throw new ExcelAnalysisStopException("以下数据修改过程中出现错误,请重新导入:"+list);}LOGGER.info("修改数据库成功!");}/*** 失败信息操作公共方法* @param data*/private void failDataOperation(CpCustomer data, String message) {ImportCustomerExcelModel model = new ImportCustomerExcelModel();BeanUtil.copyProperties(data, model);model.setMessage(message);errList.add(model);}}
以上的监听器中有我业务处理的逻辑,官网的介绍请看这里:https://www.yuque.com/easyexcel/doc/read
