EasyExcel 4.X 读写数据

文章目录

    • EasyExcel与SpringBoot集成
    • 读数据
      • 读取数据的流程
      • 定义实体类
      • 简单读取
        • 自定义监听器
      • 读取指定sheet和所有sheet
      • 多行头读取
      • 数据格式转换
        • 列表数据
        • 实体类
        • 自定义转换器
        • 自定义监听器
        • 数据读取
    • 写数据
      • 简单数据写出
        • 存储到磁盘
        • 返回前端下载
      • 写出指定列宽,和数值精度丢失问题
        • 设置列宽
        • 精度丢失问题
        • 同一sheet写多次
        • 写出多个sheet
        • 自定义样式
      • 合并单元格
        • 通过注解方式合并
        • 自定义合并
    • 百万级别数据读写
      • 多线程读取
        • 多线程读取监听器
        • 线程任务
      • 多线程写出
        • 造数据
        • 实现分析

EasyExcel是Ailibaba团队提供的一个基于Java的、快速、简洁、解决大文件内存溢出的Excel处理工具。
在不用考虑性能、内存等因素下,快速完成Excel的读、写等功能。

官方地址:https://easyexcel.opensource.alibaba.com/

EasyExcel与SpringBoot集成

创建SpringBoot项目后引入EasyExcel依赖即可,其他依赖可按须引入。思考:这玩意为什么不是一个stater呢?

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

读数据

读取数据就是将excel中的数据读到程序中,可以进行逻辑处理之后将数据根据业务需求存储到数据库、写到另一个文件中或者其他的处理均可。数据读取依据监听器实现,可通过匿名内部类的形式,也可以新建类创建监听器。问:两者的最佳应用场景是什么?

读取数据的流程

  • 需要有一个excel,判断excel中的数据格式和excel的sheet页
  • 创建对应的实体类,对应类型的字段,存储读取的数据
  • 选用不同的读取方式即可

定义实体类

