• 使用easyexcel将csv转为excel


    一.背景

            供应商系统下载的csv文件不支持域控(主要是第三方wps服务不能对csv文件加密,但是可以对office系列产品进行权限访问的加密控制)。因此思路就改为现将csv文件转为excel文件,然后对excel文件进行加域控制。本文主要介绍如何将csv文件转为excel文件。

    二.要求

    1.         Csv文件可能比较大,达到40-60M,需要控制内存使用率;
      1.         考虑接口的并发,需要进行接口的限流
    2. 三.方案

      1.         采用alibaba的easyexcel,降低内存占用率,根据压测结果,设置合理的接口限流参数(限流
      2. 本文不再介绍,可以使用java注解+redis+lua, 或者nginx限流等)
      3. 四.代码

      4. CsvController

      1. package com.xxx.xxx.controller;
      2. import java.io.IOException;
      3. import java.io.InputStream;
      4. import java.io.OutputStream;
      5. import java.util.concurrent.ExecutionException;
      6. import java.util.concurrent.Future;
      7. import javax.annotation.Resource;
      8. import javax.servlet.http.HttpServletResponse;
      9. import com.xxx.xxx.common.utils.EasyExcelUtil;
      10. import com.xxx.xxx.common.utils.ObjectUtil;
      11. import com.xxx.xxx.service.ExcelAnalysisService;
      12. import lombok.extern.slf4j.Slf4j;
      13. import org.springframework.web.bind.annotation.PostMapping;
      14. import org.springframework.web.bind.annotation.RequestMapping;
      15. import org.springframework.web.bind.annotation.RequestParam;
      16. import org.springframework.web.bind.annotation.RestController;
      17. import org.springframework.web.multipart.MultipartFile;
      18. /**
      19. * description:
      20. *
      21. * @author: lgq
      22. * @create: 2024-04-16 11:06
      23. */
      24. @Slf4j
      25. @RestController
      26. @RequestMapping("/csv")
      27. public class CsvController {
      28. @Resource
      29. private ExcelAnalysisService excelAnalysisService;
      30. /**
      31. * 读取传入的csv 文本的内容可以存入数据库
      32. *
      33. * @param file
      34. * @return
      35. */
      36. @PostMapping("/uploadCsvAndImportExcel")
      37. public void uploadCsvAndImportExcel(@RequestParam("file") MultipartFile file, HttpServletResponse response) {
      38. String[] splitName = file.getOriginalFilename().split(".csv");
      39. if (ObjectUtil.isEmpty(splitName) || ObjectUtil.isEmpty(splitName[0])) {
      40. return;
      41. }
      42. EasyExcelUtil.setResponseParam(response, splitName[0]);
      43. long startTime = System.currentTimeMillis();
      44. log.info("导出开始时间:{}", startTime);
      45. try {
      46. // 输出流可以为本地文件
      47. // OutputStream outputStream = new FileOutputStream("D:\\templateExcel\\filename.xlsx");
      48. OutputStream outputStream = response.getOutputStream();
      49. InputStream inputStream = file.getInputStream();
      50. Future future = excelAnalysisService.csv2Excel(inputStream, outputStream);
      51. future.get();
      52. } catch (IOException ioException) {
      53. log.error("csv转为excel出错!", ioException.getMessage());
      54. ioException.printStackTrace();
      55. } catch (InterruptedException interruptedException) {
      56. log.error("csv转为excel出错!", interruptedException.getMessage());
      57. interruptedException.printStackTrace();
      58. } catch (ExecutionException executionException) {
      59. log.error("csv转为excel出错!", executionException.getMessage());
      60. executionException.printStackTrace();
      61. }
      62. // 导出时间结束
      63. long endTime = System.currentTimeMillis();
      64. log.info("导出结束时间:{}", endTime + "ms");
      65. log.info("导出所用时间:{}", (endTime - startTime) / 1000 + "秒");
      66. }
      67. }

      EasyExcelGeneralCsvListener 

      1. package com.xxx.xxx.listener;
      2. import java.util.ArrayList;
      3. import java.util.Collections;
      4. import java.util.List;
      5. import java.util.Map;
      6. import com.alibaba.excel.ExcelWriter;
      7. import com.alibaba.excel.context.AnalysisContext;
      8. import com.alibaba.excel.event.AnalysisEventListener;
      9. import com.alibaba.excel.write.metadata.WriteSheet;
      10. import com.xxx.xxx.constants.ExcelConstants;
      11. /**
      12. * description:
      13. *
      14. * @author: lgq
      15. * @create: 2024-04-16 11:25
      16. */
      17. public class EasyExcelGeneralCsvListener extends AnalysisEventListener> {
      18. /**
      19. * 用于存储读取的数据
      20. */
      21. private List> dataList = new ArrayList<>();
      22. private ExcelWriter excelWriter;
      23. private WriteSheet writeSheet;
      24. public EasyExcelGeneralCsvListener() {
      25. }
      26. public EasyExcelGeneralCsvListener(ExcelWriter excelWriter, WriteSheet writeSheet) {
      27. this.excelWriter = excelWriter;
      28. this.writeSheet = writeSheet;
      29. }
      30. @Override
      31. public void invoke(Map data, AnalysisContext context) {
      32. // 数据add进入集合
      33. dataList.add(data);
      34. // size是否为2000条:这里其实就是分批.当数据等于2k的时候执行一次写入excel
      35. if (dataList.size() >= ExcelConstants.PER_WRITE_EXCEL_ROW_COUNT) {
      36. save2Excel();
      37. // 清理集合便于GC回收
      38. dataList.clear();
      39. }
      40. }
      41. @Override
      42. public void invokeHeadMap(Map headers, AnalysisContext context) {
      43. List> titles = new ArrayList<>();
      44. for (int i = 0; i < headers.size(); i++) {
      45. titles.add(Collections.singletonList(headers.get(i)));
      46. }
      47. this.writeSheet.setHead(titles);
      48. }
      49. /**
      50. * 保存数据到 excel
      51. */
      52. private void save2Excel() {
      53. if (dataList.size() > 0) {
      54. List> consumerDataList = new ArrayList<>();
      55. dataList.stream().forEach( e ->
      56. {
      57. List objects = new ArrayList<>();
      58. for (int i = 0; i < e.size(); i++) {
      59. objects.add(e.get(i));
      60. }
      61. consumerDataList.add(objects);
      62. }
      63. );
      64. this.excelWriter.write(consumerDataList, writeSheet);
      65. }
      66. }
      67. /**
      68. * Excel 中所有数据解析完毕会调用此方法
      69. */
      70. @Override
      71. public void doAfterAllAnalysed(AnalysisContext context) {
      72. save2Excel();
      73. dataList.clear();
      74. }
      75. }

      VisiableThreadPoolTaskExecutor

      1. package com.xxx.xxx.task;
      2. import java.util.concurrent.Callable;
      3. import java.util.concurrent.Future;
      4. import java.util.concurrent.ThreadPoolExecutor;
      5. import lombok.extern.slf4j.Slf4j;
      6. import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
      7. import org.springframework.util.concurrent.ListenableFuture;
      8. /**
      9. * description:VisiableThreadPoolTaskExecutor
      10. *
      11. * @author: lgq
      12. * @create: 2024-04-17 10:52
      13. */
      14. @Slf4j
      15. public class VisiableThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
      16. private void showThreadPoolInfo(String prefix){
      17. ThreadPoolExecutor threadPoolExecutor = getThreadPoolExecutor();
      18. if(null==threadPoolExecutor){
      19. return;
      20. }
      21. log.info("{}, {},taskCount [{}], completedTaskCount [{}], activeCount [{}], queueSize [{}]",
      22. this.getThreadNamePrefix(),
      23. prefix,
      24. threadPoolExecutor.getTaskCount(),
      25. threadPoolExecutor.getCompletedTaskCount(),
      26. threadPoolExecutor.getActiveCount(),
      27. threadPoolExecutor.getQueue().size());
      28. }
      29. @Override
      30. public void execute(Runnable task) {
      31. showThreadPoolInfo("1. do execute");
      32. super.execute(task);
      33. }
      34. @Override
      35. public void execute(Runnable task, long startTimeout) {
      36. showThreadPoolInfo("2. do execute");
      37. super.execute(task, startTimeout);
      38. }
      39. @Override
      40. public Future submit(Runnable task) {
      41. showThreadPoolInfo("1. do submit");
      42. return super.submit(task);
      43. }
      44. @Override
      45. public Future submit(Callable task) {
      46. showThreadPoolInfo("2. do submit");
      47. return super.submit(task);
      48. }
      49. @Override
      50. public ListenableFuture submitListenable(Runnable task) {
      51. showThreadPoolInfo("1. do submitListenable");
      52. return super.submitListenable(task);
      53. }
      54. @Override
      55. public ListenableFuture submitListenable(Callable task) {
      56. showThreadPoolInfo("2. do submitListenable");
      57. return super.submitListenable(task);
      58. }
      59. }
      ExcelAnalysisService
      1. package com.xxx.xxx.service;
      2. import java.io.OutputStream;
      3. import java.io.InputStream;
      4. import java.util.concurrent.Future;
      5. /**
      6. * description:excel文档分析处理类
      7. *
      8. * @author: lgq
      9. * @create: 2024-04-17 11:42
      10. */
      11. public interface ExcelAnalysisService {
      12. /**
      13. * csv文档转为excel文档
      14. */
      15. Future csv2Excel(InputStream inputStream, OutputStream outputStream);
      16. }

      ExcelAnalysisServiceImpl

      1. package com.xxx.xxx.service.impl;
      2. import java.io.OutputStream;
      3. import java.nio.charset.Charset;
      4. import com.alibaba.excel.EasyExcel;
      5. import com.alibaba.excel.ExcelWriter;
      6. import com.alibaba.excel.support.ExcelTypeEnum;
      7. import com.alibaba.excel.write.metadata.WriteSheet;
      8. import com.xxx.xxx.listener.EasyExcelGeneralCsvListener;
      9. import com.xxx.xxx.service.ExcelAnalysisService;
      10. import lombok.extern.slf4j.Slf4j;
      11. import java.io.InputStream;
      12. import java.util.concurrent.Future;
      13. import org.springframework.scheduling.annotation.Async;
      14. import org.springframework.scheduling.annotation.AsyncResult;
      15. import org.springframework.stereotype.Service;
      16. /**
      17. * description:ExcelAnalysisService实现类
      18. *
      19. * @author: lgq
      20. * @create: 2024-04-17 14:53
      21. */
      22. @Service
      23. @Slf4j
      24. public class ExcelAnalysisServiceImpl implements ExcelAnalysisService {
      25. @Async("asyncExcelAnalysisServiceExecutor")
      26. @Override
      27. public Future csv2Excel(InputStream inputStream, OutputStream outputStream) {
      28. try {
      29. ExcelWriter writer = EasyExcel.write(outputStream).excelType(ExcelTypeEnum.XLSX).build();
      30. EasyExcel.read(inputStream, new EasyExcelGeneralCsvListener(writer, new WriteSheet()))
      31. .excelType(ExcelTypeEnum.CSV)
      32. .charset(Charset.forName("UTF-8"))
      33. .sheet()
      34. .doRead();
      35. writer.finish();
      36. outputStream.flush();
      37. } catch (Exception e) {
      38. log.error("csv转为excel出错!", e.getMessage());
      39. e.printStackTrace();
      40. } finally {
      41. if (outputStream != null) {
      42. try {
      43. outputStream.close();
      44. } catch (Exception e) {
      45. log.error("outputStream.close() -> csv转为excel出错!", e.getMessage());
      46. e.printStackTrace();
      47. }
      48. }
      49. if (inputStream != null) {
      50. try {
      51. inputStream.close();
      52. } catch (Exception e) {
      53. log.error("inputStream.close() -> csv转为excel出错!", e.getMessage());
      54. e.printStackTrace();
      55. }
      56. }
      57. }
      58. return new AsyncResult<>("task complete!");
      59. }
      60. }

      ExecutorConfig

      1. package com.xxx.xxx.config;
      2. import java.util.concurrent.Executor;
      3. import java.util.concurrent.ThreadPoolExecutor;
      4. import com.xxx.xxx.task.VisiableThreadPoolTaskExecutor;
      5. import lombok.extern.slf4j.Slf4j;
      6. import org.springframework.context.annotation.Bean;
      7. import org.springframework.context.annotation.Configuration;
      8. import org.springframework.scheduling.annotation.EnableAsync;
      9. import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
      10. /**
      11. * description:线程池配置类
      12. *
      13. * @author: lgq
      14. * @create: 2024-04-17 10:28
      15. */
      16. @Configuration
      17. @Slf4j
      18. @EnableAsync
      19. public class ExecutorConfig {
      20. private static int corePoolSize = Runtime.getRuntime().availableProcessors() + 1;
      21. private static int maxPoolSize = Runtime.getRuntime().availableProcessors() + 1;
      22. private static int queueCapacity = 100;
      23. private static final String namePrefix = "ExcelAnalysis";
      24. @Bean(name = "asyncExcelAnalysisServiceExecutor")
      25. public Executor asyncExcelServiceExecutor() {
      26. log.info("start asyncExcelAnalysisServiceExecutor----------------");
      27. //ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
      28. //使用可视化运行状态的线程池
      29. ThreadPoolTaskExecutor executor = new VisiableThreadPoolTaskExecutor();
      30. //配置核心线程数
      31. executor.setCorePoolSize(corePoolSize);
      32. //配置最大线程数
      33. executor.setMaxPoolSize(maxPoolSize);
      34. //配置队列大小
      35. executor.setQueueCapacity(queueCapacity);
      36. //配置线程池中的线程的名称前缀
      37. executor.setThreadNamePrefix(namePrefix);
      38. // rejection-policy:当pool已经达到max size的时候,如何处理新任务
      39. // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
      40. executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
      41. //执行初始化
      42. executor.initialize();
      43. log.info("end asyncExcelAnalysisServiceExecutor------------");
      44. return executor;
      45. }
      46. }

      ExcelConstants

      1. package com.xxx.xxx.constants;
      2. /**
      3. * description:线程池配置类
      4. *
      5. * @author: lgq
      6. * @create: 2024-04-17 10:28
      7. */
      8. public class ExcelConstants {
      9. public static final Integer PER_SHEET_ROW_COUNT = 100*10000;
      10. public static final Integer PER_WRITE_ROW_COUNT = 20*10000;
      11. public static final Integer PER_WRITE_EXCEL_ROW_COUNT = 2 * 1000;
      12. public static final Integer GENERAL_ONCE_SAVE_TO_DB_ROWS_JDBC = 10*10000;
      13. public static final Integer GENERAL_ONCE_SAVE_TO_DB_ROWS_MYBATIS = 5*10000;
      14. }

      配置文件

      1. spring:
      2. servlet:
      3. multipart:
      4. enabled: true
      5. max-file-size: 100MB # 单个文件的最大值
      6. max-request-size: 100MB # 上传文件总的最大值

      pom依赖

      1. com.alibaba
      2. easyexcel
      3. 3.3.2
    3. 五.压测

    4. jvm参数(本地电脑,性能较差)
    5. -Xms2g -Xmx2g
    6. 导出日志

    性能监控

    压测结果

  • 相关阅读:
    大数据之路阿里巴巴实践
    mybatis
    Jupyter安装启动、登录密码问题解决
    微信小游戏5月畅销榜,新老产品更替显著,亿级爆款频出
    ES6(1) let 和 const
    外贸爬虫系统
    【NestJS系列】核心概念:Providers提供者
    Nanoprobes丨Nanogold 印迹和免疫化学技术
    关于VO、BO、DTO、PO、Entity、DO的说法
    echart 雷达图
  • 原文地址:https://blog.csdn.net/lgq2016/article/details/137932007