获取后台任务进度的另类办法
文章目录
今天看到jdeferred
文档中一个关于Asynchronous Servlet
的例子,如下
@WebServlet(value = "/AsyncServlet", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private ExecutorService executorService = Executors.newCachedThreadPool();
private DeferredManager dm = new DefaultDeferredManager(executorService);
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
final AsyncContext actx = request.startAsync(request, response);
dm.when(new Callable<String>() {
@Override
public String call() throws Exception {
if (actx.getRequest().getParameter("fail") != null) {
throw new Exception("oops!");
}
Thread.sleep(2000);
return "Hello World!";
}
}).then(new DoneCallback<String>() {
@Override
public void onDone(String result) {
actx.getRequest().setAttribute("message", result);
actx.dispatch("/hello.jsp");
}
}).fail(new FailCallback<Throwable>() {
@Override
public void onFail(Throwable exception) {
actx.getRequest().setAttribute("exception", exception);
actx.dispatch("/error.jsp");
}
});
}
}
突然想到在以前工作中经常前端向后端提交了一个长时间任务,为了良好的用户体验,前端还需要定时获取该任务的进度信息。之前的方案如下:
- 前端提交任务创建需要的信息至后台,后台为该任务创建对应Task,仅将该Task的ID返回至前端
- 后端向线程池提交该任务对应的Task Runnable,该Runnable的执行体里以任务的进度信息更新该Task的progress字段
- 前端定时发AJAX请求凭借Task的ID取进度
以前我一直有个疑问:就为了更新进度信息,浏览器要不停地向后端发请求,是不是代价太大了。曾经也尝试过以一个WebSocket请求代替轮寻询AJAX请求,但还是觉得比较麻烦。
今天看到异步Servlet,又想起以前看过的监控AJAX下载进度的例子,感觉可以有另一种解决方案。直接粘代码吧。
首先是获取任务进度的后端代码
package personal.xxj.servlet;
import org.jdeferred.DeferredManager;
import org.jdeferred.DoneCallback;
import org.jdeferred.FailCallback;
import org.jdeferred.impl.DefaultDeferredManager;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by jeremy on 16/5/15.
*/
public class GetTaskProgressServlet extends HttpServlet {
private ExecutorService executorService = Executors.newCachedThreadPool();
private DeferredManager dm = new DefaultDeferredManager(executorService);
private Random random = new Random();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
final AsyncContext actx = request.startAsync(request, response);
actx.setTimeout(Long.MAX_VALUE);
dm.when(new Callable<Void>() {
@Override
public Void call() throws Exception {
HttpServletResponse resp = (HttpServletResponse) actx.getResponse();
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
resp.setContentLength(100);
try {
for (int i = 0; i < 100; i++) {
Thread.sleep(random.nextInt(10) * 10);
resp.getWriter().write("*");
resp.getWriter().flush();
}
} catch (Throwable e){
e.printStackTrace();
} finally {
actx.complete();
}
return null;
}
});
}
}
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<display-name>Java Web Demo</display-name>
<servlet>
<servlet-name>GetTaskProgressServlet</servlet-name>
<servlet-class>personal.xxj.servlet.GetTaskProgressServlet</servlet-class>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>GetTaskProgressServlet</servlet-name>
<url-pattern>/api/getTaskProgress</url-pattern>
</servlet-mapping>
</web-app>
可以看到这里用到了jdeferred
与Asynchronous Servlet
,工作逻辑就是模拟一个任务在慢慢地执行,每执行1%
则向response
里打印一个*
。
为啥一定要用Asynchronous Servlet
?最大的原因是不想这些长时间运行的任务占用http线程,但又想持有请求响应上下文,可以在任务运行过程中输出合理的响应。
这里有几点要注意:
- 像
actx.setTimeout(Long.MAX_VALUE)
这样根据实际场景设置超时时间,默认好像才30秒,对于一个长时间任务来说太短了 resp.setContentType
,resp.setCharacterEncoding
,resp.setContentLength
最后都调用一遍,以免前端由于收到不这样响应头,非得接收完整的响应内容后才触发XMLHttpRequest
的progress
事件。(唉,入坑数小时,说多都是泪)- 每向response里打印一个
*
后需要调用resp.getWriter().flush();
,尽快将响应刷回客户端。 - 任务完成后要保证
actx.complete();
得到调用。 - 注册异步servlet时,在web.xml里需要
<async-supported>true</async-supported>
然后是前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Task Progress Demo</title>
</head>
<body>
<h1 id="output"></h1>
<script type="text/javascript">
var xhr = new XMLHttpRequest();
xhr.timeout = Number.MAX_VALUE;
xhr.open('GET', '/javawebdemo/api/getTaskProgress');
xhr.onprogress = function(event){
if (event.lengthComputable) {
var percentComplete = parseInt((100 * event.loaded) / event.total);
document.getElementById("output").innerHTML = "Task's progress is " + percentComplete + "%";
}
};
xhr.onerror = function(){
document.getElementById("output").innerHTML = "Task's execution is failed";
};
xhr.send();
</script>
</body>
</html>
前端代码倒没有太多要注意的地方,只有一点要注意设置xhr.timeout
。
本例使用了Servlet 3.0 API
及HTML5
中的XMLHttpRequest 2
,XMLHttpRequest 2
现在较新的主流浏览器都支持。
另外我查阅XMLHttpRequest 2
的文档时还发现在XMLHttpRequest 2
里不仅可以监控下载的进度,也可以监控上传的进度,参见XMLHttpRequest.upload的progress事件。
XMLHttpRequest 2
还可以上传文件,接收二进制数据,参见这里,真是强大地不要不要的。
文章作者 Jeremy Xu
上次更新 2016-05-16
许可协议 © Copyright 2020 Jeremy Xu