万级数据优化EasyExcel+mybatis流式查询导出封装

这篇具有很好参考价值的文章主要介绍了万级数据优化EasyExcel+mybatis流式查询导出封装。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


时间 更新内容
2023/09/23 fix: 每个sheet大小和存储内存条数一致的bug update: 增大一个sheet的默认容量

前言.万级数据优化

我们不妨先给大家讲一个概念,利用此概念我们正好给大家介绍一个数据库优化的小技巧: 需求如下:将一个地市表的数据导出70万条。

万级数据优化EasyExcel+mybatis流式查询导出封装,mybatis,android

如果你不假思索,直接一条sql语句搞上去,直接就会内存溢出,因为mysql会将结果记录统一查询出来然后返还给内存:那内存可能直接OOM!

@Test
public void testQuery1()  {

    // 1、定义资源
    Connection connection = null;
    ResultSet resultSet = null;
    PreparedStatement statement = null;
    String sql = "select * from user";
    try {
        // 获取连接
        connection = DBUtil.getConnection();
        // 获取使用预编译的statement
        statement = connection.prepareStatement(sql);

        long start = System.currentTimeMillis();
        resultSet = statement.executeQuery();
        while (resultSet.next()){
            System.out.println("name---->" + resultSet.getString("nick_name") );
        }
        long end = System.currentTimeMillis();
        System.out.println(end -start);
    } catch (SQLException e){
        e.printStackTrace();
    } finally {
        // 关闭资源
        DBUtil.closeAll(connection,statement,resultSet);
    }
}

所以我们通常有如下几种解决方案:

一. 直接上流式查询封装工具代码

使用2核4G云服务器 下载速度在40-50s之间波动. 本机或大水管.我只能说更快了
万级数据优化EasyExcel+mybatis流式查询导出封装,mybatis,android

万级数据优化EasyExcel+mybatis流式查询导出封装,mybatis,android
万级数据优化EasyExcel+mybatis流式查询导出封装,mybatis,android
万级数据优化EasyExcel+mybatis流式查询导出封装,mybatis,android

表格美化 CustomCellWeightStrategy.class
/**
 * @author YuanJie
 * @projectName vector-server
 * @package com.vector.common.utils.easyexcel
 * @className com.vector.common.utils.easyexcel.CustomCellWeightStrategy
 * @copyright Copyright 2020 vector, Inc All rights reserved.
 * @date 2023/8/28 17:58
 */
public class CustomCellWeightStrategy extends AbstractColumnWidthStyleStrategy {
    private final Map<Integer, Map<Integer, Integer>> CACHE = new HashMap<>();

    @Override
    protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
        boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList);
        if (needSetWidth) {
            Map<Integer, Integer> maxColumnWidthMap = CACHE.computeIfAbsent(writeSheetHolder.getSheetNo(), k -> new HashMap<>());

            int columnWidth = this.dataLength(cellDataList, cell, isHead)+8;
            if (columnWidth >= 0) {
                if (columnWidth > 254) {
                    columnWidth = 254;
                }

                Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex());
                if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
                    maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
                    Sheet sheet = writeSheetHolder.getSheet();
                    sheet.setColumnWidth(cell.getColumnIndex(), columnWidth * 200);
                }

                //设置单元格类型
                cell.setCellType(CellType.STRING);
                // 数据总长度
                int length = cell.getStringCellValue().length();
                // 换行数
                int rows = cell.getStringCellValue().split("\n").length;
                // 默认一行高为20
                cell.getRow().setHeightInPoints(rows * 20);
            }
        }
    }


    /**
     * 计算长度
     *
     * @param cellDataList
     * @param cell
     * @param isHead
     * @return
     */
    private Integer dataLength(List<WriteCellData<?>> cellDataList, Cell cell, Boolean isHead) {
        if (isHead) {
            return cell.getStringCellValue().getBytes().length;
        } else {
            CellData<?> cellData = cellDataList.get(0);
            CellDataTypeEnum type = cellData.getType();
            if (type == null) {
                return -1;
            } else {
                switch (type) {
                    case STRING:
                        // 换行符(数据需要提前解析好)
                        int index = cellData.getStringValue().indexOf("\n");
                        return index != -1 ?
                                cellData.getStringValue().substring(0, index).getBytes().length + 1 : cellData.getStringValue().getBytes().length + 1;
                    case BOOLEAN:
                        return cellData.getBooleanValue().toString().getBytes().length;
                    case NUMBER:
                        return cellData.getNumberValue().toString().getBytes().length;
                    default:
                        return -1;
                }
            }
        }
    }
}
封装工具类EasyExcelUtil.class
/**
 * @author YuanJie
 * @projectName vector-server
 * @package com.vector.common.utils
 * @className com.vector.common.utils.easyexcel.EasyExcelUtil
 * @copyright Copyright 2020 vector, Inc All rights reserved.
 * @date 2023/8/26 1:17
 */
