欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JAVA 导出Excel大数据内存溢出解决方案

程序员文章站 2022-07-13 13:14:49
...

最初的导出用的就是EasyPoi(EasyPoi官网:http://easypoi.mydoc.io/),简单方便,当时也没考虑太多,就图一个简单
后来发生了几次导出大数据的时候服务器直接崩掉,使我不得不注意起来
上网查了一下,基本上就是内存溢出的问题
然后发现了一个比较好的解决方法,就是使用XML规范来导出Excel

首先我们需要知道一下,XML的格式是怎样的

  1. 新建一个Excel表格,随便新增一点数据;
  2. 文件->另存为->文件格式改为XML工作表。

打开我们的XML文件,就可以清晰的看到XML的格式了,接下来我们只需要按照基本格式对数据进行封装就可以

因为我这个系统的导出只涉及到了数据,也没有太多复杂的东西,所以我只对最基本的导出功能部分代码做了一个封装,有更加深入的需求的朋友可以以这个为基础重新开发或者进行优化


首先是写一个统一的导出方法,不同的实体不同的数据都可以调用的一个方法

    public static void exportExcelByXml (HttpServletResponse response, String fileName, Class<?> clazz, List<?> list) {
        // 注意这里要用xls,如果改为xlsx打开表格的时候会报错(我这边反正会这样)
        fileName += ".xls";
        String cntentType = "application/vnd.ms-excel";
        try {
            // 解决文件中文名乱码
            fileName = new String(fileName.getBytes("UTF-8"), "ISO_8859_1");
            // 告诉浏览器用什么软件可以打开此文件
            response.setHeader("Content-Type", cntentType);
            // 下载文件的默认名称
            response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
            DataOutputStream rafs = new DataOutputStream(response.getOutputStream());
            // 创建表格数据xml
            StringBuffer data = createExcelXml(clazz, list, rafs);
            // 导出表格
            rafs.write(data.toString().getBytes()); 
            rafs.flush(); 
            rafs.close(); 
        } catch (UnsupportedEncodingException e) {
            // TODO: handle exception
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

接着我们写实际的XML的拼接方法,将传入的数据拼接成正确的XML

    // 每个sheet显示的数据行数 (因为是xls格式表格,我们将每个sheet设置为最多显示60000行数据)
    private static final int RECORDCOUNT = 60000;

    private static StringBuffer createExcelXml (Class<?> clazz, List<?> list, DataOutputStream rafs) throws IOException {
        StringBuffer stringBuffer = new StringBuffer();
        // 拼接表头和样式  (这里我对XML的内容进行了部分的删减,和直接另存为的XML文件格式是不一样的,因为有一部分不要也没啥问题,所以就直接省略了)
        stringBuffer.append("<?xml version=\"1.0\"?><?mso-application progid=\"Excel.Sheet\"?>"
                + "<Workbook xmlns=\"urn:schemas-microsoft-com:office:spreadsheet\" "
                + "xmlns:o=\"urn:schemas-microsoft-com:office:office\" "
                + "xmlns:x=\"urn:schemas-microsoft-com:office:excel\" "
                + "xmlns:ss=\"urn:schemas-microsoft-com:office:spreadsheet\" "
                + "xmlns:html=\"http://www.w3.org/TR/REC-html40\">"
                + "<Styles><Style ss:ID=\"Default\" ss:Name=\"Normal\">"
                + "<Alignment ss:Vertical=\"Center\"/><Borders/>"
                + "<Font ss:FontName=\"宋体\" x:CharSet=\"134\" ss:Size=\"12\"/>"
                + "<Interior/><NumberFormat/><Protection/></Style></Styles>");
        // 获取列数和表头
        Map<String, Object> baseMap = getColumnAndTitle(clazz);
        int column = (int) baseMap.get("column");
        List<String> titleList = (List<String>) baseMap.get("titleList");
        // 标记当前循环了多少行数据
        int count = 0;
        // 数据总数
        int total = list.size();
        // sheet个数
        int sheetNo = total % RECORDCOUNT == 0 ? total / RECORDCOUNT : total / RECORDCOUNT + 1;
        if (total == 0) sheetNo = 1;
        for (int i = 0; i < sheetNo; i ++) {
            int index = 0;
            stringBuffer.append("<Worksheet ss:Name=\"sheet"+i+"\">");
            stringBuffer.append("<Table ss:ExpandedColumnCount=\"" + column
                    + "\" ss:ExpandedRowCount=\"" + RECORDCOUNT + 1
                    + "\" x:FullColumns=\"1\" x:FullRows=\"1\">");
            // 添加表头
            stringBuffer.append("<Row>");
            for (int t = 0; t < column; t ++) {
                stringBuffer.append("<Cell><Data ss:Type=\"String\">" + titleList.get(t) + "</Data></Cell>");
            }
            stringBuffer.append("</Row>");
            for (int j = count; j < total; j ++) {
                stringBuffer.append("<Row>");
                // 保存表格数据
                List<Object> dataList = new ArrayList<Object>();
                getProperty(list.get(j), clazz, dataList);
                // 此时循环遍历表格数据
                for (int d = 0; d < column; d ++) {
                    stringBuffer.append("<Cell><Data ss:Type=\"String\">" + dataList.get(d) +"</Data></Cell>");
                }
                stringBuffer.append("</Row>");
                index ++;
                count ++;
                if (index > RECORDCOUNT || index == RECORDCOUNT) break;
            }
            stringBuffer.append("</Table><WorksheetOptions xmlns=\"urn:schemas-microsoft-com:office:excel\">"
                    + "<FrozenNoSplit/><SplitHorizontal>1</SplitHorizontal>"
                    + "<ProtectObjects>False</ProtectObjects><ProtectScenarios>False</ProtectScenarios>"
                    + "</WorksheetOptions></Worksheet>");
            // 每一个工作簿输出一次,释放资源,防止内存溢出 (如果这里不加这个代码,拼接完成了数据量还是很大,导出的时候还是会报内存溢出的异常。你甚至可以将这个数值设置的更小一些)
            rafs.write(stringBuffer.toString().getBytes());
            rafs.flush();
            stringBuffer.setLength(0);
        }
        stringBuffer.append("</Workbook>");
        return stringBuffer;
    }

获取实体类需要导出的属性的个数,及导出后表格中对应的表格头
(对了,忘了说,这个导出的功能是基于@Excel注解做的)

    private static Map<String, Object> getColumnAndTitle (Class<?> clazz) {
        Map<String, Object> map = new HashMap<String, Object>();
        // 保存列数
        int column = 0;
        // 保存表头数据
        List<String> list = new ArrayList<String>();
        Field [] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(Excel.class)) {
                // 获取当前属性的注解值
                Annotation anno = field.getDeclaredAnnotation(Excel.class);
                // 转为Excel类
                Excel excel = (Excel) anno;
                // 列数+1
                column ++;
                // 保存表头
                list.add(excel.name());
            }
        }
        map.put("column", column);
        map.put("titleList", list);
        return map;
    }

再就是获取属性的值了,如果是日期格式的,对日期进行格式化
(格式化基于@Excel注解的exportFormat属性)

    private static void getProperty (Object obj, Class<?> clazz, List<Object> dataList) {
        // 获取实体类中所有的属性
        Field [] fields = clazz.getDeclaredFields();
        // 遍历属性
        for (Field field : fields) {
            // 获取带有导出注解的属性
            if (field.isAnnotationPresent(Excel.class)) {
                // 创建一个字符数组,保存列名和值
                // 获取当前属性的注解值
                Annotation anno = field.getDeclaredAnnotation(Excel.class);
                // 转为Excel类
                Excel excel = (Excel) anno;
                // 获取当前属性
                String property = field.getName();
                // 拼接获取值方法
                String methodName = "get" + property.substring(0, 1).toUpperCase() + property.substring(1);
                try {
                    // 调用方法获取值
                    Method method = clazz.getDeclaredMethod(methodName);
                    // 获取当前值
                    Object data = method.invoke(obj);
                    // 如果此时是日期格式
                    String format = excel.exportFormat();
                    // 如果此时需要值的替换
                    String [] replace = excel.replace();
                    if (! OMSUtils.isNull(format)) {
                        // 说明此时是日期格式,按照日期格式导出
                        if (! OMSUtils.isNull(data)) {
                            dataList.add(DateUtils.getDate(new SimpleDateFormat(format), (Date) data));
                        } else {
                            dataList.add("");
                        }
                    } else if (replace.length > 0) {
                        // 此时需要值的替换
                        dataList.add(isReplace(replace, data + ""));
                    } else {
                        dataList.add(OMSUtils.isNull(data) ? "" : data);
                    }
                } catch (NoSuchMethodException e) {
                    // TODO: handle exception
                } catch (IllegalAccessException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } catch (IllegalArgumentException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }

在实际中,我们会把某些值以id_name的方式存储到数据库,这时,在我们导出的时候,就需要进行一个值的替换
(替换基于@Excel注解的replace属性)

    private static String isReplace (String [] replace, String id) {
        String result = id;
        for (int i = 0; i < replace.length; i ++) {
            String str = replace[i];
            // 拆解配置
            String [] strs = str.split("_");
            if (id.equals(strs[1])) {
                result = strs[0];
            }
        }
        return result;
    }

@Excel具体属性及用法在EasyPoi官网有详细介绍

到这里,通过XML方式导出Excel的工具类就结束了,能够实现最基本的导出功能,如果有特殊需要的就自己再改改吧
还有哦,这个导出来的表格是会首行冻结的,我挺喜欢这个的,要是不要就自己琢磨下吧
测试十四万条数据导出没任何问题
时间上基本主要耗时的是查询的时间,因为数据量太大了查询需要一段时间,导出的时间倒是很快,几秒钟的样子
具体的我也不知道,反正服务器不会崩╮(╯▽╰)╭