首页
学习
活动
专区
圈层
工具
发布

公司大佬对 Excel 导入、导出的封装,那叫一个妙啊!

Excel 这东西,业务一提就两个字:简单。 真到你接手的时候,就不是“导个表”了,是日期乱了、金额精度丢了、表头改名了、导出把内存打满了,最后锅还得你背。

我以前也写过那种“拿到 MultipartFile,for 一把,塞数据库,完事”的导入代码。跑小数据没事,一上生产就露馅:1000 行里混 3 行脏数据,你到底是整批回滚,还是跳过继续?用户传上来的“手机号”那一列,有时是字符串,有时是数字,Excel 再给你补个.0,你查半天还以为是库里有鬼。

后来见过公司一个老哥封的 Excel 组件,我第一眼就知道这人不是只会写 demo 的。

他没把 Excel 导入当“文件解析”,而是当“数据入站”。顺序也很稳:先校验模板,再逐行解析,再收集错误,最后才入库。导出也一样,不是 controller 里直接write(response.getOutputStream()),而是把列定义、字典翻译、样式、分页查询全拆开。

先看导入这块,核心不是读,而是“别让脏数据直接碰业务代码”。

public interface RowValidator<T> {

  List<String> validate(T row, int rowNum);

}

public class UserImportRow {

  private String name;

  private String phone;

  private BigDecimal salary;

  private LocalDate entryDate;

}

public class UserImportValidator implements RowValidator<UserImportRow> {

  @Override

  public List<String> validate(UserImportRow row, int rowNum) {

      List<String> errors = new ArrayList<>();

      if (row.getName() == null || row.getName().isBlank()) {

          errors.add("第" + rowNum + "行:姓名不能为空");

      }

      if (row.getPhone() == null || !row.getPhone().matches("^1\\d{10}$")) {

          errors.add("第" + rowNum + "行:手机号格式不对");

      }

      if (row.getSalary() != null && row.getSalary().scale() > 2) {

          errors.add("第" + rowNum + "行:薪资最多两位小数");

      }

      return errors;

  }

}

注意这个味道就不一样了。 不是一边解析一边saveBatch,而是先把“能不能进系统”这件事说清楚。

再往下一层,他连表头都不信。

public class ExcelHeadChecker {

  public static void check(List<String> actualHeads, List<String> expectedHeads) {

      if (!actualHeads.equals(expectedHeads)) {

          throw new IllegalArgumentException("导入模板不对,请下载最新模板后重试");

      }

  }

}

这一步看着土,其实很值钱。很多线上事故不是数据错,是运营自己改了列名,或者把“入职日期”挪到前面了。你要是不先拦,后面整列错位,导进去才真难收。

真正让我觉得“妙”的,是他不追求一次性全成功,而是给用户一份能看懂的失败结果。 比如 5000 行数据,错了 23 行,不是直接扔个“导入失败”,而是把错误行号、错误原因重新导出成一个失败文件。业务拿着这个文件改,比你在群里解释半小时都省事。

public class ImportResult<T> {

  private int successCount;

  private int failCount;

  private List<T> successRows;

  private List<String> errorMessages;

}

导入服务大概是这么个写法:

public ImportResult<UserImportRow> importUsers(List<UserImportRow> rows) {

  List<UserImportRow> passed = new ArrayList<>();

  List<String> errors = new ArrayList<>();

  int rowNum = 2;

  for (UserImportRow row : rows) {

      List<String> rowErrors = validator.validate(row, rowNum++);

      if (!rowErrors.isEmpty()) {

          errors.addAll(rowErrors);

          continue;

      }

      passed.add(row);

  }

  if (!passed.isEmpty()) {

      userService.batchUpsert(passed);

  }

  ImportResult<UserImportRow> result = new ImportResult<>();

  result.setSuccessCount(passed.size());

  result.setFailCount(errors.size());

  result.setSuccessRows(passed);

  result.setErrorMessages(errors);

  return result;

}

这里还有个细节,batchUpsert比batchInsert更像线上代码。 因为导入这事,重复数据是常态,不是异常。员工编号重复、商品编码已存在、部门数据之前导过一次,这些都很正常。你非要让业务先去库里清空,再来导,这系统基本已经输了。

导出这边更能看出老程序员的脾气。

一般人写导出,喜欢在接口里查全量,再一把写 Excel。数据量一大,JVM 先给你上课。真正稳的做法是列定义和数据查询分开,分页拉,流式写。先把“这一列叫什么、怎么取值、要不要格式化”定义好。

public class ExportColumn<T> {

  private String title;

  private Function<T, Object> valueGetter;

  public ExportColumn(String title, Function<T, Object> valueGetter) {

      this.title = title;

      this.valueGetter = valueGetter;

  }

}

List<ExportColumn<UserDTO>> columns = List.of(

  new ExportColumn<>("姓名", UserDTO::getName),

  new ExportColumn<>("手机号", UserDTO::getPhone),

  new ExportColumn<>("状态", x -> x.getStatus() == 1 ? "启用" : "停用"),

  new ExportColumn<>("入职日期", x -> x.getEntryDate() == null ? "" : x.getEntryDate().toString())

);

这种写法的好处很直接:导出规则收口了。 你不用在 8 个地方各写一份“状态 1 转启用,0 转停用”,也不用每次新增一列就去改一坨 if else。

再说个很多人容易忽略的坑:导出不是功能问题,是资源问题。 10 万行往上,别信什么“应该没事”。我一般先看两件事:是不是分页查,响应流是不是及时刷。很多导出慢,不是 Excel 库慢,是你 SQL 一次捞太狠,顺手还连表补了几个字典,最后导出还没开始写,库先喘上了。

所以这种封装,表面看是 Excel,实际上是在替业务挡脏活: 模板别乱改,数据别乱进,错误要能回显,大数据量别把机器拖死。

说到底,Excel 导入导出最怕的不是代码写不出来,是写得太轻松。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OTkKwYq8jppbMa2bB7_1hsag0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。
领券