java 数万条excel数据导入且操作其他模块功能时:使用com.alibaba.excel在自定义监听器中解析数据后用@Sync注解存入mysql数据库。【附有完整前后端代码】

一、程序需求

从web页面上导入一个excel文件,此文件中有数万条以上的关于商户的交易记录数据;
1、一次性导入到后台后需要存一个总的导入记录,以及导入数据的记录明细,导入记录与导入数据记录为一对多的关系;
2、记录明细内容中还需要根据excel的行数据计算出商户交易的积分信息;
3、如果商户表中没有导入的商户,则插入商户表数据。
4、如果客户表中没有导入的客户,则插入客户表数据。
5、每月导入一次,每次导入数据量很多,我们需要提高导入的效率。

二、使用技术

前端Jeecgboot vue3,后端框架使用jeecgboot
导入记录表:Trade
导入数据记录表:TradeRecord

三、前端代码

导入的Modal页面:

<template>
  <BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="800" @ok="handleSubmit">
      <div  style="margin: 0 35px 15px" >
        <span style="display: inline-block; height: 32px; line-height: 32px; vertical-align: middle">选择月份:</span>
        <span style="margin-left: 6px">
           <a-month-picker :format="monthFormat"  v-model="defalutMonth" @panelChange="handleChangeMonth"></a-month-picker>
        </span>
      </div>

      <!--上传-->
      <div  style="margin: 0 35px 15px" >
        <span style="display: inline-block; height: 32px; line-height: 32px; vertical-align: middle">导入文件:</span>
        <span style="margin-left: 6px">
            <a-upload name="file" accept=".xls,.xlsx" :multiple="false" :fileList="fileList" @remove="handleRemove"  :beforeUpload="beforeUpload">
             <a-button preIcon="ant-design:upload-outlined">选择导入文件</a-button>
           </a-upload>
        </span>
      </div>

      <!--页脚-->
      <template #footer>
        <a-button @click="handleClose">关闭</a-button>
        <a-button type="primary" @click="handleImport" :disabled="uploadDisabled" :loading="uploading">{{
            uploading ? '上传中...' : '开始上传'
          }}
        </a-button>
      </template>
  </BasicModal>
</template>


这里提交的内容有:月份、excel文件,点击“开始上传”按钮后执行handleImport方法,方法内容如下:

//表单提交事件
//文件上传
    function handleImport() {
      let {biz, online} = props;
      const formData = new FormData();
      if (biz) {
        formData.append('isSingleTableImport', biz);
      }
      if (unref(monthValue)) {
        formData.append("month", unref(monthValue));
      }else{
        createMessage.error("请选择月份!")
        return;
      }
      unref(fileList).forEach((file) => {
        formData.append('files', file);
      });
      uploading.value = true;

      // 请求怎样处理的问题
      let headers = {
        'Content-Type': 'multipart/form-data;boundary = ' + new Date().getTime(),
      };
      defHttp.post({url: "/business/trade/importExcelData", timeout: 60 * 60 * 1000, params: formData, headers}, {isTransformResponse: false}).then((res) => {
        uploading.value = false;
        if (res.success) {
          if (res.code == 201) {
            errorTip(res.message, res.result);
          } else {
            createMessage.success(res.message);
          }
          handleClose();
          reset();
          emit('success');
        } else {
          createMessage.warning(res.message);
        }
      }).catch(() => {
        uploading.value = false;
      });
    }

在这里插入图片描述

这里就已经提交到了后台了

四、后端代码

首先引入依赖文件

<!--Excel 操作-->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>easyexcel</artifactId>
			<version>2.1.0-beta4</version>
		</dependency>

后台执行的Controller方法:

1、controller方法:获取到MultipartFile文件

 /**
     * 通过excel导入数据
     * @return
     */
    @RequestMapping(value = "/importExcelData", method = RequestMethod.POST)
    public Result<?> importExcelData(MultipartFile[] files, String month) {
        System.out.println(month);
        // 获取上传文件对象
        try {
            MultipartFile multipartFile = files[0];

            //查询选择是否有导入过数据
            Trade dup = tradeService.getOne(new LambdaQueryWrapper<Trade>().eq(
                    Trade::getMonth, month).eq(Trade::getIzValid,PointStoreConst.NUM_1).last("limit 1"));
            if(Optional.ofNullable(dup).isPresent()){
                return Result.error("本月已导入过数据!");
            }else{
                //MultipartFile file = multipartRequest.getFile("file");
                String id = tradeService.doFileImport(month,multipartFile);
                return Result.OK("文件导入成功!");
            }
        } catch (Exception e) {
            e.printStackTrace();
            return Result.error("文件导入失败:" + e.getMessage());
        }
    }

