Java uses Easyexcel to export large amounts of data

1. Official information

GitHub - alibaba/easyexcel: A fast, concise, Java processing Excel tool that solves memory overflow of large files

About Easyexcel | Easy Excel

2. Export tool package

        <dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>easyexcel</artifactId>
			<version>3.2.1</version>
		</dependency>
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.util.MapUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.common.utils.JacksonUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 通用 XLS 下载封装
 * mybatis 中使用 List<List<?>> 接收多个结果集
 * 一个 List<?> 代表一个结果集
 *
 * @author yushanma
 * @since 2023/3/12 16:06
 */
public class CommonExcelExportUtil {

    private final static Long PAGE_SIZE = 100000L;

    private static final Logger logger = LogManager.getLogger(CommonExcelExportUtil.class.getName());

    /**
     * 保存到 xls 文件
     * @param fileName 文件名
     * @param dataArray 数据集
     */
    public static void saveLocalXlsFile(String fileName, Object[] dataArray) {
        try {
            if (dataArray != null && dataArray.length > 0) {
                String[] header = getHeader(dataArray[0]);
                List<List<String>> xlsHeader = getXlsHeader(header);
                List<List<Object>> xlsData = getXlsDataV2(header, dataArray);
                EasyExcel.write(fileName).head(xlsHeader).sheet("test_sheet").doWrite(xlsData);
            } else {
                EasyExcel.write(fileName).head(Collections.emptyList()).sheet("test_sheet").doWrite(Collections.emptyList());
            }
        } catch (Exception e) {
            logger.error(e.getMessage());
        }
    }

    /**
     * 返回文件流响应
     *
     * @param response HttpServletResponse
     * @throws IOException IOException
     */
    public static void returnXlsFile(HttpServletResponse response, Object[] dataArray) throws IOException {
        try {
            // 这里注意 使用swagger 会导致各种问题,请直接用浏览器或者用postman
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding("utf-8");
            // 这里URLEncoder.encode可以防止中文乱码
            // 下载文件名
            String fileName = URLEncoder.encode("test_xls_file", "UTF-8").replaceAll("\\+", "%20");
            response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
            if (dataArray != null && dataArray.length > 0) {
                String[] header = getHeader(dataArray[0]);
                List<List<String>> xlsHeader = getXlsHeader(header);
                List<List<Object>> xlsData = getXlsDataV2(header, dataArray);
                EasyExcel.write(response.getOutputStream()).head(xlsHeader).sheet("test_sheet").doWrite(xlsData);
            } else {
                EasyExcel.write(response.getOutputStream()).head(Collections.emptyList()).sheet("test_sheet").doWrite(Collections.emptyList());
            }
        } catch (Exception e) {
            // 重置response
            response.reset();
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            Map<String, String> map = MapUtils.newHashMap();
            map.put("status", "failure");
            map.put("message", "下载文件失败" + e.getMessage());
            response.getWriter().println(JSON.toJSONString(map));
        }
    }

    /**
     * 解析 header 返回
     *
     * @return header 列表
     */
    private static List<List<String>> getXlsHeader(String[] header) {
        List<List<String>> list = new ArrayList<List<String>>();
        for (String h : header) {
            List<String> head = new ArrayList<String>();
            head.add(h);
            list.add(head);
        }
        return list;
    }

    /**
     * 解析数据内容返回
     *
     * @return data 列表
     */
    private static List<List<Object>> getXlsDataV1(String[] header, Object[] dataset) {
        List<List<Object>> list = new ArrayList<List<Object>>();
        for (Object obj : dataset) {
            List<Object> data = new ArrayList<Object>();
            for (String s : header) {
                // fastjson
//                String jsonString = JSON.toJSONString(obj);
//                HashMap<String, Object> objectMap = (HashMap<String, Object>)JSON.parseObject(jsonString, HashMap.class);
                // jackson
                String jsonString = JacksonUtils.toJson(obj);
                HashMap<String, Object> objectMap = (HashMap<String, Object>) JacksonUtils.toObj(jsonString, HashMap.class);
                data.add(objectMap.get(s));
            }
            list.add(data);
        }
        return list;
    }

