手把手教你实现超大数据量带进度条导入导出功能(二导出篇)

概述

对于超大数据量的导出是一个长耗时的操作。在没有进度条情况下,当用户点击导出后,页面长时间得不到反馈,导致用户不清楚是导出出现问题,还是导出正在进行处理还未结束。所以,为超大数据量的导出添加进度条功能显得尤为必要。

下面以导出10000000手机号数据为例,开发带进度条的导出功能。

需求分析

  1. 进度条分析

文件的导出有两个阶段,一是查询数据库生成数据文件;二是数据文件下载。对于数据文件的下载各大浏览器都可以很好的显示下载进度,不需要添加进度条。所以,进度条展示的是一阶段查询数据库生成数据文件的进度,对于超大数据量生成文件的过程会持续很长的时间,此处使用进度条展示非常合适。

  1. 超大数据量导出查询数据分析

对于超大数据量的查询采用的基本方案为,先查询总数据量数,根据设置每次查询条数计算出需要查询的次数,然后分页查询将查询结果动态写入数据文件,在将数据文件以流的形式输出给浏览器。

传统方式导出开发

传统方式不使用进度条展示导出进度,导出效果如下:

手把手教你实现超大数据量带进度条导入导出功能(二导出篇)

  1. 页面代码如下:
<form action="download" method="post">
    <input type="submit" value="导出">
</form>
  1. Java后端导出处理,代码如下:

其中导出文件暂存/tmp目录下

public void download() throws IOException {
    final String fileName = UUID.randomUUID().toString().replace("-","");
    try (final FileWriter fileWriter = new FileWriter(new File("/tmp/" + fileName + ".csv"));
         final BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);){
        final long totalNum = remoteGetUserNum();
        final long pageSize = 100L ;
        long totalPage = (totalNum%pageSize == 0L) ? totalNum/pageSize:(totalNum/pageSize + 1);
        for (long pageNum = 1L; pageNum <= totalPage; pageNum++) {
            List<String> userList = remoteGetUser(pageNum,pageSize);
            for (String s : userList) {
                bufferedWriter.write(s+"\r\n");
            }
        }
        bufferedWriter.flush();
    }
    final HttpServletResponse response = getResponse();
    final File file = new File("/tmp/" + fileName + ".csv");
    response.addHeader("Content-Disposition","attachment;filename=" + new String(fileName.getBytes())+".csv");
    response.addHeader("Content-Length",""+file.length());
    response.setContentType("application/octet-stream");
    try (final FileInputStream in = new FileInputStream(file);
         final BufferedInputStream bufferedInputStream = new BufferedInputStream(in);
         final OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());){
        byte[] buffer = new byte[1024];
        int len;
        while ((len = bufferedInputStream.read(buffer)) != -1){
            outputStream.write(buffer,0,len);
        }
        outputStream.flush();
    }
    renderNull();
}

private List<String> remoteGetUser(long pageNum, long pageSize) {
    return userList;
}

List<String> userList = new ArrayList<>();
{
    for (long i = 0; i < 100; i++) {
        userList.add(String.valueOf(18321714630L+i));
    }
}
private long remoteGetUserNum() {
    return 10000000L;
}

待进度条导出

效果如下:

手把手教你实现超大数据量带进度条导入导出功能(二导出篇)

  1. 进度条界面设计

在导出功能按钮下方编写静态进度条代码,通过控制内部div的宽度来实现进度的变化,默认隐藏。

<form action="download" method="post">
    <input type="submit" value="导出">
    <div id="progress" style="height:20px;width:100%;background: #efefef;border:1px solid #eee;border-radius:10px;display:none;">
        <div class="bar" style="background: green;width:10%;height: 100%;border-radius:10px;line-height:20px;">0%</div>
    </div>
</form>

静态效果如下:

手把手教你实现超大数据量带进度条导入导出功能(二导出篇)

  1. 前端页面下载请求改造

