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,取小数点后几位就写几个0
                DecimalFormat 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) {
        //声明返回结果集result
        List<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) {
        //判断输入的开始值是否少于等于结束值e
        if (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
    //会在文档中忽略字段id
    private 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;
    }

    //每一条数据解析都会来调用
    @Override
    public void invoke(DemoData data, AnalysisContext context) {
        log.info("解析到一条数据:{}", JSON.toJSONString(data));
        cachedDataList.add(data);
        // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
        if (cachedDataList.size() >= BATCH_COUNT) {
            saveData();
            // 一批存储完成后清理list,再存下一批
            cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
        }
    }

    // 所有数据解析完成了 都会来调用
    @Override
    public 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. 直接读即可
     */
    @Test
    public 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;
            //临时存储到List
            private List<DemoData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
            @Override
            public void invoke(DemoData data, AnalysisContext context) {
                cachedDataList.add(data);
                if (cachedDataList.size() >= BATCH_COUNT) {
                    saveData();
                    // 存储完成清理 list
                    cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
                }
            }
            @Override
            public 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);

    @Override
    public 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);
        }
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        getMapper().batchInsert(cacheList);
        log.info("完成最后一批Excel记录的导入,条数为:{}", cacheList.size());
    }

    //获取批量插入的Mapper
    protected abstract BatchInsertMapper<T> getMapper();
}

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

  1. ExcelComponent组件
@Slf4j
@Component
public class ExcelComponent {

    @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();
    }
}
  1. ImportController
    最后在Controller注入组件调用方法就行
@RestController
@RequestMapping("/excel")
public class ImportController {
    @Resource
    private 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实现批量插入方法。

他的监听器关键代码:

    @Override
    public 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();
   	
	//获取service
    private 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)
    @ExcelIgnore
    private 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 {

    @Autowired
    ExcelComponent<BudgetUploadPO> excelComponent;

    @Autowired
    private 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;
    }
    @Override
    public 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);
        }
    }
    @Override
    public 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即可。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

5G PLMN相关概念

PLMN PLMN&#xff08;Public Land Mobile Network&#xff0c;公用陆地移动网络&#xff09;&#xff0c;是由政府或其批准的经营者为公众提供陆地移动通信业务而建立、经营的网络。PLMN与公众交换电话网&#xff08;PSTN&#xff09;互连&#xff0c;形成整个地区或国家规模…

机器人码垛机:智能仓储系统的重要组成部分

随着科技的飞速进步&#xff0c;机器人技术已经渗透到了许多行业领域&#xff0c;其中&#xff0c;仓储业尤为显著。机器人码垛机作为智能仓储系统的重要组成部分&#xff0c;不仅提高了码垛效率&#xff0c;还降低了人工成本和安全风险。然而&#xff0c;在其广泛应用的同时&a…

卸载原有的cuda,更新cuda

概述&#xff1a;看了一下自己的gpu&#xff0c;发现驱动可能装低了&#xff0c;随即尝试更新驱动&#xff0c;写下此篇 注&#xff1a;我原先是10.2的版本&#xff0c;改了之后是11.2&#xff0c;下面的图都用11.2的&#xff0c;不过不碍事 目录 第一步&#xff1a;查看现在…

关于未来自我的发展和一些学习方法(嵌入式方向)

我是一名大二的学生&#xff0c;考研还是就业&#xff0c;到底是重视专业课还是重视数学英语&#xff0c;这些问题一直困扰了我很久&#xff0c;但如今已经有了一些浅显的认识&#xff0c;所以才会想写这样一篇文章来记录一下自己的状态和未来的规划 下面的看法都是个人的看法&…

RN在android/ios手机剪切图片的操作

之前写过一个React Native调用摄像头画面及拍照和保存图片到相册全流程但是这个仅限于调用摄像头拍照并保存图片,今天再写一个版本的操作,这个博客目前实现的有三点操作: 调用摄像头拍照对照片进行剪切从相册选取图片 功能上面来说有两点: 点击按钮可以对摄像头进行拍照,拍完照…

继承 | Java

继承概念 继承&#xff1a;就是子类继承父类的属性和行为&#xff0c;使得子类对象具有与父类相同的属性、相同的行为。子类可以直接访问父类中的非私有的属性和行为。 定义父类&#xff1a; package com.yh;public class Animal {String color;String brand;int age 5;priva…

游戏赛道新机会:善用数据分析,把握游戏赛道广告变现良机 | TOPON变现干货

12月10日&#xff0c;由罗斯基联合TopOn、钛动科技共同主办的《游戏赛道新机会》主题系列沙龙在武汉举办。活动邀请了国内外多家业内知名公司的负责人到场分享&#xff0c;现场嘉宾分别从自己擅长的领域出发&#xff0c;通过数据分析&#xff0c;案例复盘等多个维度方向进行讲解…

【javaWeb 第八篇】后端-Mybatis(万字详细学习)