@Data
public class StockEntity {// 数据库Idprivate Long id;// 商品序号@ExcelProperty(value = "序号")private Integer productNo;// 商品编码@ExcelProperty(value = "商品编码")private String productCode;// 商品类型@ExcelProperty(value = "类型")private String productType;// 商品品牌@ExcelProperty(value = "品牌")private String productBrand;// 实物库存@ExcelProperty(value = "实物库存")private String productStock;// 成本 浮点类型@ExcelProperty(value = "成本")private BigDecimal costPrice;}

简单读取

public void readSimpleExcel(MultipartFile file) {try {InputStream inputStream = file.getInputStream();EasyExcel.read(inputStream, StockEntity.class,new PageReadListener<StockEntity>(dataList -> {for (StockEntity stockEntity : dataList) {log.info("读取到一条数据:{}", stockEntity);}})).sheet().doRead();} catch (IOException e) {throw new RuntimeException(e);}
}
自定义监听器

新建类实现ReadListener接口,泛型指定要将数据存到到哪个实体中

package com.stt.listener.read;import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.stt.entity.StockEntity;
import lombok.extern.slf4j.Slf4j;/*** @author Jshuai* @description* @date 2024-10-29 22:14* 自定义简单数据读取的监听器*/
@Slf4j
public class SimpleReadListener implements ReadListener<StockEntity> {/*** 每读取一条数据,就会调用invoke方法* @param stockEntity* @param analysisContext*/@Overridepublic void invoke(StockEntity stockEntity, AnalysisContext analysisContext) {log.info("读取到一条数据:{}", stockEntity);}/*** 读取完成之后,会调用doAfterAllAnalysed方法,做一些数据清理的操作* @param analysisContext*/@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {log.info("读取完成!");}
}

读取指定sheet和所有sheet

public void readMultiSheetExcel(MultipartFile file) {// 读取所有的// EasyExcel.read(file.getInputStream(), StockEntity.class,new SimpleReadListener()).doReadAll();// 读取指定的sheet页try (ExcelReader excelReader = EasyExcel.read(file.getInputStream()).build()) {// 这里为了简单 所以注册了 同样的head 和Listener 自己使用功能必须不同的ListenerReadSheet readSheet1 =EasyExcel.readSheet(0).head(StockEntity.class).registerReadListener(new SimpleReadListener()).build();ReadSheet readSheet2 =EasyExcel.readSheet(1).head(StockEntity.class).registerReadListener(new SimpleReadListener()).build();// 这里注意 一定要把sheet1 sheet2 一起传进去,不然有个问题就是03版的excel 会读取多次,浪费性能excelReader.read(readSheet1, readSheet2);} catch (IOException e) {throw new RuntimeException(e);}
}

多行头读取

public void readMultiHeadExcel(MultipartFile file) {try {EasyExcel.read(file.getInputStream(), StockEntity.class, new SimpleReadListener()).sheet(0)// 这里可以设置1,因为头就是一行。如果多行头,可以设置其他值。不传入也可以,因为默认会根据DemoData 来解析,他没有指定头,也就是默认1行.headRowNumber(2).doRead();} catch (IOException e) {throw new RuntimeException(e);}
}

数据格式转换

部分数据读写场景涉及到数据格式问题,需要转换,比如性别,数据库中存储的一般是数字类型【0表示男,1表示女】,时间在不同系统中也有固定的格式,EasyExcel实现类型转换有两种方式:

  • 通过@DateTimeFormat和@NumberFormat对日期时间和数字格式转换
  • 通过实现Converter接口实现读写数据的格式转换

以员工数据为例:

列表数据
姓名性别出生日期
加油鸭2024/10/10
酱香鸭2024/10/10
果木鸭保密2024/10/10
实体类

其中birthday通过easyexcel提供的注解设置格式,性别通过自定义的GenderConverter实现

public class MyUser {@ExcelProperty(value = "姓名",index = 0)private String name;@ExcelProperty(value = "性别",index = 1,converter = GenderConverter.class)private Integer gender;@ExcelProperty(value = "出生日期",index = 2)@DateTimeFormat("yyyy-MM-dd")private LocalDate birthday;
}
自定义转换器
@Slf4j
public class GenderConverter implements Converter<Integer> {/*** 这里读的时候会调用*/@Overridepublic Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {// 获取读取的字符串String gender = cellData.getStringValue();log.info("gender===》{}",gender);// 转换成对应的数字return GenderEnum.getCodeByLabel(gender);}
}
自定义监听器

不同的数据实体需要不同的监听器,此处定义一个读取员工数据的监听器,泛型修改,并且提供list来存储读取到的数据,外部可以通过get方法获取集合数据

@Slf4j
public class UserReadListener implements ReadListener<MyUser> {@Getterprivate List<MyUser> data;public UserReadListener() {this.data = new ArrayList<>();}@Overridepublic void invoke(MyUser myUser, AnalysisContext analysisContext) {data.add(myUser);}@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {log.info("所有数据解析完成!");}}
数据读取
public void readByConverterData(MultipartFile file) {try {// 创建监听器UserReadListener readListener = new UserReadListener();EasyExcel.read(file.getInputStream(), MyUser.class, readListener).sheet(2).doRead();// 获取读取到的数据List<MyUser> data = readListener.getData();data.forEach(System.out::println);} catch (IOException e) {throw new RuntimeException(e);}
}

写数据

写数据就是将系统内的数据写到excel中,可以将文件写到磁盘或者提供给前端下载,本案例基于MySQL +Mybatis实现从数据库中读取数据写出到excel中

简单数据写出

存储到磁盘
public void demo1(HttpServletResponse response) {List<TbExcel> tbExcels = excelMapper.selectList(null);List<ExcelVO> excelVOS = BeanUtil.copyToList(tbExcels, ExcelVO.class);EasyExcel.write("C:\\demo1.xlsx", ExcelVO.class).sheet("sheet1").doWrite(excelVOS);
}
返回前端下载
public void demo1(HttpServletResponse response) {// 查询数据库数据,查询所有的List<TbExcel> tbExcels = excelMapper.selectList(null);// 将TbExcel转换为ExcelVOList<ExcelVO> excelVOS = BeanUtil.copyToList(tbExcels, ExcelVO.class);// 提供给前端下载,需要使用到HttpServletResponse对象// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postmanresponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");// 强制调起下载
//        response.setContentType("application/octet-stream;charset=UTF-8");response.setCharacterEncoding("utf-8");try {String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");EasyExcel.write(response.getOutputStream(), ExcelVO.class).sheet("模板").doWrite(excelVOS);} catch (IOException e) {throw new RuntimeException(e);}
}

写出指定列宽,和数值精度丢失问题

设置列宽

通过@ColumnWidth注解设置

public class ExcelVO {@ExcelProperty(value = "ID", index = 0)private Long id;@ExcelProperty(value = "字符串", index = 1)private String strCol;@ExcelProperty(value = "浮点数字", index = 2)@ColumnWidth(12)private BigDecimal decCol;@ExcelProperty(value = "日期时间", index = 3)@ColumnWidth(18)private LocalDateTime datetimeCol;
}
精度丢失问题

通过设置转换器实现,将Long转为String导出

EasyExcel.write(response.getOutputStream(), ExcelVO.class).registerConverter(new LongStringConverter()).sheet("模板").doWrite(excelVOS);
同一sheet写多次
public void demo2(HttpServletResponse response) {List<TbExcel> tbExcels = excelMapper.selectList(null);List<ExcelVO> excelVOS = BeanUtil.copyToList(tbExcels, ExcelVO.class);try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), ExcelVO.class).build()) {// 这里注意 有伙伴反应使用swagger 会导致各种问题,请直接用浏览器或者用postmanresponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setCharacterEncoding("utf-8");// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");// 这里注意 如果同一个sheet只要创建一次WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();// 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来for (int i = 0; i < 5; i++) {excelWriter.write(excelVOS, writeSheet);}} catch (IOException e) {throw new RuntimeException(e);}
}
写出多个sheet

根据不同的维度将数据分别存到不同的sheet表中,比如根据日期分类

public void demo3(HttpServletResponse response) {List<TbExcel> tbExcels = excelMapper.selectList(null);List<ExcelVO> excelVOS = BeanUtil.copyToList(tbExcels, ExcelVO.class);try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), ExcelVO.class).build()) {response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setCharacterEncoding("utf-8");// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");// 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面Map<LocalDateTime, List<ExcelVO>> collect = excelVOS.stream().collect(Collectors.groupingBy(ExcelVO::getDatetimeCol));int index = 0;for (LocalDateTime localDateTime : collect.keySet()) {WriteSheet writeSheet = EasyExcel.writerSheet(index, localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))).build();// 分页去数据库查询数据 这里可以去数据库查询每一页的数据excelWriter.write(collect.get(localDateTime), writeSheet);index++;}} catch (IOException e) {throw new RuntimeException(e);}
}
自定义样式