传统导出功能页面点击导出提交表单请求获取文件,使用进度条导出功能后,既要提交导出请求又要更新进度条变化,请求就变成了多任务。需要使用ajax请求来生成数据文件和更新进度条展示,等到后台数据文件生成好ajax请求得到处理完成的响应后,再通过js发起表单请求下载处理好的数据文件即可。

代码变更如下:

function download(){
    var bar =$('#progress').show().find('.bar')
    bar.text('0%')
    bar.css({width:'0%'});
    var taskId = '' + new Date().getTime()
    var timer = setInterval(function(){
        $.ajax({
            type:'post',
            url:'getProgress',
            data:{taskId:taskId},
            dataType: "json",
        }).success(function(data){
            if(data.result){
                bar.text(data.value + '%')
                bar.animate({width:data.value + '%'},300)
            }
        }).error(function(e){
        })
    },300);
    $.ajax({
        type:'post',
        url:'download',
        data:{taskId:taskId},
        dataType: 'json',
    }).success(function(data){
        if(data.result){
            jQuery('<form action="downloadFile" method="post">' +  // action请求路径及推送方法
                '<input type="text" name="taskId" value="' + taskId + '"/>' +
                '</form>')
                .appendTo('body').submit().remove();
        }
        clearInterval(timer)
        $('#progress').hide().find('.bar').css({width:'0%'})
    }).error(function(e){
        clearInterval(timer)
        $('#progress').hide().find('.bar').css({width:'0%'})
        // do some things
    })
}
  1. 后端java代码开发

将文件导出的两个阶段分开,download方法处理一阶段内容,分批查询生成数据文件,此阶段耗时较长需要添加进度控制;downloadFile方法处理二阶段内容,将数据文件以流的形式输出到浏览器,此阶段下载进度各大浏览器展示都很完善。

public void download() throws IOException {
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("result",true);
    jsonObject.put("msg","成功");
    final String taskId = getPara("taskId");
    try (final FileWriter fileWriter = new FileWriter(new File("/tmp/" + taskId + ".csv"));
         final BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);){
        final long totalNum = remoteGetUserNum();
        final long pageSize = 100L ;
        long totalPage = (totalNum%pageSize == 0L) ? totalNum/pageSize:(totalNum/pageSize + 1);
        for (long pageNum = 1L; pageNum <= totalPage; pageNum++) {
            List<String> userList = remoteGetUser(pageNum,pageSize);
            for (String s : userList) {
                bufferedWriter.write(s+"\r\n");
            }
            taskProgressMap.put(taskId, Math.toIntExact(pageNum * 100 / totalPage));
        }
    }
    renderJson(jsonObject.toJSONString());
}

public void downloadFile() throws IOException {
    final String taskId = getPara("taskId");
    final File file = new File("/tmp/" + taskId + ".csv");
    final HttpServletResponse response = getResponse();
    response.addHeader("Content-Disposition","attachment;filename=" + new String(file.getName().getBytes()));
    response.addHeader("Content-Length",""+file.length());
    response.setContentType("application/octet-stream");
    try (final FileInputStream in = new FileInputStream(file);
         final BufferedInputStream bufferedInputStream = new BufferedInputStream(in);
         final OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());){
        byte[] buffer = new byte[1024];
        int len;
        while ((len = bufferedInputStream.read(buffer)) != -1){
            outputStream.write(buffer,0,len);
        }
        outputStream.flush();
    }
    renderNull();
}

总结

  1. 超大数据导出需要分批查询处理。
  2. 超大数据导出一阶段(查询数据生成数据文件)耗时较长,需要添加进度监控。

本文只是提供一种简单的demo,进度条功能再其他耗时操作中都可以使用,应该做成js组件的形式供以后复用。

结尾附上我的开源消息组件地址(其中包含进度条组件:手把手教你开发一款属于自己的前端jQuery消息组件

上一篇:SpringBoot + SpringBatch + Quartz整合定时批量任务


下一篇:谷粒学苑项目第一天-关于环境搭建施依赖下载失败