1. Tomcat概要介绍
Tomat是一个Servlet容器,不过它也内部也放置了Web应用服务。先上一张Tomcat静态的结构简图
![](https://img-blog.csdnimg.cn/20210513184443454.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JlbmRhd2VpNjM2,size_16,color_FFFFFF,t_70)
从上图可以看出,Tomcat,Service,Host,Context,Wrapper相邻之间的关系都是1对N的关系。其中,Service包含一个Connector,对应服务连接端口;一个Executor,用来维护内部Servlet的执行线程;
1.1 Host虚拟主机
一个Engine可以配置多个Host,也就是站点,可理解为子域名,也叫虚拟主机(这里,一个ip+port可以做一个物理主机的概念,但是Host的概念,是在这个物理主机上,虚拟出不同的访问地址),例如dicom.xxxxx.com、gotodicom.xxxxx.com;以下的配置是在一个端口上配置了2个虚拟主机的Host配置,在centOS上的部署结构是如下所示。这样,只要在DNS配置服务器上,将两个子域名都映射到此台服务器地址上,两个子域名都可以进行访问了。
<!--docBase则是每个webapp的存放目录,它可以是相对路径,也可以是绝对路径。 当提供相对路径时,它相对于appBase。-->
<Host name="dicom.xxxxx.com" appBase="dicomHelper"
unpackWARs="true" autoDeploy="true">
<Context path="/info" docBase="info" debug="1" reloadbale="true">
<WatchedResource>WEB-INF/web.xml</WatchedResource>
</Context>
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
<Host name="gotodicom.xxxxx.com" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Context path="/" docBase="ROOT" debug="1" reloadbale="true">
<WatchedResource>WEB-INF/web.xml</WatchedResource>
</Context>
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
1.2 conf/server.xml中的context标签
我们通常无须配置Context标签,因为,Tomcat会缺省的配置的path="",并且映射到的服务器的路径是$(appBase)/ROOT目录。但是,如果想要在当前的虚拟主机下,配置多个Context,就必须进行显式配置。这里的应用场景,比如,需要在当前的虚拟主机下,将静态文件,部署到服务器的另外一个大的硬盘上,就需要增加一个Context配置,并且docBase设置到存放文件的绝对的目录上。具体操作是,可以在这些虚拟主机Host下增加Context的标签, Tomcat会基于对请求URI与context中定义的path进行最大匹配前缀的规则进行挑选,从中选出使用哪个context来处理该HTTP请求。对于缺省的Context标签,内部处理的规则是,以下有描述摘自 https://www.linuxidc.com/Linux/2017-12/149921.htm
-
明确定义了<context path="" docBase=webappPATH>,此时默认context的处理路径为webappPATH。
-
明确定义了<context path="">,但却没给定docBase属性,此时该默认context处理路径为appBase/ROOT目录,注意ROOT为大写。
-
完全没有定义path=""的context时,即host容器中没有明确的path="",此时将隐式定义一个默认context,处理路径为appBase/ROOT目录。
另外,reloadable属性,文档中规定是,是否监控/WEB-INF/class和/WEB-INF/lib两个目录中文件的变化,变化时将自动重载。在测试环境下该属性很好,但在真实生产环境部署应用时不应该设置该属性,因为监控会大幅增加负载,因此该属性的默认值为false。
1.3 config/web.xml
在Tomcat文件夹下,还有config/web.xml的文件,是所有host的Context的缺省配置,整个Context会合并config/web.xml和每个应用的WEB-INF下的web.xml配置,作为最后的Context的配置。conf/web.xml 文件中,默认配置项主要有:
0 defaultServlet:用来映射/虚拟路径,将在Spring中没有映射的uri都在此Servlet中进行处理,例如,我们缺省的去访问一个jpg图像;
1 Servlet:配置Servlet、JSP、SSI、CGI引擎;
2 session 配置:控制会话时间;
3 MIME 类型:MIME映射;
4 欢迎文件列表
由于项目下也同样有WEB_INF/web.xml文件,Tomcat会将两个文件做类似合并的操作,如果遇到两个文件配置上有冲突的地方,优先匹配项目中的WEB_INFO/web.xml。
对以上的这个推论,可以做如下的实验,
实验1,在WEB_INF/web.xml中没有对应url的配置,会查找Tomcat的conf/web.xml中的配置。我们先看Tomcat下的conf/web.xml配置
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
<init-param>
<param-name>fork</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>xpoweredBy</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
在实验项目的info/目录下,放置一张图片,使用 http://dicom.xxxxx.com/info/thum.jpg来进行访问,结果是可以将此图片下载到本地的。
可见,这个/info/thum.jpg的url映射是Tomcat下的/conf/defaultServlet的配置来进行的响应的,因为我们在项目中的WEB_INF/web.xml没有做任何的映射。
实验2,使用项目下的WEB_INF/web.xml配置文件,屏蔽Tomat的conf/web.xml配置。将path="/",直接映射到SpringMVC的Servlet上,结果和我们预想的一样,请求的结果是404。
<!-- spring mvc -->
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:context-dispatcher.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- 屏蔽defaultServlet -->
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
实验3,在项目中的WEB_INF/web.xml中,再次通过配置,将以jpg扩展名的url都映射给Tomcat中/conf/web.xml中的defaultServlet的实例去处理。在通过以下的配置,将jpg文件,在此映射到defaultServlet上,这时候,又能再一次请求到图片。
<!-- spring mvc -->
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:context-dispatcher.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- 屏蔽defaultServlet -->
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--通过DefaultServlet来处理jpg-->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.jpg</url-pattern>
</servlet-mapping>
下面再讨论下,另外一种单台服务器,部署多个子服务。
单台服务器上,如果要部署多个可以协同工作的子服务,也可以将这些服务分别部署到不一样的端口上,也就是多端口(多Service模式),就可以通过以下的示例配置实现。这种方式,很适合做SaaS的2B企业,将按照业务拆分好的云化的子服务应用,私有化部署到客户小规模的单台或者有限的几台实体服务器上,同时,这种方式还可以针对特定的子业务服务去分配适合的线程数量(Executor),以最大的限度的为客户解决成本。
当然,也可以在单个Host上,将应用服务部署到不一样Context上,也就是虚拟目录上。
在云端部署时,就不用这么复杂,可以直接,将应用服务(Context)直接部署到一台ECS上。
![](https://img-blog.csdnimg.cn/20210513184752825.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JlbmRhd2VpNjM2,size_16,color_FFFFFF,t_70)
2 ServletContext和Servlet
先说下Servlet和ServletContext中的概念
1 Servlet的生命周期,如下图所示
![](https://img-blog.csdnimg.cn/20210513184925145.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JlbmRhd2VpNjM2,size_16,color_FFFFFF,t_70)
2 Servlet是单例的,所以,线程是不安全的,所有请求线程都使用同一个Servlet实例。如果需要线程安全,需要实现STM(single thread mothod)
而ServletContext是对这些Servlet的生命周期和使用时机进行管理,主要的职责有
1 对Servlet进行创建,销毁,调用
2 分配线程,每次http的请求都会生成或者从线程池获取一个可用线程,service调用结束后,将线程返还给线程池
3 针对网络访问端口的监听,特定协议的解析,字符流或者字节流的解析和编码(自己写过这种编解码,真的很不健壮)
下面就从源码解读上,去窥视下整个Servlet工作的动态过程。首先,自定义一个Servlet类JavacCommand,并在Web.xml中,进行配置
![](https://img-blog.csdnimg.cn/20210513185132595.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JlbmRhd2VpNjM2,size_16,color_FFFFFF,t_70)
3 Tomcat中Service,Host,Context,Wrapper四容器原理和源码解读
public class StandardPipeline{
private Valve first = null;
private Valve basic = null;
public void addValve(Valve valve) {
if (first == null) {
first = valve;
valve.setNext(basic);
} else {
Valve current = first;
while (current != null) {
if (current.getNext() == basic) { //插入到队列的倒数第二位置,最后一个位置一直都是basic
current.setNext(valve);
valve.setNext(basic);
break;
}
current = current.getNext();
}
}
}
}
public class StandardHost {
protected Pipeline pipeline = new StandardPipeline(this);
public StandardHost() {
super();
pipeline.setBasic(new StandardHostValve());
}
}
接下来,我们开始讲整个动态调用到Servlet实例的过程;我们直接看StandardHostValve的invoke函数
final class StandardHostValve extends ValveBase {
public final void invoke(Request request, Response response) {
// 1 选择对应的Context容器
Context context = request.getContext();
// 2 在容器的阀链中,找到第一个阀类,执行invoke
context.getPipeline().getFirst().invoke(request, response);
}
}
大概画了一张面条图,这些容器和阀类,通过设计的一个职责链模式的模式组织到一起,进行工作。
![](https://img-blog.csdnimg.cn/20210513190728300.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JlbmRhd2VpNjM2,size_16,color_FFFFFF,t_70)
在网上找到这张图,感觉比我做的好,很能说明整个执行的过程。大家注意,这的StandardHost容器中,有个AccessLogValve阀,就是在平时的配置中常常使用的阀类,大家可以看上文中的一个Tomcat部署结构的图中的server.xml中的配置。
![](https://img-blog.csdnimg.cn/20210513185619198.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JlbmRhd2VpNjM2,size_16,color_FFFFFF,t_70)
最后,还有一个问题没有解决,就是在各个标准阀类中查找对应的容器的过程。
3 针对当前request容器类的创建和查询
大家看上边的图“自定义Servlet堆栈调用“,CoyoteAdapter在执行Service之前,首先找到当前uri对应的各级容器,是通过CoyoteAdapter.postParseRequest获取的,
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
throws Exception {
//这个地方,是我省略掉一堆代码,直接也是new出来的
Request request = new Request();
Response response = new Response();
//查找到合适的容器并且附加到request上
postParseSuccess = postParseRequest(req, request, res, response);
//就是2小节中描述的过程
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
}
postParseRequest函数更长,直接吧我能看懂的代码段贴上来
protected boolean postParseRequest(org.apache.coyote.Request req, Request request,
org.apache.coyote.Response res, Response response) throws IOException, ServletException {
//通过一个Mapper类来实现的,最后将request.mappingData给填充完毕
connector.getService().getMapper().map(serverName, decodedURI,
version, request.getMappingData());
return true;
}
下面看Mapper.java类
public void map(MessageBytes host, MessageBytes uri, String version,
MappingData mappingData) throws IOException {
internalMap(host.getCharChunk(), uri.getCharChunk(), version, mappingData);
}
通过内部的结构将,需要的各级容器查询到。
这篇文章的篇幅太大,下一篇,详细将这个Mapper的创建和查询介绍下。