JavaSec - JavaWeb - Tomcat 框架以及与Servlet之间的联系

写在前面

最近一直都说想学Java内存马,但也一直都停在Tomcat内存马这里,原因就是很多的教程都建议要掌握一下Tomcat的框架,但是迫于一直都没有找到好的教程和文章来看,如果只是单纯地给每个模块下一个定义,感觉很难说服自己懂了。

虽然很想从头看一遍《How Tomcat Works》但是确实也不现实,偶然一个机会需要找一下Tomcat的 server.xml 配置文件来看一下,又碰巧找到了一篇解读 server.xml 的文章,于是就有了这篇文章或者说学习笔记。

在本文中,我们将一起学习以下内容:

  1. 通过解读一个基础的Tomcat server.xml 配置文件,了解Tomcat下面的组件是如何分工处理一个HTTP请求的;
  2. 进而了解Tomcat下各个组件的作用,以及它们之间的关系;
  3. 通过Tomcat以及Java EE源码来了解一下Tomcat是如何与Java EE中的三大组件(Servlet,Filter,Listener)是如何关联起来的;
  4. 最后通过了解Tomcat Pipeline-Valve责任链机制了解Tomcat与Java Web具体在代码中是如何处理一个HTTP请求的;

如果对于Java Web中三大组件有疑问的话,可以看看我总结的内容: JavaSec - Java Web - 三大核心组件 - Servlet

JavaSec - Java Web - 三大核心组件 - Listener & Filter

Tomcat基础框架

Tomcat作为一个Web应用服务器,其最核心的功能自然就是:

  1. 接受来自用户的HTTP request请求;
  2. 根据用户的HTTP请求进行业务处理;
  3. 最后返回Response响应给用户;

在接下来的文章中,记住这个最基本的功能可以让我们更好的理解Tomcat到底是怎么来对HTTP请求进行处理的。

Figure 1: Tomcat框架

Figure 1: Tomcat框架

我们先来根据上图看一下最简单的Tomcat server框架,其中包含了很多陌生的抽象名词,大家不要着急,后面我们会具体展开来讲:

  1. 一个Tomcat服务器包含多个Service;
  2. 一个Service可以绑定 多个 Connector,并同时与 唯一 一个Container绑定;
  3. Connector 负责处理由客户端发出的HTTP请求,并封装成ServletRequest对象发送给Container进行处理;
  4. Container 负责对收到的请求(ServletRequest对象)进行处理,并将响应Response封装成ServletResponse对象发回Connector;
  5. 最后由Connector发送HTTP响应返回给客户端,至此完成一次HTTP请求与响应。

从Tomcat配置文件server.xml了解Tomcat

server.xml是Tomcat服务器的配置文件,其中的每一个xml元素都代表了tomcat中的一个组件,这些组件都在处理请求中拥有自己的职责,我们通过对配置文件的解读学习,可以大致了解Tomcat服务器的整体架构组成,有助于我们进一步的源码阅读;

Tomcat的配置文件server.xml的整体结构如下:

<Server>
    <Service>
        <Connector />
        <Connector />
        <Engine>
            <Host>
                <Context />
            </Host>
        </Engine>
    </Service>
</Server>

接下来我们讲一个个元素来进行理解,并了解这些元素所配置的Tomcat组件的功能;

Server

<Server>元素是配置文件的根元素,用来代表着我们整一个Tomcat服务器;

根据上面的框架图我们知道,一个Tomcat Server可以包含多个Service,而Server的主要任务,就是为客户端提供了一个能够访问所有这些Service的接口,同时维护所有Service的生命周期,包括如何找到客户端想要访问的Service:

<Server port="8005" shutdown="SHUTDOWN">
</Server>

上面的这个实例包含了两个属性,port以及shutdown,这两个属性组合在一起表示,当我们向8005端口发送"SHUTDOWN"这个信息时,服务器就会关闭(当shutdown的属性设为-1时就可以禁用这个功能)

如果大家用过nc或者ncat的话,就不难想到我们可以使用nc来创建一个client socket去连接8005端口并发出指令关闭端口:

nc localhost 8005
SHUTDOWN

Service

Tomcat框架中的这个Service,我认为是和我们传统意义上的Service是一个概念,即监听一个端口,与客户端建立连接来进行通信交互;

一般来说,默认只开启一个名叫Catalina的Service,也就是最核心的Web容器,当然Tomcat也可以提供多个Service,但是一般来说用不上,我们只要知道根据name属性,Catalina就是Tomcat server下面的一个Service就可以了:

<Service name="Catalina">
  <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
  <Engine name="Catalina" defaultHost="localhost">
    <Host name="localhost"  appBase="webapps"
          unpackWARs="true" autoDeploy="true">
    </Host>
  </Engine>
</Service>

Connector

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

<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

我们之前提到过Service包含多个Connector和唯一的Container,我们在上面的配置中,发现了两个Connector元素监听了不同的端口,可这就让Service的概念不是不同了吗?

其实仔细看,可以发现两个Connector都使用了不同的协议,HTTP/1.1 以及 AJP/1.3;

我们接着看看元素内的属性,port,protocol就是Connector所监听的端口,我们可以看到这里使用了8080,而不是传统的80端口,这是因为:

“在正式的生产环境中,Tomcat也常常监听8080端口,而不是80端口。这是因为在生产环境中,很少将Tomcat直接对外开放接收请求,而是在Tomcat和客户端之间加一层代理服务器(如nginx),用于请求的转发、负载均衡、处理静态文件等;通过代理服务器访问Tomcat时,是在局域网中,因此一般仍使用8080端口。”