Mybatis框架 前言MybatisMybatis入门配置SQL提示JDBC数据库连接池lombok Mybatis基础操作日志输出Mybatis的动态SQL 前言 这篇是作者学习数据持久层框架Mybatis的学习笔记&#xff0c;希望对大家有所帮助&#xff0c;希望大家能够与作者交流讨论 Mybatis Mybatis是一款优秀的…

海豚【货运系统源码】货运小程序【用户端+司机端app】源码物流系统搬家系统源码师傅接单

技术栈&#xff1a;前端uniapp后端vuethinkphp 主要功能&#xff1a; 不通车型配置不通价格参数 多城市定位服务 支持发货地 途径地 目的地智能费用计算 支持日期时间 预约下单 支持添加跟单人数选择 支持下单优惠券抵扣 支持司机收藏订单评价 支持订单状态消息通知 支…

uniapp数组合并函数使用几录

let that { listAll: [1, 2, 3] }; let data [4, 5, 6]; let mergedArray that.listAll.concat(data); console.log(mergedArray); // 输出: [1, 2, 3, 4, 5, 6] console.log(that.listAll); // 输出: [1, 2, 3]&#xff0c;原始数组没有改变 唯有美景&#xff0c;可以抚…

1.ByteBuffer介绍

一、Buffer基本使用 /*** buffer正确使用姿势* 1.向buffer写入数据&#xff0c;调用channel.read(buffer)* 2.调用flip方法切换到读模式* 3.从buffer读数据&#xff0c;通过get方法&#xff0c;每次读取一个字节或字符* 4.调用clear方法或者compact方法切换到写模式*/ Slf4j p…

静态住宅IP优缺点,究竟要怎么选?

在进行海外 IP 代理时&#xff0c;了解动态住宅 IP 和静态住宅 IP 的区别以及如何选择合适的类型非常重要。本文将介绍精态住宅 IP 特点和&#xff0c;并提供选择建议&#xff0c;帮助您根据需求做出明智的决策。 静态住宅 IP 的特点 静态住宅 IP 是指 IP 地址在一段时间内保…

2-linux磁盘常用的命令lsblk

目录 1.lsblk常用命令 2.1-lsblk -a&#xff1a;显示所有设备&#xff08;包括空设备&#xff09;的信息。 2.2 lsblk -f&#xff1a;显示文件系统类型。 2.3 lsblk -m&#xff1a;显示磁盘和分区的归属帐号权限 1.lsblk常用命令 lsblk的英文&#xff1a;List block 列出磁…

windows下QT如何集成OpenCV

说明 我在windows下使用QT Creator12创建的CMake项目&#xff0c;需要OpenCV的一些功能。由于安装的时候我选择的QT组件都是MInGW的&#xff0c;所以无法使用VS studio版本的dll库。 为什么vs的版本不能用 我安装QT选择的是MinGW版本&#xff0c;本地编译QT工程只能选择MinG…

安全SCDN的威胁情报库对DDOS防护有什么好处

目前网络攻击事件频频发生&#xff0c;DDoS&#xff08;分布式拒绝服务&#xff09;攻击已成为各种企业&#xff08;小到区域性小公司大到各种跨国公司&#xff09;的主要威胁&#xff0c;DDoS 攻击可能会对企业造成重大损害和破坏&#xff0c;比如对目标公司的业务造成产生不利…

OpenHarmony无人机MAVSDK开源库适配方案分享

MAVSDK 是 PX4 开源团队贡献的基于 MavLink 通信协议的用于无人机应用开发的 SDK&#xff0c;支持多种语言如 C/C、python、Java 等。通常用于无人机间、地面站与通信设备的消息传输。 MAVLink 是一种非常轻量级的消息传递协议&#xff0c;用于与无人机&#xff08;以及机载无…

Halcon3D表面平面度检测-平面差值法

//倾斜平面矫正 https://blog.csdn.net/m0_51559565/article/details/137146179前言 通常我们对表面平面度进行检测时&#xff0c;通常使用2种方式。1&#xff1a;通过大卷积核的高斯滤波进行拟合平面&#xff0c;然后求取拟合平面与3D模型间的点间的距离。2&#xff1a;通过平…

向机器人传授人类社会同步的艺术

在数字时代&#xff0c;社交机器人正迅速成为主流社会的一部分&#xff0c;从培训医生和教育儿童到提供谈话疗法和客户服务。现在&#xff0c;这是一个价值数十亿美元的全球产业&#xff0c;对具有类似人类社交智能的机器人的需求不断增长&#xff0c;这标志着我们技术史上的一…

CavalierContours 二维线操作

CavalierContours 二维线操作 2D polyline library for offsetting, combining, etc. 用于偏移、交并补等组合等操作的 2D 多折段线库。 Polyline Structure 多段线结构 Polylines are defined by a sequence of vertexes and a bool indicating whether the polyline is cl…

深度学习入门简单实现一个神经网络

实现一个三层神经网络 引言测试数据 代码 引言 今天我们实现一个简单的神经网络 俩个输入神经元 隐藏层两个神经元 一个输出神经元 激活函数我们使用sigmoid 优化方法使用梯度下降 我们前期准备是需要把这些神经元的关系理清楚 x1&#xff1a;第一个输入 x2&#xff1a;第二个…