@Slf4j
public class EasyExcelUtil {

    private static final String DATE_FORMAT = "yyyy-MM-dd";


    /**
     * 设置批量存储最大值,也影响sheet页数
     */
    private static final Integer MAX_SHEET_DATA = 100000;
    /**
     * 设置内存最大值
     */
    private static final Integer MAX_MEMORY_DATA = 1000;

    /**
     * 使用EasyExcel生成Excel  xls
     *
     * @param response      响应对象
     * @param fileNameParam 文件名
     * @param sheetName     表格名
     * @param clazz         导出实体类
     * @param t             查库入参
     * @param func          流式查询方法 Cursor<ResultVo> listOrders(@Param("userName") String userName);
     */
    public static <T> void writeExcelXls(HttpServletResponse response, String fileNameParam,
                                         String sheetName, Class<?> clazz, T t,
                                         Function<T, Cursor<?>> func) throws Exception {
        streamExportExcel(response, fileNameParam, sheetName, clazz, ExcelTypeEnum.XLS.getValue(), t, func);
    }

    /**
     * 使用EasyExcel生成Excel  xlsx
     *
     * @param response      响应对象
     * @param fileNameParam 文件名
     * @param sheetName     表格名
     * @param clazz         导出实体类
     * @param t             查库入参
     * @param func          流式查询方法 Cursor<ResultVo> listOrders(@Param("userName") String userName);
     */
    public static <T> void writeExcelXlsx(HttpServletResponse response, String fileNameParam,
                                          String sheetName, Class<?> clazz, T t,
                                          Function<T, Cursor<?>> func) throws Exception {
        streamExportExcel(response, fileNameParam, sheetName, clazz, ExcelTypeEnum.XLSX.getValue(), t, func);
    }

    /**
     * 流式导出 Excel
     *
     * @param response      响应对象
     * @param fileNameParam 文件名
     * @param sheetName     表格名
     * @param clazz         导出实体类
     * @param excelType     导出类型
     * @param t             查库入参
     * @param func          流式查询方法 Cursor<ResultVo> listOrders(@Param("userName") String userName);
     * @throws Exception 异常
     */
    private static <T> void streamExportExcel(HttpServletResponse response, String fileNameParam,
                                              String sheetName, Class<?> clazz, String excelType,
                                              T t, Function<T, Cursor<?>> func) throws Exception {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
        String fileName = fileNameParam + dateTimeFormatter.format(LocalDateTime.now()) + excelType;
        ExcelWriter excelWriter = EasyExcel
                .write(getOutputStream(fileName, response, excelType), clazz)
                .registerWriteHandler(new CustomCellWeightStrategy())
                .build();

        // 内容样式
        WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
        // 水平居中
        contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
        // 垂直居中
        contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        // 设置自动换行,前提内容中需要加「\n」才有效
        contentWriteCellStyle.setWrapped(true);
        // 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现
        HorizontalCellStyleStrategy horizontalCellStyleStrategy =
                new HorizontalCellStyleStrategy(null, contentWriteCellStyle);
        Cursor<?> cursor;
        List<Object> list = new ArrayList<>();
        int page = 0;
        WriteSheet writeSheet = EasyExcel
                .writerSheet(++page, sheetName + page)
                .registerWriteHandler(horizontalCellStyleStrategy)
                .build();
        // 流式数据库查询
        cursor = func.apply(t);
        int count = 0;
        try {
            for (Object o : cursor) {
                list.add(o);
                if(list.size() == MAX_MEMORY_DATA){
                    count += list.size();
                    excelWriter.write(list, writeSheet);
                    list.clear();
                    // 每个sheet页最大存储MAX_SHEET_DATA条数据
                    if (count >= MAX_SHEET_DATA) {
                        writeSheet = EasyExcel
                                .writerSheet(++page, sheetName + page)
                                .registerWriteHandler(horizontalCellStyleStrategy)
                                .build();
                        count = 0;
                    }
                }
            }
            // 处理最后不足MAX_SHEET_DATA的数据
            if (list.size() > 0) {
                writeSheet = EasyExcel
                        .writerSheet(++page, sheetName + page)
                        .registerWriteHandler(horizontalCellStyleStrategy)
                        .build();
                excelWriter.write(list, writeSheet);
                list.clear();
            }
        } catch (Exception e) {
            // 重置response
            response.reset();
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            String json = JacksonInstance.toJson(R.errorResult(EnumHttpCode.SYSTEM_ERROR, "下载文件失败" + e.getMessage()));
            response.getWriter().println(json);
        } finally {
            if (cursor != null) {
                cursor.close();
            }
            if (excelWriter != null) {
                excelWriter.finish();
            }
        }
    }

