Tomcat Container Analysis


Container 分析

1、ContainerBase 的结构

Container 是 Tomcat 容器的接口,通常使用的 Servlet 就封装在其子接口 Wrapper 中。

Container 一共有 4 个子接口 Engine、Host、Context、Wrapper 和一个默认实现类 ContainerBase,每个子接口都是一个容器,这 4 个子容器都由一个对应的 StandardXXX 实现类,并且这些实现类都继承 ContainerBase 类。另外 Container 还继承 Lifecycle 接口,而且 ContainerBase 间接继承 LifecycleBase,所以 Engine、Host、Context、Wrapper 4 个子容器都符合前面讲过的 Tomcat 生命周期管理模式,结构图如下:

2、Container 的四个子容器

Container 的四个子容器:Engine、Host、Context、Wrapper 是逐层包含的关系,其中 Engine 是最顶层,每个 Service 最多只能有一个 Engine,Engine 里面可以有多个 Host,每个 Host 下可以有多个 Context,每个 Context 下可以有多个 Wrapper,它们的装配关系如下:

四个容器的作用分别是:

  • Engine:引擎,用来管理多个站点,一个 Service 最多只能有一个 Engine
  • Host:代表一个站点,也可以叫做虚拟主机,通过配置 Host 就可以添加站点
  • Context:代表一个应用程序,对应着平时开发的一套程序,或者一个 WEB-INF 目录及下面的 web.xml 文件
  • Wrapper:每个 Wrapper 封装着一个 Servlet

Context 和 Host 的区别是 Context 表示一个应用,比如,默认设置下 webapps 下的每一个目录都是一个应用,其中 ROOT 目录中存放着主应用,其他目录存放着别的子应用,而整个 webapps 是一个站点。假如 www.xxx.com 域名对应着 webapps 目录所代表的站点,其中的 ROOT 目录里的应用就是主应用,访问时直接使用域名就可以,而 webapps/test 目录存放的是 test 子应用,访问时需要用 www.xxx.com/test,每一个应用对应一个 Context,所有 webapps 下的应用都属于 www.xxx.com 站点,而 blog.xxx.com 则是另外一个站点,属于另外一个 Host。

3、4 种容器的配置方法

Engine 和 Host 的配置方法

Engine 和 Host 的配置都是在 conf/server.xml 中,server.xml 文件是 Tomcat 中最重要的配置文件,Tomcat 的大部分功能都可以在这个文件中配置,比如下面是简化了的默认配置:

<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">

  <Service name="Catalina">

    <Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>
    <Connector protocol="AJP/1.3"
                 address="::1"
                 port="8009"
                 redirectPort="8443" />
      
    <Engine defaultHost="localhost" name="Catalina">
        
      <Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true">
      	<Context docBase="E:\apache-tomcat-9.0.35\webapps\Java_Web_Experiment" path="/Java_Web_Experiment" reloadable="true" source="org.eclipse.jst.jee.server:Java_Web_Experiment"/>
       </Host>
    </Engine>
  </Service>
</Server>

这里定义了一个 Server,在 8005 端口监听关闭命令 “SHUTDOWN”;

Server 里定义了一个名为 Catalina 的 Service;

Service 里定义了两个 Connector,一个是 HTTP 协议,一个是 AJP 协议,AJP 主要用于集成(比如和 Apache 集成);

Service 里还定义了一个名为 Catalina 的 Engine;

Engine 里面定义了一个名为 localhost 的 Host。

Engine 和 Host 直接用 Engine、Host 标签定义到相应位置就可以了。Host 标签中的 name 属性代表域名,所以上面定义的站点可以通过 localhost 访问,appBase 属性指定站点的位置,比如,上面定义的站点就是默认的 webapps 目录,unpackWARS 属性表示是否自动解压 war 文件,autoDeploy 属性表示是否自动部署,如果 autoDeploy 为 true 那么 Tomcat 在运行过程中会在 webapps 目录中加入新的应用会自动部署并启动。另外 Host 还可以有一个 Alias 子标签,可以通过这个标签来定义别名,如果多个域名访问同一个站点就可以这么定义,如 www.aaa.comwww.bbb.com 要访问同一个站点,可以这一配置:

<Host name="www.aaa.com" appBase="/hosts/aaa.com" autoDeploy="true" unpackWARs="true">
	<Alias>www.bbb.com</Alias>
