今天看到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"); } }); } }
突然想到在以前工作中经常前端向后端提交了一个长时间任务,为了良好的用户体验,前端还需要定时获取该任务的进度信息。之前的方案如下:
以前我一直有个疑问:就为了更新进度信息,浏览器要不停地向后端发请求,是不是代价太大了。曾经也尝试过以一个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
事件。(唉,入坑数小时,说多都是泪)*
后需要调用resp.getWriter().flush();
,尽快将响应刷回客户端。actx.complete();
得到调用。<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
还可以上传文件,接收二进制数据,参见这里,真是强大地不要不要的。
本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。
我来说两句