而配置了第二个AJP协议的Connector连接器是因为Tomcat作为Servlet容器,对于静态资源处理速度相对较慢,因此可以用AJP协议与Apache等服务器连接,让Apache等服务器来处理静态资源;

静态资源
  • 静态资源就是服务器不需要经过任何处理就可以直接返回给用户的内容,例如HTML页面,Flash,JavaScript图片等内容;
  • 相对的,动态资源就是例如ASP、PHP、JSP等文件,需要经过服务器处理之后,然后再返回给用户;
为什么我们要用Apache等HTTP服务器来处理静态资源?
  • 我们知道Tomcat会使用Connector连接器对客户端发来的HTTP请求进行解析,然后转换成Request以及Response对象进行处理之后再返回给客户端;
  • 但如果是静态资源,根本不需要服务器进行任何处理,如果每次碰到静态资源,例如图片啊,JS这样的资源如果还问Tomcat来要,那就会浪费时间在对于所有这些HTTP请求的无用的解析和构造对象上了;
  • 而AJP(Apache JServ Protocol) 是一个二进制的协议,并且与Tomcat服务器建立一个永久的连接,这两点都极大地提高了通信的效率;

Figure 2: Tomcat与Apache集成

Figure 2: Tomcat与Apache集成

总结一下,Connector的主要功能就是提供一个接口,来接收所有来自客户端的HTTP请求,并将HTTP Request以及Response封装成ServletRequest对象发送给Container进行处理;

我们在开头说过,我们要关注Tomcat本质上是来处理客户端传来的HTTP请求,我们接下来就拿一个例子来具体介绍:

URL:

http://localhost:8080/LearnFilter_war/admin/a.jsp

HTTP请求:

GET /LearnFilter_war/admin/a.jsp HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:107.0) Gecko/20100101 Firefox/107.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1

直到Connector进行处理,我们的请求首先已经经历这些处理(暂时不考虑nginx反向代理):

  1. localhost经过了本机默认的DNS服务器,被解析成了127.0.0.1,然后建立了一个client socket并尝试与服务器的8080端口建立连接;
  2. Tomcat的HTTP Connector连接器接收到了HTTP请求,并将请求转换为ServletRequest以及对应的ServletResponse对象传递给Container。

Container

<Engine name="Catalina" defaultHost="localhost">
  <Host name="localhost"  appBase="webapps"
        unpackWARs="true" autoDeploy="true">
  </Host>
</Engine>

Container? 谁是Container?哪里有Container,配置文件里没看到啊?

我们之前介绍的Container容器其实都是一个抽象的概念,其实真正在使用的都是继承了Container接口的所有子类,包括Engine,Host,Context以及Wrapper;

Figure 3: Container 类层次

Figure 3: Container 类层次

接下来我们会依次介绍他们的作用,不过就和之前介绍的那样,整一个Container模块的功能就是处理请求以及构造响应。

Engine与Host

一个Service只包含一个Engine;一个Engine与Service中的多个Connector进行绑定;同时一个Engine可以内嵌一个或多个Host(即Host是Engine的子容器);

Engine是最顶层的Container(容器),而Host作为子容器,表示Engine中的一个虚拟主机,对应了服务器中的网络实体名(例如域名或者IP地址);

客户端通过主机名来标识他们想要连接的主机,主机名会作为 HOST Header被放在HTTP请求中,Engine会根据HTTP Header中Host的值决定由哪一个HOST来继续处理请求。

<Engine name="Catalina" defaultHost="localhost">
  <Host name="localhost"  appBase="webapps"
        unpackWARs="true" autoDeploy="true">
  </Host>
  <Host name="anotherHost" appbase="otherApps">
  </Host>
</Engine>

在Engine元素的属性中:

  1. name属性用于用户日志和错误信息,在整个Server中应该保持唯一;
  2. defaultHost表示如果HTTP请求中的HOST Header的值与所有的Host元素都不匹配,默认交由defaultHost所设定的Host处理;

最后再说明一下Host的配置属性:

  1. name表示Tomcat上虚拟主机的主机名,一般来说主机名是DNS服务器中已经注册的域名(当然defaultHost不需要,只要保证能接受到就好了);
  2. unpackWARs指示了是否要将代表Web应用的WAR文件解压:
    • 如果为true,则解压之后再运行web应用
    • 如果为false,则直接使用WAR文件运行Web应用
  3. autoDeploy以及appBase属性我们放到后面Web应用自动部署再介绍,先知道appBase就表示存放Web应用的目录就可以了

最后回到我们的HTTP请求的例子(当然这时候这些数据都是从Connector整合的ServletRequest对象中获取了):

GET /LearnFilter_war/admin/a.jsp HTTP/1.1
Host: localhost:8080
... ...

至此我们就知道了一个Tomcat服务器可以有唯一一个公网IP,而客户端可以通过多个域名(localhost, anotherHost)来访问服务器(只要DNS能够解析到对应的IP地址即可),然后Engine会根据HTTP请求中的HOST头将请求分配给对应的HOST组件,比如我们的例子中的localhost这个HOST。

Context

我们继续套娃,一个Service有唯一一个Engine,一个Engine内嵌一个或者多个Host,而一个Host则可以内嵌一个多个Context,用来表示一个虚拟主机HOST下所运行的一个或者多个Web应用程序(同样也是子容器);

我们知道Host具体的表现形式就是HOST Header,而Context的具体表现形式就是我们在HTTP请求中的URI,在我们的例子中就是 LearnFilter_war;

这个URI其实也就是我们在使用IDEA开发的时候,创建的项目名,记得我们在Servlet的介绍中,曾经说过一个web项目,只有一个ServletContext对象吗,记住这一点,我们在将Wrapper的时候还会再提到(其实Wrapper就是我们所开发的一个个Servlet);