2、service类:调用EasyExcel.read方法

	String doFileImport( String month,MultipartFile file) throws Exception;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String doFileImport(String month,MultipartFile file) throws Exception {
        log.info(" --- 执行开始,时间:"+ DateUtils.now()+"---");
        String code = PointStoreConst.SH + TimeUtil.getTimestampStr(TimeUtil.STR_yyyyMMddHHmmss);
        Trade trade = new Trade();
        trade.setCode(code);
        trade.setIzValid(PointStoreConst.NUM_0.toString());
        trade.setMonth(month);
        tradeMapper.insert(trade);

        StringBuilder errMsg = new StringBuilder();
        //获取计算积分的规则
        BusiRules rules = busiRulesService.getOne(new LambdaQueryWrapper<BusiRules>().isNotNull(BusiRules::getId).last("limit 1"));

        //部门信息
        List<SysDepart> departs = departService.list();

        EasyExcel.read(file.getInputStream(), TradeRecord.class, new TradeExcelListener(recordService, customerService,errMsg, rules, trade.getId(),trade.getMonth(), departs)).sheet().doRead();
        if (errMsg.length() > 0) {
            throw new Exception(errMsg.toString());
        }
        log.info(" --- 执行完毕,时间:"+ DateUtils.now()+"---");
        return trade.getId();
    }

3、在TradeExcelListener中进行解析以及处理

@Slf4j
public class TradeExcelListener extends AnalysisEventListener<TradeRecord> {
    /**
     * 行计数器
     */
    private AtomicInteger lineCounter = new AtomicInteger(1);

    /**
     * 错误信息
     */
    private StringBuilder errMsg;

    /**
     * 主表id
     */
    private String tradeId;

    /**
     * 主表月份
     */
    private String month;

    @Resource
    private ITradeRecordService recordServiceService;

    @Resource
    private ICusCustomerService customerService;

    /**
     * 导入的数据列表
     */
    private List<TradeRecord> recordList = new ArrayList<>();


    /**
     * 导入的客户列表
     */
    private List<BusMerchant> merchantList = new ArrayList<>();

    /**
     * 换算业务规则
     */
    private BusiRules rule;

    /**
     * 部门列表
     */
    private List<SysDepart> departs;

    /**
     * 构造器
     */
    public TradeExcelListener(ITradeRecordService tradeService, ICusCustomerService customerService, StringBuilder errMsg, BusiRules rule, String tradeId,String month, List<SysDepart> departs) {
        this.recordServiceService = tradeService;
        this.customerService = customerService;
        this.errMsg = errMsg;
        this.tradeId = tradeId;
        this.month = month;
        this.rule = rule;
        this.departs = departs;
    }

    /**
     * 获取表头
     *
     * @param dataMap
     * @param context
     */
    @Override
    public void invokeHeadMap(Map<Integer, String> dataMap, AnalysisContext context) {
        if (dataMap.size() != 11) {
            errMsg.append("格式错误!请下载模板再重试!");
            return;
        }
        if(!"网点".equals(dataMap.get(0))){
            errMsg.append("格式错误!请下载模板再重试!");
            return;
        }
    }

