Java POI导出之数据验证
maven 依赖
这里用的是apache.poi, 没有使用EasyExcel
<!-- poi依赖-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.0.1</version>
</dependency>
<!-- poi对于excel 2007的支持依赖-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.0.1</version>
</dependency>
<!-- poi对于excel 2007的支持依赖-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>4.0.1</version>
</dependency>
业务需求场景:
- 系统导入商户数据:一般Excel格式导入
- 系统导入会员数据:数据正确才导入,否则将数据错误信息写入上传的Excel中,并在前端网页进行倒出下载到用户电脑上,以便根据提示修改数据
用户上传的Excel数据如下:属于异常数据
需求期望格式如下:
上代码
- 上传接口类:ImportMemberController.java
@RestController @RequestMapping public class ImportMemberController { @Autowired private IImportService importService; @PostMapping(value = "/import") public void importDynamic(@RequestPart("file") MultipartFile file, HttpServletResponse response) throws IOException { importService.validFormData(file, response); } }
- 业务服务类:IImportService.java
public interface IImportService throws IOException{ // 这里不做返回, // 业务需要可以在HttpServletResponse中自行添加返回,这里方便做记录 void validFormData(MultipartFile file, HttpServletResponse response); }
- 业务服务实现类:ImportServiceImpl.java
@Service public class IImportService implements IImportService throws IOException{ /** * 校验数据,如果数据未按照约定格式,则给出相应错误提示,并导出下载到用户电脑上 * **/ public void validFormData(MultipartFile file, HttpServletResponse response) { Workbook wb = new XSSFWorkbook(file.getInputStream()); Sheet sheet = wb.getSheetAt(0); /** * 不推荐使用以下方法,因为因为带有格式等问题,会出现空白行也算内容, * 导致行数超出,得到真实行数不准 * int rowNums = wb.getSheetAt(0).getLastRowNum(); * int rowNums = wb.getSheetAt(0).getPhysicalNumberOfRows(); **/ // 获取Excel 真是内容占用行数 int rowNums = ExcelUtil.getVaildRows(wb); int maxColumnNums = sheet.getRow(0).getLastCellNum() + 1; // 所有存在合并单元格的行 List<CellRangeAddress> mrList = sheet.getMergedRegions(); // 标记Excel数据是否存在错误 boolean isError = false; // 这里2 是因为以上图中的数据是从第三行开始读取的,往下遍历 for (int i = 2; i < rowNums; i++) { Row row = sheet.getRow(i); // 标记当前行是否存在错误 boolean currentError = false; // 用来存储错误信息 List<String> errorMessage = new LinkedList<>(); // 遍历当前行每一个单元格 for (int j = 0; j < maxColumnNums; j++) { Cell cell = row.getCell(j); String tableName = ExcelUtil.getCellValue(cell); if (StringUtils.isBlank(tableName)) { if (j == 0) { // 避免重复写入 if (!isError) { isError = true; sheet.getRow(2).createCell(maxColumnNums).setCellValue("错误信息"); } boolean isValided = false; for (CellRangeAddress cra : mrList) { int firstRow = cra.getFirstRow(); int lastRow = cra.getLastRow(); if (i >= firstRow && i <= lastRow && lastRow - firstRow > 0) { Row mergedRow = sheet.getRow(cra.getFirstRow()); String tableNameEn = ExcelUtil.getCellValue(mergedRow.getCell(0)); if (StringUtils.isNotBlank(tableNameEn)) { isValided = true; break; } } } if (!isValided) { currentError = true; errorMessage.add("门店名称不能为空"); } } else if (j == 2){ currentError = true; errorMessage.add("门店编号不能为空"); } else if (j == 3){ currentError = true; errorMessage.add("会员姓名不能为空"); } else if (j == 4){ currentError = true; errorMessage.add("会员手机号不能为空"); } else if (j == 5){ currentError = true; errorMessage.add("是否新客不能为空"); } } } // 当前行存在错误,创建写入错误的列,并写入错误信息 if (currentError) { // 设置写入错误信息所在列宽度自适应 sheet.autoSizeColumn(maxColumnNums); sheet.setColumnWidth(maxColumnNums,sheet.getColumnWidth(maxColumnNums)*17/10); // 写入错误信息 AtomicInteger index = new AtomicInteger(1); row.createCell(maxColumnNums) .setCellValue(errorMessage.stream() .map(x-> index.getAndIncrement() + "." + x).collect(Collectors.joining(", "))); } } // 如果文件中存在任何一处数据校验错误,则将Excel文件返回前端进行下载 if (isError) { wb.write(response.getOutputStream()); } } }
- 工具类:ExcelUtil
public class ExcelUtil { /** * * @ReturnType String * @Description 获取单元格内容 * @param cell 单元格 * @return */ public static String getCellValue(Cell cell) { if (cell == null) { return ""; } CellType type = cell.getCellType(); if (type == CellType.STRING) { return cell.getStringCellValue(); } else if (type == CellType.BOOLEAN) { return String.valueOf(cell.getBooleanCellValue()); } else if (type == CellType.FORMULA) { return cell.getCellFormula(); } else if (type == CellType.NUMERIC) { double valuedouble = cell.getNumericCellValue(); int valueint = (int)valuedouble; if(valueint == valuedouble) { return String.valueOf(valueint); }else { return String.valueOf(valuedouble); } } return ""; } } /** * * @ReturnType int * @Description 获取Excel 真实内容行数 * @param Workbook 文档模板 * @return */ public static int getVaildRows(Workbook wb) { Sheet sheet = wb.getSheetAt(0); CellReference cellReference = new CellReference("A4"); boolean flag = false; for (int i = cellReference.getRow(); i <= sheet.getLastRowNum(); ) { Row r = sheet.getRow(i); if (r == null) { // 如果是空行(即没有任何数据、格式),直接把它以下的数据往上移动 sheet.shiftRows(i + 1, sheet.getLastRowNum(), -1); continue; } flag = false; for (Cell c : r) { if (c.getCellType() != CellType.BLANK) { flag = true; break; } } if (flag) { i++; continue; } else { //如果是空白行(即可能没有数据,但是有一定格式) if (i == sheet.getLastRowNum()){ //如果到了最后一行,直接将那一行remove掉 sheet.removeRow(r); } else { //如果还没到最后一行,则数据往上移一行 sheet.shiftRows(i + 1, sheet.getLastRowNum(), -1); } } } return sheet.getLastRowNum() + 1; }
注意:上传接口类:ImportMemberController.java
- 在上传接口 importDynamic 里我没有直接 return 做返回,这是因为如果存在错误时会写文件到前端,
那么在此再做返回的话, 接口处返回的数据会混在文件内容里,导致文件格式错误, Office 打开时报文件格式错误或者损坏,无法打开, WPS应该可以,那玩意儿强大的一批
所以在此也提醒前端的朋友们,如果下载后台返回的Excel模板后,Office 打开报格式错误,那么可以从浏览器Network中查看请求得到的文件数据,中间数据看不懂没关系,直接拉到最后,看看数据最后面是不是追加了有不属于Excel模板的数据, 去掉后就可以正常打开了
如果能让用户都是用WPS的请无视