Springboot+MybatisPlus+EasyExcel实现文件导入数据

记录一下写Excel文件导入数据所经历的问题。

springboot提供的文件处理MultipartFile有关方法,我没有具体看文档,但目测比较复杂,
遂了解学习了一下别的文件上传方法,本文第1节记录的是springboot原始的导入文件方法写法,第二节大概介绍了一下EasyExcel的使用,第三节是EasyExcel的实际使用。

只看实现,请直接跳转第三节。

1 原springboot写法,不使用EasyExcel

看了一下springboot原版写法如下,大致意思是写个工具类ExcelReadUtil来处理文件内容读取和转化,ServiceImpl实现文件存储,代码量很可怕。

1.1 SalaryController

    public R<?> addSalaryInfo(@RequestParam("file") MultipartFile file, @RequestParam("importTime") String importTime, @RequestParam("type") Integer type, @RequestParam("fileId") Long fileId) {salaryService.addSalaryInfo(file, importTime, type, fileId);return R.ok("操作成功");}

1.2 ServiceImpl

    @Override@Transactional(rollbackFor = Throwable.class)public void addSalaryInfo(MultipartFile file, String importTime, Integer type, Long fileId) {SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM");if (file == null || "".equals(file.getOriginalFilename())) {throw new PMValidationException("上传附件为空");}if (StringUtils.isEmpty(type) || Objects.isNull(SalaryType.getByCode(type))) {throw new PMValidationException("薪酬类型错误");}Date importDate;try {importDate = sdf.parse(importTime);} catch (ParseException e) {throw new PMValidationException("薪酬所属月份格式错误");}long loginUserId = userRemoteService.getLoginUser().getData().getUserId();List<Map<String, Object>> maps = ExcelReadUtil.readExcelByRC(file, 2, -1, true);SalaryInfo salaryInfo = new SalaryInfo();salaryInfo.setImportTime(importDate);salaryInfo.setCreateBy(loginUserId);salaryInfo.setUpdateBy(loginUserId);salaryInfo.setType(type);salaryInfo.setFileId(fileId);salaryInfoMapper.insert(salaryInfo);if (SalaryType.PAYABLE_SALARY.getCode() == type) {insertSalary(maps, loginUserId, importDate, salaryInfo.getId());} else {insertOrUpdateOtherSalary(maps, loginUserId, type, importDate);}}public void insertSalary(List<Map<String, Object>> maps, Long loginUserId, Date importDate, Long salaryInfoId) {List<SalaryData> salaryInfoList = new LinkedList<>();maps.forEach(map -> {String deptName = String.valueOf(map.get(SalaryField.DEPT_NAME) == null ? "" : map.get(SalaryField.DEPT_NAME));String nickName = String.valueOf(map.get(SalaryField.NICK_NAME) == null ? "" : map.get(SalaryField.NICK_NAME));String userNo = String.valueOf(map.get(SalaryField.USER_NO) == null ? "" : map.get(SalaryField.USER_NO));BigDecimal salary = null;if (map.get(SalaryField.SALARY) != null) {salary = new BigDecimal(String.valueOf(map.get(SalaryField.SALARY)));}BigDecimal performance = null;if (map.get(SalaryField.PERFORMANCE) != null) {performance = new BigDecimal(String.valueOf(map.get(SalaryField.PERFORMANCE)));}BigDecimal seniorityPay = null;if (map.get(SalaryField.SENIORITY_PAY) != null) {seniorityPay = new BigDecimal(String.valueOf(map.get(SalaryField.SENIORITY_PAY)));}BigDecimal postSalary = null;if (map.get(SalaryField.POST_SALARY) != null) {postSalary = new BigDecimal(String.valueOf(map.get(SalaryField.POST_SALARY)));}BigDecimal tenementSubsidy = null;if (map.get(SalaryField.TENEMENT_SUBSIDY) != null) {tenementSubsidy = new BigDecimal(String.valueOf(map.get(SalaryField.TENEMENT_SUBSIDY)));}BigDecimal communicateSubsidy = null;if (map.get(SalaryField.COMMUNICATE_SUBSIDY) != null) {communicateSubsidy = new BigDecimal(String.valueOf(map.get(SalaryField.COMMUNICATE_SUBSIDY)));}BigDecimal trafficFee = null;if (map.get(SalaryField.TRAFFIC_FEE) != null) {trafficFee = new BigDecimal(map.get(SalaryField.TRAFFIC_FEE) == null ? "" : String.valueOf(map.get(SalaryField.TRAFFIC_FEE)));}BigDecimal mealFee = null;if (map.get(SalaryField.MEAL_FEE) != null) {mealFee = new BigDecimal(String.valueOf(map.get(SalaryField.MEAL_FEE)));}BigDecimal changeFee = null;if (map.get(SalaryField.CHANGE_FEE) != null) {changeFee = new BigDecimal(String.valueOf(map.get(SalaryField.CHANGE_FEE)));}BigDecimal salaryPayable = null;if (map.get(SalaryField.SALARY_PAYABLE) != null) {salaryPayable = new BigDecimal(map.get(SalaryField.SALARY_PAYABLE) == null ? "" : String.valueOf(map.get(SalaryField.SALARY_PAYABLE)));}BigDecimal endowmentInsurance = null;if (map.get(SalaryField.ENDOWMENT_INSURANCE) != null) {endowmentInsurance = new BigDecimal(String.valueOf(map.get(SalaryField.ENDOWMENT_INSURANCE)));}BigDecimal medicalInsurance = null;if (map.get(SalaryField.MEDICAL_INSURANCE) != null) {medicalInsurance = new BigDecimal(String.valueOf(map.get(SalaryField.MEDICAL_INSURANCE)));}BigDecimal unemploymentInsurance = null;if (map.get(SalaryField.UNEMPLOYMENT_INSURANCE) != null) {unemploymentInsurance = new BigDecimal(String.valueOf(map.get(SalaryField.UNEMPLOYMENT_INSURANCE)));}BigDecimal accumulationFundDeduct = null;if (map.get(SalaryField.ACCUMULATION_FUND) != null) {accumulationFundDeduct = new BigDecimal(String.valueOf(map.get(SalaryField.ACCUMULATION_FUND)));}BigDecimal checkoff = null;if (map.get(SalaryField.CHECKOFF) != null) {checkoff = new BigDecimal(String.valueOf(map.get(SalaryField.CHECKOFF)));}BigDecimal preTaxTotal = null;if (map.get(SalaryField.PRE_TAX_TOTAL) != null) {preTaxTotal = new BigDecimal(String.valueOf(map.get(SalaryField.PRE_TAX_TOTAL)));}BigDecimal personalIncomeTax = null;if (map.get(SalaryField.PERSONAL_INCOME_TAX) != null) {personalIncomeTax = new BigDecimal(String.valueOf(map.get(SalaryField.PERSONAL_INCOME_TAX)));}BigDecimal realSalary = null;if (map.get(SalaryField.REAL_SALARY) != null) {realSalary = new BigDecimal(String.valueOf(map.get(SalaryField.REAL_SALARY)));}SalaryData salaryData = new SalaryData();salaryData.setSalaryInfoId(salaryInfoId);salaryData.setImportTime(importDate);salaryData.setCreateBy(loginUserId);salaryData.setUpdateBy(loginUserId);salaryData.setDeptName(deptName);salaryData.setNickName(nickName);salaryData.setUserNo(userNo);salaryData.setSalary(salary);salaryData.setPerformance(performance);salaryData.setSeniorityPay(seniorityPay);salaryData.setPostSalary(postSalary);salaryData.setTenementSubsidy(tenementSubsidy);salaryData.setCommunicateSubsidy(communicateSubsidy);salaryData.setTrafficFee(trafficFee);salaryData.setMealFee(mealFee);salaryData.setChangeFee(changeFee);salaryData.setSalaryPayable(salaryPayable);salaryData.setEndowmentInsurance(endowmentInsurance);salaryData.setMedicalInsurance(medicalInsurance);salaryData.setUnemploymentInsurance(unemploymentInsurance);salaryData.setAccumulationFundDeduct(accumulationFundDeduct);salaryData.setCheckoff(checkoff);salaryData.setPreTaxTotal(preTaxTotal);salaryData.setPersonalIncomeTax(personalIncomeTax);salaryData.setRealSalary(realSalary);if (!StringUtils.isEmpty(userNo)) {salaryInfoList.add(salaryData);}});

里面重点在ExcelReadUtil.readExcelByRC()调用了自定义工具类方法,其余类代码就不过多解释

1.3 ExcelReadUtil

这个工具类封装了主要的Excel处理方法,并将读取的文件内容转换为List<Map<String, Object>>格式的数据结构。

  1. getWorkbook方法根据文件输入流和文件类型创建相应的工作簿对象(HSSFWorkbook 或 XSSFWorkbook),以便对Excel文件进行操作。
  2. convertCellValueToString方法将Excel单元格的内容转换为字符串形式。它根据单元格类型(如数字、字符串、布尔值等)进行相应的转换处理。
  3. handleData方法处理Excel数据内容,遍历指定范围内的所有行(由StatrRow和EndRow决定),并将每一行的数据转化为一个Map,其中键为列名(可以从Excel表头获得),值为单元格内容。
  4. readExcelByRC方法是对外提供的接口,接收MultipartFile类型的文件对象以及读取Excel的起始行、结束行和是否存在表头标志。此方法首先校验参数合理性,然后通过文件输入流获取工作簿,接着调用handleData方法处理数据,并在最后确保资源正确关闭。
@Slf4j
public class ExcelReadUtil {/*** 根据文件后缀名类型获取对应的工作簿对象* @param inputStream 读取文件的输入流* @param fileType    文件后缀名类型(xls或xlsx)* @return 包含文件数据的工作簿对象*/private static Workbook getWorkbook(InputStream inputStream, String fileType) throws IOException {//用自带的方法新建工作薄Workbook workbook = WorkbookFactory.create(inputStream);return workbook;}//将单元格内容转换为字符串private static String convertCellValueToString(Cell cell) {if (cell == null) {return null;}String returnValue = null;switch (cell.getCellType()) {case NUMERIC://数字Double doubleValue = cell.getNumericCellValue();// 格式化科学计数法,取一位整数,如取小数,值如0.0,取小数点后几位就写几个0DecimalFormat df = new DecimalFormat("0");returnValue = df.format(doubleValue);break;case STRING://字符串returnValue = cell.getStringCellValue();break;case BOOLEAN://布尔Boolean booleanValue = cell.getBooleanCellValue();returnValue = booleanValue.toString();break;case BLANK:// 空值break;case FORMULA:// 公式returnValue = cell.getCellFormula();break;case ERROR:// 故障break;default:break;}return returnValue;}/*** 处理Excel内容转为List<Map<String,Object>>输出* workbook:已连接的工作薄* StatrRow:读取的开始行数(默认填0,0开始,传过来是EXcel的行数值默认从1开始,这里已处理减1)* EndRow:读取的结束行数(填-1为全部)* ExistTop:是否存在头部(如存在则读取数据时会把头部拼接到对应数据,若无则为当前列数)*/private static List<Map<String, Object>> handleData(Workbook workbook, int StatrRow, int EndRow, boolean ExistTop) {//声明返回结果集resultList<Map<String, Object>> result = new ArrayList<>();//声明一个Excel头部函数ArrayList<String> top = new ArrayList<>();//解析sheet(sheet是Excel脚页)/***此处会读取所有脚页的行数据,若只想读取指定页,不要for循环,直接给sheetNum赋值,脚页从0开始(通常情况Excel都只有一页,所以此处未进行进一步处理)*/for (int sheetNum = 0; sheetNum < workbook.getNumberOfSheets(); sheetNum++) {Sheet sheet = workbook.getSheetAt(sheetNum);// 校验sheet是否合法if (sheet == null) {continue;}//如存在头部,处理头部数据if (ExistTop) {int firstRowNum = sheet.getFirstRowNum();Row firstRow = sheet.getRow(firstRowNum);if (null == firstRow) {log.warn("解析Excel失败,在第一行没有读取到任何数据!");}for (int i = 0; i < firstRow.getLastCellNum(); i++) {top.add(convertCellValueToString(firstRow.getCell(i)));}}//处理Excel数据内容int endRowNum;//获取结束行数if (EndRow == -1) {endRowNum = sheet.getPhysicalNumberOfRows();} else {endRowNum = EndRow <= sheet.getPhysicalNumberOfRows() ? EndRow : sheet.getPhysicalNumberOfRows();}//遍历行数for (int i = StatrRow - 1; i < endRowNum; i++) {Row row = sheet.getRow(i);if (null == row) {continue;}Map<String, Object> map = new HashMap<>();//获取所有列数据for (int y = 0; y < row.getLastCellNum(); y++) {if (top.size() > 0) {if (top.size() >= y) {map.put(top.get(y), convertCellValueToString(row.getCell(y)));} else {map.put(String.valueOf(y + 1), convertCellValueToString(row.getCell(y)));}} else {map.put(String.valueOf(y + 1), convertCellValueToString(row.getCell(y)));}}result.add(map);}}return result;}/*** 根据行数和列数读取Excel* fileName:Excel文件路径* startRow:读取的开始行数(默认填0)* endRow:读取的结束行数(填-1为全部)* existTop:是否存在头部(如存在则读取数据时会把头部拼接到对应数据,若无则为当前列数)* 返回一个List<Map<String,Object>>*/public static List<Map<String, Object>> readExcelByRC(MultipartFile file, int startRow, int endRow, boolean existTop) {//判断输入的开始值是否少于等于结束值eif (startRow > endRow && endRow != -1) {log.warn("输入的开始行值比结束行值大,请重新输入正确的行数");return null;}//声明返回的结果集List<Map<String, Object>> result = new ArrayList<>();//声明一个工作薄Workbook workbook = null;//声明一个文件输入流FileInputStream inputStream = null;try {// 获取Excel后缀名,判断文件类型String fileType = file.getOriginalFilename();// 获取Excel工作簿inputStream = (FileInputStream) file.getInputStream();workbook = getWorkbook(inputStream, fileType);//处理Excel内容result = handleData(workbook, startRow, endRow, existTop);} catch (Exception e) {log.warn("解析Excel失败,文件名:" + file.getName() + " 错误信息:" + e.getMessage());} finally {try {if (null != workbook) {workbook.close();}if (null != inputStream) {inputStream.close();}} catch (Exception e) {log.warn("关闭数据流出错!错误信息:" + e.getMessage());return null;}}return result;}
}

2 EasyExcel

参考EasyExcel官方文档,对EasyExcel的介绍如下
在这里插入图片描述
浅略看了一下文档,EasyExcel直接实现了前面涉及的文件读取,文件内容转化,省略了很多代码。而且使用门槛非常低,最简单的实现甚至不需要写其他代码,主要涉及的方法有

①注解绑定列字段 ②内容转化Convert ③监听器处理内容读取 ④EasyExcel.read()使用

2.1 常用方法

读取 Excel 文件的方法:

read:读取 Excel 文件的入口方法,用于读取 Excel 文件中的数据。
head:指定 Excel 文件的头部信息,即指定 Excel 文件中数据的起始行,默认为第一行。
registerReadListener:注册读取监听器,用于处理 Excel 文件读取过程中的事件。
sheet:指定要读取的 Excel 文件的 sheet,可以通过索引或者 sheet 名称来指定。
doRead:执行读取操作,开始读取 Excel 文件中的数据。

写入 Excel 文件的方法:

write:写入 Excel 文件的入口方法,用于创建 Excel 文件并写入数据。
head:指定 Excel 文件的头部信息,即指定 Excel 文件中数据的起始行,默认为第一行。
sheet:指定要写入的 Excel 文件的 sheet,可以通过索引或者 sheet 名称来指定。
doWrite:执行写入操作,将数据写入到 Excel 文件中。

其他常用方法:

finish:完成 Excel 文件的读写操作,释放资源。
withXXX:一些配置方法,如 withTemplate、withEncrypt 等,用于指定模板文件、加密等。

2.2 添加依赖

使用EasyExcel,第一件事是在pom.xml添加依赖:

 <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.1.1</version></dependency>

2.3 文档内容对象

要获取Excel中文档列名对应的对象属性,在前面的做法是在Service中将读取到的内容一一和实体类属性绑定,可以跳回1.2节看看以前的写法有多繁琐。EasyExcel这里直接提供了一个注解@ExcelProperty,加在实体类属性上直接实现了绑定属性,例如

@Data
@TableName("budget_info")
public class BudgetUploadPO {@TableId(value = "id", type = IdType.AUTO)@ExcelIgnore//会在文档中忽略字段idprivate String id;@ExcelProperty//会默认绑定Excel中列名"projectCode"private String projectCode;@ExcelProperty("项目名称")//绑定列名"项目名称"private String projectName;@ExcelProperty("预算金额")private String budgetNum;
}

具有的属性如下:

value:指定 Excel 列的标题。默认值为 “”,表示将使用 Java 对象属性名作为 Excel 列标题。
index:指定 Excel 列的索引,即列的位置。默认值为 -1,表示按照 value 指定的标题进行匹配。当 Excel 文件中的列标题与 Java 对象的属性名不匹配时,可以使用 index 属性指定列的位置。
converter:指定数据转换器,用于将 Excel 文件中的数据转换为 Java 对象属性的类型。默认值为 DefaultConverter.class,表示使用 EasyExcel 默认的转换器。可以自定义转换器,比如把文档中的"性别"男女转化为"0""1"存数据库。
format:指定 Excel 列的格式。默认值为 “”,表示不指定格式。可以在这里指定日期格式、数字格式等。
use1904windowing:是否使用 Excel 1904 窗口模式。默认值为 false。当日期使用 1904 窗口模式时,Excel 使用 1904 年 1 月 1 日作为第一个日期。如果 Excel 文件中使用了 1904 窗口模式,需要设置此属性为 true。

2.4 最简单的使用(同步读取)

下面的代码中EasyExcel.read() 是EasyExcel最常用的方法,其中 doReadSync() 表示开启同步读取,同步异步机制就不过多赘述,总之调用该方法时将等待该任务完成才能执行其他任务,所以不推荐使用同步读取,当读取文件较小的时候可以这样做,可以省掉写监听器。

// 将读取到的Excel内容转为List,接下来只需要对数据执行需要的操作List<DataEntity> list = EasyExcel.read(inputStream).head(DataEntity.class).sheet().doReadSync();
// 或也可以不指定class,返回一个list,返回每条数据的键值对 表示所在的列 和所在列的值List<Map<Integer, String>> listMap = EasyExcel.read(inputStream).sheet().doReadSync();

2.5 监听器(异步读取)

先解释一下同步异步读取的区别:

2.5.1 同步读取和异步读取的区别
  1. 同步读取使用doReadSync(),异步使用doRead()
  2. 同步读取需要new数据对象来接收数据,以及另行处理数据
  3. 异步读取需要new监听器对象,对数据的处理会在监听器方法中实现,可选不接收数据

当异步调用EasyExcel.read()方法时,对读取到内容的操作是通过监听器来处理的,可以是自己声明的监听器类或是在方法中实现一个内部类,例如:

2.5.2 自定义监听器类

自定义监听类DemoDataListener,实现EasyExcel提供的接口 ReadListener< T > ,重写invoke()和doAfterAllAnalysed()方法
这两个监听器方法分别会在每条数据解析时和所有数据解析完成时调用

// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
@Slf4j
public class DemoDataListener implements ReadListener<DemoData> {// 每隔100条存储数据库,实际使用中可以1000条,然后清理list ,方便内存回收private static final int BATCH_COUNT = 100;// 缓存的数据private List<DemoData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);// 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。private DemoDAO demoDAO;public DemoDataListener() {// 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数demoDAO = new DemoDAO();}//如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来public DemoDataListener(DemoDAO demoDAO) {this.demoDAO = demoDAO;}//每一条数据解析都会来调用@Overridepublic void invoke(DemoData data, AnalysisContext context) {log.info("解析到一条数据:{}", JSON.toJSONString(data));cachedDataList.add(data);// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOMif (cachedDataList.size() >= BATCH_COUNT) {saveData();// 一批存储完成后清理list,再存下一批cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);}}// 所有数据解析完成了 都会来调用@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {// 这里也要保存数据,确保最后遗留的数据也存储到数据库saveData();log.info("所有数据解析完成!");}//存储数据库的方法,需要自己去demoDAO实现save()方法private void saveData() {log.info("{}条数据,开始存储数据库!", cachedDataList.size());demoDAO.save(cachedDataList);log.info("存储数据库成功!");}
}

调用EasyExcel.read(文件流,对象类,监听器实例)直接往read()中传参

	EasyExcel.read(inputStream, DataDemo.class, new DemoDataListener()).sheet().doRead(); == 注意:1. 异步必须是doRead()2. DemoDataListener会根据自己方法自动处理数据
2.5.3 内部监听器类

当逻辑较少时也可以不自行实现监听器,在方法内部类实现逻辑,例如以下2种写法

    /*** 最简单的读* 1. 创建excel对应的实体对象 * 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照DemoDataListener* 3. 直接读即可*/@Testpublic void simpleRead() {// 写法1:JDK8+ ,不用额外写一个DemoDataListener// since: 3.0.0-beta1// 这里默认每次会读取100条数据 然后返回过来 直接调用使用数据就行// 具体需要返回多少行可以在`PageReadListener`的构造函数设置EasyExcel.read(inputStream, DemoData.class, new PageReadListener<DemoData>(dataList -> {for (DemoData demoData : dataList) {log.info("读取到一条数据{}", JSON.toJSONString(demoData));}})).sheet().doRead();
        // 写法2:// 匿名内部类 不用额外写一个DemoDataListener// 这里 需要指定读用哪个实体类class去读,然后读取第一个sheet 文件流会自动关闭EasyExcel.read(inputStream, DemoData.class, new ReadListener<DemoData>() {//单次缓存的数据量public static final int BATCH_COUNT = 100;//临时存储到Listprivate List<DemoData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);@Overridepublic void invoke(DemoData data, AnalysisContext context) {cachedDataList.add(data);if (cachedDataList.size() >= BATCH_COUNT) {saveData();// 存储完成清理 listcachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);}}@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {saveData();}private void saveData() {//自行实现存储数据库方法log.info("{}条数据,开始存储数据库!", cachedDataList.size());log.info("存储数据库成功!");}}).sheet().doRead();}

2.6 EasyExcel总结

EasyExcel提供的支持还有很多,可以自行看一下官方文档。
在此总结一下读取文件的流程

  • 在实体类字段上用@ExcelProperty来绑定,对于Excel文件不存在的内容,如id,使用@ExcelIgnore忽略
  • 根据数据量决定同步读取还是异步读取,同步读取可以省略监听器类,自行处理获取的数据
  • 异步读取需要监听器对象,可以自定义实现类或是匿名内部类,在监听器可以实现数据处理
  • 以上不管是同步还是异步读取,所调用的保存方法都要自行在DAO实现

3 批量保存方法

3.1基于Mybatis的批量保存

上面各种数据处理,最终都涉及到了有关批量保存的方法,批量保存速度是比单条插入时间复杂度要低的,如前方官方文档里说的是自行在DAO实现一个批量保存方法,提供给监听器调用。但我也思考如何使用MybatisP去实现,降低代码耦合性。
参考这个前辈的文章。
他是这样的思路:实体类PersonPO就不说了

  1. BatchInsertMapper< T >
    声明一个含批量插入方法的Mapper接口
//批量插入的Mapper, 用xml配置文件自定义批量插入,避免MyBatis的逐条插入降低性能
public interface BatchInsertMapper<T> {void batchInsert(List<T> list);
}
  1. PersonMapper
    PersonPO类对应的Mapper在继承BaseMapper的同时,也继承BatchInsertMapper接口,需要实现批量插入方法
//(Person)表数据库访问层
@Mapper
public interface PersonMapper extends BaseMapper<PersonPO>, BatchInsertMapper<PersonPO> {
}
  1. PersonMapper.xml
    自己写SQL语句实现批量插入方法
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.spring.accumulator.dao.PersonMapper"><insert id="batchInsert" parameterType="list">insert into wangrubin_db.person(name, age, male)values<foreach collection="list" item="item" index="index" separator=",">(#{item.name},#{item.age},#{item.male})</foreach></insert>
</mapper>

把监听类抽象出来

  1. ExcelImportListener抽象监听器
@Slf4j
public abstract class ExcelImportListener<T> implements ReadListener<T> {private static final int BATCH_SIZE = 100;private List<T> cacheList = new ArrayList<>(BATCH_SIZE);@Overridepublic void invoke(T po, AnalysisContext analysisContext) {cacheList.add(po);if (cacheList.size() >= BATCH_SIZE) {log.info("完成一批Excel记录的导入,条数为:{}", cacheList.size());getMapper().batchInsert(cacheList);cacheList = new ArrayList<>(BATCH_SIZE);}}@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {getMapper().batchInsert(cacheList);log.info("完成最后一批Excel记录的导入,条数为:{}", cacheList.size());}//获取批量插入的Mapperprotected abstract BatchInsertMapper<T> getMapper();
}

定义一个组件类,里面写了一个ExcelImportListener 的子类匿名内部类。重写了 getMapper() 方法,并返回了 personMapper 对象。

  1. ExcelComponent组件
@Slf4j
@Component
public class ExcelComponent {@Resourceprivate PersonMapper personMapper;// Excel文件分批导入数据库public void importPersonFile(MultipartFile file) throws IOException {EasyExcel.read(file.getInputStream()).head(PersonPO.class).registerReadListener(new ExcelImportListener<PersonPO>() {@Overrideprotected BatchInsertMapper<PersonPO> getMapper() {return personMapper;}}).sheet().doRead();}
}
  1. ImportController
    最后在Controller注入组件调用方法就行
@RestController
@RequestMapping("/excel")
public class ImportController {@Resourceprivate ExcelComponent excelComponent;@PostMapping("/import-person")public void importPersonFile(@RequestParam("file") MultipartFile file) throws IOException {excelComponent.importPersonFile(file);}

他的设计还是很有亮点,回调监听器体现了策略模式,监听器方法构成了一个模板方法模式,这些方法定义了算法的骨架,具体的步骤由子类实现。
基于抽象与接口BatchInsertMapper、ExcelComponent、ExcelImportListener,可以实现业务代码进一步拆分与模块化。如果需要再添加其他对象的上传,只需要

1.对应实体类
2.对应Mapper继承BaseMapper和BatchInsertMapper< T >
3.对应Mapper.xml批量插入语句
4.ExcelComponent增加注入对应Mapper,实现对应importXXXFile方法
5.Controller逻辑

3.2 基于MybatisPlus的批量保存

前面3.1提到了别人的写法,声明了抽象的监听器类和一个component,这样的封装可以降低代码耦合和复写,但是有个核心问题,以后有导入文件需求的所有类都要自己在Mapper实现批量插入方法。

他的监听器关键代码:

    @Overridepublic void invoke(T po, AnalysisContext analysisContext) {cacheList.add(po);if (cacheList.size() >= BATCH_SIZE) {//调用了Mapper的批量插入方法getMapper().batchInsert(cacheList);cacheList = new ArrayList<>(BATCH_SIZE);}}//获取批量插入的Mapper,这个Mapper需要实现batchInsert(cacheList)方法protected abstract BatchInsertMapper<T> getMapper();

我看了一下,不管是Mybatis还是MybatisPlus,其提供的BaseMapper里面是没有批量插入方法的。

但是,IService里面有实现saveBatch方法,需要接收一个List和一个size(size非必须)
IService提供的saveBatch
我思考是否可以把监听器改为获取一个IService类型的service ,然后就能调用saveBatch()方法了呢?

   	//移除 protected abstract BatchInsertMapper<T> getMapper();//获取serviceprivate final IService<T> service;public ExcelImportListener(IService<T> service) {this.service = service;}

再修改Component类

//    @Resource
//    private PersonMapper personMapper;
//
//    // Excel文件分批导入数据库
//    public void importPersonFile(MultipartFile file) throws IOException {
//        EasyExcel.read(file.getInputStream())
//            .head(PersonPO.class)
//            .registerReadListener(new ExcelImportListener<PersonPO>() {
//                @Override
//                protected BatchInsertMapper<PersonPO> getMapper() {
//                    return personMapper;
//                }
//            }).sheet().doRead();
//    }public void importExcel(MultipartFile file, Class<T> clazz, IService<T> service ) throws IOException {EasyExcel.read(file.getInputStream()).head(clazz).registerReadListener(new ExcelImportListener<>(service)).sheet().doRead();}

这样就能在不同的controller调用component,指定不同的实体类和Service,且不需要再自行实现Mapper方法

最终代码

代码经验证导入excel到数据库有效,但我认为在泛型应用和异常处理上还存在问题,希望有人指点。
以下是全部代码:

数据库表

project_budget_info表

entity.BudgetUploadPO

@Data
@TableName("project_budget_info")
public class BudgetUploadPO {@TableId(value = "id", type = IdType.AUTO)@ExcelIgnoreprivate String id;@ExcelProperty("")private String projectCode;@ExcelProperty("项目名称")private String projectName;@ExcelProperty("预算金额")private String budgetNum;
}

component.ExcelComponent

@Slf4j
@Component
public class ExcelComponent<T> {public void importExcel(MultipartFile file, Class<T> clazz, IService<T> service ) throws IOException {EasyExcel.read(file.getInputStream()).head(clazz).registerReadListener(new ExcelImportListener<>(service)).sheet().doRead();}
}

controller.BudgetUploadController

@RestController
@RequestMapping("/budget")
public class BudgetUploadController {@AutowiredExcelComponent<BudgetUploadPO> excelComponent;@Autowiredprivate BudgetUploadService budgetUploadService;@Operation(description = "上传文件")@PostMapping("/upload")public void uploadExcel2(@RequestParam("file") MultipartFile file) throws IOException {excelComponent.importExcel(file, BudgetUploadPO.class, budgetUploadService);}
}

listener.ExcelImportListener

@Slf4j
public class ExcelImportListener<T> implements ReadListener<T> {private static final int BATCH_SIZE = 100;private List<T> cacheList = new ArrayList<>(BATCH_SIZE);private final IService<T> service;public ExcelImportListener(IService<T> service) {this.service = service;}@Overridepublic void invoke(T po, AnalysisContext analysisContext) {cacheList.add(po);if (cacheList.size() >= BATCH_SIZE) {log.info("完成一批Excel记录的导入,条数为:{}", cacheList.size());service.saveBatch(cacheList);cacheList = new ArrayList<>(BATCH_SIZE);}}@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {service.saveBatch(cacheList);log.info("完成最后一批Excel记录的导入,条数为:{}", cacheList.size());}
}

mapper.BudgetUploadMapper

@Mapper
public interface BudgetUploadMapper extends BaseMapper<BudgetUploadPO>{
}

service.BudgetUploadService

public interface BudgetUploadService extends IService<BudgetUploadPO> {
}

service.impl.BudgetUploadUploadServiceImpl

@Service
public class BudgetUploadUploadServiceImpl extends ServiceImpl<BudgetUploadMapper, BudgetUploadPO> implements BudgetUploadService {
}

若要增加其他实体的上传方法,在对应controller调用component类方法,以同样的方式指定对应实体类和Service即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/578679.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Linux】体验一款开源的Linux服务器运维管理工具

今天为大家介绍一款开源的 Linux 服务器运维管理工具 - 1panel。 一、安装 根据官方那个提供的在线文档&#xff0c;这款工具的安装需要执行在线安装&#xff0c; # Redhat / CentOScurl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_start…

科普:从神经网络到 Hugging Face——神经网络和深度学习简史

活中没有什么可怕的东西&#xff0c;只有需要理解的东西。—— 居里夫人 深度信念网络 2006年&#xff0c;加拿大多伦多大学教授杰弗里辛顿在研究如何训练多层神经网络&#xff0c;他已经在神经网络领域默默耕耘了三十多年&#xff0c;尽管在这个领域他算得上是泰斗级的人物&…

unity3d for web

时光噶然 一晃好多年过去了&#xff08;干了5年的u3d游戏&#xff09;&#xff0c;记得最后一次使用的版本好像是 unity 2017。 那个是 unity3d for webgl 还需要装个插件。用起来很蛋疼。 最近做一个小项目 在选择是用 Layabox 还是 cocosCreate 的时候 我想起了老战友 Uni…

阿里云魔搭发起“ModelScope-Sora开源计划”,将为中国类Sora模型开发提供一站式工具链

在2024年3月23日的全球开发者先锋大会上&#xff0c;阿里云的魔搭社区宣布了一个新计划&#xff1a;“ModelScope-Sora开源计划”。这个计划旨在通过开源方式&#xff0c;帮助中国在Sora模型类型上做出更多创新。这个计划提供了一整套工具&#xff0c;包括处理数据的工具、多模…

JUC:synchronized优化——锁的升级过程(偏向锁->轻量级锁->重量级锁)以及内部实现原理

文章目录 锁的类型轻量级锁重量级锁自旋优化偏向锁偏向锁的细节偏向锁的撤销批量重偏向批量撤销锁消除 锁的类型 重量级锁、轻量级锁、偏向锁。 加锁过程&#xff1a;偏向->轻量级->重量级 轻量级锁 轻量级锁的使用场景&#xff1a;如果一个对象虽然有多线程要加锁&am…

数据结构与算法(2)顺序表的初始化、插入、打印、删除、查找元素——C语言版

目录 1.前言 2.头文件的定义 3.菜单栏的设置 4.顺序表的初始化 5.添加元素 6.打印元素 7.查找元素 8.删除元素 9.插入元素 10.主函数 11.完整的代码实现 1.前言 数据结构包括三个方面 逻辑结构存储结构运算 而线性表有两种典型的存储结构 顺序存储结构链式存储结构 具体关系可…

提取gdip-yolo与ia-seg中的图像自适应模块进行图像去雾与亮度增强

gdip-yolo与ia-seg都是一种将图像自适应模块插入模型前面,从而提升模型在特定数据下检测能力的网络结构。gdip-yolo提出了gdip模块,可以应用到大雾数据与低亮度数据(夜晚环境),然后用于目标检测训练;ia-seg将ia-yolo中的代码修改了一下修车了ipam模块,应用到低亮度数据(…

在线随机密码生成器源码

纯HTML&#xff0c;该去的已去掉&#xff0c;该简化的简化&#xff0c;最高支持32位混合随机密码生成。 源码下载&#xff1a;在线随机密码生成器源码

Golang生成UUID

安装依赖 go get -u github.com/google/uuid文档 谷歌UUID文档 示例 函数签名func NewV7() ( UUID ,错误) func (receiver *basicUtils) GenerateUUID() uuid.UUID {return uuid.Must(uuid.NewV7()) } uid : GenerateUUID()

RH850从0搭建Autosar开发环境【3X】- Davinci Configurator之RTE模块配置详解(上)

RTE模块配置详解 - 上 一、RTE模块配置实操1.1 打开RTE模块1.2 RTE模块错误消除在这里插入图片描述 这里我们利用工具的自动处理功能。二、Configurator工具Validation总结本节我们就手把手详解RTE配置实现,其实也没有什么过多的操作。。。这个模块更多是工具自动处理的。 一、…

Dockerfile和Docker-compose

一、概述 Dockerfile和Docker Compose是用于构建和管理 Docker 容器的两个工具&#xff0c;但它们的作用和使用方式不同。 Dockerfile Dockerfile 是一个文本文件&#xff0c;用于定义 Docker 镜像的构建规则。它包含一系列指令&#xff0c;如 FROM&#xff08;指定基础镜像…

吴恩达2022机器学习专项课程(一) 4.3 梯度下降的直观理解

问题预览/关键词 本节内容是&#xff1f;J对w求导的含义是&#xff1f;如何确定切线的方向&#xff1f;w在函数J递增处的切线方向是&#xff1f;导数项为正数&#xff0c;w和函数J的关系是&#xff1f;w在函数J递减处的切线方向是&#xff1f;导数项为负数&#xff0c;w和函数…