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.rhkb.cn/news/290860.html

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

相关文章

【生活】相机/图像各参数

文章目录 专业模式图片编辑-滤镜实体滤镜软件模拟滤镜 图片编辑-增强曝光亮度对比度饱和度自然饱和度色温色调高光阴影HSL色调分离褪色颗粒锐化晕影清晰度暗角 参考 专业模式 第一个参数WB是白平衡&#xff0c;调节色彩的。 第二个是对焦F&#xff0c;近距离拍摄物体&#xf…

Android R 广播注册与发送流程分析

静态广播注册时序图 动态广播注册时序图 发送广播时序图 前言 广播接收器可以分为动态和静态&#xff0c;静态广播接收器就是在 AndroidManifest.xml 中注册的&#xff0c;而动态的广播接收器是在代码中通过 Context#registerReceiver() 注册的。 这里先从静态广播的流程开始…

FANUC机器人故障诊断—报警代码(一)

一、SRVO-050碰撞检测报警 [原因]检测出碰撞 [对策] 1.确认机器人是否碰撞。 2.确认是否正确进行了负载设定。 3.确认是否有过载、过度的加速度附加指令。 4.在长期停用后启动&#xff0c;或者外部气温较低时发生该报警。启动后&#xff0c;先短时间内低速运转设备&#…

uniapp开发App——登陆流程 判断是否登陆,是,进入首页,否,跳转到登录页

一、登陆流程 文字描述&#xff1a;用户进入App&#xff0c;之后就是判断该App是否有用户登陆过&#xff0c;如果有&#xff0c;直接进入首页&#xff0c;否则跳转到登陆页&#xff0c;登陆成功后&#xff0c;进入首页。 流程图如下&#xff1a; 二、在uniapp项目中代码实现 实…

google浏览器网站不安全与网站的连接不安全怎么办?

使用google谷歌浏览器访问某些网站打开时google谷歌浏览器提示网站不安全,与网站的连接不安全,您之所以会看到此警告,是因为该网站不支持https造成的怎么办? 目录 1、打开谷歌google浏览器点击右上角【┇】找到设置

React Native框架开发APP,安装免费的图标库(react-native-vector-icons)并使用详解

一、安装图标库 要使用免费的图标库&#xff0c;你可以使用 React Native Vector Icons 库。 首先&#xff0c;确保你已经安装了 react-native-vector-icons&#xff1a; npm install --save react-native-vector-iconsnpm install --save-dev types/react-native-vector-ic…

vue3使用vuedraggable实现拖拽(有过渡)

1. 安装与使用 vue中vuedraggable安装&#xff1a; pnpm i -S vuedraggablenext或者 yarn add vuedraggablenext注意&#xff1a;vue2和vue3安装的是不同版本的vuedraggable&#xff0c;写法上也会有一些区别。 比如在vue3中使用拖拽&#xff0c;要以插槽的方式&#xff0c;…

从 Azure 部署生成本地 .NET 密钥

作者&#xff1a;Frank Boucher 排版&#xff1a;Alan Wang 通常&#xff0c;示例项目以一些“魔术字符串”开始&#xff0c;这些变量包含与部署或外部资源相关的 URL 和关键信息&#xff0c;我们必须更改这些信息才能使用示例。例如在 .NET 中&#xff0c;它可能如下所示&…

使用vue2实现在线创建组件的功能

前言 我们使用vue2构建了一个项目&#xff0c;项目有个需求&#xff1a;用户可以在线创建vue组件&#xff0c;创建后的组件可以动态编译&#xff0c;项目无需重新部署&#xff0c;就可以在表单等位置引入使用组件。 实现记录 引用vue的esm包 项目中引入vue的代码&#xff0…

【Roadmap to learn LLM】Large Language Models in Five Formulas

by Alexander Rush Our hope: reasoning about LLMs Our Issue 文章目录 Perpexity(Generation)Attention(Memory)GEMM(Efficiency)用矩阵乘法说明GPU的工作原理 Chinchilla(Scaling)RASP(Reasoning)结论参考资料 the five formulas perpexity —— generationattention —— m…