    /**
     * 解析数据内容返回
     *
     * @return data 列表
     */
    private static List<List<Object>> getXlsDataV2(String[] header, Object[] dataset) {
        List<List<Object>> list = new ArrayList<List<Object>>();
        long pageMax = dataset.length / PAGE_SIZE;
        // 小于10W数据时
        if(dataset.length > 0 && pageMax == 0){
            pageMax = 1;
        }
        // 百万数据分页
        for (int i = 0; i < pageMax; i++) {
            List<Object> collect = Arrays.stream(dataset).skip(i * PAGE_SIZE).limit(PAGE_SIZE).collect(Collectors.toList());
            // fastjson
//            String jsonString = JSON.toJSONString(collect);
//            List<HashMap<String, Object>> objectMapList = (List<HashMap<String, Object>>) JSON.parseObject(jsonString, List.class);
            // jackson
            String jsonString = JacksonUtils.toJson(collect);
            List<HashMap<String, Object>> objectMapList = (List<HashMap<String, Object>>) JacksonUtils.toObj(jsonString, List.class);
            for (HashMap<String, Object> objectMap : objectMapList) {
                List<Object> data = new ArrayList<Object>();
                for (String s : header) {
                    data.add(objectMap.get(s));
                }
                list.add(data);
            }
        }

        return list;
    }

    /**
     * 获取 header
     *
     * @param obj
     * @return
     */
    private static String[] getHeader(Object obj) {
        // fastjson
//        String jsonString = JSON.toJSONString(obj);
//        HashMap<String, Object> objectMap = (HashMap<String, Object>)JSON.parseObject(jsonString, HashMap.class);
        // jackson
        String jsonString = JacksonUtils.toJson(obj);
        HashMap<String, Object> objectMap = (HashMap<String, Object>) JacksonUtils.toObj(jsonString, HashMap.class);
        return objectMap.keySet().toArray(new String[objectMap.keySet().size()]);
    }

}

Convert the List result set to an Object array, and then obtain the corresponding columns and values ​​by serializing HashMap<K,V>. Here, Jackson is used instead of Fastjson.

3. Test

public static void dynamicHeadWrite() throws IOException {
        String fileName = "D:\\test\\" + System.currentTimeMillis() + ".xlsx";
        List<TestEntity> list = new ArrayList<>(10);
        for (int i = 0; i < 500000; i++) {
            TestEntity data = new TestEntity();
            data.setFeild1(UUID.randomUUID().toString());
            data.setFeild2(UUID.randomUUID().toString());
            data.setFeild3(UUID.randomUUID().toString());
            data.setFeild4(UUID.randomUUID().toString());
            data.setFeild5(UUID.randomUUID().toString());
            data.setFeild6(UUID.randomUUID().toString());
            data.setFeild7(UUID.randomUUID().toString());
            data.setFeild8(UUID.randomUUID().toString());
            data.setFeild9(UUID.randomUUID().toString());
            data.setFeild10(UUID.randomUUID().toString());
            data.setFeild11(UUID.randomUUID().toString());
            data.setFeild12(UUID.randomUUID().toString());
            data.setFeild13(UUID.randomUUID().toString());
            data.setFeild14(UUID.randomUUID().toString());
            data.setFeild15(UUID.randomUUID().toString());
            data.setFeild16(UUID.randomUUID().toString());
            data.setFeild17(UUID.randomUUID().toString());
            data.setFeild18(UUID.randomUUID().toString());
            data.setFeild19(UUID.randomUUID().toString());
            data.setFeild20(UUID.randomUUID().toString());
            list.add(data);
        }
        System.out.println(">>> list size " + list.size());
        LocalDateTime start = LocalDateTime.now();
        CommonExcelExportUtil.saveLocalXlsFile(fileName, list.toArray());
        LocalDateTime end = LocalDateTime.now();
        Duration cost = Duration.between(start, end);
        System.out.println(">>> start " + start.toString() + " >>> end " + end.toString() + " >>> cost " +cost.toString());
    }

