3 ContainerBase 的结构

3.1 ContainerBase 的结构

  Container 是Tomcat 中容器的接口,通常使用的Serviet 就封装在其子接口Wrapper 中。Container 一共有4 个子接口Engine 、Host 、Context 、Wrapper 和一个默认实现类ContainerBase,每个子接口都是一个容器,这4 个子容器都有一个对应的StandardXXX 实现类,并且这些实现类都继承ContainerBase 类。另外Container 还继承Lifecycle 接口,而且ContainerBase 间接继承LifecycleBase,所以Engine 、Host 、Context 、Wrapper 4 个子容器都符合前面讲过的Tomcat生命周期管理模式,结构图如图所示。
这里写图片描述

3.2 Container 的4 个子容器

  Container 的子容器Engine 、Host 、Context 、Wrapper 是逐层包含的关系,其中Engine是最顶层,每个service 最多只能有一个Engine, Engine 里面可以有多个Host ,每个Host 下可以有多个Context ,每个Context 下可以有多个Wrapper,它们的装配关系如下图所示。
这里写图片描述
4个容器的作用分别是

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

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

3.3 4 种容器的配置方法

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

<?xml version='1.0' encoding='utf-8'?>
<Server port="8005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <Listener className="org.apache.catalina.core.JasperListener" />
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
  <GlobalNamingResources>
    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
  </GlobalNamingResources>
  <Service name="Catalina">
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
    <Engine name="Catalina" defaultHost="localhost">
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>

      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
        <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>
    </Engine>
  </Service>
</Server>

  这里首先定义了-个Server ,在8005 端口监昕关闭命令“ SHUTDOWN ”; Server里定义了一个名为Catalina 的Service ; Service 里定义了两个Connector,一个是HTIP 协议,一个是AJP 协议, AJP 主要用于集成(如与Apache 集成) ; Service 里还定义了一个名为Catalina 的Engine; Engine 里定义了一个名为localhost 的Host。
  Engine 和l Host 直接用Engine 、Host 标签定义到相应位置就可以了。Host 标签中的name属性代表域名,所以上面定义的站点可以通过Jocalhost 访问, appBase 属性指定站点的位置,比如,上面定义的站点就是默认的webapps 目录, unpackWARs 属性表示是否自动解压war 文件, autoDeploy 属性表示是否自动部署,如果autoDeploy 为true 那么Tomcat 在运行过程中在webapps 目录中加入新的应用将会自动部署并启动。另外Host 还有一个Alias 子标签,可以通过这个标签来定义别名,如果有多个域名访问同一个站点就可以这么定义。
  Engine 在定义的时候有个defaultHost 属性,它表示接收到请求的域名如果在所有的Host 的name 和Alias 中都找不到时使用的默认Host。
Context 通过文件配置的方式一共有5 个位置可以配置:

conf/server.xml 文件中的Context 标签。

  • conf/[ enginename ]/[hostnarne ]/目录下以应用命名的xrnl 文件。
  • 应用自己的/META-fNF/context.xml 文件。
  • conf/context.xml 文件。
  • conf/[ enginename ]/[hostname ]/context.xml.default 文件。

  其中前三个位置用于配置单独的应用,后两个配置的Context 是共享的, conf/context.xml文件中配置的内容在整个Tomcat 中共享第5 种配置的内容在对应的站点( Host )中共享。另外,因为conf/server.xrnl 文件只有在Tomcat 重启的时候才会重新加载,所以第一种配置方法不推荐使用。
  Wrapper 的配置就是我们在web.xml 中配置的Servlet , 一个Serviet 对应一个Wrapper。另外也可以在conf/web.xml 文件中配置全局的Wrapper,处理Jsp 的JspServlet 就配置在这里,所以不需要自己配置Jsp 就可以处理Jsp 请求了。
  4 个Container 容器配置的方法就介绍完了。需要注意的是,同一个Service 下的所有站点由于是共享Connector ,所以监听的端口都一样。如果想要添加监听不同端口的站点,可以涵过不同的Service 来配置, Service 也是在conf/server.xrnl 文件中配置的。

3.4 Container 的启动

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

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

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

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

  ThreadPoo!Executor 继承向Executor 用于管理线程,特别是Runable 类型的线程,具体用法在异步处理的相关内容中具体讲解。另外需要注意的是,这里并没有设置生命周期的相应状态,所以如果具体容器也没有设置相应生命周期状态,那么即使已经调用init 方法进行了初使化,在start 进行启动前也会再次调用init 方法。