    @Override
    public void invoke(TradeRecord record, AnalysisContext context) {
        //log.info("解析到一条数据:{}", recordImpVo.toString());

        // 不为空说明有错误 不继续执行
        if (errMsg.length() > 0) {
            return;
        }
        try {
            record.setTradeId(tradeId);
            record.setMonth(month);
            record.setMoneyNum(ObjUtil.isEmpty(record.getMoneyNum()) ? new Double(0) : record.getMoneyNum());
            record.setTradeNum(ObjUtil.isEmpty(record.getTradeNum()) ? new Integer(0) : record.getTradeNum());
            record.setTradeDays(ObjUtil.isEmpty(record.getTradeDays()) ? new Integer(0) : record.getTradeDays());
            record.setMonthAvg(ObjUtil.isEmpty(record.getMonthAvg()) ? new Double(0) : record.getMonthAvg());
            //设置积分
            switch (rule.getBusiRules()) {
                 //积分的各种处理方法
            }
            //总和
            record.setIntegralSum(record.getMoneyNumInte() + record.getTradeNumInte() + record.getTradeDaysInte() + record.getMonthAvgInte());
            //数据加密
            record.setMerchCode(AESUtil.encrypt(record.getMerchCode(), AESUtil.KEY.getBytes()));

            //商户列表
            BusMerchant merchant= new BusMerchant();
            merchant.setMerchCode(record.getMerchCode()).setMerchAbbr(record.getMerchAbbr()).setConncatPhone(record.getConncatPhone());
            merchantList.add(merchant);
        } catch (Exception e) {
            log.error("积分转换失败:" + e.getMessage());
            errMsg.append("积分转换失败:" + e.getMessage());
            e.printStackTrace();
            return;
        }


        //设置部门id
        SysDepart dep = departs.stream().filter(d -> d.getOrgCode().equals(record.getOrgCode())).findFirst().orElse(null);
        if (Optional.ofNullable(dep).isPresent()) {
            record.setDepartId(dep.getId());
        } else {
            errMsg.append(lineCounter.get()+"行数据:部门编号不存在:" + record.getOrgCode() + "\n");
        }

        recordList.add(record);

        lineCounter.set(lineCounter.get() + 1);
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        if (errMsg.length() > 0) {
            return;
        }
        //执行存入数据方法
        try {
            recordServiceService.saveRecords(recordList);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

4、CountDownLatch处理列表数据后使用@Async注解方式保存

/**
	 * 处理列表数据,并保存
	 * @param recordList
	 */
	void saveRecords(List<TradeRecord> recordList) throws InterruptedException, SQLException;
/**
     * 导入时,只是增加商户数据记录,以及客户表信息、商户表信息
     *
     * @param recordList
     * @throws InterruptedException
     * @throws SQLException
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveRecords(List<TradeRecord> recordList) throws InterruptedException, SQLException {
		LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();

        List<TradeRecord> recordsAdd = new ArrayList<>();
        //客户
        List<CusCustomer> addCustomers = new ArrayList<>();
        //商户
        List<BusMerchant> addMerchantss = new ArrayList<>();

        //查询导入数据在客户表中的数据
        List<String> recordPhones = recordList.stream().distinct().map(TradeRecord::getConncatPhone).collect(Collectors.toList());
        recordPhones = recordPhones.stream().map(phone -> {
            try {
                return AESUtil.encrypt(phone, AESUtil.KEY.getBytes());
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }).collect(Collectors.toList());
        List<CusCustomer> customerList = customerService.list(new LambdaQueryWrapper<CusCustomer>().in(CusCustomer::getPhone, recordPhones));
        
       /**获取需要插入的商户编号集合*/
       List<String> addMerCodes = recordList.stream().distinct().map(TradeRecord::getMerchCode).collect(Collectors.toList());
       List<String> existCodeList = busMerchantService.list().stream().distinct().map(BusMerchant::getMerchCode).collect(Collectors.toList());
        //从导入List中移除已存在的商户List中包含的元素
        if(CollUtil.isNotEmpty(existCodeList)){
            addMerCodes.removeAll(existCodeList);
        }
        
       /**循环获取需要添加的客户信息、记录内容、商户信息*/
        CountDownLatch downLatch = new CountDownLatch(recordList.size());
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        recordList.stream().forEach(record -> {
            try {
                executorService.submit(() -> {
                    try {
                    	//处理内容
                      } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                    downLatch.countDown();
                });
            } catch (Exception e) {
                TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                throw new RuntimeException(e);
            }
        });
        executorService.shutdown();
        downLatch.await();

 		 //批量存入新客户
        if (CollUtil.isNotEmpty(addCustomers)) {
            customerService.saveCustomers(addCustomers);
            customerList.addAll(addCustomers);
        }
  
		//导入数据列表中客户id设置

        //保存记录
        if (CollUtil.isNotEmpty(recordsAdd)) {
            Date start = DateUtils.getDate();
            //每1000条数据插入开一个线程
            List<List<TradeRecord>> lists = groupList(recordsAdd, 1000);
            CountDownLatch countDownLatch = new CountDownLatch(lists.size());
            for (List<TradeRecord> listSub : lists) {
                executeAsync(listSub, countDownLatch);   //分批次执行数据
            }
            try {
                countDownLatch.await(); //保证之前的所有的线程都执行完成,才会走下面的; 这样就可以在下面拿到所有线程执行完的集合结果
            } catch (Exception e) {
                e.printStackTrace();
                log.error("阻塞异常:" + e.getMessage());
            }
            Date end = DateUtils.getDate();
            System.out.println("插入记录消耗时间" + DateUtils.getDiffSeconds(end, start));
        }

		 /**商户数据:存入不存在的商户数据*/
        if (CollUtil.isNotEmpty(addMerchantss)) {
            try {
                addMerchantss.stream().distinct().collect(Collectors.toList());//去重
                busMerchantService.saveMerchants(addMerchantss);//这里的插入要在mybatis.xml文件中使用foreach一次性插入,不要调用saveBatch()方法
            } catch (Exception e) {
                e.printStackTrace();
                log.error("存入商户数据失败:" + e.getMessage());
            }
        }
  }

数据分组:

 /**
     * 根据每个线程执行的数量拆分成多个数组
     *
     * @param list 所有数据
     * @param size 拆分每个批次数量
     */
    public List<List<TradeRecord>> groupList(List<TradeRecord> list, int size) {
        List<List<TradeRecord>> resList = new ArrayList<>();
        int totalBatches = (int) Math.ceil((double) list.size() / size);
        for (int i = 0; i < totalBatches; i++) {
            int startIndex = i * size;
            int endIndex = Math.min((i + 1) * size, list.size());
            List<TradeRecord> batch = list.subList(startIndex, endIndex);
            resList.add(batch);
        }
        return resList;
    }

executeAsync方法:

/**
     * 开启多线程
     *
     * @param list           每个批次的数据
     * @param countDownLatch Java 中的一个并发工具类,用于协调多个线程之间的同步
     */
    @Async("asyncServiceExecutor")
    public void executeAsync(List<TradeRecord> list, CountDownLatch countDownLatch) {
        try {
            log.warn("start executeAsync");
            //异步线程要做的事情 批量新增插入数据库
            tradeRecordMapper.saveRecords(list);
            log.warn("end executeAsync");
        } finally {
            countDownLatch.countDown();// 很关键, 无论上面程序是否异常必须执行countDown,否则await无法释放
        }
    }

其中的asyncServiceExecutor注解为自定义一个异步线程池,使用@EnableAsync注解

5、自定义异步线程池@EnableAsync


@Configuration
@EnableAsync
@Slf4j
public class ExecutorConfig {
    @Value("${async.executor.thread.core_pool_size}")
    private int corePoolSize;
    @Value("${async.executor.thread.max_pool_size}")
    private int maxPoolSize;
    @Value("${async.executor.thread.queue_capacity}")
    private int queueCapacity;
    @Value("${async.executor.thread.name.prefix}")
    private String namePrefix;

    @Bean(name = "asyncServiceExecutor")
    public Executor asyncServiceExecutor() {
        log.warn("start asyncServiceExecutor");
        //在这里修改
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //配置核心线程数
        executor.setCorePoolSize(corePoolSize);
        //配置最大线程数
        executor.setMaxPoolSize(maxPoolSize);
        //配置队列大小
        executor.setQueueCapacity(queueCapacity);
        //配置线程池中的线程的名称前缀
        executor.setThreadNamePrefix(namePrefix);
        // rejection-policy:当pool已经达到max size的时候,如何处理新任务
        // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //执行初始化
        executor.initialize();
        return executor;
    }
}

总结

1、不要使用Jeecgboot自动生成的poi.excel的ExcelImportUtil.importExcel()方法,可使用EasyExcel.read(),自定义继承AnalysisEventListener的监听方法来解析数据,方便数据预处理;
2、获取到导入数据列表后,进行再次数据处理,与其他模块功能联动时,使用线程,以及自定义异步线程池的方式提高效率;
3、插入到数据库的时候,不要直接调用mybatis plus的多次单条插入或者修改方法,使用mybatis中的foreach循环数据来插入,减少数据库连接压力;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

平芜尽处是春山-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值