文档章节

异步Servlet编程 | Servlet3.0新特性

Float_Luuu
 Float_Luuu
发布于 2016/01/03 21:57
字数 2394
阅读 1538
收藏 15

前天在扒Tomcat源码的时候在装配Servlet的时候我们除了看见了比较熟悉的loadOnStartup参数之外,另外一个不太熟悉的参数asyncSupported就是我们今天要讨论的主题,我们的关注点随即也从Servlet上下文转向了Tomcat对请求的处理与分发,也就是更底层一些的东西,待会会涉及Tomcat Endpoint相关的东西,很开心和大家一起分享。

背景知识一:tomcat的容器架构

我们先看下conf/server.xml里面的一端配置:

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443" />

这个配置位于Service组件标签的里面,在Tomcat的容器架构图中Connector和Service是父子关系,我先画一张图:

解释下这张图,Connector是作为Service容器的组件,当Service被父容器启动的时候同事会启动Connector组件,Connector组件关联一个ProtocolHandler,Connector会启动这个ProtocolHandler,ProtocolHandler关联着一个Endpoint,ProtocolHandler同样也会启动这个Endpoint。Endpoint是干嘛的呢,Tomcat定义Endpoint作为网络层的组件,用于绑定及监听服务端的端口,将接收到的客户端的连接分发到工作线程去处理,Endpoint启动的时候做些什么事情以及包括哪些内容呢?Endpoint具体有多个实现,我拿最简单的JIoEndpoint来扒一扒,它启动的时候会做下面这些事情:

  1. bind本地指定的端口,我们最熟悉的就是8080了。

  2. 初始化内部工作线程池。

  3. 启动Acceptor线程,Acceptor线程是用来接受客户端socket并包装交给工作线程处理了,Acceptor线程只负责接客,接完之后就包装成SocketProcessor丢给工作线程池去处理了。

  4. 启动Timeout线程,用来异步检查超时连接。

好了,下面继续看看Tomcat对请求处理的逻辑。

背景知识二:Tomcat对异步请求的处理逻辑

我们在SocketProcessor的实现里面找到了一个代码片段:

if (state == SocketState.CLOSED) {
    // Close socket
    if (log.isTraceEnabled()) {
        log.trace("Closing socket:"+socket);
    }
    countDownConnection();
    try {
        socket.getSocket().close();
    } catch (IOException e) {
        // Ignore
    }
} else if (state == SocketState.OPEN ||
        state == SocketState.UPGRADING ||
        state == SocketState.UPGRADING_TOMCAT  ||
        state == SocketState.UPGRADED){
    socket.setKeptAlive(true);
    socket.access();
    launch = true;
} else if (state == SocketState.LONG) {
    socket.access();
    waitingRequests.add(socket);
}

上面可以看出,第一个if分支是当状态等于CLOSED的时候,这里会将连接数减1并且关闭服务器与客户端的socket连接,其他两个分支并没有断开连接。再看看SocketProcessor的实现中另一个代码片段:

if ((state != SocketState.CLOSED)) {
    if (status == null) {
        state = handler.process(socket, SocketStatus.OPEN_READ);
    } else {
        state = handler.process(socket,status);
    }
}

(下面我想用记流水账的形式描述逻辑代码的执行堆栈)上面的handler process是具体处理socket的分支,相关实现由AbstractProtocol下沉到AbstractHttp11Processor的asyncDispatch中,在asyncDispatch会调用adapter的asyncDispatch方法来处理,这个adapter的具体实现在Connector被启动的时候初始化的,具体是CoyoteAdapter类,在CoyoteAdapter的实现中会去调用StandardWrapperValve的invoke方法,再具体一点就会调用用户在WebXML中配置的过滤器链以及Servlet啦。

上面讲了那么一连串的源码堆栈逻辑,其实是想连贯Tomcat从接收到客户端请求与调用Servlet这条线。

简单来说,Tomcat对异步Servlet的处理逻辑即Tomcat接收客户端的请求之后,如果这个请求对应的Servlet是异步的,那么Tomcat会将请求委托给异步线程来处理,并会保持与客户端的连接,当请求处理完成之后再由委托线程来通知监听器异步处理已经完成,于此同时Tomcat的工作线程已经被Tomcat工作线程池回收。

下面我们就可以继续看看上层是如何写异步Servlet的了。

利用Servlet3的API实现异步Servlet

在这一节,我们主要看看如何从零开始实现一个异步的Servlet,为了不让篇幅过长,我尽量精简一下例子。

