Tomcat框架和Servlet在Tomcat中执行原理

6 篇文章 0 订阅

1. Tomcat概要介绍          

Tomat是一个Servlet容器,不过它也内部也放置了Web应用服务。先上一张Tomcat静态的结构简图

Tomcat静态结构

 从上图可以看出,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 &quot;%r&quot; %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 &quot;%r&quot; %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

  1. 明确定义了<context path="" docBase=webappPATH>,此时默认context的处理路径为webappPATH。

  2. 明确定义了<context path="">,但却没给定docBase属性,此时该默认context处理路径为appBase/ROOT目录,注意ROOT为大写。

  3. 完全没有定义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上。

一个Tomcat部署结构

2 ServletContext和Servlet 

先说下Servlet和ServletContext中的概念

1 Servlet的生命周期,如下图所示

Servlet生命周期图

2 Servlet是单例的,所以,线程是不安全的,所有请求线程都使用同一个Servlet实例。如果需要线程安全,需要实现STM(single thread mothod)

而ServletContext是对这些Servlet的生命周期和使用时机进行管理,主要的职责有

1 对Servlet进行创建,销毁,调用

2 分配线程,每次http的请求都会生成或者从线程池获取一个可用线程,service调用结束后,将线程返还给线程池

3 针对网络访问端口的监听,特定协议的解析,字符流或者字节流的解析和编码(自己写过这种编解码,真的很不健壮)

        下面就从源码解读上,去窥视下整个Servlet工作的动态过程。首先,自定义一个Servlet类JavacCommand,并在Web.xml中,进行配置

Servlet执行堆栈标题
以上看到的代码是不是和第一节讲的内容说的类有很大不同?其实,任何概念都必须要在源码上验证的。如果只是了解下,其实,第一小节以及足够了,毕竟我们平时也没必要太关注这些问题。

3 Tomcat中Service,Host,Context,Wrapper四容器原理和源码解读

        首先,介绍几个概念,容器,也就是在一小节中介绍的Service、Host、Context、Wrapper的概念,对应到代码中,这些都是接口类。可实例化的类分别对应StandardService、StandardHost、StandardContext、StandardWrapper这四个类。这个容器中,都有个保存真真执行任务的pipe列表,列表中记录着每个可以执行业务的阀类,阀类都继承于ValveBase类,ValueBase类又实现了Value接口类,在Value接口类中,就有重要的接口函数invoke。
       这里主要介绍下,pipeline生成的逻辑,这里可以对照下源代码,篇幅有限,我只粘贴主要逻辑代码,在pipeLine中,是将新的阀类插入到倒数第二的位置,倒数第一的位置留给了basic,我们再看容器代码,任意找个容器 StandardHost
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);
    }
}

大概画了一张面条图,这些容器和阀类,通过设计的一个职责链模式的模式组织到一起,进行工作。

一个Servlet在Tomcat中执行的调用顺序,阀体类和容器类

在网上找到这张图,感觉比我做的好,很能说明整个执行的过程。大家注意,这的StandardHost容器中,有个AccessLogValve阀,就是在平时的配置中常常使用的阀类,大家可以看上文中的一个Tomcat部署结构的图中的server.xml中的配置。

阀类和容器类调用过程

最后,还有一个问题没有解决,就是在各个标准阀类中查找对应的容器的过程。

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的创建和查询介绍下。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值