easyexcel导出的表格有默认样式,我们还可以自定义头,表格内容和字体样式

public void demo4(HttpServletResponse response) {// 头的策略WriteCellStyle headWriteCellStyle = new WriteCellStyle();// 背景设置为红色headWriteCellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());WriteFont headWriteFont = new WriteFont();headWriteFont.setFontHeightInPoints((short)20);headWriteCellStyle.setWriteFont(headWriteFont);// 内容的策略WriteCellStyle contentWriteCellStyle = new WriteCellStyle();// 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.头默认了 FillPatternType所以可以不指定contentWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);// 背景绿色contentWriteCellStyle.setFillForegroundColor(IndexedColors.GREEN.getIndex());WriteFont contentWriteFont = new WriteFont();// 字体大小contentWriteFont.setFontHeightInPoints((short)20);contentWriteCellStyle.setWriteFont(contentWriteFont);// 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现HorizontalCellStyleStrategy horizontalCellStyleStrategy =new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);List<TbExcel> tbExcels = excelMapper.selectList(null);List<ExcelVO> excelVOS = BeanUtil.copyToList(tbExcels, ExcelVO.class);response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setCharacterEncoding("utf-8");String fileName = null;try {fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");EasyExcel.write(response.getOutputStream(), ExcelVO.class).registerWriteHandler(horizontalCellStyleStrategy).sheet("模板").doWrite(excelVOS);} catch (IOException e) {throw new RuntimeException(e);}
}

合并单元格

