1.需求
- 对于成千上万数据量的excel导入,后台处理耗时长,体验差.需要实时展示当前导入的进度,提高使用体验
2.实现思路
- 采用多线程进行实现
- 在导入开始执行前,生成一个uuid和进度对象,储存到静态Map中
- 使用一个新线程执行导入,导入执行时,将导入的进度信息放到uuid对应的对象里
- 将uuid返回给前端.请求结束
-前端循环发请求,从后台获取uuid对应的进度对象,将展示到页面上
3.页面实现效果(仅供参考)
正在导入导入完成
4.定义用于存储导入进度的对象
- 将进度的一些常用信息进行定义,同时声明一个静态Map,用于存储所有进度信息
/**
* 用于存储学生信息导入的进度信息
* @author authstr
* @time 2019年10月24日16:56:21
*/
public class ImportAsynInfo {
//用于存储所有的导入进度信息
public static Map<String,ImportAsynInfo> allAsynInfo=new HashMap<String,ImportAsynInfo>();
//提示信息或 异常信息
private String msg;
//数据总数
private Integer totality=0;
//已处理的数据条数
private Integer doneSum=0;
//失败的数据条数
private Integer errorSum=0;
//成功的数据条数
private Integer successSum=0;
//错误文件的路径
public String errorFilePath;
//导入是否结束
public Boolean isEnd= false;
/**
* 创建一个进度信息,并获取对应的uuid
* @return
*/
public static String createAsynInfo(){
ImportAsynInfo asynInfo=new ImportAsynInfo();
String uuid=UUID.randomUUID().toString().replace("-","");
allAsynInfo.put(uuid,asynInfo);
return uuid;
}
/**
* 通过uuid获取进度信息
* @param uuid
* @return
*/
public static ImportAsynInfo getAsynInfo(String uuid){
return allAsynInfo.get(uuid);
}
/**
* 通过uuid删除对应的进度信息
* @param uuid
* @return
*/
public static void deleteAsynInfo(String uuid){
allAsynInfo.remove(uuid);
}
/**
* uuid对应的进度 已处理的数据条数+1
* @param uuid
*/
public static void doneSumAddOne(String uuid){
ImportAsynInfo asynInfo= getAsynInfo(uuid);
asynInfo.setDoneSum(asynInfo.getDoneSum()+1);
}
/**
* uuid对应的进度 失败的数据条数+1
* @param uuid
*/
public static void errorSumAddOne(String uuid){
ImportAsynInfo asynInfo= getAsynInfo(uuid);
asynInfo.setErrorSum(asynInfo.getErrorSum()+1);
}
/**
* uuid对应的进度 成功的数据条数+1
* @param uuid
*/
public static void successSumAddOne(String uuid){
ImportAsynInfo asynInfo= getAsynInfo(uuid);
asynInfo.setSuccessSum(asynInfo.getSuccessSum()+1);
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Integer getTotality() {
return totality;
}
public void setTotality(Integer totality) {
this.totality = totality;
}
public Integer getDoneSum() {
return doneSum;
}
public void setDoneSum(Integer doneSum) {
this.doneSum = doneSum;
}
public Integer getErrorSum() {
return errorSum;
}
public void setErrorSum(Integer errorSum) {
this.errorSum = errorSum;
}
public Integer getSuccessSum() {
return successSum;
}
public void setSuccessSum(Integer successSum) {
this.successSum = successSum;
}
public String getErrorFilePath() {
return errorFilePath;
}
public void setErrorFilePath(String errorFilePath) {
this.errorFilePath = errorFilePath;
}
public Boolean getEnd() {
return isEnd;
}
public void setEnd(Boolean end) {
isEnd = end;
}
}
5.Controller层开启线程进行导入
- 获取线程池,通过线程池来启动线程来执行导入,并将uuid传入
- 文件需要通过输入流来传入,直接传文件对象,可能无法读取到文件
- 后面定义一个接口,来向前端返回指定uuid对应的进度对象.并对进度对象进行清理
@RestController
@RequestMapping("student_import/v1")
public class StudentImportController extends AbstractAPIController {
@Autowired
StudentImportServiceImpl studentImportService;
private ExecutorService executor = Executors.newCachedThreadPool() ;
//下载导入模板
@RequestMapping("/excelExport")
public void excelExport(HttpServletResponse response) {
studentImportService.excelExport(response);
}
//数据导入处理
@RequestMapping("/save_excel_auto_studentno")
public Map saveExcelStudentno(HttpServletResponse response, @RequestParam("file") MultipartFile file){
Map m = new HashMap<>();
String uuid=ImportAsynInfo.createAsynInfo();
try {
final InputStream inputStream = file.getInputStream();
executor.submit(new Runnable(){
@Override
public void run() {
try {
studentImportService.saveExcel_auto_studentno(response, inputStream,uuid);
}catch(Exception e) {
e.printStackTrace();
ImportAsynInfo.getAsynInfo(uuid).setMsg(e.getMessage());
ImportAsynInfo.getAsynInfo(uuid).setEnd(true);
throw new Exception("无法进行导入!");
}
}
});
} catch (IOException e) {
e.printStackTrace();
}
m.put("uuid",uuid);
return m;
}
//下载导入的错误文件
@RequestMapping("downloadErrorExcel")
public void downloadErrorExcel(HttpServletResponse response, String fileName){
studentImportService.downloadErrorExcel(response, fileName);
}
//获取导入的进度
@RequestMapping("get_import_plan")
public Map get_import_plan(String uuid) {
Map m = new HashMap<>();
ImportAsynInfo asynInfo=ImportAsynInfo.getAsynInfo(uuid);
//如果导入结束,复制进度对象进行返回,将储存的进度对象删除
if(asynInfo!=null&&asynInfo.getEnd()){
ImportAsynInfo newAsynInfo=new ImportAsynInfo();
newAsynInfo.setEnd(asynInfo.getEnd());
newAsynInfo.setMsg(asynInfo.getMsg());
newAsynInfo.setErrorFilePath(asynInfo.getErrorFilePath());
newAsynInfo.setTotality(asynInfo.getTotality());
newAsynInfo.setDoneSum(asynInfo.getDoneSum());
newAsynInfo.setErrorSum(asynInfo.getErrorSum());
newAsynInfo.setSuccessSum(asynInfo.getSuccessSum());
ImportAsynInfo.deleteAsynInfo(uuid);
asynInfo=newAsynInfo;
}
m.put("data",asynInfo);
return m;
}
6. service进行执行导入
- 在导入过程中,设置导入进度信息
- 其他业务相关代码已省略
@Service
public class StudentImportServiceImpl extends AbstractService implements StudentImportService {
@Override
public void excelExport(HttpServletResponse response) {
//导入模板下载 略
}
@Override
public void downloadErrorExcel(HttpServletResponse response, String fileName) {
//下载错误文件 略
}
@Transactional
@Override
public Map<String, Object> saveExcel(HttpServletResponse response, InputStream inputStream,String uuid) {
//其他代码...
//获取excel导入数据数量后
ImportAsynInfo.getAsynInfo(uuid).setTotality( 数量 );
//其他代码...
for (int i = 0; i < 数量; i++) {
//其他代码...
//在一条数据处理结束后
ImportAsynInfo.doneSumAddOne(uuid);
//其他代码...
if(数据有错误){
//其他代码...
ImportAsynInfo.errorSumAddOne(uuid);
}else{
//其他代码...
ImportAsynInfo.successSumAddOne(uuid);
}
}
//其他代码...
//错误文件创建后
ImportAsynInfo.getAsynInfo(uuid).setErrorFilePath(errorFileName);
//其他代码...
//导入完成后
ImportAsynInfo.getAsynInfo(uuid).setEnd(true);
}
}
7.显示进度条页面
- 在网上没找到方便的可以显示多种颜色的进度条,这里曲线救国,采用ECharts的饼图来显示进度信息(具体样式可以根据需求调整)
- 一些js与样式 已略过
<!doctype html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta content="always" name="referrer">
<script src="/.../echarts/echarts.min.js"></script>
</head>
<body>
<div class="J_conWarp g-lr-mg g-warning-box">
<h4 class="g-title"> <a href="javascript:history.back()" style="font-size: 16px; color: black; " >< 返回</a></h4>
<div class="J_containerWarp">
<div class="container-fluid g-t-mg2">
<div class="row" >
<div class="col-xs-12" >
<div id="import_file">
<form class="form-horizontal" role="form" id="excelExportForm">
</form>
<form class="form-horizontal" role="form" id="downloadErrorExcelForm">
<input type="hidden" name="fileName" id="fileName" value="" />
</form>
<form class="form-horizontal" role="form" id="form1">
<div class="form-group ">
<label class="col-xs-2 control-label"><em class="text-red">*</em>导入文件</label>
<div class="col-xs-10">
<input type="file" id="file" name="file" class="form-control" style="width:400px" accept=".xls" />
</div>
</div>
</form>
<div class="form-group ">
<label class="col-xs-2 control-label"></label>
<a class="gbn gbn-m" href="javascript:" onclick="E.excelExport();" >下载导入模板</a>
<a class="gbn gbn-m" href="javascript:" onclick="E.saveExcel();" >导入</a>
<a class="gbn gbn-m gbn-red" href="javascript:" onclick="history.back();" >返回</a>
</div>
</div>
<!-- 为 ECharts 准备一个具备大小(宽高)的 DOM -->
<div id="main" style="width:100%;height:550px;"></div>
<div id="downloadErrorExcelA"class="col-xs-12" style="height:150px;width:100%;display:none">
<div class="col-xs-4"></div>
<div class="form-group ">
<a class="gbn gbn-m gbn-red" href="javascript:" onclick="E.downloadErrorExcel();" >下载错误文件</a>
<a id="quxiao" class="gbn gbn-m" href="javascript:" onclick="E.quxiao();" >取消</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
<script type="text/javascript">
window.onload = window.onresize = function() {
$(".J_containerWarp").height($(window).height() - 60);
$(".J_containerWarp").niceScroll({});
}
$.ajaxSettings.async=true;
var uuid=null;
var setInterval_id=null;
var myChart = echarts.init(document.getElementById('main'));
var option = {
title : {
text: '正在进行导入中...',
subtext: '当前进度',
x:'center'
},
tooltip : {
trigger: 'item',
formatter: "{a} <br/>{b} : {c} ({d}%)"
},
legend: {
orient: 'vertical',
left: 'right',
data: ['导入成功','导入失败','未处理']
},
series : [
{
name: '导入进度',
type: 'pie',
// radius : '55%',
radius: ['50%', '70%'],
center: ['50%', '60%'],
data:[
{value:0, name:'导入成功'},
{value:0, name:'导入失败'},
{value:100, name:'未处理'}
],
itemStyle: {
emphasis: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
color: ['#2ECC71','#E67E22','#BDC3C7'],
}
]
};
var E = {
excelExport : function() {
$("form[id=excelExportForm]").attr("action",
"${request.contextPath}/student_import/v1/excelExport");
$("#excelExportForm").submit();
},
saveExcel : function() {
var file = $("#file").val();
if(!file){
Message.error("导入的文件不能为空");
return ;
}
//循环获取进度信息
setInterval_id=setInterval(E.getAsynInfo,500);
myChart.setOption(option);
var index = layer.load(1);
var formData = new FormData();
//隐藏导入区域和错误文件下载区域,显示进度条区域
$("#import_file").hide();
$("#main").show();
$("#downloadErrorExcelA").hide();
formData.append('file', $('#file')[0].files[0]);
$.ajax({
type : "POST",
url : "${request.contextPath}/student_import/v1/save_excel",
data : formData,
async: false,
cache: false,
contentType: false,
processData: false,
success : function(o) {
if(o.code==1){
//设置uuid
uuid=o.uuid;
} else {
Message.error(o.msg);
}
}
});
},
downloadErrorExcel : function() {
$("form[id=downloadErrorExcelForm]").attr("action",
"${request.contextPath}/student_import/v1/downloadErrorExcel");
$("#downloadErrorExcelForm").submit();
},
getAsynInfo:function(){
//如果uuid存在,进行获取数据
if(uuid!=null){
$.post("${request.contextPath}/student_import/v1/get_plan",{"uuid":uuid},function(o){
console.log(o);
//如果获取到了数据
if(o.code==1&&o.data!=null){
// 使用指定的配数据显示图表。
option.title.subtext="当前进度 [共"+o.data.totality+"]条";
option.series[0].data[0].value=o.data.successSum;
option.series[0].data[1].value=o.data.errorSum;
option.series[0].data[2].value=o.data.totality-o.data.doneSum;
myChart.setOption(option);
//如果导入结束了
if(o.data.isEnd){
option.title.text="导入完成";
myChart.setOption(option);
clearInterval(setInterval_id);
//如果有错误数据,展示错误文件的下载
if(o.data.totality>0&&o.data.errorSum>0){
$("#fileName").val(o.data.errorFilePath);
$("#downloadErrorExcelA").show();
}
//如果导入中出现的异常
if(o.data.msg!=null){
$("#import_file").show();
$("#main").hide();
Message.error(o.data.msg);
}else{
Message.success("导入结束,"+o.data.successSum+"条数据导入成功,"+o.data.errorSum+"条数据导入失败");
}
}
}else{
Message.error(o.msg);
}
});
}
},
quxiao:function () {
$("#import_file").show();
$("#main").hide();
$("#downloadErrorExcelA").hide();
uuid=null;
}
}
</script>
</html>
8.还未完成的功能
- 对线程进行处理和关闭
- 增加取消导入功能
- 导入进度对象中,增加一个 是否取消 的标识
- 在导入的每次循环中,判断这个标识,如果true,跳出整个导入方法,并回滚事务
- 增加中断导入功能
- 导入进度对象中,增加一个 是否中断 的标识
- 在导入的每次循环中,判断这个标识,如果true,直接提交事务,将之前的错误数据信息和之后还未处理的数据合并成一个excel给前端,然后跳出导入方法
网友评论