</Host>

Engine 在定义的时候有一个 defaultHost 属性,它表示接收到请求的域名如果在所有的 Host 的 name 和 Alias 中都找不到时使用默认的 Host,另外这 Host 的名称也是默认和 IP 关联的,比如上面的用 localhost 和 127.0.0.1 都可以访问。

Context 的配置方法

Context 有三种配置方法:

  1. 通过文件配置;
  2. 将 WAR 应用直接放到 Host 目录下,Tomcat 会自动查找并添加到 Host 中;
  3. 将应用的文件夹放到 Host 目录下,Tomcat 也会自动查找并添加到 Host

文件配置:

一共有 5 个位置可以配置:

  • conf/server.xml 文件中的 Context 标签
  • conf/[enginename]/[hostname]/ 目录下以应用命名的 xml 文件
  • 应用自己的 /META-INF/context.xml 文件
  • conf/context.xml 文件
  • conf/[enginename]/[hostname]/context.xml.default 文件

其中前三个位置用于配置单独的应用,后两个配置的 Context 是共享的,conf/context.xml 文件中配置的内容在整个 Tomcat 中共享,第 5 种配置的内容在对应的站点(Host)中共享。

另外,因为 conf/server.xml 文件只有在 Tomcat 重启的时候才会重新加载,所以第一种配置方法不推荐使用。

Wrapper 的配置

Wrapper 的配置就是我们在 web.xml 中配置的 Servlet,一个 Servlet 对应一个 Wrapper。另外也可以在 conf/web.xml 中配置全局的 Wrapper,处理 Jsp 的 JspServlet 就配置在这里,所以不需要自己配置 Jsp 就可以处理 Jsp 请求了。

注意

同一个 Service 下的所有站点共享 Connector,所以监听的端口都一样。如果想要添加监听不同端口的站点,可以通过不同的 Service 来配置,Service 也是在 conf/server.xml 文件中配置的。

4、Container 的启动

Container 的启动是通过 init 和 start 方法来完成的,前面已经分析过这两个方法会在 Tomcat 启动时被 Service 调用。Container 也是按照 Tomcat 的生命周期来管理的,init 和 start 方法也会调用 initInternal 和 startInternal 模板方法来具体处理,不过 Container 和前面讲的 Tomcat 整体结构启动的过程稍微有点不一样,主要有三点区别:

  • Container 的 4 个子容器有一个共同的父类 ContainerBase,这里定义了 Container 容器的 initInternal 和 startInternal 方法通用处理内容,具体容器还可以添加自己的内容;
  • 除了最顶层容器的 init 是被 Service 调用的,子容器的 init 方法并不是在容器中逐层循环调用的,而是在执行 start 方法的时候通过状态判断还没有初始化才会调用;
  • start 方法除了在父容器的 startInternal 方法中调用,还会在父容器的添加子容器的 addChild 方法中调用,这主要是因为 Context 和 Wrapper 是动态添加的,我们在站点目录下放一个应用的文件夹或者 war 包就可以添加一个 Context,在 web.xml 文件中配置一个 Servlet 就可以添加一个 Wrapper,所以 Context 和 Wrapper 是在容器启动的过程中才动态查找出来添加到相应的父容器中的。

先分析 Container 的基础实现类 ContainerBase 中的 initInternal 和 startInternal 方法,然后再对具体容器进行分析。

4.1、ContainerBase

ContainerBase 的 initInternal 方法主要初始化 ThreadPoolExecutor 类型的 startStopExecutor 属性,用于管理启动和关闭的线程,代码如下:

// org.apache.catalina.core.ContainerBase

@Override
protected void initInternal() throws LifecycleException {
    BlockingQueue<Runnable> startStopQueue = new LinkedBlockingQueue<>();
    startStopExecutor = new ThreadPoolExecutor(
        getStartStopThreadsInternal(),
        getStartStopThreadsInternal(), 10, TimeUnit.SECONDS,
        startStopQueue,
        new StartStopThreadFactory(getName() + "-startStop-"));
    startStopExecutor.allowCoreThreadTimeOut(true);
    super.initInternal();
}

ThreadPoolExecutor 继承自 Executor 用于管理线程,特别是 Runnable 类型的线程。另外需要注意的是,这里并没有设置生命周期的相应状态,所以如果具体容器也没有设置相应生命周期状态,那么即使已经调用 init 方法进行了初始化,在 start 进行启动前也会再次调用 init 方法。

