文档章节

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

Float_Luuu
 Float_Luuu
发布于 2016/01/03 21:57
字数 2394
阅读 1447
收藏 15
点赞 2
评论 2

前天在扒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
粉丝 199
博文 46
码字总数 102357
作品 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
Servlet 3.0 新特性概述

Servlet 3.0 作为 Java EE 6 规范体系中一员,随着 Java EE 6 规范一起发布。该版本在前一版本(Servlet 2.5)的基础上提供了若干新特性用于简化 Web 应用的开发和部署。其中有几项特性的引入...

张超
2012/12/22
0
0
Servlet 3.0 新特性概述

Servlet 3.0 新特性概述 Servlet 3.0 作为 Java EE 6 规范体系中一员,随着 Java EE 6 规范一起发布。该版本在前一版本(Servlet 2.5)的基础上提供了若干新特性用于简化 Web 应用的开发和部...

巴顿
2013/11/19
0
2
Java中的文件上传(原始Servlet实现)

从原始的Servlet来实现文件的上传,代码如下: 参考:https://my.oschina.net/Barudisshu/blog/150026 采用的是Multipart/form-data的方式上传文件。针对Multipart/form-data方式的上传解释,...

easonjim
2017/03/15
0
0
servlet3异步原理与实践

一、什么是Servlet servlet 是基于 Java 的 Web 组件,由容器进行管理,来生成动态内容。像其他基于 Java 的组件技术一样,servlet 也是基于平台无关的 Java 类格式,被编译为平台无关的字节...

新栋BOOK
2017/10/24
0
5
Servlet 3.0 新特性

Servlet 3.0 作为 Java EE 6 规范体系中一员,随着 Java EE 6 规范一起发布。该版本在前一版本(Servlet 2.5)的基础上提供了若干新特性用于简化 Web 应用的开发和部署。其中有几项特性的引入...

壹炮倾城
2013/06/13
0
1

没有更多内容

加载失败,请刷新页面

加载更多

下一页

jquery刷新页面(局部及全页面刷新)

window.location.reload()刷新当前页面. parent.location.reload()刷新父亲对象(用于框架) opener.location.reload()刷新父窗口对象(用于单开窗口) top.location.reload()刷新最顶端对象...

uug
3分钟前
0
0
CoreText进阶(五)- 文字排版样式和效果

CoreText进阶(五)- 文字排版样式和效果 效果 以下是三个设置了不同属性的效果图 第一个设置了文字颜色为红色,字体为16号 第二个设置了文字颜色为灰色,字体为16号,对其为居中 第三个设置...

aron1992
29分钟前
1
0
10.23 linux任务计划cron~10.27 target介绍

crontab命令被用来提交和管理用户的需要周期性执行的任务,与windows下的计划任务类似,当安装完成操作系统后,默认会安装此服务工具,并且会自动启动crond进程,crond进程每分钟会定期检查是...

洗香香
40分钟前
0
0
告警系统主脚本、告警系统配置文件、告警系统监控项目

20.20 告警系统主脚本 告警系统主脚本 main.sh内容 #!/bin/bash#Written by aming.# 是否发送邮件的开关export send=1# 过滤ip地址export addr=`/sbin/ifconfig |grep -A1 "en...

lyy549745
43分钟前
0
0
Don’t Repeat Yourself

在软件工程中,Don’t Repeat Yourself(DRY)是软件开发的原则,旨在减少重复,用抽象代替它,使用数据规范化来避免冗余。 这个原则在维基百科上是说是由Andy Hunt和Dave Thomas《The Pragmat...

woshixin
45分钟前
0
0
搭建webpack项目框架

作者:汪娇娇 时间:2018年6月4日 一、说明 随着业务发展和前端人员的增加,搭建一个通用框架以及制定统一规范就成了必然。对于选型这方面,一开始好像就没考虑其他框架,直接选了webpack。w...

娇娇jojojo
52分钟前
0
0
Java基础——面向对象(内部类)

声明:本栏目所使用的素材都是凯哥学堂VIP学员所写,学员有权匿名,对文章有最终解释权;凯哥学堂旨在促进VIP学员互相学习的基础上公开笔记。 内部类: 1.有名内部类 2.无名内部类 内部类申请...

凯哥学堂
今天
0
0
HttpClient内部三个超时时间的区别

RequestConfig requestConfig = RequestConfig.custom() .setConnectionRequestTimeout(config.connReqTimeout) //从连接池中获取连接的超时时间 ......

1713716445
今天
0
0
每天一个命令SCP

每天一个命令:SCP scp是secure copy的简写,用于在Linux下进行远程拷贝文件的命令,和它类似的命令有cp,不过cp只是在本机进行拷贝不能跨服务器,而且scp传输是加密的。可能会稍微影响一下速...

河图再现
今天
0
0
cron/chkconfig/systemd/unit/target

linux任务计划 : cron工具 任务计划在运维工作中用到的比较多,大部分系统管理工作都是通过定期自动执行某个脚本来完成。 查看linux中任务计划的配置文件: /etc/crontab [root@yolks-001 ~]...

Hi_Yolks
今天
1
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部