一、实现一个ServletContextListener来初始化我们自己的线程池,这个池子和Tomcat的工作线程池是完全独立的:

/**
 * @author float.lu
 */
@WebListener
public class AppContextListener implements ServletContextListener {

    private static final String EXECUTOR_KEY = AppContextListener.class.getName();
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(100, 200, 50000L,
                TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(100));
        servletContextEvent.getServletContext().setAttribute(EXECUTOR_KEY,
                executor);
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        ThreadPoolExecutor executor = (ThreadPoolExecutor) servletContextEvent
                .getServletContext().getAttribute(EXECUTOR_KEY);
        executor.shutdown();
    }
}

这里只做两件事情,第一、在Servlet容器初始化完成的时候初始化线程池,这个时候Servlet还没有被初始化,这是上篇文章的知识了。第二,在Servlet容器销毁的时候销毁线程池。

二、实现一个AsyncListener接口的类,这个接口是Servlet3 API提供的接口,用于监听工作线程的执行情况从而正确的响应异步处理结果,因为我的例子实现代码没有什么意义这里就不贴了,记住实现javax.servlet.AsyncListener这个接口就好。

三、自定义一个实现Runnable接口的类,我的实现是这样的:

/**
 * @author float.lu
 */
public class AsyncRequestProcessor implements Runnable {

    private AsyncContext asyncContext;


    public AsyncRequestProcessor(AsyncContext asyncCtx) {
        this.asyncContext = asyncCtx;
    }

    @Override
    public void run() {
        try {
            PrintWriter out = this.asyncContext.getResponse().getWriter();
            out.write("Async servlet started !\n");
            out.flush();
        } catch (Exception e) {

        }
        asyncContext.complete();
    }
}

主要是通过构造方法拿到了异步上下文AsyncContext对应于ServletContext。然后线程实现里面可以拿到请求进行响应的处理。

四,最后一个是异步Servlet的实现:

/**
 * @author float.lu
 */
@WebServlet(value = "/asyncservlet", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        AsyncContext asyncContext = req.startAsync();
        asyncContext.addListener(new AppAsyncListener());
        asyncContext.setTimeout(2000);

        ThreadPoolExecutor executor = (ThreadPoolExecutor) req
                .getServletContext().getAttribute("executor");
        executor.execute(new AsyncRequestProcessor(asyncContext));
    }
}

这里面需要注意的有几点:

  1. 将@WebServlet注解的asyncSupported的值设置为true,代表这个Servlet是异步Servlet。

  2. 通过req.startAsync获取异步上下文。

  3. 设置上文中自定义的Listener。

  4. 设置超时时间。

  5. 以异步上下文为参数构造线程丢进工作线程池中。

到此,我们自己的异步Servlet实现就结束了,其实这只是其中一种实现方式,具体可以根据实际情况巧妙设计。举个例子,如果使用单线程模型的话我们可以维护着一个队列来保存异步上下文,一个工作线程不断的从队列中拿到异步上下文进行处理,完了之后调用AsyncContext定义的complete接口告知监听器处理完成即可。第一种模型其实只是将原来可能附加给Tomcat工作线程池的任务拿到自定义的线程池处理而已,而第二种模型是只用一个工作线程去利用队列来处理异步任务。具体应用要看实际情况来定。

异步还是不异步?

现在知道了Tomcat对异步Servlet的支持,有知道了如何实现异步Servlet,那么问题来了,异步Servlet适合什么样的场景呢?

我们分析下并设想一下,当然下面可能是我自己在YY,不正确的欢迎指出,也欢迎读者能够举一些其他的应用场景。首先问题肯定出现在当请求处理时间可能很长的时候,这让我想到了报表导出功能。报表导出其实是一个非常常见的功能,我们需要通过查询数据库,对数据进行处理,然后根据处理完的数据生成Excel并导出。这个过程时间一般都是相对比较长的,通常会引发数据库连接数不够这种问题,当然这是另外一个话题了,数据层相关问题我可能会通过为报表导出任务建立单独的数据源来处理,或者是其他方法。而我们现在讨论的是比较上层的请求占用问题,这个时候我们可以使用异步Servlet来处理这个耗时比较长的任务,从而不会长时间占用Tomcat宝贵的工作线程,因为Tomcat工作线程被占用完的后果将是不接受任何请求。