在知道了这些内容之后,你可能最想问的还是,为什么我没有在server.xml中看到Context元素啊?

原因就是我们在Host的配置中开启了自动部署(autoDeploy=true),在这个场景下,Tomcat会自动匹配URI的path与 appBase 属性中的目录名称进行匹配,一旦匹配上则默认这个目录就是要继续处理请求的Context,也就不需要再进行额外配置的:

<Host name="localhost"  appBase="webapps"
      unpackWARs="true" autoDeploy="true"
      deployOnstartup="true">
</Host>

如果deployOnStartup和autoDeploy设置为true,则tomcat启动自动部署:

  • 当检测到新的Web应用或Web应用的更新时,会触发应用的部署(或重新部署)。
  • 二者的主要区别在于:
    • deployOnStartup为true时,Tomcat在启动时检查Web应用,且检测到的所有Web应用视作新应用;
    • autoDeploy为true时,Tomcat在运行时定期检查新的Web应用或Web应用的更新。除此之外,二者的处理相似。

下面的这些Web应用都可以直接使用localhost:8080/xxx进行访问:

Figure 4: webapps目录下的Context Web应用

Figure 4: webapps目录下的Context Web应用

我们也可以手动配置静态Context元素,其中 path 表示URI, docBase 表示Web应用的war文件或者目录的位置,而 reloadable 属性则表示当class文件发生改动时,是否要触发整个Web应用的重新加载:

<Context path="/" docBase="other_directory_absolute_path/app1.war" reloadable="true"/>

但是一般不建议使用静态配置,因为 server.xml 这个文件在修改后需要重新启动服务器才能加载,因此一旦需要修改Path的信息,就需要重启服务器,而在自动部署的时候Tomcat就可以通过定期的扫描了检查Web应用的路径是否发生了修改;

至此,我们想要根据 server.xml 所获取的组件元素信息已经全部介绍完毕,但是Container作为处理HTTP请求的容器,肯定和我们在Java Web编程中的核心组件Servlet时分不开的,而Servlet其实是被Container的最底层子类Wrapper所包裹使用,我们将在下面的章节中介绍。

Wrapper

Wrapper是Context的子容器,也是Tomcat的最底层容器了,我们知道一个Context代表一个Web应用,那么一个Wrapper就代表一个Web应用下的一个个功能组件,也就是我们之前介绍过的Servlet:一个Wrapper就是一个Servlet的封装。

在我们介绍Tomcat三大组件:Servlet,Filter以及Lisntener的时候我们都介绍了这些组件的一个生命周期,包括init(), destroy()方法,而Wrapper就是来管理Servlet的生命周期的:

void load() throws ServletException

Load and initialize an instance of this Servlet, if there is not already at least one initialized instance. This can be used, for example, to load Servlets that are marked in the deployment descriptor to be loaded at server startup time.

在StandardWrapper的load()方法中使用loadServlet来初始化一个Servlet,接着再用initServlet来初始化这个Servlet:

 public synchronized void load() throws ServletException {
     this.instance = this.loadServlet();
     if (!this.instanceInitialized) {
         this.initServlet(this.instance);
     }

... ... 
 }

接着我们具体来看这两个方法:

public Servlet loadServlet() throws ServletException

Load and initialize an instance of this servlet, if there is not already at least one initialized instance. This can be used, for example, to load servlets that are marked in the deployment descriptor to be loaded at server startup time.

Returns: the loaded Servlet instance

StandardWrapper对象的loadServlet()先是获取了其父容器StandardContext来加载这个Servlet类,再利用initServetlet()来进行初始化:

public synchronized Servlet loadServlet() throws ServletException {
   ... ... 
            InstanceManager instanceManager = ((StandardContext)this.getParent()).getInstanceManager();

            try {
                servlet = (Servlet)instanceManager.newInstance(this.servletClass);
            } 

            this.initServlet(servlet);
            this.fireContainerEvent("load", this);
            this.loadTime = System.currentTimeMillis() - t1;
            var12 = false;

        return servlet;
    }

而initServlet(Servlet servlet)做的最重要的一件事情就是调用了servlet的init()方法,开启了其生命周期:


    private synchronized void initServlet(Servlet servlet) throws ServletException {
        if (!this.instanceInitialized || this.singleThreadModel) {
            try {
                if (Globals.IS_SECURITY_ENABLED) {
                    boolean success = false;

                    try {
                        Object[] args = new Object[]{this.facade};
                        SecurityUtil.doAsPrivilege("init", servlet, classType, args);
                        success = true;
                    } finally {
                        if (!success) {
                            SecurityUtil.remove(servlet);
                        }

                    }
                } else {
                    servlet.init(this.facade);
                }

                this.instanceInitialized = true;
            }
... ... 
        }
    }

至此,Tomcat服务器也就和我们使用Java EE定义和创建的Servlet联系起来了,我们也能绘制出整一个container的子容器之间的关系了:

Figure 5: Tomcat Container子容器结构

Figure 5: Tomcat Container子容器结构

Tomcat中的三种Context辨析

既然我们说到了Servlet是如何与Tomcat服务器结合起来使用的,那自然当然也少不了Filter以及Listener,我们就借此机会,来好好说道说道Context;

如果大家了解过Servlet的开发,那就一定知道一个概念就是ServletContext,这和Tomcat组件中的Context会不会有什么联系呢?

org.apache.catalina.Context

org.apache.catalina.core.StandardContext

jakarta.servlet.ServletContext

org.apache.catalina.core.ApplicationContext

org.apache.catalina.core.ApplicationContextFacade

