【Tomcat】【三】Container 分析

1  前言

  这节我们来看下 Container 哈,一样边看边记录一下,先通读一遍理解,理解完再记录加强一遍。

2  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生命周期管理模式,结构图如图所示。

3  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。

4  4种容器的配置方法

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

  

   这里首先定义了一个 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.excelib.com和excelib.com 要访问同一个站点,可以做如下配置;

  

   Engine在定义的时候有个defaultHost 属性,它表示接收到请求的域名如果在所有的Host的name 和Alias 中都找不到时使用的默认 Host,比如,我们定义了 www.excelib.com和excelib.com 的 Host,但是 blog.excelib.com 也解析到了这台主机的IP,但是 Tomcat 却找不到blog.excelib.com 对应的 Host,这时就会使用 Engine 中配置的 defaultHost 来处理,另外如果使用的是IP 直接访问也会用到 defaultHost,如果将 Engine 的 defaultHost 属性删除,然后启动后用127.0.01来访问本机的Tomcat 就不可能了。

  Context有三种配置方法:1、通过文件配置;2、将WAR 应用直接放到 Hot目录下Tomcat 会自动查找并添加到 Host 中;3、将应用的文件夹放到 Host 目录下,Tomcat 也会自动查找并添加到Host 中。

  Context通过文件配置的方式一共有5个位置可以配置:

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

  其中前三个位置用于配置单独的应用,后两个配置的Context 是共享的,conf/context.xml文件中配置的内容在整个 Tomcat 中共享,第 5种配置的内容在对应的站点 (Host)中共享另外,因为confserverxml 文件只有在 Tomcat 重启的时候才会重新加载,所以第一种配置方
法不推荐使用。

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

  4个Container 容器配置的方法就介绍完了。需要注意的是,同一个Service下的所有站点由于是共享 Connector,所以监听的端口都一样。如果想要添加监听不同端口的站点,可以通过不同的 Service来配置,Service 也是在 conf/server.xml文件中配置的。

5  Container的启动

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

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

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

5.1  ContainerBase

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

  

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

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

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

  

 

   

  这里首先启动了 Cluster 和 Realm,启动方法是直接调用它们的start方法。Cluster 用于配置集群,在serverxml中有注释的参考配置,它的作用就是同步Session,Realm 是 Tomcat 的安全域,可以用来管理资源的访问权限。

  子容器是使用startStopExecutor 调用新线程来启动的,这样可以用多个线程来同时启动,效率更高,具体启动过程是通过一个 for 循环对每个子容器启动了一个线程,并将返回的Future 保存到一个List中(更多线程相关内容会在异步处理中介绍)然后遍历每个 Future并调用其get 方法。遍历 Future 主要有两个作用:D其get方法是阻塞的,只有线程处理完之后才会向下走,这就保证了管道 Pipeline 启动之前容器已经启动完成了;2可以处理启动过程中遇到的异常。

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

  

  因为这里的startInternal方法是定义在所有容器的父类ContainerBase中的,所以所有容器启动的过程中都会调用子容器的start 方法来启动子容器。子容器启动完成后接着启动容器的管道,管道在 7.4节详细讲解,管道启动也是直接调用start方法来完成的。管道启动完之后设置了生命周期的状态,然后调用threadStart 方法启动了后台线程。

  threadStart 方法启动的后台线程是一个 while循环,内部会定期调用 backgroundProcess方法做一些事情,间隔时间的长短是通过 ContainerBase 的backgroundProcessorDelay 属性来设置的,单位是秒,如果小于0就不启动后台线程了,不过其 backgroundProcess 方法会在父容器的后台线程中调用。backgroundProcess 方法是 Container 接口中的一个方法,一共有3个实现,分别在 ContainerBase、StandardContext和 StandardWrapper 中,ContainerBase 中提供了所有容器共同的处理过程,StandardContext 和 StandardWrapper 的 backgroundProcess 方法除了处理自己相关的业务,也调用ContainerBase 中的处理。ContainerBase 的 backgroundProcess方法中调用了 Cluster、Realm 和管道的 backgroundProcess 方法;StandardContext的 background.Process 方法中对 Sessin 过期和资源变化进行了处理;StandardWrapper 的 backgroundProcess方法会对Jsp生成的Servlet 定期进行检查。

5.2  Engine

  Service会调用最顶层容器的init 和start 方法,如果使用了 Engine 就会调用 Engine 的。Engine的默认实现类StandardEngine 中的initInternal和startInternal方法如下:

  

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

  

5.3  Host

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

  

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

  

   

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

  

  一共有三种部署方式:通过XML 描述文件、通过 WAR 文件和通过文件夹部署。XML文件指的是conf/[enginename]/[hostname]/*xml文件WAR文件和文件夹是 Host站点目录下的 WAR 文件和文件夹,这里会自动找出来并部署上,所以我们如果要添加应用只需要直接放在Host站点的目录下就可以了。部署完成后,会将部署的 Context 通过 StandardHost的addChild方法添加到Host 里面。StandardHost的addChild方法会调用父类 ContainerBase 的addChild 方法,其中会调用子类(这里指Context)的stat 方法来启动子容器。

5.4  Context

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

  

 

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

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

5.5  Wrapper

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

  

   这里主要做了三件事情:

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

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

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

6  小结

这节我们主要看了 Engine、Host、Context、Wrapper 和一个默认实现类 ContainerBase的相关内容,大致了解 Container的一些基础工作,有理解不对的地方欢迎指正哈。

posted @ 2023-03-20 22:48  酷酷-  阅读(124)  评论(0编辑  收藏  举报