    /**
     * 导出文件时为Writer生成OutputStream
     *
     * @param finalName 文件名
     * @param response 响应对象
     * @param excelType 导出文件类型
     * @return OutputStream
     */
    private static OutputStream getOutputStream(String finalName, HttpServletResponse response, String excelType) throws Exception {
        response.reset();
        finalName = URLEncoder.encode(finalName, StandardCharsets.UTF_8);
        if (ExcelTypeEnum.XLS.getValue().equals(excelType)) {
            response.setContentType("application/vnd.ms-excel");
        } else if (ExcelTypeEnum.XLSX.getValue().equals(excelType)) {
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        }
        response.setCharacterEncoding("utf8");
        response.setHeader("Content-Disposition", "attachment; filename=" + finalName);
        response.setHeader("Pragma", "public");
        response.setHeader("Cache-Control", "no-store");
        response.addHeader("Cache-Control", "max-age=0");
        return response.getOutputStream();
    }
}
导出实体 Dto.class
@Data
public class Dto {
    @NumberFormat("#")
    @ExcelProperty(value = "地市编码", index = 0)
    Long code;
    @ExcelProperty(value = "地市名称", index = 1)
    String name;
    @NumberFormat("#")
    @ExcelProperty(value = "地市级别", index = 2)
    Integer level;
    @NumberFormat("#")
    @ExcelProperty(value = "地市父编码", index = 3)
    Long pcode;
    @NumberFormat("#")
    @ExcelProperty(value = "地市父名称", index = 4)
    Integer category;
}
测试用例 MeTestController.class
    @Resource
    private ExportMapper exportMapper;

    @Resource
    private HttpServletRequest request;
    @Resource
    private HttpServletResponse response;
    @GetMapping("/export")
    @Transactional
    public void export() throws Exception {
        Long params = 110101001000L;
        EasyExcelUtil.writeExcelXlsx(
                response,
                "地市信息",
                "地市区域",
                Dto.class,
                params,
                param -> exportMapper.export(null));
    }
测试Dao ExportMapper.class
public interface ExportMapper {
    @Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = Integer.MIN_VALUE)
    @ResultType(Dto.class)
    @Select("select * from area_code_2023")
    Cursor<Dto> export(@Param("params") Long params);
}


二. 传统分页导出查询

大表的深度分页性能很差,也受制于表设计的影响
@Test
public void testQuery2()  {

    // 1、定义资源
    Connection connection = null;
    ResultSet resultSet = null;
    PreparedStatement statement = null;
    String sql = "select * from user limit ?,?";
    try {
        // 获取连接
        connection = DBUtil.getConnection();
        // 获取使用预编译的statement
        statement = connection.prepareStatement(sql);
        // 获取结果集
        long start = System.currentTimeMillis();
        long begin = 0L, offset = 10000L;
        while (true){
            statement.setLong(1,begin);
            statement.setLong(2,offset);
            begin += offset;
            resultSet = statement.executeQuery();
            boolean flag = resultSet.next();
            if(!flag) break;
            while (flag){
                System.out.println("name---->" + resultSet.getString("nick_name") );
                flag = resultSet.next();
            }
        }
        long end = System.currentTimeMillis();
        System.out.println(end -start);
    } catch (SQLException e){
        e.printStackTrace();
    } finally {
        // 关闭资源
        DBUtil.closeAll(connection,statement,resultSet);
    }
}