电脑分辨率怎么调,电脑分辨率怎么调整

随着电脑的普及以及网络的发展&#xff0c;我们现在在工作中都离不开对电脑的使用&#xff0c;今天小编教大家设置电脑分辨率&#xff0c;现在我们先了解这个分辨率是什么?通常电脑的显示分辨率就是屏幕分辨率&#xff0c;显示屏大小固定时&#xff0c;显示分辨率越高图像越清…

数据可视化为什么能在智慧港口中发挥作用?

随着全球贸易活动日益频繁&#xff0c;港口作为国际贸易的重要节点&#xff0c;其运营效率与智能化程度直接影响着整个物流链的效能。在此背景下&#xff0c;智慧港口的概念应运而生&#xff0c;它借助先进的信息技术手段对传统港口进行改造升级&#xff0c;其中&#xff0c;数…

【AIGC】如何在Windows/Linux上部署stable diffusion

文章目录 整体安装步骤windows10安装stable diffusion环境要求安装步骤注意事项参考博客其他事项安装显卡驱动安装cuda卸载cuda安装对应版本pytorch安装git上的python包Q&A linux安装stable diffusion安装anaconda安装cudagit 加速配置虚拟环境挂载oss&#xff08;optional…

Spring Aop 源码解析(下)

ProxyFactory选择cglib或jdk动态代理原理 ProxyFactory在生成代理对象之前需要决定到底是使用JDK动态代理还是CGLIB技术: config就是ProxyFactory对象,把自己传进来了,因为ProxyFactory继承了很多类,其中一个父类就是ProxyConfig // config就是ProxyFactory对象// 是不是…

02正式学习第一天

1、windows上加载socket库 链接输入ws2_32.lib 代码code&#xff1a; #ifdef _WIN32 #include<windows.h> #else #include <sys/socket.h> #include<sys/types.h> #include<unistd.h> #include<cstring> #include<arpa/inet.h> #include…

【Java】LinkedList模拟实现

目录 整体框架IMyLinkedList接口IndexNotLegalException异常类MyLinkedList类成员变量(节点信息)addFirst(头插)addLast(尾插)在指定位置插入数据判断是否存在移除第一个相等的节点移除所有相等的节点链表的长度打印链表释放回收链表 整体框架 IMyLinkedList接口 这个接口用来…

WPF 路由事件 数据驱动 、Window 事件驱动

消息层层传递&#xff0c;遇到安装有事件侦听器的对象&#xff0c;通过事件处理器响应事件&#xff0c;并决定事件是否继续传递&#xff1b; 后置代码中使用AddHandler方法设置事件监听器&#xff0c;该方法的 第一个参数是指定监听的路由事件类型对象&#xff0c; 第二个参数…

微服务demo(四)nacosfeigngateway

一、gateway使用&#xff1a; 1、集成方法 1.1、pom依赖&#xff1a; 建议&#xff1a;gateway模块的pom不要去继承父工程的pom&#xff0c;父工程的pom依赖太多&#xff0c;极大可能会导致运行报错&#xff0c;新建gateway子工程后&#xff0c;pom父类就采用默认的spring-b…

CSS之动画

一&#xff0c;动画的制作 实现盒子绕圈走 二&#xff0c; 动画的常用属性 三&#xff0c;动画简写属性 前面两个属性一定要写&#xff0c;第三个linear是指匀速的意思&#xff08;默认是ease&#xff09;

【快速解决】解决谷歌自动更新的问题,禁止谷歌自动更新,如何防止chrome自动升级 chrome浏览器禁止自动升级设置方法

目录 问题描述 解决方法 1、搜索栏搜索控制面板 2、搜索&#xff1a;服务 ​编辑 3、点击Windows工具 4、点击服务 ​5、禁止谷歌更新 问题描述 由于我现在需要装一个谷歌的驱动系统&#xff0c;但是目前的谷歌驱动系统的版本都太旧了&#xff0c;谷歌自身的版本又太新了…