Web应用中,有时会遇到一些耗时很长的操作(比如:在后台生成100张报表再呈现,或 从ftp下载若干文件,综合处理后再返回给页面下载),用户在网页上点完按钮后,通常会遇到二个问题:页面超时、看不到处理进度。
对于超时,采用异步操作,可以很好的解决这个问题,后台服务收到请求后,执行异步方法不会阻塞线程,因此就不存在超时问题。但是异步处理的进度用户也需要知道,否则不知道后台的异步处理何时完成,用户无法决定接下来应该继续等候? or 关掉页面?
思路:
1、browser -> Spring-MVC Controller -> call 后台服务中的异步方法 -> 将执行进度更新到redis缓存 -> 返回view
2、返回的view页面上,ajax -> 轮询 call 后台服务 -> 查询redis中的进度缓存数据,并实时更新UI进度显示 -> 如果完成 call 后台服务清理缓存
注:这里采用了redis保存异步处理的执行进度,也可以换成session或cookie来保存。
步骤:
一、spring配置文件中,增加Task支持
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:task="http://www.springframework.org/schema/task" 5 xsi:schemaLocation="http://www.springframework.org/schema/beans 6 http://www.springframework.org/schema/beans/spring-beans.xsd 7 http://www.springframework.org/schema/task 8 http://www.springframework.org/schema/task/spring-task.xsd"> 9 10 11 12 <!-- 支持异步方法执行 --> 13 <task:annotation-driven/> 14 15 </beans>
二、后台Service中,在方法前加上@Async
先定义服务接口:
1 package ctas.web.service; 2 3 public interface AsyncService { 4 5 /** 6 * 异步执行耗时较长的操作 7 * 8 * @param cacheKey 9 * @throws Exception 10 */ 11 void asyncMethod(String cacheKey) throws Exception; 12 13 /** 14 * 获取执行进度 15 * 16 * @param cacheKey 17 * @return 18 * @throws Exception 19 */ 20 String getProcess(String cacheKey) throws Exception; 21 22 /** 23 * 执行完成后,清除缓存 24 * 25 * @param cacheKey 26 * @throws Exception 27 */ 28 void clearCache(String cacheKey) throws Exception; 29 }
服务实现:
1 package ctas.web.service.impl; 2 import ctas.web.service.AsyncService; 3 import org.springframework.beans.factory.annotation.Autowired; 4 import org.springframework.data.redis.core.StringRedisTemplate; 5 import org.springframework.scheduling.annotation.Async; 6 import org.springframework.stereotype.Service; 7 8 @Service("asyncService") 9 public class AsyncServiceImpl extends BaseServiceImpl implements AsyncService { 10 11 @Autowired 12 StringRedisTemplate stringRedisTemplate; 13 14 15 @Override 16 @Async 17 public void asyncMethod(String cacheKey) throws Exception { 18 //模拟总有20个步骤,每个步骤耗时2秒 19 int maxStep = 20; 20 for (int i = 0; i < maxStep; i++) { 21 Thread.sleep(2000); 22 //将执行进度放入缓存 23 stringRedisTemplate.opsForValue().set(cacheKey, (i + 1) + "/" + maxStep); 24 } 25 } 26 27 @Override 28 public String getProcess(String cacheKey) throws Exception { 29 return stringRedisTemplate.opsForValue().get(cacheKey); 30 } 31 32 @Override 33 public void clearCache(String cacheKey) throws Exception { 34 //完成后,清空缓存 35 stringRedisTemplate.delete(cacheKey); 36 } 37 38 39 }
注意:asyncMethod方法前面的@Async注解,这里模拟了一个耗时的操作,并假设要完成该操作,共需要20个小步骤,每执行完一个步骤,将进度更新到redis缓存中。
三、Controller的处理
1 @RequestMapping(value = "async/{key}") 2 public String asyncTest(HttpServletRequest req, 3 HttpServletResponse resp, @PathVariable String key) throws Exception { 4 asyncService.asyncMethod(key); 5 return "common/async"; 6 } 7 8 @RequestMapping(value = "async/{key}/status") 9 public String showAsyncStatus(HttpServletRequest req, 10 HttpServletResponse resp, @PathVariable String key) throws Exception { 11 String status = asyncService.getProcess(key); 12 ResponseUtil.OutputJson(resp, "{"status":"" + status + ""}"); 13 return null; 14 } 15 16 @RequestMapping(value = "async/{key}/clear") 17 public String clearAsyncStatus(HttpServletRequest req, 18 HttpServletResponse resp, @PathVariable String key) throws Exception { 19 asyncService.clearCache(key); 20 ResponseUtil.OutputJson(resp, "{"status":"ok"}"); 21 return null; 22 }
四、view上的ajax处理
1 <script type="text/javascript" language="JavaScript"> 2 3 var timerId = null;//定时器ID 4 5 $(document).ready(function () { 6 7 /* 8 定时轮询执行进度 9 */ 10 timerId = setInterval(function () { 11 getStatus(); 12 }, 1000); 13 getStatus(); 14 }); 15 16 /** 17 获取执行进度 18 */ 19 function getStatus() { 20 var statusUrl = window.location.href + "/status"; 21 $.get(statusUrl, function (data) { 22 if (data == null || data.status == null || data.status == "null") { 23 updateStatus("准备中"); 24 return; 25 } 26 var status = data.status; 27 updateStatus(status); 28 var temp = status.split("/"); 29 if (temp[0] == temp[1]) { 30 updateStatus("完成"); 31 clearInterval(timerId);//停止定时器 32 clearStatus();//清理redis缓存 33 } 34 }) 35 } 36 37 /** 38 * 执行完成后,清理缓存 39 */ 40 function clearStatus() { 41 var clearStatusUrl = window.location.href + "/clear"; 42 $.get(clearStatusUrl, function (data) { 43 //alert(data.status); 44 }) 45 } 46 47 /** 48 更新进度显示 49 */ 50 function updateStatus(msg) { 51 $("#status").html(msg); 52 } 53 </script> 54 <div id="msgBox"> 55 <span>请稍候,服务器正在处理中...</span> 56 57 <h1>当前处理进度:<span style="color:red" id="status">准备中</span></h1> 58 </div>
浏览 http://localhost:8080/xxx/async/123123后的效果