三. 流式查询概念

采用传统的Stream流式思想,将直接提供数据替换成提供获取数据的管道,客户端读取数据时直接从管道中遍历获取;整个读取的过程需要客户端保持和服务端的连接,也很好理解,它实际是一个管道,管道得通着才能取数据。 流式查询有两种使用方式,一种是用Cursor作为返回值,对数据进行遍历操作;一种是不设置返回值,在入参中传入一个ResultHandler作为回调处理数据。本文将基于Mybatis具体介绍使用方法。 这两种返回值的使用方式是相似的,唯一区别就是返回值不同。Mybatis查询有两种方式,一种是基于注解加在Mapper接口上方,一种是写在xml文件中,主要需要设置以下几个属性:
ResultSetType 结果集读取方式
FetchSize MySQL服务端单次发送至客户端的数据条数
ResultType 这个眼熟吧,设置返回实体类映射

ResultSetType有4种可选项

DEFAULT(-1),
FORWARD_ONLY(1003),
SCROLL_INSENSITIVE(1004),
SCROLL_SENSITIVE(1005);

FORWARD_ONLY顾名思义只能向前,即数据只能向前读取,是不是就类似一个流水的管道,读一条就相当于水流过去一些。也是我们需要选用的。
SCROLL_INSENSITIVE不敏感滚动,和下面那个差不多,都是可以向后读或向前读;这意味着已读取过的数据不能丢掉,要继续保存在内存中,因为有可能会回去再次读取他们。
SCROLL_SENSITIVE敏感滚动,和上面那个差不多。
这么一比较就看得出来,当选的一定是FORWARD_ONLY,我们亟需解决的就是大数据量对内存的影响,再用后面两个还是会放在内存中。

FetchSize这个概念在许多服务中都有提及,例如RabbitMQ中是消费者取过来预处理的消息数量,但在MySQL中完全不是一个概念。MySQL的数据传输是基于C/S的阻塞机制,即Client设置FetchSize = 1000,而Server查出来10000条数据,按照常理应该是Server智能地使用分页策略1000条1000条取;实际不是,Server查出来多少就是多少,他会放在自己特定的内存空间内,只是会根据FetchSize的大小一点一点传送给Client——利用C/S的通讯阻塞,发1000条、堵一下、发1000条、堵一下……。

JDBC官方给出的答案是设置为“Integer.MIN_VALUE”,具体原因不清楚,但我猜是为了和游标查询区分开,因为一会你会发现流式查询和游标查询唯一的区别就是FetchSize的大小。

注解式

@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = Integer.MIN_VALUE)
@ResultType(ResultVo.class)
@Select("SELECT *, 0 orderType FROM `table`\n" +
        "        WHERE username = #{userName}")
Cursor<ResultVo> listOrders(@Param("userName") String userName);
 
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = Integer.MIN_VALUE)
@ResultType(ResultVo.class)
@Select("SELECT *, 0 orderType FROM `table`\n" +
        "        WHERE username = #{userName}")
void listOrders2(@Param("userName") String userName, ResultHandler<ResultVo> handler);

使用Mybatis的注解,在 @Options 中指定查询配置参数,在@ResultType中指定返回值类型 ,在 @Select中指定查询语句。最后用Cursor接收返回值,Cursor是可遍历的,所以直接Foreach遍历即可;或者返回void 用ResultHandler处理数据回调,在调用方式时传入new ResultHandler并写明处理逻辑。

xml式

<select id="listOrders" resultType="com.vo.ResultVo" resultSetType="FORWARD_ONLY" fetchSize="Integer.MIN_VALUE">
    SELECT *, 1 stuffCount, 1 orderType FROM `table`
    WHERE username = #{userName}
</select>

需要注意的是,不可以注解 + xml混合使用,比如注解指定fetchSize,xml只写查询语句,这种只有xml语句会生效!!!要不全用注解,要不全用xml!!!