ContainerBase 的 startInternal 方法主要做了 5 件事:

  • 如果有 Cluster 和 Realm 则调用其 start 方法
  • 调用所有子容器的 start 方法启动子容器
  • 调用管道中 Value 的 start 方法来启动管道
  • 启动完成后将生命周期状态设置为 LifecycleState.STARTING 状态
  • 启用后台线程定时处理一些事情

代码如下:

// org.apache.catalina.core.ContainerBase

@Override
protected synchronized void startInternal() throws LifecycleException {

    // Start our subordinate components, if any
    logger = null;
    getLogger();
    
    // 如果有 Cluster 和 Realm 则启动
    Cluster cluster = getClusterInternal();
    if (cluster instanceof Lifecycle) {
        ((Lifecycle) cluster).start();
    }
    Realm realm = getRealmInternal();
    if (realm instanceof Lifecycle) {
        ((Lifecycle) realm).start();
    }

    // 获取所有子容器
    Container children[] = findChildren();
    List<Future<Void>> results = new ArrayList<>();
    for (Container child : children) {
        // 这里通过线程调用子容器的 start 方法,相当于 child.start();
        results.add(startStopExecutor.submit(new StartChild(child)));
    }

    MultiThrowable multiThrowable = null;
	// 处理子容器启动线程的 Future
    for (Future<Void> result : results) {
        try {
            result.get();
        } catch (Throwable e) {
            log.error(sm.getString("containerBase.threadedStartFailed"), e);
            if (multiThrowable == null) {
                multiThrowable = new MultiThrowable();
            }
            multiThrowable.add(e);
        }
    }
    if (multiThrowable != null) {
        throw new LifecycleException(sm.getString("containerBase.threadedStartFailed"),
                                     multiThrowable.getThrowable());
    }

    // 启动管道
    if (pipeline instanceof Lifecycle) {
        ((Lifecycle) pipeline).start();
    }

    // 将生命周期状态设置为 LifecycleState.STARTING
    setState(LifecycleState.STARTING);

    // 启动后台线程
    threadStart();
}

这里先判断是否需要启动 Cluster 和 Realm,启动方法是调用它们的 start 方法。Cluster 用于配置集群,在 server.xml 中有注释的参考配置,它的作用是同步 Session,Realm 是 Tomcat 的安全域,可以用来管理资源的访问权限。

子容器是使用 startStopExecurtor 调用新线程启动的,这样可以用多个线程来同时启动,效率更高,具体启动过程是通过一个增强 for 循环对每个姿容启动了一个线程,并将返回的 Future 保存到一个 List 中,然后遍历每个 Futrue 并调用其 get 方法。

遍历 Futrue 主要有两个作用:

  • 其 get 方法是阻塞的,只有线程处理完之后才会向下走,这就保证了管道 Pipeline 启动之前容器已经启动完成了;
  • 可以处理启动过程中遇到的异常。

启动子容器的线程类型 StartChild 是一个实现了 Callable 的内部类,主要作用就是调用子容器的 start 方法:

// org.apache.catalina.core.ContainerBase

private static class StartChild implements Callable<Void> {

    private Container child;

    public StartChild(Container child) {
        this.child = child;
    }

    @Override
    public Void call() throws LifecycleException {
        child.start();
        return null;
    }
}

因为这里的 startInternal 方法是定义在所有容器的父类 ContainerBase 中的,所以所有容器启动的过程都会调用子容器的 start 方法来启动子容器(模板方法模式)。

使用多线程在实际运行过程中可以提高效率,但调试起来比较麻烦,在调试分析 Tomcat 代码的过程中可以直接调用子容器的 start 方法,也就是将上面的:

for (Container child : children) {
    // 这里通过线程调用子容器的 start 方法,相当于 child.start();
    results.add(startStopExecutor.submit(new StartChild(child)));
}

// 改为:
for (Container child : children) {
  	child.start();
}

这样调试起来会简单一些,Future 相关的代码可以暂时注释起来。

子容器启动完成后就该启动容器的管道了,管道启动也是直接调用 start 方法来完成的。管道启动之后设置了生命周期的状态,然后调用 threadStart 方法启动了后台线程。