通过注解方式合并
@Data
@AllArgsConstructor
@NoArgsConstructor
// 将第6-7行的2-3列合并成一个单元格
// @OnceAbsoluteMerge(firstRowIndex = 5, lastRowIndex = 6, firstColumnIndex = 1, lastColumnIndex = 2)
public class ExcelVO {// 每两行合并@ContentLoopMerge(eachRow = 2)@ExcelProperty(value = "ID", index = 0)private Long id;@ExcelProperty(value = "字符串", index = 1)private String strCol;@ExcelProperty(value = "浮点数字", index = 2)private BigDecimal decCol;@ExcelProperty(value = "日期时间", index = 3)private LocalDateTime datetimeCol;
}
自定义合并
public void demo5(HttpServletResponse response) {List<ExcelVO> excelVOS = new ArrayList<>();excelVOS.add(new ExcelVO(1L,"字符串1",BigDecimal.ONE, LocalDateTime.now()));excelVOS.add(new ExcelVO(1L,"字符串2",BigDecimal.ONE, LocalDateTime.now()));excelVOS.add(new ExcelVO(2L,"字符串3",BigDecimal.ONE, LocalDateTime.now()));excelVOS.add(new ExcelVO(2L,"字符串4",BigDecimal.ONE, LocalDateTime.now()));// 相加BigDecimal sum = excelVOS.stream().map(ExcelVO::getDecCol).reduce(BigDecimal.ZERO, BigDecimal::add);// 将合计追加到集合中excelVOS.add(new ExcelVO(null,"合计", sum , null));response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setCharacterEncoding("utf-8");String fileName = null;try {fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");// 设置合并规则LoopMergeStrategy loopMergeStrategy = new LoopMergeStrategy(2, 0);EasyExcel.write(response.getOutputStream(), ExcelVO.class).sheet("模板").registerWriteHandler(loopMergeStrategy).doWrite(excelVOS);} catch (IOException e) {throw new RuntimeException(e);}}

百万级别数据读写

多线程读取

  • 思路

    • 分析瓶颈在哪里?
    • 数据读取的性能:读取可以使用多线程【重点】
    • 数据库中插入数据,这个性能是比较不错的,可以使用批量插入【使用数据库连接池】
  • 多线程读取数据的问题

    • 需要避免重复读取,每个线程需要设置读取哪个区间的数据
public void importExcel(MultipartFile file) {try {// 多线程读取EasyExcel.read(file.getInputStream(),ExcelVO.class,new MutliReadListener(excelMapper)).sheet().doRead();} catch (IOException e) {throw new RuntimeException(e);}}
多线程读取监听器
@Service
@Slf4j
@NoArgsConstructor
public class MutliReadListener extends AnalysisEventListener<ExcelVO> {private ExcelMapper excelMapper;public MutliReadListener(ExcelMapper excelMapper) {this.excelMapper = excelMapper;}/*** 使用线程安全集合*/private List<ExcelVO> dataList = Collections.synchronizedList(new ArrayList<>());/*** 创建线程池必要参数*/private static final int CORE_POOL_SIZE = 5;//核心线程数private static final int MAX_POOL_SIZE = 10;//最大线程数private static final int QUEUE_CAPACITY = 100;//队列大小private static final Long KEEP_ALIVE_TIME = 1L;//存活时间@Overridepublic void invoke(ExcelVO data, AnalysisContext context) {if (dataList != null) {dataList.add(data);}}@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {//创建线程池ExecutorService executor = new ThreadPoolExecutor(CORE_POOL_SIZE,MAX_POOL_SIZE,KEEP_ALIVE_TIME,TimeUnit.SECONDS,new ArrayBlockingQueue<>(QUEUE_CAPACITY),new ThreadPoolExecutor.CallerRunsPolicy());//指定每个线程需要处理的导入数量,假设每个线程处理1000条int singleThreadDealCount = 100000;//根据假设每个线程需要处理的数量以及总数,计算需要提交到线程池的线程数量int threadsize = (dataList.size() / singleThreadDealCount) + 1;log.info("threadsize===>{}",threadsize);//计算需要导入的数据总数,用于拆分时线程需要处理数据时使用int rowsize = dataList.size() + 1;//测试开始时间long startTime = System.currentTimeMillis();//申明该线程需要处理数据的开始位置int startPosition = 0;//申明该线程需要处理数据的结束位置int endPosition = 0;//为了让每个线程执行完后回到当前线程,使用CountDownLatch,值为线程数,每次线程执行完就会执行countDown方法减1,为0后回到主CountDownLatch count = new CountDownLatch(threadsize);//计算每个线程要处理的数据for (int i = 0; i < threadsize; i++) {//如果是最后一个线程,为保证程序不发生空指针异常,特殊判断结束位置if ((i + 1) == threadsize) {//计算开始位置startPosition = (i * singleThreadDealCount);//当前线程为划分的最后一个线程,则取总数据的最后为此线程的结束位置endPosition = rowsize - 1;} else {//计算开始位置startPosition = (i * singleThreadDealCount);//计算结束位置endPosition = (i + 1) * singleThreadDealCount;}log.info("线程开启====》count:{},startPosition:{},endPosition:{}",count,startPosition,endPosition);DeadMainThread deadMainThread = new DeadMainThread(count, excelMapper, dataList, startPosition, endPosition);executor.execute(deadMainThread);}try {count.await();} catch (InterruptedException e) {e.printStackTrace();}//逻辑处理完,关闭线程池executor.shutdown();long endTime = System.currentTimeMillis();System.out.println("总耗时:" + (endTime - startTime));}
}
线程任务
@Component
@Slf4j
public class DeadMainThread implements Runnable {/*** 当前线程需要处理的总数据中的开始位置*/private int startPosition;/*** 当前线程需要处理的,总数据中的结束位置*/private int endPosition;/*** 需要处理的拆分之前的全部数据*/private List<ExcelVO> list = Collections.synchronizedList(new ArrayList<>());private CountDownLatch count;private ExcelMapper excelMapper;public DeadMainThread() {}public DeadMainThread(CountDownLatch count, ExcelMapper excelMapper, List<ExcelVO> list, int startPosition, int endPosition) {this.startPosition = startPosition;this.endPosition = endPosition;this.excelMapper = excelMapper;this.list = list;this.count = count;}@Overridepublic void run() {try {List<ExcelVO> newList = list.subList(startPosition, endPosition);//批量新增excelMapper.insertBatch(BeanUtil.copyToList(newList, TbExcel.class));} catch (Exception e) {e.printStackTrace();} finally {//当一个线程执行完了计数要减一不然这个线程会被一直挂起count.countDown();log.info("减一===》{}",count.getCount());}}
}

多线程写出

造数据
public void init() {LocalDateTime now = LocalDateTime.now();// 通过计数器,信号量CountDownLatch countDownLatch = new CountDownLatch(100);for (int i = 0; i < 100; i++) {// 开启虚拟线程Thread.ofVirtual().start(() -> {List<TbExcel> list = new ArrayList<>();for (int j = 0; j < 10000; j++) {list.add(new TbExcel("字符串" + j, new BigDecimal(j), now));}excelMapper.insertBatch(list);// 计数器减一countDownLatch.countDown();log.info("线程{}执行完毕", Thread.currentThread().getName());});}// 是否结束try {countDownLatch.await();log.info("初始化完成");} catch (InterruptedException e) {throw new RuntimeException(e);}
}
实现分析
  • 百万级别的数据考虑两个核心问题

    • 内存不能爆掉,不能产生OOM
    • 速度,性能
  • 实现逻辑就是多线程,导出数据瓶颈在

    • 数据查询,一次性查询多少数据
    • 内存不能溢出

至于数据写到excel中,这个就是easyexcel的表现了。

public void exportData(HttpServletResponse response) {long start1 = System.currentTimeMillis();// 每次查询10万条,考虑性能,内存Integer pageSize = 100000;// 线程池大小跟cpu有关,一般是cpu数量 * 2 + 1Integer poolSize = 10;// 随机文件名String fileName = String.valueOf(UUID.randomUUID());// 查询数据总数Long totalCount = excelMapper.selectCount(null);if (totalCount == 0) {log.info("没有数据需要导出");return; // 如果没有数据,直接返回}int loopCount = (int) Math.ceil((double) totalCount / pageSize);  // 使用 Math.ceil 计算循环次数// 设置final CountDownLatch latch = new CountDownLatch(loopCount);log.info("要查询的次数===>{}", loopCount);ExecutorService executorService = Executors.newFixedThreadPool(poolSize);OutputStream outputStream = null;try {outputStream = response.getOutputStream();// 创建写对象ExcelWriter excelWriter = EasyExcel.write(outputStream).build();for (int i = 0; i < loopCount; i++) {final int pageNum = i + 1; // 改为从1开始,直接使用 i + 1 作为页码executorService.execute(() -> {long start = System.currentTimeMillis();// 查询数据IPage<TbExcel> data = excelMapper.selectPage(new Page<>(pageNum, pageSize), null);List<TbExcel> records = data.getRecords();log.info("第{}页,查询耗时===>{}", pageNum,System.currentTimeMillis() - start);WriteSheet writeSheet = EasyExcel.writerSheet(pageNum ,"第" + pageNum + "页").head(ExcelVO.class).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build();synchronized (excelWriter) {long start2 = System.currentTimeMillis();excelWriter.write(BeanUtil.copyToList(records, ExcelVO.class), writeSheet);log.info("数据写出耗时===》{}",System.currentTimeMillis() - start2);}latch.countDown();});}latch.await();response.setContentType("application/octet-stream");response.setCharacterEncoding("utf-8");response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");excelWriter.finish();outputStream.flush();executorService.shutdown(); // 关闭线程池outputStream.close();log.info("总耗时====》{}",System.currentTimeMillis() - start1);} catch (IOException | InterruptedException e) {throw new RuntimeException(e);}}/*** 虚拟线程* @param response*/
@Override
public void exportData2(HttpServletResponse response) {long start1 = System.currentTimeMillis();Integer pageSize = 100000;Integer poolSize = 10;String fileName = String.valueOf(UUID.randomUUID());// 查询数据总数Long totalCount = excelMapper.selectCount(null);if (totalCount == 0) {log.info("没有数据需要导出");return; // 如果没有数据,直接返回}int loopCount = (int) Math.ceil((double) totalCount / pageSize);  // 使用 Math.ceil 计算循环次数// 设置final CountDownLatch latch = new CountDownLatch(loopCount);log.info("要查询的次数===>{}", loopCount);ExecutorService executorService = Executors.newFixedThreadPool(poolSize);OutputStream outputStream = null;try {outputStream = response.getOutputStream();// 创建写对象ExcelWriter excelWriter = EasyExcel.write(outputStream).build();for (int i = 0; i < loopCount; i++) {final int pageNum = i + 1; // 改为从1开始,直接使用 i + 1 作为页码Thread.ofVirtual().start(() -> {long start = System.currentTimeMillis();// 查询数据IPage<TbExcel> data = excelMapper.selectPage(new Page<>(pageNum, pageSize), null);List<TbExcel> records = data.getRecords();log.info("第{}页,查询耗时===>{}", pageNum,System.currentTimeMillis() - start);WriteSheet writeSheet = EasyExcel.writerSheet(pageNum ,"第" + pageNum + "页").head(ExcelVO.class).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build();synchronized (excelWriter) {long start2 = System.currentTimeMillis();excelWriter.write(BeanUtil.copyToList(records, ExcelVO.class), writeSheet);log.info("数据写出耗时===》{}",System.currentTimeMillis() - start2);}latch.countDown();});}latch.await();response.setContentType("application/octet-stream");response.setCharacterEncoding("utf-8");response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");excelWriter.finish();outputStream.flush();executorService.shutdown(); // 关闭线程池outputStream.close();log.info("总耗时====》{}",System.currentTimeMillis() - start1);} catch (IOException | InterruptedException e) {throw new RuntimeException(e);}}/*** 单sheet页超过100万条数据就写不进去了* @param response*/
@Override
public void exportData3(HttpServletResponse response) {long start1 = System.currentTimeMillis();Integer pageSize = 100000;Integer poolSize = 10;String fileName = String.valueOf(UUID.randomUUID());// 查询数据总数Long totalCount = excelMapper.selectCount(null);if (totalCount == 0) {log.info("没有数据需要导出");return; // 如果没有数据,直接返回}int loopCount = (int) Math.ceil((double) totalCount / pageSize);  // 使用 Math.ceil 计算循环次数// 设置final CountDownLatch latch = new CountDownLatch(loopCount);log.info("要查询的次数===>{}", loopCount);ExecutorService executorService = Executors.newFixedThreadPool(poolSize);OutputStream outputStream = null;try {outputStream = response.getOutputStream();// 创建写对象ExcelWriter excelWriter = EasyExcel.write(outputStream).build();WriteSheet writeSheet = EasyExcel.writerSheet(1 ,"第" + 1 + "页").head(ExcelVO.class).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).registerConverter(new LongStringConverter()).build();for (int i = 0; i < loopCount; i++) {final int pageNum = i + 1; // 改为从1开始,直接使用 i + 1 作为页码Thread.ofVirtual().start(() -> {long start = System.currentTimeMillis();// 查询数据IPage<TbExcel> data = excelMapper.selectPage(new Page<>(pageNum, pageSize), null);List<TbExcel> records = data.getRecords();log.info("第{}页,查询耗时===>{}", pageNum,System.currentTimeMillis() - start);synchronized (excelWriter) {long start2 = System.currentTimeMillis();excelWriter.write(BeanUtil.copyToList(records, ExcelVO.class), writeSheet);log.info("数据写出耗时===》{}",System.currentTimeMillis() - start2);}latch.countDown();});}latch.await();response.setContentType("application/octet-stream");response.setCharacterEncoding("utf-8");response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");excelWriter.finish();outputStream.flush();executorService.shutdown(); // 关闭线程池outputStream.close();log.info("总耗时====》{}",System.currentTimeMillis() - start1);} catch (IOException | InterruptedException e) {throw new RuntimeException(e);}}

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

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

相关文章

JVM内存管理<一>:Java内存异常问题排查

一、 内存溢出问题的排查 1. 使用工具 - jdk自带 jmapvisualvm 2. 流程 堆转储&#xff1a; (1) 方法一&#xff1a;程序运行时&#xff0c;采用&#xff1a;jmap -dump:formatb,filed:\\data\\xxlJob.hprof 23300 进行堆文件的转储 (2) 方法二&#xff1a;在内存溢出的时候…

Android中Glide.with().load().into() 应付面试源码解析

1. with(this)&#xff1a;生命周期绑定 Glide.with(Activity/Fragment/Context) 核心机制&#xff1a;创建与 UI 生命周期绑定的 RequestManager 底层实现&#xff1a; 通过 RequestManagerRetriever 获取单例 非 Application 上下文&#xff1a; 向 Activity/Fragment 添加…

#### es相关内容的索引 ####

倒排索引 结构 #### es倒排索引的结构 ####-CSDN博客 向量索引 结构应用 #### es向量检索 的 结构及应用_es 向量 文本检索-CSDN博客 ann算法 ann算法的种类有哪些&#xff0c;之间的区别&#xff0c;各自的适用场景-CSDN博客 地理信息索引 es地理信息索引的类型以及geo_po…

小飞电视:智能电视与移动设备的娱乐新选择

在数字娱乐时代&#xff0c;人们对于影视内容的需求日益增长&#xff0c;不仅追求丰富多样的节目选择&#xff0c;还希望获得便捷、个性化的观看体验。小飞电视正是这样一款专为智能电视和移动设备设计的视频娱乐应用&#xff0c;它凭借海量的影视资源、高清流畅的播放效果以及…

删除node并且重装然后重装vue

参考第一篇文章 node.js卸载与安装超详细教程_node卸载重装-CSDN博客 第二篇文章安装vue Vue安装与配置教程&#xff08;非常详细&#xff09;_安装vue-CSDN博客

基于YOLOv10算法的交通信号灯检测与识别

目录 一.&#x1f981; 写在前面1.1 实现模块划分1.2 优化与实时性支持 二.&#x1f981; 相关技术与理论基础2.1 各版本yolo对比2.2 YOLOv10网络结构 三.&#x1f981; 结果分析3.1 训练损失与验证损失分析3.2 精确率&#xff08;Precision&#xff09;、召回率&#xff08;Re…

洪水风险图制作全流程:HEC-RAS 与 ArcGIS 的耦合应用

技术点目录 一、HER-RAS理论二、一维数学模型基本地形导入三、恒定流、非恒定流一维数学模型水流计算四、一维数学模型计算结果分析五、一维数学模型增设构筑物六、二维河道水动力模拟七、HEC-RAS在溃坝模型中的应用八、HEC-RAS在洪水风险图中的应用了解更多 —————————…

视觉大语言模型未能充分利用视觉表征

视觉大语言模型未能充分利用视觉表征 FesianXu 20250612 at Wechat Search Team 前言 这两天看到一篇新挂在arxiv上的文章 [1]&#xff0c;讨论了下视觉大语言模型的视觉表征退化问题。先前的研究将VLM缺陷归咎于视觉编码器薄弱&#xff0c;并提出集成编码器方案以弥补不足&am…

SSRF3 任意文件读取

一.任意文件读取 http://192.168.112.12/pikachu-master/vul/ssrf/ssrf_curl.php?urlfile:///etc/passwd 读取文件使用 file://文件路径即可&#xff0c;这里我们换协议为file&#xff0c;然后从根目录开始读取。 /etc/passwd 我们这样修改完url路径后查看结果可以看到文件内…

洛谷P3953 [NOIP 2017 提高组] 逛公园

洛谷P3953 [NOIP 2017 提高组] 逛公园 洛谷题目传送门 题目背景 NOIP2017 D1T3 题目描述 策策同学特别喜欢逛公园。公园可以看成一张 N N N 个点 M M M 条边构成的有向图&#xff0c;且没有 自环和重边。其中 1 1 1 号点是公园的入口&#xff0c; N N N 号点是公园的出…

Vue3+TypeScript+Element Plus 表格展开行优化方案

在 Vue3 TypeScript Element Plus 项目中优化表格展开行的内存使用&#xff0c;主要从 渲染优化、数据管理 和 内存回收 三方面入手。以下是最佳实践和完整解决方案&#xff1a; 1. 懒加载展开内容&#xff08;核心优化&#xff09; 只当行展开时才渲染内容&#xff0c;避免…

OpenCV——直方图与匹配

直方图与匹配 一、直方图简介二、直方图统计三、直方图比较四、直方图均衡化五、自适应的直方图均衡化六、直方图反向投影七、模板匹配 一、直方图简介 图像直方图&#xff08;Histogram&#xff09;是一种频率分布图&#xff0c;它描述了不同强度值在图像中出现的频率。图像直…

通义大模型在文档自动化处理中的高效部署指南(OCR集成与批量处理优化)

1. 传统OCR解决方案常面临识别精度低、版面分析能力弱、处理效率瓶颈等问题。通义大模型凭借其多模态理解和生成能力&#xff0c;为文档处理领域带来革命性突破。本文将深入探讨如何高效部署通义大模型实现端到端的文档自动化处理&#xff0c;特别聚焦OCR集成与批量处理优化两…

Ubuntu20.04通过ssh协议配置远程终端

一、在目标计算机&#xff08;即被连接的计算机&#xff09;上操作&#xff1a; 1、安装 OpenSSH 服务器&#xff1a; sudo apt update sudo apt install openssh-server3、启动并设置 SSH 服务开机自启&#xff1a; sudo systemctl enable --now ssh二、在源计算机&#xf…

《HTTP权威指南》 第7章 缓存

带着问题学习&#xff1a; 缓存如何提高性能如何衡量缓存的有效性缓存置于何处作用最大HTTP如何保持缓存副本的新鲜度缓存如何与其他缓存及服务器通信 web缓存是可以自动保存常见文档副本的HTTP设备。 缓存优点 减少冗余的数据传输&#xff0c;节省网络费用缓解网络瓶颈问题&…

第十三章 模板

函数模板 函数模板使用 函数模板注意事项 自动类型推导&#xff0c;必须推导出一致的数据类型T,才可以使用 模板必须要确定出T的数据类型&#xff0c;才可以使用 普通函数和函数模板的类型转化 普通函数隐式类型转化&#xff08;char转int&#xff09; 函数模板正常使用不会发生…

云计算-专有网络VPC

&#x1f310; 什么是 VPC&#xff1f;&#xff08;Virtual Private Cloud&#xff09; VPC&#xff08;Virtual Private Cloud&#xff0c;虚拟私有云&#xff09; 是公有云服务商提供的一种网络隔离服务&#xff0c;允许用户在云中创建一个逻辑隔离的私有网络环境。你可以在这…

关于*gin.Context的理解

关于*gin.Context的理解 作为初学者&#xff0c;在学习go语言用gin开发web时&#xff0c;我对*gin.Context感到困惑。本文章以自我总结为主&#xff0c;大部分为来自询问ai后的总结&#xff0c;如有问题欢迎指出。 *gin.Context可以理解为一个gin框架的上下文对象指针&#x…

Qt中的OpenGL (6)[坐标系统]

文章目录 文章说明学习目标目录结构坐标系统局部空间世界空间观察空间裁剪空间正射投影矩阵透视投影矩阵组合进入3D世界顶点数据着色器设置数据矩阵设置文章说明 本文是学习OpenGL的笔记,主要参考大神JoeyDeVries的LearnOpenGL第八课《坐标系统》,并将教程中的代码基于Qt进行…

Spring Aop @After (后置通知)的使用场景?

核心定义 After 是 Spring AOP 中的另一种通知&#xff08;Advice&#xff09;类型&#xff0c;通常被称为“后置通知”或“最终通知”。 它的核心作用是&#xff1a; 无论目标方法是正常执行完成&#xff0c;还是在执行过程中抛出了异常&#xff0c;After 通知中的代码 总是…