ContainerBase 的startlntemal 方法主要做了5 件事:

  • 如果有Cluster 和Realm 则调用其start 方法;
  • 调用所有子容器的start 方法启动子容器;
  • 调用管道中Value 的start 方法来启动管道(管道请见第四篇);
  • 启动完成后将生命周期状态设置为LifecycleState.STARTING 状态;
  • 启用后台线程定时处理一些事情。
    @Override
    protected synchronized void startInternal() throws LifecycleException {

        // Start our subordinate components, if any
        Loader loader = getLoaderInternal();
        if ((loader != null) && (loader instanceof Lifecycle))
            ((Lifecycle) loader).start();
        logger = null;
        getLogger();
        Manager manager = getManagerInternal();
        if ((manager != null) && (manager instanceof Lifecycle))
            ((Lifecycle) manager).start();
        Cluster cluster = getClusterInternal();
        if ((cluster != null) && (cluster instanceof Lifecycle))
            ((Lifecycle) cluster).start();
        Realm realm = getRealmInternal();
        if ((realm != null) && (realm instanceof Lifecycle))
            ((Lifecycle) realm).start();
        DirContext resources = getResourcesInternal();
        if ((resources != null) && (resources instanceof Lifecycle))
            ((Lifecycle) resources).start();

        // Start our child containers, if any
        Container children[] = findChildren();
        List<Future<Void>> results = new ArrayList<Future<Void>>();
        for (int i = 0; i < children.length; i++) {
            results.add(startStopExecutor.submit(new StartChild(children[i])));
        }

        boolean fail = false;
        for (Future<Void> result : results) {
            try {
                result.get();
            } catch (Exception e) {
                log.error(sm.getString("containerBase.threadedStartFailed"), e);
                fail = true;
            }

        }
        if (fail) {
            throw new LifecycleException(
                    sm.getString("containerBase.threadedStartFailed"));
        }

        // Start the Valves in our pipeline (including the basic), if any
        if (pipeline instanceof Lifecycle)
            ((Lifecycle) pipeline).start();


        setState(LifecycleState.STARTING);

        // Start our thread
        threadStart();

    }

  这里首先启动了Cluster 和Realm ,启动方法是直接调用它们的start 方法。Cluster 用于配置集群,在server.xml 中有注释的参考配置,它的作用就是同步Session , Realm 是Tomcat 的安全域,可以用来管理资源的访问权限。
  子容器是使用startStopExecutor 调用新线程来启动的,这样可以用多个线程来同时启动,效率更高,具体启动过程是通过一个for 循环对每个子容器启动了一个线程,并将返回的Future 保存到一个List 中(更多线程相关内容会在异步处理中介绍),然后遍历每个Future 并调用其get 方法。遍历Future 主要有两个作用:①其get 方法是阻塞的,只有线程处理完之后才会向下走,这就保证了管道Pipeline 启动之前容器已经启动完成了;②可以处理启动过程中遇到的异常。
  启动子容器的线程类型StartChild 是一个实现了Callable 的内部类,主要作用就是调用子容器的start 方法,代码如下:

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 方法来启动子容器。
  子容器启动完成后接着启动容器的管道,管道第四篇文章中详细讲解,管道启动也是直接调用start 方法来完成的。管道启动完之后设置了生命周期的状态,然后调用threadStart 方法启动了后台线程。
  threadStart 方法启动的后台线程是一个while 循环,内部会定期调用backgroundProcess 方法做一些事情,间隔时间的长短是通过ContainerBase 的backgroundProcessor Delay 属性来设置的,单位是秒,如果小于0 就不启动后台线程了,不过其backgroundProcess 方法会在父容器的后俞线程中调用。backgroundProcess 方法是Container 接口中的一个方法, 一共有3 个实现,分别在ContainerBase 、StandardContext 和StandardWrapper 中, ContainerBase 中提供了所有容器共同的处理过程, StandardContext 和StandardWrapper 的backgroundProcess 方法除了处理I’! 己相关的业务,也调用ContainerBase 中的处理。ContainerBase 的backgroundProcess 方法中调用了Cluster 、Realm 和管道的backgroundProcess 方法; StandardContext 的backgroundProcess方法中对Session 过期和资源变化进行了处理; StandardWrapper 的backgroundProcess方法会对Jsp 生成的Servlet 定期进行检查。