50W rows of data, 20 columns, 36 characters in each column, a total of 360 million characters, and it takes 68 seconds.

4. Adaptive column width strategy

    /**
     * 重写自适应列宽策略
     */
    static class CustomLongestMatchColumnWidthStyleStrategy extends AbstractColumnWidthStyleStrategy {
        private static final int MAX_COLUMN_WIDTH = 255;
        private final Map<Integer, Map<Integer, Integer>> cache = MapUtils.newHashMapWithExpectedSize(8);

        public CustomLongestMatchColumnWidthStyleStrategy() {
        }

        @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) {
                HashMap<Integer, Integer> maxColumnWidthMap = (HashMap)this.cache.computeIfAbsent(writeSheetHolder.getSheetNo(), (key) -> {
                    return new HashMap(16);
                });
                Integer columnWidth = this.dataLength(cellDataList, cell, isHead);
                if (columnWidth >= 0) {
                    if (columnWidth > MAX_COLUMN_WIDTH) {
                        columnWidth = 255;
                    }

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

                }
            }
        }

        private Integer dataLength(List<WriteCellData<?>> cellDataList, Cell cell, Boolean isHead) {
            if (isHead) {
                return cell.getStringCellValue().getBytes().length;
            } else {
                WriteCellData<?> cellData = (WriteCellData)cellDataList.get(0);
                CellDataTypeEnum type = cellData.getType();
                if (type == null) {
                    return -1;
                } else {
                    switch(type) {
                        case STRING:
                            return cellData.getStringValue().getBytes().length;
                        case BOOLEAN:
                            return cellData.getBooleanValue().toString().getBytes().length;
                        case NUMBER:
                            return cellData.getNumberValue().toString().getBytes().length;
                        default:
                            return -1;
                    }
                }
            }
        }
    }
    /**
     * 保存到 xls 文件
     * @param fileName 文件名
     * @param dataArray 数据集
     */
    public static void saveLocalXlsFile(String fileName, Object[] dataArray) {
        try {
            if (dataArray != null && dataArray.length > 0) {
                String[] header = getHeader(dataArray[0]);
                List<List<String>> xlsHeader = getXlsHeader(header);
                List<List<Object>> xlsData = getXlsDataV2(header, dataArray);
                EasyExcel
                        .write(fileName)
                        //.registerWriteHandler(getCustomHorizontalCellStyleStrategy())
                        .registerWriteHandler(new CustomLongestMatchColumnWidthStyleStrategy())
                        .head(xlsHeader)
                        .sheet("test_sheet")
                        .doWrite(xlsData);
            } else {
                EasyExcel.write(fileName).head(Collections.emptyList()).sheet("test_sheet").doWrite(Collections.emptyList());
            }
        } catch (Exception e) {
            logger.error(e.getMessage());
        }
    }

Effect: 10W20C, accumulating 7.2KW characters, taking 21 S

5. Optimization

JVisualVM Performance Analysis and Mybatis ResultHandler Practical Combat_Yu Shanma's Blog-CSDN Blog This article will simulate the performance analysis of a production scenario: return millions of rows of data from the Mysql database and export the data to an Excel file. During this period, use the JVisualVM monitoring tool to view the code performance . https://blog.csdn.net/weixin_47560078/article/details/131639790?spm=1001.2014.3001.5501

6. Correct errors

An incorrect writing is corrected here, and no secondary processing of the data set is required.

The best and correct answer should be: directly pass List<Object> to easyExcel for writing. To set the table header, you can add an entity class object in EasyExcel.write().

Thanks @farewell927 for pointing out the error.

Guess you like

Origin blog.csdn.net/weixin_47560078/article/details/129477514