Tomcat中的容器是如何处理请求的
前言
上一篇《Tomcat中的连接器是如何设计的》介绍了Tomcat中连接器的设计,我们知道连接器是负责监听网络端口,获取连接请求,然后转换符合Servlet标准的请求,交给容器去处理,那么我们这篇文章将顺着上一篇文章的思路,看看一个请求到了容器,容器是如何请求的。
说明:本文tomcat版本是9.0.21,不建议零基础读者阅读。
从Adapter中说起
我们继续跟着上篇文章Adapter
的源码,继续分析,上篇文章结尾的源码如下:
//源码1.类: CoyoteAdapter implements Adapter
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
throws Exception {
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
//check valves if we support async
request.setAsyncSupported(
connector.getService().getContainer().getPipeline().isAsyncSupported());
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
}
}
上面的源码的主要作用就是获取到容器,然后调用getPipeline()
获取Pipeline
,最后去invoke
调用,我们来看看这个Pipeline
是做什么的。
//源码2.Pipeline接口
public interface Pipeline extends Contained {
public Valve getBasic();
public void setBasic(Valve valve);
public void addValve(Valve valve);
public Valve[] getValves();
public void removeValve(Valve valve);
public Valve getFirst();
public boolean isAsyncSupported();
public void findNonAsyncValves(Set<String> result);
}
//源码3. Valve接口
public interface Valve {
public Valve getNext();
public void setNext(Valve valve);
public void backgroundProcess();
public void invoke(Request request, Response response)
throws IOException, ServletException;
public boolean isAsyncSupported();
我们从字面上可以理解Pipeline
就是管道,而Valve
就是阀门,实际上在Tomcat中的作用也是和字面意思差不多。每个容器都有一个管道,而管道中又有多个阀门。我们通过后面的分析来证明这一点。
管道-阀门(Pipeline-Valve)
我们看到上面的源码是Pipeline
和Valve
的接口,Pipeline
主要是设置Valve
,而Valve
是一个链表,然后可以进行invoke
方法的调用。我们回顾下这段源码:
//源码4
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
这里是直接获取容器的管道,然后获取第一个Valve
进行调用。我们在之前提到过Valve
是一个链表,这里只调用第一个,也就是可以通过Next去调用到最后一个。我们再回顾下我们第一篇文章《Tomcat在SpringBoot中是如何启动的》中提到过,容器是分为4个子容器,分别为Engine
、Host
、Context
、Wrapper
,他们同时也是父级和子级的关系,Engine
>Host
>Context
>Wrapper
。
我之前提到过,每个容器都一个Pipeline
,那么这个是怎么体现出来的呢?我们看容器的接口源码就可以发现,Pipeline
是容器接口定义的一个基本属性:
//源码5.
public interface Container extends Lifecycle {
//省略其他代码
/**
* Return the Pipeline object that manages the Valves associated with
* this Container.
*
* @return The Pipeline
*/
public Pipeline getPipeline();
}
我们知道了每个容器都有一个管道(Pipeline
),管道中有许多阀门(Valve
),Valve
可以进行链式调用,那么问题来了,父容器管道中的Valve
怎么调用到子容器中的Valve
呢?在Pipeline
的实现类StandardPipeline
中,我们发现了如下源码:
/**
// 源码6.
* The basic Valve (if any) associated with this Pipeline.
*/
protected Valve basic = null;
/**
* The first valve associated with this Pipeline.
*/
protected Valve first = null;
public void addValve(Valve valve) {
//省略部分代码
// Add this Valve to the set associated with this Pipeline
if (first == null) {
first = valve;
valve.setNext(basic);
} else {
Valve current = first;
while (current != null) {
//这里循环设置Valve,保证最后一个是basic
if (current.getNext() == basic) {
current.setNext(valve);
valve.setNext(basic);
break;
}
current = current.getNext();
}
}
container.fireContainerEvent(Container.ADD_VALVE_EVENT, valve);
}
根据如上代码,我们知道了basic
是一个管道(Pipeline
)中的最后一个阀门,按道理只要最后一个阀门是下一个容器的第一个阀门就可以完成全部的链式调用了。我们用一个请求debug下看看是不是和我们的猜测一样,我们在CoyoteAdapter
中的service
方法中打个断点,效果如下:
这里我们可以知道,在适配器调用容器的时候,也就是调用Engine
的管道,只有一个阀门,也就是basic,值为StandardEngineValve
。我们发现这个阀门的invoke方法如下:
//源码7.
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// Select the Host to be used for this Request
Host host = request.getHost();
if (host == null) {
// HTTP 0.9 or HTTP 1.0 request without a host when no default host
// is defined. This is handled by the CoyoteAdapter.
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}
// Ask this Host to process this request
host.getPipeline().getFirst().invoke(request, response);
}
我们继续debug查看结果如下:
所以这里的basic
实际上将会调用到Host
容器的管道(Pipeline
)和阀门(Valve
),也就是说,每个容器管道中的basic
是负责调用下一个子容器的阀门。我用一张图来表示:
这张图清晰的描述了,Tomcat内部的容器是如何流转请求的,从连接器(Connector
)过来的请求会进入Engine
容器,Engine
通过管道(Pieline
)中的阀门(Valve
)来进行链式调用,最后的basic
阀门是负责调用下一个容器的第一个阀门的,一直调用到Wrapper
,然后Wrapper
再执行Servlet
。
我们看看Wrapper
源码,是否真的如我们所说:
//源码8.
public final void invoke(Request request, Response response)
throws IOException, ServletException {
//省略部分源码
Servlet servlet = null;
if (!unavailable) {
servlet = wrapper.allocate();
}
// Create the filter chain for this request
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
filterChain.doFilter(request.getRequest(),
response.getResponse());
}
看到这里,你可能会说这里明明只是创建了过滤器(Filter
)并且去调用而已,并没有去调用Servlet
,没错,这里确实没有去调用Servlet
,但是我们知道,过滤器(Filter
)是在Servlet
之前执行的,也就是说,filterChain.doFilter
执行完之后变会执行Servlet
。我们看看ApplicationFilterChain
的源码是否如我们所说:
//源码9.
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
//省略部分代码
internalDoFilter(request,response);
}
//源码10.
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
//省略部分代码
// Call the next filter if there is one
if (pos < n) {
//省略部分代码
ApplicationFilterConfig filterConfig = filters[pos++];
Filter filter = filterConfig.getFilter();
filter.doFilter(request, response, this);
return;
}
//调用servlet
// We fell off the end of the chain -- call the servlet instance
servlet.service(request, response);
通过源码我们发现,在调用完所有的过滤器(Filter
)之后,servlet
就开始调用service
。我们看看servlet
的实现类
这里我们熟悉的HttpServlet
和GenericServlet
是Tomcat
包的类,实际上只有HttpServlet
,因为GenericServlet
是HttpServlet
的父类。后面就是移交给了框架去处理了,Tomcat内部的请求已经到此是完成了。
Tomcat的多应用隔离实现
我们知道,Tomcat是支持部署多个应用的,那么Tomcat是如何支持多应用的部署呢?是怎么保证多个应用之间不会混淆的呢?要想弄懂这个问题,我们还是要回到适配器去说起,回到service
方法
//源码11.类:CoyoteAdapter
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
throws Exception {
//省略部分代码
// Parse and set Catalina and configuration specific
// request parameters
//处理URL映射
postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
//check valves if we support async
request.setAsyncSupported(
connector.getService().getContainer().getPipeline().isAsyncSupported());
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
}
}
我们在之前的源码中只谈到了connector.getService().getContainer().getPipeline().getFirst().invoke( request, response)
这段代码,这部分代码是调用容器,但是在调用容器之前有个postParseRequest
方法是用来处理映射请求的,我们跟进看看源码:
//源码12.类:CoyoteAdapter
protected boolean postParseRequest(org.apache.coyote.Request req, Request request,
org.apache.coyote.Response res, Response response) throws IOException, ServletException {
省略部分代码
boolean mapRequired = true;
while (mapRequired) {
// This will map the the latest version by default
connector.getService().getMapper().map(serverName, decodedURI,
version, request.getMappingData());
//没有找到上下文就报404错误
if (request.getContext() == null) {
// Don't overwrite an existing error
if (!response.isError()) {
response.sendError(404, "Not found");
}
// Allow processing to continue.
// If present, the error reporting valve will provide a response
// body.
return true;
}
}
这里就是循环去处理Url映射,如果Context
没有找到,就返回404错误,我们继续看源码:
//源码13.类:Mapper
public void map(MessageBytes host, MessageBytes uri, String version,
MappingData mappingData) throws IOException {
if (host.isNull()) {
String defaultHostName = this.defaultHostName;
if (defaultHostName == null) {
return;
}
host.getCharChunk().append(defaultHostName);
}
host.toChars();
uri.toChars();
internalMap(host.getCharChunk(), uri.getCharChunk(), version, mappingData);
}
//源码14.类:Mapper
private final void internalMap(CharChunk host, CharChunk uri,
String version, MappingData mappingData) throws IOException {
//省略部分代码
// Virtual host mapping 处理Host映射
MappedHost[] hosts = this.hosts;
MappedHost mappedHost = exactFindIgnoreCase(hosts, host);
//省略部分代码
if (mappedHost == null) {
mappedHost = defaultHost;
if (mappedHost == null) {
return;
}
}
mappingData.host = mappedHost.object;
// Context mapping 处理上下文映射
ContextList contextList = mappedHost.contextList;
MappedContext[] contexts = contextList.contexts;
//省略部分代码
if (context == null) {
return;
}
mappingData.context = contextVersion.object;
mappingData.contextSlashCount = contextVersion.slashCount;
// Wrapper mapping 处理Servlet映射
if (!contextVersion.isPaused()) {
internalMapWrapper(contextVersion, uri, mappingData);
}
}
由于上面的源码比较多,我省略了很多代码,保留了能理解主要逻辑的代码,总的来说就是处理Url包括三部分,映射Host
,映射Context
和映射Servlet
(为了节省篇幅,具体细节源码请感兴趣的同学自行研究)。
这里我们可以发现一个细节,就是三个处理逻辑都是紧密关联的,只有Host
不为空才会处理Context
,对于Servlet
也是同理。所以这里我们只要Host
配置不同,那么后面所有的子容器都是不同的,也就完成了应用隔离的效果。但是对于SpringBoot内嵌Tomcat方式(使用jar包启动)来说,并不具备实现多应用的模式,本身一个应用就是一个Tomcat。
为了便于理解,我也画了一张多应用隔离的图,这里我们假设有两个域名admin.luozhou.com
和web.luozhou.com
然后我每个域名下部署2个应用,分别是User
,log
,blog
,shop
。那么当我去想去添加用户的时候,我就会请求admin.luozhou.com
域名下的User
的Context
下面的add
的Servlet(说明:这里例子设计不符合实际开发原则,add这种粒度应该是框架中的controller完成,而不是Servlet)。
总结
这篇文章我们研究了Tomcat中容器是如何处理请求的,我们来回顾下内容:
- 连接器把请求丢给适配器适配后调用容器(
Engine
) - 容器内部是通过管道(
Pieline
)-阀门(Valve
)模式完成容器的调用的,父容器调用子容器主要通过一个basic
的阀门来完成的。 - 最后一个子容器
wrapper
完成调用后就会构建过滤器来进行过滤器调用,调用完成后就到了Tomcat内部的最后一步,调用servlet。也可以理解我们常用的HttpServlet
,所有基于Servlet
规范的框架在这里就进入了框架流程(包括SpringBoot)。 - 最后我们还分析了Tomcat是如何实现多应用隔离的,通过多应用的隔离分析,我们也明白了为什么Tomcat要设计如此多的子容器,多子容器可以根据需要完成不同粒度的隔离级别来实现不同的场景需求。
版权声明:原创文章,转载请注明出处。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
编程语言这一年:Rust、Kotlin 上位,Python 横行,PHP……
作者:开源中国(OSCHINA) 内容来源:开源中国(OSCHINA) 最近开源中国(OSCHINA)在庆祝 11 周年生日,编辑部借着这个机会梳理了一下这一年来我们追过的那些开源界/开发界的热点新闻,算作一个阶段性小结。(其实只有 9 个月~) 开源中国是目前国内为数不多深耕于围绕“开源/自由软件”为开发者分享开源业内事、传播开源价值观的平台。目前社区收录的开源项目数量已经超过 50k,其中国产项目数量 10k+。 关于社区会发布哪些类型的新闻内容,开源中国的传统是: 最核心的当然是开源相关的,比如开源项目的更新公告、各个开源社区的相关大事。 知名开发工具的相关动态,包括各种 IDE、浏览器与各种协议/规范等。 开发者强相关的大事件,比如 996。 偶尔有一些泛 IT 界的能够引起开发者共鸣/思考的大事件,比如删库跑路。 …… 而这些内容来自开源中国官方编辑,也来自社区的用户 OSCer,借此也感谢 OSCer 们对开源中国社区的支持与维护。“@红薯 你起来啊,怎么就跪下了。” 今年这几个月下来,已经积累了庞大规模的内容,我们按不同方向整理成了数篇文章,接下来会陆续推出,这是第一篇,...
-
下一篇
基于TensorFlow的开源JS库的网页前端人物动作捕捉的实现
前言 随着前端生态的发展,Java已经不仅仅局限于作为网页开发,也越来越活跃于服务器端,移动端小程序等应用开发中。甚至通过Electron等打包工具,甚至能够开发多系统的桌面应用。其涉足的领域宽泛也使得能够实现的功能也不再是简单的UI控件制作和内容的展示,在互动娱乐,小游戏领域也有着极大的发展前景。本文以通过Java开发一个基于浏览器摄像头的实时人物动作捕捉小程序为例,介绍一下前端在这一领域的可行性。 什么是TensorFlow TensorFlow 最初是由Google大脑小组的研究员和工程师们开发出来,采用数据流图(Data Flow Graphs)用于机器学习和深度神经网络方面数值计算的开源软件库。其高度的可移植性和多语言性使得它可以通过各种常用编程语言编写,轻松的运行在多种平台的设备上。是一个集性能,可靠性,通用性,易用性为一体的强大开源库。 本文所使用的开源Java模型库: l tfjs-models/posenet: 一个机器学习模型,功能为对图像或者视频中的人物进行动作捕捉,输出人体各个部位的keypoints(坐标集)。具有单一人物分析和多人物分析的特点。 l tfj...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- MySQL数据库在高并发下的优化方案
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Dcoker安装(在线仓库),最新的服务器搭配容器使用
- Docker容器配置,解决镜像无法拉取问题
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Docker快速安装Oracle11G,搭建oracle11g学习环境