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

最初的导出用的就是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(""
                + "
                + "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\">"
                + "");
        // 获取列数和表头
        Map baseMap = getColumnAndTitle(clazz);
        int column = (int) baseMap.get("column");
        List titleList = (List) 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("+i+"\">");
            stringBuffer.append(" + column
                    + "\" ss:ExpandedRowCount=\"" + RECORDCOUNT + 1
                    + "\" x:FullColumns=\"1\" x:FullRows=\"1\">");
            // 添加表头
            stringBuffer.append("");
            for (int t = 0; t < column; t ++) {
                stringBuffer.append("" + titleList.get(t) + "");
            }
            stringBuffer.append("");
            for (int j = count; j < total; j ++) {
                stringBuffer.append("");
                // 保存表格数据
                List dataList = new ArrayList();
                getProperty(list.get(j), clazz, dataList);
                // 此时循环遍历表格数据
                for (int d = 0; d < column; d ++) {
                    stringBuffer.append("" + dataList.get(d) +"");
                }
                stringBuffer.append("");
                index ++;
                count ++;
                if (index > RECORDCOUNT || index == RECORDCOUNT) break;
            }
            stringBuffer.append("
"
+ "1" + "FalseFalse" + "
"
); // 每一个工作簿输出一次,释放资源,防止内存溢出 (如果这里不加这个代码,拼接完成了数据量还是很大,导出的时候还是会报内存溢出的异常。你甚至可以将这个数值设置的更小一些) rafs.write(stringBuffer.toString().getBytes()); rafs.flush(); stringBuffer.setLength(0); } stringBuffer.append("
"
); return stringBuffer; }

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

    private static Map getColumnAndTitle (Class clazz) {
        Map map = new HashMap();
        // 保存列数
        int column = 0;
        // 保存表头数据
        List list = new ArrayList();
        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的工具类就结束了,能够实现最基本的导出功能,如果有特殊需要的就自己再改改吧
还有哦,这个导出来的表格是会首行冻结的,我挺喜欢这个的,要是不要就自己琢磨下吧
测试十四万条数据导出没任何问题
时间上基本主要耗时的是查询的时间,因为数据量太大了查询需要一段时间,导出的时间倒是很快,几秒钟的样子
具体的我也不知道,反正服务器不会崩╮(╯▽╰)╭

你可能感兴趣的:(JAVA 导出Excel大数据内存溢出解决方案)