上面我们一共提到了5中Context,我们依次先来简单认识一下,之后再展开讲他们的具体关系:

  1. org.apache.catalina.Context接口:
    • 定义了Tomcat中的Context容器,其父容器为Host,其子容器为Wrapper;
    • 用来表示一个Web应用
  2. org.apache.catalina.core.StandardContext类:
    • 是org.apache.catalina.Context接口的标准实现
    • 之后方便辨认,我们统一用StandardContext来表示Context接口以及实现类
  3. jakarta.servlet.ServletContext接口
    • 定义了一系列servlet可以用来与servlet容器通信的方法;
    • 每一个JVM中的每一个web应用都有唯一一个ServletContext(web应用直观的来说就是表示访问服务器所用的URL中所跟着的URI例如/catalog,并且整个应用可能就是一个 .war 文件);
    • 细心的师傅应该发现了这个接口是在JAVA EE的包里定义了,和其他在org.apache.catalina中定义的不同,这是Java EE先提供的接口,来帮助web应用中的Servlet存储数据,进行通信;
  4. org.apache.catalina.core.ApplicationContext类:
    • 这个类就是Tomcat对于ServletContext的具体实现;
  5. org.apache.catalina.core.ApplicationContextFacade类:
    • 这个类是ApplicationContext的一个门面类,可以理解为讲ApplicationContext的属性和方法封装在这个Facade门面类里面,对外就调用Facade中的方法就可以了(具体可以参考:JAVA设计模式之门面模式(外观模式)

这样讲其实还挺割裂的,好像两个实现类StandardContext以及ApplicationContext没有什么直接的联系,但是事实上他们却有着千丝万缕的联系:

StandardContext中就有一个 getServletContext() 方法,我们可以观察到,其中生成的就是ServletContext的实现类 ApplicationContext, 同时返回值就是ApplicationContext的门面类的对象:

public class StandardContext extends ContainerBase implements Context, NotificationEmitter {
    protected ApplicationContext context = null;
    public ServletContext getServletContext() {
        if (this.context == null) {
            this.context = new ApplicationContext(this);
            if (this.altDDName != null) {
                this.context.setAttribute("org.apache.catalina.deploy.alt_dd", this.altDDName);
            }
        }

        return this.context.getFacade();
    }
}

上面的代码还有值得注意的一点,就是ApplicationContext的构造器使用了StandardContext的对象,我们接着去看看ApplicationContext的源码:

public class ApplicationContext implements ServletContext {

    public ApplicationContext(StandardContext context) {
        this.context = context;
        this.service = ((Engine)context.getParent().getParent()).getService();
        this.sessionCookieConfig = new ApplicationSessionCookieConfig(context);
        this.populateSessionTrackingModes();
    }
}

我们发现在ApplicationContext中, this.context 竟然是StandardContext的对象,我们接着查找 this.context 的使用情况,发现了有很大一部分的方法中都在使用了这个 StandardContext 的对象:

Figure 6: this.context在ApplicationContext中的使用情况

Figure 6: this.context在ApplicationContext中的使用情况

Figure 7: 76处使用context

Figure 7: 76处使用context

Figure 8: 47处调用了StandardContext的get方法

Figure 8: 47处调用了StandardContext的get方法

Figure 9: 调用StandardContext的事件触发方法

Figure 9: 调用StandardContext的事件触发方法

我们隐约可以感觉到ServletContext就像是StandaardContext下属的一个部门,负责管理Servlet的一些事务,但是都要向StandardContext进行汇报以及获取物资的感觉。

Tomcat如何启动三大组件

接着我们来具体看看StandardContext是启动Tomcat的三大组件,借此进一步了解Tomcat所定义的Context与Java EE自带的这三大组件是如何协调工作的:

ContextConfig#configureContext() 与 StandardContext#startInternal()

首先,我们要问第一个问题:

StandardContext是怎么知道应该加载哪些Servlet, Filter以及Listener的?

答案其实很容易就可以猜到,就是从web.xml或者annotation中读取:

我们看到下面的这个 configureContext() 方法中,通过获取一个webxml对象,然后从中分别读取了这三大组件的相关配置内容:

private void configureContext(WebXml webxml) {
    for (Entry<String, String> entry : webxml.getContextParams().entrySet()) {
        context.addParameter(entry.getKey(), entry.getValue());
    }

    // 加载Filter的配置
    for (FilterDef filter : webxml.getFilters().values()) {
        if (filter.getAsyncSupported() == null) {
            filter.setAsyncSupported("false");
        }
        context.addFilterDef(filter);
    }
    for (FilterMap filterMap : webxml.getFilterMappings()) {
        context.addFilterMap(filterMap);
    }
    // 加载Listner的配置
    for (String listener : webxml.getListeners()) {
        context.addApplicationListener(listener);
    }
    // 加载Servlet的配置
    for (ServletDef servlet : webxml.getServlets().values()) {
        Wrapper wrapper = context.createWrapper();

        if (servlet.getLoadOnStartup() != null) {
            wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
        }
        if (servlet.getEnabled() != null) {
            wrapper.setEnabled(servlet.getEnabled().booleanValue());
        }
        wrapper.setName(servlet.getServletName());
        Map<String,String> params = servlet.getParameterMap();
        for (Entry<String, String> entry : params.entrySet()) {
            wrapper.addInitParameter(entry.getKey(), entry.getValue());
        }
        wrapper.setRunAs(servlet.getRunAs());
        Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
        for (SecurityRoleRef roleRef : roleRefs) {
            wrapper.addSecurityReference(
                    roleRef.getName(), roleRef.getLink());
        }
        wrapper.setServletClass(servlet.getServletClass());
        wrapper.setOverridable(servlet.isOverridable());
        context.addChild(wrapper);
    }
    for (Entry<String, String> entry :
            webxml.getServletMappings().entrySet()) {
        context.addServletMappingDecoded(entry.getKey(), entry.getValue());
    }

}

加载配置完毕之后,接下来就是我们在介绍Filter以及Listener中的文章中曾经介绍过,StandardContext通过 startInternal() 这个方法来依次启动加载 Listener, Filter 以及 Servlet:

protected synchronized void startInternal() throws LifecycleException {
            ... ...
        try {
            ... ...
            // 1. ListenerStart(), 启动加载Listener
            if (ok && !this.listenerStart()) {
                log.error(sm.getString("standardContext.listenerFail"));
                ok = false;
            }

            // 2. filterStart(), 启动加载Filter
            if (ok && !this.filterStart()) {
                log.error(sm.getString("standardContext.filterFail"));
                ok = false;
            }
            // 3. loadOnStartup(), 启动加载Servlet
            if (ok && !this.loadOnStartup(this.findChildren())) {
                log.error(sm.getString("standardContext.servletFail"));
                ok = false;
            }

            super.threadStart();
            ... ...
    }

下面就让我们来针对每一个组件来看StandardContext是如何读取配置,如何初始化和加载对象的:

Listener

加载Listener配置

通过ContextConfig.configureContext从 web.xml 中向StandardContext加载listener配置:

private void configureContext(WebXml webxml) {
    // 加载Listner的配置
    for (String listener : webxml.getListeners()) {
        context.addApplicationListener(listener);
    }
}

其中,StandardContext.addApplicationListener() 方法来将Listener的类名到StandardContext中的一个字符串数组 applicationListeners 中:

private String applicationListeners[] = new String[0];
@Override
public void addApplicationListener(String listener) {

    synchronized (applicationListenersLock) {
        String results[] = new String[applicationListeners.length + 1];
        for (int i = 0; i < applicationListeners.length; i++) {
            if (listener.equals(applicationListeners[i])) {
                log.info(sm.getString("standardContext.duplicateListener",listener));
                return;
            }
            results[i] = applicationListeners[i];
        }
        results[applicationListeners.length] = listener;
        applicationListeners = results;
    }
    fireContainerEvent("addApplicationListener", listener);

}

初始化和加载Listener

接着我们来看在StandardContext.startInternal()中是如何初始化Listener的:

listenerStart()是用来将所有我们在Web工程中所定义的Listeners进行初始化的方法,它先是从StandardContext的private属性applicationListeners这样一个字符串变量中拿到所有需要加载的Listeners的名字,接着用 this.getInstanceManager().newInstance(listener) 的形式来进行加载。

/**
     * Configure the set of instantiated application event listeners
     * for this Context.
     * 
     * @return <code>true</code> if all listeners wre
     * initialized successfully, or <code>false</code> otherwise.
     */

    private String[] applicationListeners = new String[0];

    public boolean listenerStart() {
       ... ...

        String[] listeners = this.findApplicationListeners();
        Object[] results = new Object[listeners.length];
        boolean ok = true;

        for(int i = 0; i < results.length; ++i) {
            ... ... 
            try {
                String listener = listeners[i];
                results[i] = this.getInstanceManager().newInstance(listener);
            }
            ... ...
        }

        ... ...        
    }

我们最后来看这个 newInstance() 是如何来加载listener的,如果看过我们有关Java反射的文章或者了解反射的师傅应该很快就能反应过来,这个方法就是通过Java反射机制来根据Listener的类名来生成一个新的Listener对象:

@Override
    public Object newInstance(final String className, final ClassLoader classLoader)
            throws IllegalAccessException, NamingException, InvocationTargetException,
            InstantiationException, ClassNotFoundException, IllegalArgumentException,
            NoSuchMethodException, SecurityException {
        Class<?> clazz = classLoader.loadClass(className);
        return newInstance(clazz.getConstructor().newInstance(), clazz);
    }

其实通过这个例子,我们也能理解到使用反射的妙处了,就是无论你创建的Listener叫什么名字,只要你往配置里写好,就可以自动进行加载以及利用;

同时,如果你修改了这个类的名字,那也只需要修改一下配置文件里修改一下就好了,不需要去一个个改源码。

Filter

加载Filter相关配置

private void configureContext(WebXml webxml) {

    // 加载Filter的配置
    for (FilterDef filter : webxml.getFilters().values()) {
        if (filter.getAsyncSupported() == null) {
            filter.setAsyncSupported("false");
        }
        context.addFilterDef(filter);
    }
    for (FilterMap filterMap : webxml.getFilterMappings()) {
        context.addFilterMap(filterMap);
    }

}

我们可以看到 configureContext() 在对filter进行处理的时候,添加了两个内容:FilterDef以及FilterMap,这也就对应了我们在开发的过程中注册一个filter需要在 web.xml 中完成的配置:<filter>以及<filter-mapping>

<filter>
    <filter-name>HelloFilter</filter-name>
    <filter-class>pers.javasec.javafilter.HelloFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>HelloFilter</filter-name>
    <url-pattern>/admin/*</url-pattern>
</filter-mapping>

接着我们可以根据StandardContext中两个具体的add方法看到,他们都是将得到配置信息分别存入HashMap当中:

/**
     * Add a filter definition to this Context.
     *
     * @param filterDef The filter definition to be added
     */
    @Override
    public void addFilterDef(FilterDef filterDef) {

        synchronized (filterDefs) {
            filterDefs.put(filterDef.getFilterName(), filterDef);
        }


    }

Figure 10: 单个FilterDef中所存放的信息:

Figure 10: 单个FilterDef中所存放的信息:

/**
     * Add a filter mapping to this Context at the end of the current set
     * of filter mappings.
     *
     * @param filterMap The filter mapping to be added
     *
     * @exception IllegalArgumentException if the specified filter name
     *  does not match an existing filter definition, or the filter mapping
     *  is malformed
     */
    @Override
    public void addFilterMap(FilterMap filterMap) {
        validateFilterMap(filterMap);
        // Add this filter mapping to our registered set
        filterMaps.add(filterMap);
        fireContainerEvent("addFilterMap", filterMap);
    }

Figure 11: 单个FilterMap中存放的内容

Figure 11: 单个FilterMap中存放的内容

初始化和加载Filter

我们在上面的filter配置加载中看到了StandardContext会存放每一个filter的基本信息在 filterDefs 中。

那么在接着来看StandardContext在初始化和加载Filter的过程中,就是通过读取 filterDefs 中的filter信息,接着为每一个filter都创建一个filterconfig对象,并存入hashmap filterConfigs 中:

/**
 * The set of filter configurations (and associated filter instances) we
 * have initialized, keyed by filter name.
 */
private HashMap<String, ApplicationFilterConfig> filterConfigs =
        new HashMap<>();

/**
 * The set of filter definitions for this application, keyed by
 * filter name.
 */
private HashMap<String, FilterDef> filterDefs = new HashMap<>();

filterStart():

  • 对于每一个保存的FilterDef对象都创建一个FilterConfig来进行封装保存;
    • 目的是为了在之后快速创建实例;
public boolean filterStart() {
    // Instantiate and record a FilterConfig for each defined filter
    boolean ok = true;
    synchronized (filterConfigs) {
        filterConfigs.clear();
        for (Entry<String,FilterDef> entry : filterDefs.entrySet()) {
            String name = entry.getKey();
            if (getLogger().isDebugEnabled()) {
                getLogger().debug(" Starting filter '" + name + "'");
            }
            try {
                // 为每一个filterDef都创建一个filterConfig
                ApplicationFilterConfig filterConfig =
                        new ApplicationFilterConfig(this, entry.getValue());
                // 将创建的filterConfig存入filterConfigs中
                filterConfigs.put(name, filterConfig);
            } catch (Throwable t) {
                t = ExceptionUtils.unwrapInvocationTargetException(t);
                ExceptionUtils.handleThrowable(t);
                getLogger().error(sm.getString(
                        "standardContext.filterStart", name), t);
                ok = false;
            }
        }
    }

    return ok;
}

Figure 12: 单个filterConfig存放的内容

Figure 12: 单个filterConfig存放的内容

Servlet

加载Servlet相关配置

最后我们来看看Servlet,处理的语句明显变多了;

<servlet>
    <servlet-name>MyServlet</servlet-name>
    <servlet-class>pers.medic.jsplearn.MyServlet</servlet-class>
    <init-param>
        <param-name>username</param-name>
        <param-value>root</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>MyServlet</servlet-name>
    <url-pattern>/my</url-pattern>
</servlet-mapping>

对于所有Servlet的配置,configureContext都会为其单独建立一个Wrapper来进行封装;

并且这些Wrapper会被当作子容器通过 addChild() 被嵌入 StandardContext 当中;

private void configureContext(WebXml webxml) {
    // 加载Servlet的配置
    for (ServletDef servlet : webxml.getServlets().values()) {
        // 将每一个<servlet>配置都封装到Wrapper中去
        Wrapper wrapper = context.createWrapper();
        wrapper.setName(servlet.getServletName());


        // 将当前<servlet>下的所有存储的params变量信息进行加载
        Map<String,String> params = servlet.getParameterMap();
        for (Entry<String, String> entry : params.entrySet()) {
            wrapper.addInitParameter(entry.getKey(), entry.getValue());
        }
        wrapper.setRunAs(servlet.getRunAs());

        wrapper.setServletClass(servlet.getServletClass());
        context.addChild(wrapper);
    }
    // 增加<servlet-mapping>中的信息
    for (Entry<String, String> entry :
            webxml.getServletMappings().entrySet()) {
        context.addServletMappingDecoded(entry.getKey(), entry.getValue());
    }

}

初始化和加载Servlet

loadOnStartup是StandardContext中用来启动所有注册的Servlet(Wrapper)的方法,我们主要关注下半部分的for循环中,可以看到 wrapper.load() 就是用来启动Wrapper的:

/**
 * Load and initialize all servlets marked "load on startup" in the
 * web application deployment descriptor.
 *
 * @param children Array of wrappers for all currently defined
 *  servlets (including those not declared load on startup)
 * @return <code>true</code> if load on startup was considered successful
 */
public boolean loadOnStartup(Container children[]) {

    // Collect "load on startup" servlets that need to be initialized
    TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
    for (Container child : children) {
        Wrapper wrapper = (Wrapper) child;
        int loadOnStartup = wrapper.getLoadOnStartup();
        if (loadOnStartup < 0) {
            continue;
        }
        Integer key = Integer.valueOf(loadOnStartup);
        ArrayList<Wrapper> list = map.get(key);
        if (list == null) {
            list = new ArrayList<>();
            map.put(key, list);
        }
        list.add(wrapper);
    }

    // Load the collected "load on startup" servlets
    for (ArrayList<Wrapper> list : map.values()) {
        for (Wrapper wrapper : list) {
            try {
                wrapper.load();
            } catch (ServletException e) {
                getLogger().error(sm.getString("standardContext.loadOnStartup.loadException",
                      getName(), wrapper.getName()), StandardWrapper.getRootCause(e));
                // NOTE: load errors (including a servlet that throws
                // UnavailableException from the init() method) are NOT
                // fatal to application startup
                // unless failCtxIfServletStartFails="true" is specified
                if(getComputedFailCtxIfServletStartFails()) {
                    return false;
                }
            }
        }
    }
    return true;

}

我们再凑近点看看这个 wrapper.load() 方法:

    @Override
    public synchronized void load() throws ServletException {
        instance = loadServlet();

        if (!instanceInitialized) {
            initServlet(instance);
        }
... ...
    }

我们可以本质上就是通过调用initServlet()方法进而最终调用 servlet.init() 来最终开启servlet的生命周期

    private synchronized void initServlet(Servlet servlet)
            throws ServletException {
... ...
        // Call the initialization method of this servlet
        try {
            if( Globals.IS_SECURITY_ENABLED) {
                boolean success = false;
                try {
                    Object[] args = new Object[] { facade };
                    SecurityUtil.doAsPrivilege("init",
                                               servlet,
                                               classType,
                                               args);
                    success = true;
                } finally {
                    if (!success) {
                        // destroy() will not be called, thus clear the reference now
                        SecurityUtil.remove(servlet);
                    }
                }
            } else {
                // 开启servlet的生命周期
                servlet.init(facade);
            }

            instanceInitialized = true;
        }
... ...
    }

小结

通过上面对于Java EE中的三大组件与Tomcat之间的联系,我们可以进一步的了解Tomcat是如何通过web.xml来读取由开发者定义的Servlet, Listener以及Filter的,读取之后会以各种形式被保存在 StandardContext 实例当中,在 StandardContext 启动的时候就会再被拿出来进行初始化和加载。

Figure 13: Tomcat与三大组件的关系

Figure 13: Tomcat与三大组件的关系

Tomcat和Servlet处理HTTP请求

Pipeline-Valve责任链

目前我们已经了解了Container中所有子容器的作用以及结构,也通过代码来学习了Tomcat与Java Web三大组件是如何共同使用的了;

最后就让我们再一次深入代码,看看Tomcat的Container容器和Java EE的三大组件是如何一步步处理一次来自客户端的HTTP请求的;

我们知道Tomcat的Connector连接器会负责接受客户端发来的HTTP请求,并将Request以及Response封装好交由Container进行处理:

而这四个Container子容器类的实现类都以StandardXXX命名(e.g., StandardContext), 而这些实现类都无一例外的继承了 ContainerBase 抽象类,而这个抽象类的定义就应用了一个设计模式:责任链模式(菜鸟教程),其思想也很简单,就是将对于一个请求的处理任务拆分开,将责任分配到一条责任链的每一环上,从而进行分步处理:

public abstract class ContainerBase extends LifecycleMBeanBase implements Container

根据这个设计模式,每一个Container子容器都会管理一个 Pipeline 类实例,用“管道”来表示一条“责任链”,而一个或者多个 Valve 阀门实例将会被添加到 Pipeline 当中来分别对请求进行处理。

下面是两个ContainerBase所定义的需要被实现的abstract方法:

Type Data Description
addChild Container 添加子容器(e.g, Engine.addChild(Host))
pipeline.addValve Valve 将一个Valve添加到Pipeline中

每一个Container都会用 setBasic() 方法在构造器中会向pipeple中添加一个默认的 Valve, 比如下面的 StandardEngine:

    /**
     * The Pipeline object with which this Container is associated.
     */
    protected final Pipeline pipeline = new StandardPipeline(this);

    /**
     * Create a new StandardEngine component with the default basic Valve.
     */
    public StandardEngine() {

        super();
        // setBasic(): 向pipeline中添加默认的Valve
        pipeline.setBasic(new StandardEngineValve());
        /* Set the jmvRoute using the system property jvmRoute */
... ...
    }

我们说过Pipeline中的所有Valve都是用来分批处理请求的,那么他们将会被自带的 invoke() 方法所依次调用,默认的Valve一般叫做StandardXXXValve,会被放在责任链的最后一个执行,如果需要自定义vavle,需要添加配置到 server.xml 中,并且每一个vavle都会指向后一个valve,最终将由StandardXXXValve处理,之后再传递给相对应的子容器:

StandardEngineValve.invoke(),就像我们之前的说的,默认的Valve会继续调用对应子容器的pipeline中的vavle继续处理请求:

@Override
public final void invoke(Request request, Response response)
    throws IOException, ServletException {

    // Select the Host to be used for this Request
    // 调用Request#getHost()方法来根据HTTP请求获取对应的Host
    Host host = request.getHost();
    if (host == null) {
        // HTTP 0.9 or HTTP 1.0 request without a host when no default host
        // is defined.
        // Don't overwrite an existing error
        if (!response.isError()) {
            response.sendError(404);
        }
        return;
    }
    if (request.isAsyncSupported()) {
        request.setAsyncSupported(host.getPipeline().isAsyncSupported());
    }

    // Ask this Host to process this request
    host.getPipeline().getFirst().invoke(request, response);
}

就这样按照顺序一直执行下去:Engine->Host->Context->Wrapper:

  1. 根据HTTP请求调用Request类中的getHost(), getContext(), getWrapper()方法来获取对应的子容器;
  2. 然后执行子容器中的Pipleline-Valve责任链,直到最后WrapperValve#invoke()方法;

Figure 14: Pipleline-Valve责任链

Figure 14: Pipleline-Valve责任链

Tomcat Container容器处理HTTP请求的终点

我们来看最后的StandardWrappeValve的invoke()方法:

  1. 创建Servlet实例;
  2. 创建一个新的FilterChain实例(FilterChain也是一个责任链,将对于请求的过滤行为分散到了多个Filter上);
  3. 调用新创建的FilterChain实例的doFilter()方法来继续处理请求;
    @Override
    public final void invoke(Request request, Response response)
        throws IOException, ServletException {

        // Initialize local variables we may need
... ...
        StandardWrapper wrapper = (StandardWrapper) getContainer();
        Servlet servlet = null;
        Context context = (Context) wrapper.getParent();
... ...
        // Allocate a servlet instance to process this request
        // 1. 分配一个Servlet实例
        try {
            if (!unavailable) {
                servlet = wrapper.allocate();
            }
        }
... ...
        // Create the filter chain for this request
        // 2. 使用Factory工厂类创建一个filterChain实例
        ApplicationFilterChain filterChain =
                ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

        // Call the filter chain for this request
        // NOTE: This also calls the servlet's service() method
        Container container = this.container;
        try {
            if ((servlet != null) && (filterChain != null)) {
                // Swallow output if needed
                if (context.getSwallowOutput()) {
                    try {
                        SystemLogHandler.startCapture();
                        if (request.isAsyncDispatching()) {
                            request.getAsyncContextInternal().doInternalDispatch();
                        } else {
                            // 3. 调用创建的filterChain的doFilter方法来处理请求
                            filterChain.doFilter(request.getRequest(),
                                    response.getResponse());
                        }
... ...
                    }
... ...
                    }
                } else {
                    if (request.isAsyncDispatching()) {
                        request.getAsyncContextInternal().doInternalDispatch();
                    } else {
                        // 3. 调用创建的filterChain的doFilter方法来处理请求
                        filterChain.doFilter
                            (request.getRequest(), response.getResponse());
                    }
                }

            }
... ...
    }

ApplicationFilterChain是FilterChain的实现类,doFilter方法将继续调用 ApplicationFilterChain 中的 internalDoFilter() 方法处理请求:

    @Override
    public void doFilter(ServletRequest request, ServletResponse response)
        throws IOException, ServletException {

        if( Globals.IS_SECURITY_ENABLED ) {
            final ServletRequest req = request;
            final ServletResponse res = response;
            try {
                java.security.AccessController.doPrivileged(
                    new java.security.PrivilegedExceptionAction<Void>() {
                        @Override
                        public Void run()
                            throws ServletException, IOException {
                            internalDoFilter(req,res);
                            return null;
                        }
                    }
                );
            }
... ...
        } else {
            internalDoFilter(request,response);
        }
    }

ApplicationFilterChain.internalDoFilter():

  1. 根据FilterConfig来创建一个Filter实例;
    • 这时候我们终于知道之前在加载Filter的时候为什么要保存FilterConfigs了,就是为了封装Filter对象,并保存更多的信息和功能;
  2. 调用Filter中的过滤方法;
    • 过滤操作结束之后,需要再次调用 ApplicationFilterChain#doFilter() 来执行下一个Filter的doFilter();
  3. 最后在所有的Filter的过滤操作都结束后,正式调用Servlet的service()方法来处理请求;
    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++];
            try {
                // 1. 根据FilterConfig来获取Filter实例
                Filter filter = filterConfig.getFilter();
... ...
                if( Globals.IS_SECURITY_ENABLED ) {
... ...      
                } else {
                    // 2. 调用Filter中的过滤方法
                    filter.doFilter(request, response, this);
                }
            }
... ...
            return;
        }

        // We fell off the end of the chain -- call the servlet instance
        try {
... ...
            // Use potentially wrapped request from this point
            if ((request instanceof HttpServletRequest) &&
                    (response instanceof HttpServletResponse) &&
                    Globals.IS_SECURITY_ENABLED ) {
... ...
            } else {
                // 3. 所有的Filter的过滤操作都结束后,正式调用Servlet的service()方法来处理请求
                servlet.service(request, response);
            }
        }