流式查询由于需要保持客户端与服务端的连接,而一般查询提交完连接就会关闭;因此我们需要保持事务开启,否则会报“A Cursor is already closed.”,即Cursor已经关闭,没法再读取了。最简单的方法就是在方法上加@Transactional,在查询完毕以前事务会一直持有这个数据库连接,但我们在使用完毕后也要自行关闭连接,显式调用Cursor.close(),或者用try with resource语句。

游标查询和流式查询的区分是fetchSize = Integer.MIN_VALUE

Cursor 还提供了三个方法:

  1. isOpen():用于在取数据之前判断 Cursor 对象是否是打开状态。只有当打开时 Cursor 才能取数据;
  2. isConsumed():用于判断查询结果是否全部取完;
  3. getCurrentIndex():返回已经获取了多少条数据。
try(Cursor cursor = OrderMapper.listOrders()) {
  cursor.forEach(rowObject -> {
      // ...
  });
}

  OrderMapper.listOrders2(queryWrapper,resultContext -> {
    ResultVo result = resultContext.getResultObject();
   //这边循环调用就可以实现业务了

}

游标查询

和流式查询类似fetchSize不设置为MIN_VALUE即可
JDBC查询默认是不支持FetchSize属性的,需要在JDBC连接URL后面加上**“useCursorFetch=true”。**

useCursorFetch=true 是针对 MySQL 数据库的 JDBC 连接参数,用于启用服务器端游标获取数据。在 MyBatis 中,当使用流式查询(例如:分页查询、结果集处理和使用游标等)时,这个配置可以帮助逐行从服务器检索数据,而不是一次性将所有数据加载到内存中,从而降低内存占用。

当使用 MySQL 数据库时,在 JDBC 连接字符串中加入 useCursorFetch=true,并结合设置合适的 fetchSize,可以避免因一次性加载过多数据导致的内存溢出问题。注意,此配置仅对 MySQL 数据库有效。 如果不设置 useCursorFetch=true 这个配置,仅使用之前提到的那些配置(如设置 defaultFetchSize、分页查询、结果集处理和使用游标等),在大多数情况下,这些配置仍然可以有效地避免查询导致的内存溢出。

但需要注意的是,对于 MySQL 数据库,如果不启用服务器端游标获取数据,这可能会影响到流式查询的效果。因为在默认情况下,MySQL JDBC 驱动会一次性将所有数据加载到内存中。此时,即使使用了其他配置,也可能无法达到预期的内存优化效果。

总的来说,在使用 MySQL 数据库时,推荐在 JDBC 连接字符串中加入 useCursorFetch=true 配置,以更好地支持流式查询和降低内存占用。在其他数据库中,可以根据实际需求和场景选择合适的配置和策略来避免查询导致的内存溢出。

还要知道如何判断自己是否使用了流式查询或游标查询,下面是几个数据集的对应关系

普通分页 ResultsetRowsStatic RowDataStatic
查询方式 结果集类型 行数据类型
流式查询 ResultsetRowsStreaming RowDataDynamic
游标查询 ResultsetRowsCursor RowDataCursor

这3种查询方式,常规非大数据模式下普通查询最快,其次是流式查询,最次是游标查询.

主要是由于游标查询需要和数据库进行多次网络交互,Client处理完这部分后再拉取下一部分数据,因此会比较慢。但是流式查询又会长时间占用同一个数据库连接,因此要取舍一下是能接受连接一直持有但是可能会堵住导致响应慢,还是可能占用较多连接数但单次响应快。当通过流式查询获取一个ResultSet后,在你通过next迭代出所有元素之前或者调用close关闭它之前,你不能使用同一个数据库连接去发起另外一个查询,否者抛出异常(第一次调用的正常,第二次的抛出异常)。文章来源地址https://www.toymoban.com/news/detail-681534.html

到了这里,关于万级数据优化EasyExcel+mybatis流式查询导出封装的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 积木报表Excel数据量大导出慢导不出问题、大量数据导不出问题优化方案和分析解决思路(优化前一万多导出失败,优化后支持百万级跨库表导出)

    原积木导出有两种导出,直接导出和大数据导出(大数据导出是做了优化去掉了一些样式之类的,性能更好) 实测中发现 原积木大数据导出性能:1万条数据导出耗时30秒,1.5万条耗时1.5分钟导出失败,数据超过一万条后经常导出失败,还会导致容器实例探活失败/内存撑爆重

    2024年04月11日
    浏览(45)
  • Java,excel大量百万数据导出优化措施,SXSSFWorkbook流式、分批次导出示例

    在导出百万级的数据时,如果不采用适当的优化措施,确实可能会造成死机和内存崩溃等问题。 为避免这些问题,可以采用以下优化措施: 分批次读取数据:将需要导出的数据分成多个批次进行读取和写入,每次读取部分数据,写入 Excel 后即时清除内存。这样可以避免一次

    2024年02月16日
    浏览(29)
  • 【EasyExcel】封装一个分页写数据的通用方法(保姆级),继上一篇easyExcel导出上线后的优化

    需求:通过elasticsearch查询出来一次性写,在大数据量时存在OOM的隐患分页查询、分批次写数据,避免导出大数据量时内存消耗陡增基于elasticsearch分页查询;mybatis-puls同理 在上个博客中解决了线上导出字体依赖的问题,由于涉及的导出模块较多,因为打算封装一个方法做通用

    2024年02月07日
    浏览(30)
  • 【QT性能优化】QT性能优化之QT6框架高性能模型视图代理框架千万级数据表分页查询优化

    QT性能优化之QT6框架高性能模型视图代理框架千万级数据表分页查询优化 简介 本文介绍了QT模型视图代理框架中的QT表格控件和QT数据库模块中的QT数据库查询模型结合使用的一个应用实践案例:QT高性能表格控件分页展示千万行数据。本文介绍了这个应用实践案例的运行效果

    2024年02月14日
    浏览(36)
  • 深入解析MySQL视图、索引、数据导入导出:优化查询和提高效率

    目录 1. 视图(View): 什么是视图? 为什么要使用视图? 视图的优缺点 1) 定制用户数据,聚焦特定的数据 2) 简化数据操作 3) 提高数据的安全性 4) 共享所需数据 5) 更改数据格式 6) 重用 SQL 语句 示例操作 没使用前 使用后 2. 索引(Index): 什么是索引? 为什么要使用索引?

    2024年02月13日
    浏览(47)
  • MySQL千万级数据优化方案

                              ↓↓↓处理千万级数据的MySQL数据库,可以采取以下优化措施↓↓↓                                                           使用索引:确保对经常用于查询和排序的字段添加索引。不要在查询中使用SELECT *,而是明确指定需要的字段。

    2024年02月07日
    浏览(28)
  • EasyExcel导入和导出数据

    1.cmtroller 调用service方法,完成导出 2.service 调用工具类的方法完成导出 传入response,标题控制类(标题名称,合并的列数),员工列表,文件名称,excel的标题名称,要导出的数据类 3.工具类中的方法 4.控制标题类 这个类控制数据之前的显示内容 效果 2022-10-28 乱码解决 因为文件

    2023年04月09日
    浏览(28)
  • EasyExcel快速导出 100W 数据

    导出是后台管理系统的常用功能,当数据量特别大的时候会内存溢出和卡顿页面,曾经自己封装过一个导出,采用了分批查询数据来避免内存溢出和使用SXSSFWorkbook方式缓存数据到文件上以解决下载大文件EXCEL卡死页面的问题。 不过一是存在封装不太友好使用不方便的问题,二

    2024年01月16日
    浏览(26)
  • 数据导入导出(POI以及easyExcel)

            将一些数据库信息导出为Excel表格         将Excel表格数据导入数据库         大量数据的导入导出操作 常⽤的解决⽅案为: Apache POI 与阿⾥巴巴 easyExcel Apache POI 是基于 Office Open XML 标准( OOXML )和 Microsoft 的 OLE 2 复合⽂档 格式( OLE2 )处理各种⽂件格式的

    2024年02月13日
    浏览(32)
  • Spring boot easyexcel 实现复合数据导出、按模块导出

    场景: 导出数据为1对多的复合数据 一个模块是一条数据,直接填充数据无法实现 如图: 红框内为一条数据(1对多),下方箭头指向为第二条数据 如果直接填充,只能填充第一条,第二条就没办法了。 由于多行都包含许多,固定表头,只能走填充路线,怎么实现呢 实现思路

    2024年02月07日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包