threadStart 方法启动的后台线程是一个实现了 Runnable 接口的内部类,这个线程会执行一个 while 循环,内部定期调用 processChildren 方法做一些事情,间隔时间的长短是通过 ContainerBase 的 backgroundProcessorDelay 属性来设置的,单位是秒,如果小于 0 就不启动后台线程了,processChildren 方法是该内部类的方法,其内部调用了传入的容器的 backgroundProcess 方法。

backgroundProcess 方法是 Container 接口中的一个方法,一共有 4 个实现,其中三个是 ContainerBase、StandardContext、StandardWrapper,还有一个 FailedContext。

  • ContainerBase 中提供了所有容器共同的处理过程,StandardContext 和 StandardWrapper 的 backgroundProcess 方法除了处理自己相关的业务,也调用了 ContainerBase 的处理。

  • ContainerBase 的 backgroundProcess 方法中调用了 Cluster、Realm 和管道的 backgroundProcess 方法;

  • StandardContext 的 backgroundProcess 方法中对 Session 过期和资源变化进行了处理;

  • StandardWrapper 的 backgroundProcess 方法回对 Jsp 生成的 Servlet 定期进行检查。

4.2、Engine

Service 会调用最顶层容器的 init 和 start 方法,如果使用了 Engine 就会调用 Engine 的。

Engine 的默认实现了 StandardEngine 中的 initInternal 和 startInternal 方法如下:

// org.apache.catalina.core.StandardEngine

@Override
protected void initInternal() throws LifecycleException {
    // Ensure that a Realm is present before any attempt is made to start
    // one. This will create the default NullRealm if necessary.
    getRealm();
    // 调用 ContainerBase 的 initInternal 方法
    super.initInternal();
}

@Override
protected synchronized void startInternal() throws LifecycleException {

    // Log our server identification information
    if (log.isInfoEnabled()) {
        log.info(sm.getString("standardEngine.start", ServerInfo.getServerInfo()));
    }

    // Standard container startup
    super.startInternal();
}

它们分别调用了 ContainerBase 中相应的方法,initInternal 还调用了 getRealm 方法,其作用是如果没有配置 Realm,就使用一个默认的 NullRealm。

4.3、Host

Host 的默认实现类是 StandardHost 没有重写 initInternal 方法,初始化默认调用 ContainerBase 的 initInternal 方法,startInternal 方法如下:

// org.apache.catalina.core.StandardHost
@Override
protected synchronized void startInternal() throws LifecycleException {

    // 如果管道中没有 ErrorReportValve 就将其加入管道
    String errorValve = getErrorReportValveClass();
    if ((errorValve != null) && (!errorValve.equals(""))) {
        try {
            boolean found = false;
            Valve[] valves = getPipeline().getValves();
            for (Valve valve : valves) {
                if (errorValve.equals(valve.getClass().getName())) {
                    found = true;
                    break;
                }
            }
            if(!found) {
                Valve valve =
                    (Valve) Class.forName(errorValve).getConstructor().newInstance();
                getPipeline().addValve(valve);
            }
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString(
                "standardHost.invalidErrorReportValveClass",
                errorValve), t);
        }
    }
    super.startInternal();
}

这里代码看起来很多,但功能却非常简单,就是检查 Host 的管道中有没有指定的 Value,如果没有就添进去。检查的方法就是遍历所有 Value 然后通过名字判断,检查的 Value 的类型通过 getErrorReportValueClass 方法获取,它返回 errorReportValueClass 属性,可以配置,默认值是 org.apache.catalina.values.ErrorReportValue:

// org.apache.catalina.core.StandardHost

private String errorReportValveClass = "org.apache.catalina.valves.ErrorReportValve";

public String getErrorReportValveClass() {
    return this.errorReportValveClass;
}

这就是 StandardHost 的 startInternal 方法处理的过程。Host 的启动除了 startInternal 方法,还有 HostConfig 中相应的方法,HostConfig 继承自 LifecycleListener 的监听器(Engine 也有对应的 EngineConfig 监听器,只不过里面只是简单的做了日志记录),在接收到 Lifecycle.START_EVENT 事件时会调用 start 方法来启动,HostConfig 的 start 方法会检查配置的 Host 站点配置的位置是否存在以及是不是目录,最后调用 deployApps 方法部署应用,deployApps 方法代码如下:

// org.apache.catalina.startup.HostConfig
protected void deployApps() {
    File appBase = host.getAppBaseFile();
    File configBase = host.getConfigBaseFile();
    String[] filteredAppPaths = filterAppPaths(appBase.list());
    // 部署 XML 描述文件
    deployDescriptors(configBase, configBase.list());
    // 部署 WAR 文件
    deployWARs(appBase, filteredAppPaths);
    // 部署文件夹
    deployDirectories(appBase, filteredAppPaths);
}

一共三种部署方式:通过 XML 描述文件、通过 WAR 文件和通过文件夹部署。

XML 文件指 conf/[enginename]/[hostname]/*.xml 文件,WAR 文件和文件夹是指 Host 站点目录下的 WAR 文件和文件夹,这里会自动找出来并部署上,所以我们如果要添加应用只需要直接放在 Host 站点的目录下就可以了。

部署完成后,会将部署的 Context 通过 StandardHost 的 addChild 方法添加到 Host 里面。StandHost 的 addChild 方法会调用父类 ContainerBase 的 addChild 方法,其中会调用子类(这里指 Context)的 start 方法来启动子容器。

4.4、Context

Context 的默认实现类 StandardContext 在 startInternal 方法中调用了 web.xml 中定义的 Listener,另外还初始化了其中的 Filter 和 load-on-startup 的 Servlet,代码如下:

// org.apache.catalina.core.StandardContext
@Override
protected synchronized void startInternal() throws LifecycleException {

    ......
    // 触发 listener
    if (ok) {
       if (!listenerStart()) {
           log.error(sm.getString("standardContext.listenerFail"));
           ok = false;
       }
    }
    
	// 初始化 filter
    if (ok) {
        if (!filterStart()) {
            log.error(sm.getString("standardContext.filterFail"));
            ok = false;
        }
    }
    
    // 初始化 Servlets
    // Load and initialize all "load on startup" servlets
    if (ok) {
        if (!loadOnStartup(findChildren())){
            log.error(sm.getString("standardContext.servletFail"));
            ok = false;
        }
    }
}

listenerStart、filterStart 和 loadStartup 方法分别调用配置在 Listener 的 contextInitialized 方法以及 Filter 和配置了 load-on-startup 的 Servelt 的 init 方法。


Context 和 Host 一样也有一个 LifecycleListener 类型的监听器 ContextConfig,其中 configureStart 方法用来处理 CONFIGURE_START_EVENT 事件,这个方法里面调用了 webConfig 方法,webConfig 方法中解析了 web.xml 文件,相应的创建了 Wrapper 并使用 addChild 添加到 Context 里面。

4.5、Wrapper

Wrapper 的默认实现 StandardWrapper 没有重写 initInternal 方法,初始化时会默认调用 ContainerBase 的 initinternal 方法,startInternal 方法代码如下:

// org.apache.catalina.core.StandardWrapper

@Override
protected synchronized void startInternal() throws LifecycleException {

    // Send j2ee.state.starting notification
    if (this.getObjectName() != null) {
        Notification notification = new Notification("j2ee.state.starting",
                                                     this.getObjectName(),
                                                     sequenceNumber++);
        broadcaster.sendNotification(notification);
    }

    // Start up this component
    super.startInternal();

    setAvailable(0L);

    // Send j2ee.state.running notification
    if (this.getObjectName() != null) {
        Notification notification =
            new Notification("j2ee.state.running", this.getObjectName(),
                             sequenceNumber++);
        broadcaster.sendNotification(notification);
    }

}

这里主要做了三件事:

  • 用 broadcaster 发送通知,主要用于 JMX;
  • 调用了父类 ContainerBase 的 startInternal 方法;
  • 调用 setAvailable 方法让 Servlet 有效。

这里的 setAvailable 方法是 Wrapper 接口的方法,其作用是设置 Wrapper 所包含的 Servlet 有效的起始时间,如果设置的时间为将来的时间,那么调用所对应的 Servlet 就会产生错误,指导过了所设置的时间之后才可以正常调用,它的类型是 long,如果设置为 Long.MAX_VALUE 就一直不可以调用了。

Wrapper 没有别的容器那种 XXXConfig 样式的 LifecycleListener 监听器。


Author: NaiveKyo
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source NaiveKyo !
  TOC