... ...
     }

小结

Tomcat的Container容器通过Pipeline-Valve责任链机制,对于HTTP请求中不同信息逐渐选择对应的子容器,最终到达StandardWrapperValve中;

接着,StandardWrapperValve会每个请求的处理都创建一个ApplicaitonFilterChain过滤链,并根据FilterConfig中封装的Filter进行doFilter过滤操作,所有定义的Filter都完成过滤操作之后,执行Wrapper所定义Servlet的service()方法,完成对于HTTP请求的处理。

写在最后

很长的一篇文章,或者说这个长度已经不配叫做文章了(个人感觉文章还是应该更为简洁干练一些),更像是一篇学习笔记,这也导致了一开始的目标并不明确,不知道这样一篇内容需要放多少东西进来,很多知识应该学习到什么程度,这也极大地打击了效率,应该吸取教训,精简每篇文章,这样也可以分批次提高成就感。

如果读者对于内容有任何的疑问或者对于写作有任何的意见,请您务必通过邮件告诉我,提前感谢!

Reference

深入理解Tomcat(八)Container

Java安全学习——Tomcat架构浅析

Understanding Virtual Host Concept in Tomcat

Apache Tomcat 10 Architecture

详解Tomcat 配置文件server.xml

什么是静态资源,什么是动态资源

What is Static Content?

What Is the AJP Protocol?

Apache JServ Protocol

What is AJP protocol used for?

深入理解Tomcat(八)Container

JAVA设计模式之门面模式(外观模式)

责任链模式(菜鸟教程)

tomcat架构分析(valve机制)

【技术分享】Tomcat 内存马技术分析(一)—— Filter型

comments powered by Disqus
Cogito, ergo sum
Built with Hugo
Theme Stack designed by Jimmy