获取后台任务进度的另类办法

今天看到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");
      }
    });
  }
}

突然想到在以前工作中经常前端向后端提交了一个长时间任务,为了良好的用户体验,前端还需要定时获取该任务的进度信息。之前的方案如下:

  1. 前端提交任务创建需要的信息至后台,后台为该任务创建对应Task,仅将该Task的ID返回至前端
  2. 后端向线程池提交该任务对应的Task Runnable,该Runnable的执行体里以任务的进度信息更新该Task的progress字段
  3. 前端定时发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>

可以看到这里用到了jdeferredAsynchronous Servlet,工作逻辑就是模拟一个任务在慢慢地执行,每执行1%则向response里打印一个*

为啥一定要用Asynchronous Servlet?最大的原因是不想这些长时间运行的任务占用http线程,但又想持有请求响应上下文,可以在任务运行过程中输出合理的响应。

这里有几点要注意:

  • actx.setTimeout(Long.MAX_VALUE)这样根据实际场景设置超时时间,默认好像才30秒,对于一个长时间任务来说太短了
  • resp.setContentTyperesp.setCharacterEncodingresp.setContentLength最后都调用一遍,以免前端由于收到不这样响应头,非得接收完整的响应内容后才触发XMLHttpRequestprogress事件。(唉,入坑数小时,说多都是泪)
  • 每向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 APIHTML5中的XMLHttpRequest 2XMLHttpRequest 2现在较新的主流浏览器都支持。

另外我查阅XMLHttpRequest 2的文档时还发现在XMLHttpRequest 2里不仅可以监控下载的进度,也可以监控上传的进度,参见XMLHttpRequest.upload的progress事件。

XMLHttpRequest 2还可以上传文件,接收二进制数据,参见这里,真是强大地不要不要的。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java帮帮-微信公众号-技术文章全总结

Web-第十五天 Ajax学习【悟空教程】

在实际开发中,完成注册功能前,如果用户填写用户信息,准备填写其他信息时,将提示当前用户的用户名是否可用。效果图如下:

14230
来自专栏扎心了老铁

redis的sentinel主从切换(failover)与Jedis线程池自动重连

本文介绍如何通过sentinel监控redis主从集群,并通过jedis自动切换ip和端口。 1、配置redis主从实例 10.93.21.21:6379 10...

54160
来自专栏代码拾遗

OkHttp 使用示例

可以用来下载文件,打印header,打印body。string()方法对于小文档的响应来说是个既方便又高效的方法。但是如果一个文档太大(大于1M),就不要使用s...

40610
来自专栏LIN_ZONE

javaweb 与jsp页面的交互流程 (初次接触时写)

2. jsp 页面一般情况下放在 top(前台页面) back(后台页面) 3. 后台代码 放在src下面,分为: 1. dao层(与数据库相关) 2...

47920
来自专栏Android先生

OKHttp源码解析--初阶

这段时间老李的新公司要更换网络层,知道现在主流网络层的模式是RxJava+Retrofit+OKHttp,所以老李开始研究这三个项目的源代码,在更换网络层后,开...

14520
来自专栏技术碎碎念

GET、POST编码问题

12540
来自专栏西安-晁州

webservice随记

WebService:跨平台、系统、跨语言间相互调用 CXF: Axis(Apache)-> Axis2(Apache) XFire -> CXF(Celt...

26800
来自专栏Java帮帮-微信公众号-技术文章全总结

Web-第八天 Servlet学习【悟空教程】

在网站的首页上,登录的链接,点击登录的链接,可以跳转到登录的页面.在登录的页面中输入用户名和密码点击登录的案例.完成登录的功能.

16510
来自专栏Android 研究

OkHttp源码解析(十) OKHTTP中连接与请求及总结

主要看下ConnectInterceptor()方法,里面代码已经很简单了,受限了通过streamAllocation的newStream方法获取一个流(Htt...

21340
来自专栏Java3y

Servlet第三篇【request和response简介、response的常见应用】

response、request对象 Tomcat收到客户端的http请求,会针对每一次请求,分别创建一个代表请求的request对象、和代表响应的respon...

454110

扫码关注云+社区

领取腾讯云代金券