Java 使用 Easyexcel 导出大量数据

一、官方资料

GitHub - alibaba/easyexcel: 快速、简洁、解决大文件内存溢出的java处理Excel工具

关于Easyexcel | Easy Excel

二、导出工具封装

        <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()]);
    }

}

将 List 结果集转 Object 数组,再通过序列化 HashMap<K,V> 获取对于的列与值,这里采用 Jackson 替代 Fastjson 。

三、测试

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 行数据,20 列,每列 36 字符,累计 3.6 亿字符,耗时 68 秒。

四、自适应列宽策略

    /**
     * 重写自适应列宽策略
     */
    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());
        }
    }

效果:10W20C,累积 7.2KW 字符,耗时 21 S

五、优化

JVisualVM 性能分析与 Mybatis ResultHandler 实战_余衫马的博客-CSDN博客本文将模拟一个生产场景的性能分析:从 Mysql 数据库中返回百万行数据并且导出数据到 Excel 文件,期间使用JVisualVM 监控工具查看代码性能。https://blog.csdn.net/weixin_47560078/article/details/131639790?spm=1001.2014.3001.5501

六、更正错误

这里更正一个错误写法,无需二次加工数据集。

最优正解应为:直接传 List<Object> 给 easyExcel 去写,表头设置可以在 EasyExcel.write() 里加一个实体类的 class 对象。

感谢 @告辞927 指出的错误。

猜你喜欢

转载自blog.csdn.net/weixin_47560078/article/details/129477514