Engine
  Service 会调用最顶层容器的init 和start 方法,如果使用了Engine 就会调用Engine 的。Engine 的默认实现类StandardEngine 中的initlntemal 和startInternal 方法如下:

 @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();
        super.initInternal();
    }

    @Override
    protected synchronized void startInternal() throws LifecycleException {

        // Log our server identification information
        if(log.isInfoEnabled())
            log.info( "Starting Servlet Engine: " + ServerInfo.getServerInfo());

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

它们分别调用了ContainerBase 中的相应方法, initlntemal 方法还调用了getRealm 方法,其作用是如果没有配置Realm ,则使用一个默认的Null Realm ,代码如下:

@Override
    public Realm getRealm() {
        Realm configured = super.getRealm();
        // If no set realm has been called - default to NullRealm
        // This can be overridden at engine, context and host level  
        if (configured == null) {
            configured = new NullRealm();
            this.setRealm(configured);
        }
        return configured;
    }

Host
  Host 的默认实现类StandardHost 没有重写initlntemal 方法,初始化默认调用ContainerBase的initlntemal 方法,startlntemal 方法代码如下:

@Override
    protected synchronized void startInternal() throws LifecycleException {
        // Set error report valve
        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).newInstance();
                    getPipeline().addValve(valve);
                }
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                log.error(sm.getString(
                        "standardHost.invalidErrorReportValveClass",
                        errorValve), t);
            }
        }
        super.startInternal();
    }

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

 private String errorReportValveClass =
        "org.apache.catalina.valves.ErrorReportValve";
/**
     * Return the Java class name of the error report valve class
     * for new web applications.
     */
    public String getErrorReportValveClass() {

        return (this.errorReportValveClass);

    }

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

    protected void deployApps() {

        File appBase = appBase();
        File configBase = configBase();
        String[] filteredAppPaths = filterAppPaths(appBase.list());
        // Deploy XML descriptors from configBase
        deployDescriptors(configBase, configBase.list());
        // Deploy WARs
        deployWARs(appBase, filteredAppPaths);
        // Deploy expanded folders
        deployDirectories(appBase, filteredAppPaths);

    }

  一共有三种部署方式:通过XML 描述文件、通过WAR 文件和通过文件夹部署。XML文件指的是conf/[enginename ]/[hostname ]/* .xml 文件, WAR 文件和文件夹是Host 站点目录下的WAR 文件和文件夹,这里会自动找出来并部署上,所以我们如果要添加应用只需要直接放在Host 站点的目录下就可以了。部署完成后,会将部署的Context 通过StandardHost 的add Child 方法添加到Host 里面。StandardHost 的addChild 方法会调用父类ContainerBase 的addChild 方法, 其中会调用子类(这里指Context )的start 方法来启动子容器。
Context
  Context 的默认实现类StandardContext 在startlntemal 方法中调用了在web.xrr世中定义的Listener,另外还初始化了其中的Filter 和load-on-startup 的Servlet 。代码如下:

  @Override
    protected synchronized void startInternal() throws LifecycleException {
    ....
     if (ok) {
                if (!listenerStart()) {
                 log.error(sm.getString("standardContext.listenerFail"));
                    ok = false;
                }
            }
     if (ok) {
                if (!filterStart()) {
                    log.error(sm.getString("standardContext.filterFail"));
                    ok = false;
                }
            }

            // Load and initialize all "load on startup" servlets
            if (ok) {
                if (!loadOnStartup(findChildren())){
                    log.error(sm.getString("standardContext.servletFail"));
                    ok = false;
                }
            }

  listenerStart 、filterStart 和loadOnStartup 方法分别调用配置在Listener 的contextlnitialized 方法以及Filter 和配置了load-on-startup 的Servlet 的init 方法。
  Context 和l Host 一样也有一个LifecycleListener 类型的监听器ContextConfig , 其中configureStart 方法用来处CONFTGURE_START_EVENT 事件,这个方法里面调用webConfig方法, webConfig 方法巾解析了web刀nl 文件,相应地创建了Wrapper 并使用addChild 添加到了Context 里面。
Wrapper
  Wrapper 的默认实现类StandardWrapper 没有重写initlntemal 方法,初始化时会默认调用ContainerBase 的initlntemal 方法, startlntemal 方法代码如下:

@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 中的startlntemal 方法;
  • 调用setAvailable 方法让Servlet 有效。

这里的setAvailable 方法是Wrapper 接口中的方法,其作用是设置Wrapper 所包含的Servlet 有效的起始时间,如果所设置的时间为将来的时间,那么调用所对应的Se凹let 就会产生错误,直到过了所设置的时间之后才可以正常调用,它的类型是long,如果设置为Long.MAX VALUE 就一直不可以调用了。Wrapper 没有别的容器那种XXXConfig 样式的LifecycleListener 监听器。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