无论场景如何,结果是我们可以用自己的线程代理工作线程来处理请求了,当然用单线程还是用多线程模型这个也要看实际情况,如果你能拿出实验数据来证明具体的应用场景下哪种模型更好,这是再好不过的了,

扩展

上面的例子都是直接使用Servlet来实现的,实际应用中这种方式可能很少有人用了,不过没关系。Spring MVC从3.2版本就支持异步Servlet了,可能上层的表现形式不一样也就是具体码的姿势不一样,但是都知道原理了,可以直接Hack起。Struts貌似还不支持???另外提一下,对于异步Servlet,其实tomcat支持的comet Servlet就是一种异步Servlet。comet的原理是请求到达Servlet之后客户端就和服务器保持着长连接,这样服务端可以随时将内容推送到客户端。


本文相关代码基于tomcat7.0.56和servlet3.1.0版本,由作者原创,欢迎补充或纠正。


作者:陆晨

2016年1月3日


© 著作权归作者所有

共有 人打赏支持
Float_Luuu
粉丝 208
博文 47
码字总数 104674
作品 0
长宁
高级程序员
私信 提问
加载中

评论(2)

muffe
muffe
真搞不懂,这个无非就是自己搞了个异步线程池,跟开多点tomcat线程池数量有什么区别?异步编程不是这个意思,而是充分利用IO
Alive_
Alive_
good!
Servlet3.0提供的异步处理

在以前的Servlet规范中,如果Servlet作为控制器调用了一个耗时的业务方法,那么Servlet必须等到业务方法完全返回之后才会生成响应,这将使得Servlet对业务方法的调用变成一种阻塞式的调用,因...

摆渡者
2014/03/03
0
0
Servlet3.0中Servlet的使用

目录 1.注解配置 2.异步调用 3.文件上传 相对于之前的版本,Servlet3.0中的Servlet有以下改进: 支持注解配置。 支持异步调用。 直接有对文件上传的支持。 在这篇文章中我将主要讲这三方面的...

王爵nice
2014/08/04
0
0
@WebListener 为什么不起作用

@WebListener 是servlet3.0后的新特性,需要在web.xml中配置,如下: <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:w......

Beaver_
2015/03/11
0
0
Java EE 6体系结构的变革

尽管 Java 在展示层框架上竞争的非常激烈,但 JSF 仍然固守着自己的领地。虽然有很多关于 JSF 的易用性和健壮性的质疑声,但 JSF2.0 就是为正面解决这些问题而提出来的,它的易用,创新以及可...

晨曦之光
2012/03/09
0
0
Servlet3.0 异步处理机制

在早前版本的Servlet规范中,如果Servlet作为控制器调用了一个较耗时的业务方法,那么Servlet必须等到业务方法完全返回之后才生成响应,这使得Servlet对业务方法的调用变成一种阻塞式的调用,...

键盘小生
2013/03/28
0
1

没有更多内容

加载失败,请刷新页面

加载更多

技术工坊|如何开发一款以太坊钱包(深圳)

【好消息!】HiBlock区块链技术工坊已经成功举办了26期,其中北京1期,西安1期,成都2期,上海22期。经常有社区的小伙伴问定期举办技术工坊的除了上海以外,其他城市有没有?现在区块链技术工...

HiBlock
11分钟前
1
0
Redis 梳理笔记

安装 安装gccyum install gcc-c++下载传输到服务器上解压tar -xzvf *.tar.gzcd redis-3.2.9编译make安装 make PREFIX=/usr/local/redis install将配置文件拷贝出来cp redis...

晨猫
13分钟前
0
0
PyCharm flask 'Debug mode off' 调试模式关闭的解决方法

flask的几种debug模式的方法 # 1.app.run 传参debug=trueapp.run(debug=True) #2 设置app的配置app = Flask(__name__)app.config['DEBUG'] = True #3 配置文件方式# config.py中添加d...

yimingkeji
19分钟前
1
0
聊聊storm TridentWindowManager的pendingTriggers

序 本文主要研究一下storm TridentWindowManager的pendingTriggers TridentBoltExecutor.finishBatch storm-core-1.2.2-sources.jar!/org/apache/storm/trident/topology/TridentBoltExecut......

go4it
26分钟前
1
0
java实现多线程两种基本方式

我们在开发当中经常会使用到多线程,这里我们来写两个小案例通过最基本的两种方式继承Thread类或实现Runnable接口来实现一个多线程。 继承Thread类 我们可以通过继承Thread类,并重写run()方...

王子城
27分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部