Tomcat学习笔记(5)- 容器(Engine、Host、Context、Wrapper)

Engine即为全局引擎容器,它的标准实现是StandardEngine。


Host在整个Servlet引擎中抽象出Host容器用于表示虚拟主机,它是根据URL地址中的主机部分抽象的,一个Servlet引擎可以包含若干个Host容器,而一个Host容器可以包含若干个Context容器。在Tomcat中Host的标准实现是StandardHost,它从虚拟主机级别对请求和响应进行处理。


一个Context对应一个Web应用程序,但Web项目的组成比较复杂,它包含很多组件。对于Web容器,需要将Web应用程序包含的组件转换成容器的组件。


Wrapper属于Tomcat中4个级别容器中最小级别的容器,与之相对应的是Servlet。

1. Container

下面有段注释十分重要:

  • Engine - 整个Catalina servlet引擎的表示, 引擎 -整个Catalina servlet引擎的表示,
  • Host -包含数字的虚拟主机的表示形式的上下文。
  • Context - 表示单个ServletContext,它将通常为受支持的servlet包含一个或多个包装器。
  • Wrapper -单个servlet定义的表示
  • /**
    * A Container是一个对象,它可以执行从
    *客户端,并返回基于这些请求的响应。一个容器可以
    *可选地支持一个管道阀门处理请求
    *通过实现管道接口,在运行时配置顺序
    *。
    
    *容器将存在于Catalina的几个概念层次上。的
    *以下是常见的例子:
     * <ul>
     * <li><b>Engine</b> - 整个Catalina servlet引擎的表示,
    引擎 -整个Catalina servlet引擎的表示,
     * <li><b>Host</b> -包含数字的虚拟主机的表示形式的上下文。
     * <li><b>Context</b> - 表示单个ServletContext,它将
    *通常为受支持的servlet包含一个或多个包装器。
     * <li><b>Wrapper</b> -单个servlet定义的表示
     *   (如果servlet本身支持多个servlet实例)
    *实现SingleThreadModel)。
     * </ul>
    *给定的卡特琳娜部署不需要包括所有的容器
    *以上描述的级别。例如,管理应用程序
    *嵌入式网络设备(如路由器)可能只包含
    *一个上下文和几个包装器,甚至一个包装器如果
    应用程序相对较小。因此,容器实现
    *需要进行设计,使它们能在不在场的情况下正常运行
    *指定部署中的父容器。
     * <p>
    *容器还可以与许多支持组件相关联
    *提供可能被共享的功能(通过附加到
    *父容器)或单独定制。以下支持
    *目前认可的组件:
     * <ul>
     * <li><b>Loader</b> - 用于集成新Java类的类装入器 将此容器放入运行Catalina的JVM中。
     * <li><b>Logger</b> -实现log()方法
     *     ServletContext接口的属性。
     * <li><b>Manager</b> -相关联的会话池的管理这个容器。
     * <li><b>Realm</b> - 安全域的只读接口,用于验证用户身份及其对应角色。
     * <li><b>Resources</b> - 支持访问静态的JNDI目录上下文
    *资源,启用自定义链接到现有的服务器组件时 ,Catalina被嵌入到一个更大的服务器中。
     * </ul>
    
     */
    public interface Container extends Lifecycle {
    

    1.1 ContainerBase

    ContainerBase实现Container,定义了容器所需要的一些共有组件与方法。

    public abstract class ContainerBase extends LifecycleMBeanBase
            implements Container {
        /**
         * 属于此容器的子容器,按名称键入。
         */
        protected final HashMap<String, Container> children = new HashMap<>();
    
    
        /**
         * 处理器延迟这个组件。
         */
        protected int backgroundProcessorDelay = -1;
    
    
        /**
       *此容器的容器事件监听器。实现为一个
    	* CopyOnWriteArrayList,因为监听器可能会调用方法来添加/删除
    	*自己或其他听众,并与读写洛克,将触发
    	*死锁。
         */
        protected final List<ContainerListener> listeners = new CopyOnWriteArrayList<>();
    
        /**
        与此容器相关联的日志程序实现。
         */
        protected Log logger = null;
    
    
        /**
        *关联的日志记录器名称。
         */
        protected String logName = null;
    
    
        /**
        与此容器相关联的集群。
         */
        protected Cluster cluster = null;
        private final ReadWriteLock clusterLock = new ReentrantReadWriteLock();
    
    
        /**
         *容器名称。
         */
        protected String name = null;
    
    
        /**
         * 此容器是其子容器的父容器。
         */
        protected Container parent = null;
    
    
        /**
         *安装加载器时要配置的父类加载器。
         */
        protected ClassLoader parentClassLoader = null;
    
    
        /**
         * 与此容器相关联的管道对象。
         */
        protected final Pipeline pipeline = new StandardPipeline(this);
    
    
        /**
         * 与此容器相关联的领域。
         */
        private volatile Realm realm = null;
    
    
        /**
         * 用于控制对领域的访问的锁。
         */
        private final ReadWriteLock realmLock = new ReentrantReadWriteLock();
    
    
        /**
         * 这个包的字符串管理器。
         */
        protected static final StringManager sm =
            StringManager.getManager(Constants.Package);
    
    
        /**
         * 当添加子节点时是否会自动启动。
         */
        protected boolean startChildren = true;
    
        /**
       *此组件的属性更改支持。
         */
        protected final PropertyChangeSupport support =
                new PropertyChangeSupport(this);
    
    
        /**
       *后台线程。
         */
        private Thread thread = null;
    
    
        /**
       *后台线程完成信号。
         */
        private volatile boolean threadDone = false;
    
    
        /**
    	*该容器通常处理的请求使用的访问日志
    	*在处理链中较早处理的。
         */
        protected volatile AccessLog accessLog = null;
        private volatile boolean accessLogScanComplete = false;
    
    
        /**
        *任何可用于处理启动和停止事件的线程数
    	*与此容器关联的子容器。
         */
        private int startStopThreads = 1;
        protected ThreadPoolExecutor startStopExecutor;
    
    

    2. Engine接口

    注意Engine接口与Host接口都继承了Container,其共有的组件都继承自ContainerBase类。

    /**
     * An <b>Engine</b> 是容器是否表示整个Catalina servlet
     * engine.  它在以下类型的场景中非常有用:
     *您希望使用拦截器来查看每个被处理的请求整个引擎。
     您希望使用独立的HTTP连接器运行Catalina,但仍然是这样希望支持多个虚拟主机。
     
    *一般来说,你不会使用引擎部署卡特琳娜连接
    *连接到web服务器(如Apache),因为连接器将具有
    *利用web服务器的功能来确定上下文(或
    *甚至可能是哪个包装)应该被用来处理这个请求。
    
     * 附加到引擎的子容器通常是实现
    主机(表示虚拟主机)或上下文(表示个体)的*
    *一个单独的servlet上下文),取决于引擎实现。
    
    *如果使用引擎,引擎总是卡特琳娜的顶级容器
    *层次结构。因此,实现的setParent()方法
    *应该抛出IllegalArgumentException。
     */
    public interface Engine extends Container {
    
        /**
         * @return 获取此引擎的默认主机名。
         */
        public String getDefaultHost();
    
    
        /**
         * 设置此引擎的默认主机名。
         *
         * @param defaultHost The new default host
         */
        public void setDefaultHost(String defaultHost);
    
    
        /**
         * @return 此引擎的JvmRouteId。
         */
        public String getJvmRoute();
    
    
        /**
         *	设置此引擎的JvmRouteId。
         *
         * @param jvmRouteId(新的)JVM路由ID。集群中的每个引擎
         *      必须有唯一的JVM路由ID。
         */
        public void setJvmRoute(String jvmRouteId);
    
    
      
        public Service getService();
    
    
        /**
         *设置与我们关联的service (如果有的话)。
         *
         * @param 拥有这个引擎的服务
         */
        public void setService(Service service);
    }
    
    

    3. StandardEngine

    Engine标准实现是StandardEngine,包含的主要组件有Host组件、AccessLog组件、Pipeline组件、Cluster组件、Realm组件、LifecycleListener组件和Log组件。
    组件主要继承自org.apache.catalina.core.ContainerBase:

    • Host
      Host组件是Engine容器的子容器,它表示一个虚拟主机。Host也包含很多其他的组件。

    • AccessLog
      Engine容器里的AccessLog组件负责客户端请求访问日志的记录。因为Engine容器是一个全局的Servlet容器,所以这里的访问日志作用的范围是所有客户端的请求访问,不管访问哪个虚拟主机都会被该日志组件记录。

    • Pipeline
      Pipeline其实属于一种设计模式,在Tomcat中可以认为它是将不同容器级别串联起来的通道,当请求进来时就可以通过管道进行流转处理。Tomcat中有4个级别的容器,每个容器都会有一个属于自己的Pipeline。

    • Cluster
      Tomcat中有Engine和Host两个级别的集群,而这里的集群组件正是属于全局引擎容器。它主要把不同JVM上的全局引擎容器内的所有应用都抽象成集群,让它们能在不同的JVM之间互相通信,使会话同步、集群部署得以实现。

    • 2.5 Realm
      Realm对象其实就是一个存储了用户、密码及权限等的数据对象,它的存储方式可能是内存、xml文件或数据库等。它的作用主要是配合Tomcat实现资源认证模块。
      在配置文件的<Engine>节点下配置Realm,则在启动时对应的域会添加到Engine容器中。

    • LifeCycleListener
      Engine容器内的生命周期监听器是为了监听Tomcat从启动到关闭整个过程的某些事件,然后根据这些事件做不同的逻辑处理。

    • Log
      日志组件负责的事情就是不同级别的日志输出,几乎所有系统都有日志组件。

    4. Host接口

    /**
    * A Host是一个容器,它表示一个虚拟主机在
    * Catalina servlet引擎。它在下列情况下是有用的:
     * <ul>
     * <li>您希望使用拦截器来查看每个被处理的请求
    	*由这个特定的虚拟主机。
     * <li>您希望使用独立的HTTP连接器运行Catalina,但仍然是这样
    	*希望支持多个虚拟主机。
     * </ul>
     *一般来说,你不会在部署Catalina connected时使用主机
    *连接到web服务器(如Apache),因为连接器将具有
    *利用web服务器的功能来确定上下文(或
    *甚至可能是哪个包装)应该被用来处理这个请求。
     * <p>
    *连接到主机的父容器通常是一个引擎,但也可能是
    *是一些其他的实现,或者可能被省略,如果没有必要。
     * <p>
    附加到主机的子容器通常是实现
    上下文的*(表示单个servlet上下文)。
     */
    public interface Host extends Container {
    

    接口内方法如下:
    在这里插入图片描述

    5. StandardHost

    Host容器包含了若干Context容器、AccessLog组件、Pipeline组件、Cluster组件、Realm组件、HostConfig组件和Log组件。
    其中的AccessLog组件、Pipeline组件、Cluster组件、Realm组件和Log组件,继承自ContainerBase。

    • Context
      每个Host容器包含若干个Web应用(Context)。对于Web项目来说,其结构相对比较复杂,而且包含很多机制,Tomcat需要对它的结构进行解析,同时还要具体实现各种功能和机制,这些复杂的工作就交给了Context容器。Context容器对应实现了Web应用包含的语义,实现了Servlet和JSP的规范。
    
        /**
         * The Java class name of the default Context implementation class for
         * deployed web applications.
         */
        private String contextClass =
            "org.apache.catalina.core.StandardContext";
    
    

    Context也是比较复杂的一块。

    • AccessLog
      Host容器的访问日志作用的范围是该虚拟主机的所有客户端的请求访问,不管访问哪个应用都会被该日志组件记录。
    • Pipeline
      不同级别容器的管道完成的工作都不一样,每个管道要搭配阀门(Valve)才能工作。Host容器的Pipeline默认以StandardHostValve作为基础阀门,这个阀门主要的处理逻辑是先将当前线程上下文类加载器设置成Context容器的类加载器,让后面Context容器处理时使用该类加载器,然后调用子容器Context的管道。
    • Cluster略
    • Realm略
    • HostConfig
      见下一小节。

    5.1 HostConfig

    Host作为虚拟主机容器用于放置Context级别容器,而Context其实对应的就是Web应用,实际上每个虚拟主机可能会对应部署多个应用,每个应用都有自己的属性。

    当Tomcat启动时,必须把对应Web应用的属性设置到对应的Context中,根据Web项目生成Context,并将Context添加到Host容器中。另外,当我们把这些Web应用程序复制到指定目录后,还有一个重要的步骤就是加载,把Web项目加载到对应的Host容器内。

    当Tomcat启动时,它必须把所有Web项目都加载到对应的Host容器内,完成这些任务的就是HostConfig监听器。
    在这里插入图片描述

    HostConfig实现了Lifecycle接口,当START_EVENT事件发生时则会执行Web应用部署加载动作。
    Web应用有三种部署类型:
    Descriptor描述符、WAR包以及目录。所以部署时也要根据不同的类型做不同的处理。

    Descriptor描述符略过,感兴趣的可以自行搜索。


    WAR包类型如下:
    WAR包类型的部署是直接读取%CATALINA_HOME%/webapps目录下所有以war包形式打包的Web项目,然后根据war包的内容生成Tomcat内部需要的各种对象。
    为了优化多个应用项目部署时间,使用了线程池和Future机制。


    目录类型如下:
    目录类型的部署是直接读取%CATALINA_HOME%/webapps目录下所有目录形式的Web项目。
    优化同上。

    5.2 代码跟踪

    -> org.apache.catalina.core.StandardHost#startInternal,调用父类
    -> org.apache.catalina.core.ContainerBase#startInternal 调用setState
    -> org.apache.catalina.util.LifecycleBase#setStateInternal 调用fireLifecycleEvent方法,执行监听任务

      protected void fireLifecycleEvent(String type, Object data) {
            LifecycleEvent event = new LifecycleEvent(this, type, data);
            for (LifecycleListener listener : lifecycleListeners) {
                listener.lifecycleEvent(event);
            }
        }
    

    -> org.apache.catalina.startup.HostConfig#lifecycleEvent

       @Override
        public void lifecycleEvent(LifecycleEvent event) {
    
            // Identify the host we are associated with
            try {
                host = (Host) event.getLifecycle();
                if (host instanceof StandardHost) {
                    setCopyXML(((StandardHost) host).isCopyXML());
                    setDeployXML(((StandardHost) host).isDeployXML());
                    setUnpackWARs(((StandardHost) host).isUnpackWARs());
                    setContextClass(((StandardHost) host).getContextClass());
                }
            } catch (ClassCastException e) {
                log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
                return;
            }
    
            // Process the event that has occurred
            if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
                check();
            } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
                beforeStart();
            } else if (event.getType().equals(Lifecycle.START_EVENT)) {
                start();
            } else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
                stop();
            }
        }
    

    -> org.apache.catalina.startup.HostConfig#start
    在这里插入图片描述

      /**
       *为找到的任何目录或WAR文件部署应用程序
    	*在我们的“应用程序根目录”中。
         */
        protected void deployApps() {
    
            File appBase = host.getAppBaseFile();
            File configBase = host.getConfigBaseFile();
            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);
    
        }
    

    6. Context接口

    /**
    *  Context是一个表示servlet上下文的容器
    因此,一个独立的web应用程序,在Catalina servlet引擎中。
    因此,它在几乎所有的部署Catalina(即使a
    连接到web服务器(如Apache)的连接器使用web服务器的
    *识别处理此请求的适当包装器的功能。
    *它还提供了一种方便的机制来使用see的拦截器
    每个请求由这个特定的web应用程序处理。
     * <p>
    附加到上下文的父容器通常是宿主,但也可能是宿主
    *是一些其他的实现,或者可能被省略,如果没有必要。
     * <p>
    附加到上下文的子容器通常是实现
    包装器的*(表示单独的servlet定义)。
     * <p>
     */
    public interface Context extends Container, ContextBind {
    

    在这里插入图片描述
    下面以StandardContext为例。

    6.1 Context相关的配置文件

    • Tomcat的server.xml配置文件中的节点可用于配置Context,它直接在Tomcat解析server.xml时就完成Context对象的创建,而不用交给HostConfig监听器创建。
    • Web应用的/META-INF/context.xml文件可用于配置Context,此配置文件用于配置该Web应用对应的Context属性。
    • 用%CATALINA_HOME%/conf/[EngineName]/[HostName]/[WebName].xml文件声明创建一个Context。
    • Tomcat全局配置为conf/context.xml,此文件配置的属性会设置到所有Context中。
    • Tomcat的Host级别配置文件为/conf/[EngineName]/[HostName]/context.xml.default文件,它配置的属性会设置到某Host下面的所有Context中。

    6.2 StandardContext

    public class StandardContext extends ContainerBase
            implements Context, NotificationEmitter {
    

    其继承的ContainerBase的组件就不过多介绍。

    • Wrapper
      Context容器会包含若干个子容器,这些子容器就叫Wrapper容器。它属于Tomcat中最小级别的容器,它不能再包含其他子容器,而且它的父容器必须为Context容器。每个Wrapper其实就对应一个Servlet, Servlet的各种定义在Tomcat中就Wrapper的形式存在。
      /**
       *将要添加的LifecycleListeners的类名集
    	*通过createWrapper()来创建每个新创建的包装器。
         */
        private String wrapperLifecycles[] = new String[0];
    
    
    • ErrorPage
      每个Context容器都拥有各自的错误页面对象,它用于定义在Web容器处理过程中出现问题后向客户端展示错误信息的页面,这也是Servlet规范中规定的内容。它可以在Web部署描述文件中配置。
       private final ErrorPageSupport errorPageSupport = new ErrorPageSupport();
    
    • Manager
      Context容器的会话管理器用于管理对应Web容器的会话,维护会话的生成、更新和销毁。每个Context都会有自己的会话管理器,如果显式在配置文件中配置了会话管理器,则Context容器会使用该会话管理器;否则,Tomcat会分配默认的标准会话管理器(StandardManager)。
     /**
         * 与此容器关联的管理器实现。
         */
        protected Manager manager = null;
    
    • DirContext
      DirContext接口其实是属于JNDI的标准接口,实现此接口即可实现目录对象相关属性的操作。
      例如:通过“/META-INF/context.xml”获取到context.xml的文件。
    • JarScanner
       /**
       * Jar扫描器用于搜索可能包含的Jar
    	*配置信息,如tld或web-fragment.xml文件。
         */
        private JarScanner jarScanner = null;
    

    从JarScanner的名字上已经知道它的作用了,它一般包含在Context容器中,专门用于扫描Context对应的Web应用的Jar包。每个Web应用初始化时,在对TLD文件和web-fragment.xml文件处理时都需要对该Web应用下的Jar包进行扫描,因为Jar包可能包含这些配置文件,Web容器需要对它们进行处理。
    JarScanner在设计上采用了回调机制,每扫描到一个Jar包时都会调用回调对象进行处理。回调对象需要实现JarScannerCallback接口。

    • 过滤器
      过滤器提供了为某个Web应用的所有请求和响应做统一逻辑处理的功能。也就是我们意义上理解的过滤器。

    • NamingResource
      将配置文件中声明的不同的资源及其属性映射到内存中,这些映射统一由NamingResource对象封装
      命名资源的配置有两个地方,分别为Tomcat容器的server.xml文件和每个Web项目的context.xml文件。

    • Mapper
      Context容器包含了一个请求路由映射器(Mapper)组件,它属于局部路由映射器,它只能负责本Context容器内的路由导航。即每个Web应用包含若干Servlet,而当对请求使用请求分发器RequestDispatcher以分发到不同的Servlet上处理时,就用了此映射器。

    • WebappLoader(重点部分)
      每个Web应用都有各自的Class类和Jar包。一般来说,在Tomcat启动时要准备好相应的类加载器,包括加载策略及Class文件的查找,方便后面对Web应用实例化Servlet对象时通过类加载器加载相关类。因为每个Web应用不仅要达到资源的互相隔离,还要能支持重加载,所以这里需要为每个Web应用安排不同的类加载器对象加载,重加载时可直接将旧的类加载器对象丢弃而使用新的。
      每个Web应用对应一个WebappLoader,每个WebappLoader互相隔离,各自包含的类互相不可见。


    • ApplicationContext
    	 /**
         * 与此上下文关联的ServletContext实现。
         */
        protected ApplicationContext context = null;
    

    在Servlet的规范中规定了一个ServletContext接口,它提供了Web应用所有Servlet的视图,通过它可以对某个Web应用的各种资源和功能进行访问。
    首先来看ServletContext,包属于JDK的包:

    package javax.servlet;
    

    在这里插入图片描述
    等等方法。。。
    对于Tomcat容器,Context容器才是其运行时真正的环境。为了满足Servlet规范,它必须要包含一个ServletContext接口的实现,这个实现就是ApplicationContext。ApplicationContext是ServletContext的标准实现,用它表示某个Web应用的运行环境,每个Tomcat的Context容器都会包含一个ApplicationContext。

    public class ApplicationContext implements ServletContext {
    

    ApplicationContext对ServletContext接口的所有方法都进行了实现,所以Web开发人员可以在Servlet中通过getServletContext()方法获得该上下文,进而再对上下文进行操作或获取上下文中的各种资源。但实际上getServletContext()获取到的并非ApplicationContext对象,而是一个ApplicationContext的门面对象ApplicationContextFacade。
    门面模式的作用就是提供一个类似代理的访问模式,把ApplicationContext里面不该暴露的方法和属性屏蔽掉,不让Web开发人员访问。

    ApplicationContext就是为了满足Servlet标准的ServletContext接口而实现的一个类,它按Servlet的规范要求提供了各种实现方法。


    • InstanceManager
      Context容器中包含了一个实例管理器,它主要的作用就是实现对Context容器中监听器、过滤器以及Servlet等实例的管理。其中包括根据监听器Class对其进行实例化,对它们的Class的注解进行解析并处理,对它们的Class实例化的访问权限的限制,销毁前统一调用preDestroy方法等。

    • ServletContainerInitializer
      在Web容器启动时为让第三方组件机做一些初始化工作,例如注册Servlet或者Filters等,Servlet规范中通过ServletContainerInitializer实现此功能。

    /**
     *方法中的一个条目注册了ServletContainerInitializers (SCIs)
     * 文件meta - inf / services / javax.servlet。ServletContainerInitializer必须是包含在包含SCI实现的JAR文件中。
     * <p>
     * SCI处理的执行与元数据完成的设置无关。
    * SCI处理可以通过片段排序来控制每个JAR文件。如果
    *定义了绝对顺序,然后只将jar包含在顺序中
    *将为科学资讯系统处理。若要完全禁用SCI处理,请使用空
    *可以定义绝对顺序。
     * <p>
     *SCIs注册对注释(类、方法或字段)和/或感兴趣
    *通过{@link javax.servlet.annotation类型。HandlesTypes}注释,被添加到类中。
     */
    public interface ServletContainerInitializer {
    
        /**
         * Receives notification during startup of a web application of the classes
         * within the web application that matched the criteria defined via the
         * {@link javax.servlet.annotation.HandlesTypes} annotation.
         *
         * @param c     The (possibly null) set of classes that met the specified
         *              criteria
         * @param ctx   The ServletContext of the web application in which the
         *              classes were discovered
         *
         * @throws ServletException If an error occurs
         */
        void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
    }
    

    每个框架要使用ServletContainerInitializer就必须在对应的Jar包的META-INF/services目录中创建一个名为javax.servlet.ServletContainerInitializer的文件。文件内容指定具体的ServletContainerInitializer实现类,于是,当Web容器启动时,就会运行这个初始化器做一些组件内的初始化工作。

    Tomcat容器的ServletContainerInitializer机制,主要交由Context容器和ContextConfig监听器共同实现。ContextConfig监听器首先负责在容器启动时读取每个Web应用的WEB-INF/lib目录下包含的Jar包的META-INF/services/javax.servlet.ServletContainerInitializer,以及Web根目录下的META-INF/services/javax.servlet.ServletContainerInitializer,通过反射完成这些Servlet ContainerInitializer的实例化,然后再设置到Context容器中。最后,Context容器启动时就会分别调用每个ServletContainerInitializer的onStartup方法,并将感兴趣的类作为参数传入。

    • Context容器的监听器
      Tomcat启动过程中一般默认会在Context容器中添加4个监听器,分别为ContextConfig、TldConfig、NamingContextListener以及MemoryLeakTrackingListener。
      ContextConfig监听器主要负责在适当的阶段对Web项目的配置文件进行相关处理;TldConfig监听器主要负责对TLD标签配置文件的相关处理;NamingContextListener监听器主要负责对命名资源的创建、组织、绑定等相关的处理工作,使之符合JNDI标准;MemoryLeakTrackingListener监听器主要用于跟踪重加载可能导致内存泄漏的相关处理。

    在这里插入图片描述

    6.3 Tomcat的类加载机制

    贴一篇写得很不错的博客
    org.apache.catalina.core.StandardContext#startInternal

    
            if (getLoader() == null) {
                WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
                webappLoader.setDelegate(getDelegate());
                setLoader(webappLoader);
            }
    

    WebappLoader的核心工作其实交给其内部的WebappClassLoader,它才是真正完成类加载工作的加载器,它是一个自定义的类加载器。
    那么我们进入WebappLoader看看:

      /**
         * 由此加载器组件管理的类加载器。
         */
        private WebappClassLoaderBase classLoader = null;
    
     /**
         * 创建相关的类加载器。
         */
        private WebappClassLoaderBase createClassLoader()
            throws Exception {
    
            Class<?> clazz = Class.forName(loaderClass);
            WebappClassLoaderBase classLoader = null;
    
            if (parentClassLoader == null) {
                parentClassLoader = context.getParentClassLoader();
            }
            Class<?>[] argTypes = { ClassLoader.class };
            Object[] args = { parentClassLoader };
            Constructor<?> constr = clazz.getConstructor(argTypes);
            classLoader = (WebappClassLoaderBase) constr.newInstance(args);
    
            return classLoader;
        }
    
    public class WebappClassLoader extends WebappClassLoaderBase {
    public abstract class WebappClassLoaderBase extends URLClassLoader {
    

    WebappClassLoader间接继承了URLClassLoader,只需要把/WEB-INF/lib和/WEB-INF/classes目录下的类和Jar包以URL形式添加到URLClassLoader中即可,后面就可以用该类加载器对类进行加载。

    WebappClassLoader并没有遵循双亲委派机制,而是按自己的策略顺序加载类。根据委托标识,加载分为两种方式。
    org.apache.catalina.loader.WebappClassLoaderBase#getResources(java.lang.String)
    org.apache.catalina.loader.WebappClassLoaderBase#loadClass(java.lang.String, boolean)

        boolean delegateFirst = delegate || filter(name, false);
    
    • 当委托标识delegate为false时,WebappClassLoader类加载器首先先尝试从本地缓存中查找加载该类,然后用System类加载器尝试加载类,接着由自己尝试加载类,最后才由父类加载(Common)器尝试加载。所以此时它搜索的目录顺序是:
      <JAVA_HOME>/jre/lib→<JAVA_HOME>/jre/lib/ext→CLASSPATH→/WEB-INF/classes→/WEB-INF/lib→$CATALINA_BASE/lib和$CATALINA_HOME/lib。
    • 当委托标识delegate为true时,WebappClassLoader类加载器首先先尝试从本地缓存中查找加载该类,然后用System类加载器尝试加载类,接着由父类加载器(Common)尝试加载类,最后才由自己尝试加载。所以此时它的搜索的目录顺序是<JAVA_HOME>/jre/lib→<JAVA_HOME>/jre/lib/ext→CLASSPATH→$CATALINA_BASE/lib和$CATALINA_HOME/lib→/WEB-INF/classes→/WEB-INF/lib。

    在这里插入图片描述
    对于公共资源可以共享,而属于Web应用的资源则通过类加载器进行了隔离。对于重加载的实现,也比较清晰,只需要重新实例化一个WebappClassLoader对象并把原来WebappLoader中旧的置换掉即可完成重加载功能,置换掉的将被GC回收。

    6.4 内存泄漏

    因为Tomcat提供了不必重启容器而只须重启Web应用以达到热部署的功能,其实现是通过定义一个WebappClassLoader类加载器,当热部署时,就将原来的类加载器废弃并重新实例化一个WebappClassLoader类加载器。但这种方式可能存在内存泄漏问题,因为类加载器是一个结构复杂的对象,导致它不能被GC回收的可能性比较多。除了对类加载器对象有引用可能导致其无法回收之外,对其加载的元数据(方法、类、字段等)有引用也可能会导致它无法被GC回收。

    Tomcat的类加载器之间有父子关系。这里只看启动类加载器Bootstrap ClassLoader和Web应用类加载器WebappClassLoader。在JVM中,BootstrapClassLoader负责加载rt.jar包的java.sql.DriverManager,而WebappClassLoader负责加载Web应用中的Mysql驱动包。其中有一个很重要的步骤是Mysql的驱动类需要注册到DriverManager中,即DriverManager.registerDriver(new Driver()),它由Mysql驱动包自动完成。这样一来,当Web应用进行热部署操作时,如果没有将Mysql的Driver从DriverManager中反注册掉,则会导致整个WebappClassLoader无法回收,造成内存泄漏。
    在这里插入图片描述
    Tomcat如何对此内存泄漏进行监控,要判断WebappClassLoader会不会导致内存泄漏,只须判断WebappClassLoader有没有被GC回收即可。在Java中有一种引用叫弱引用,它能很好地判断WebappClassLoader有没有被GC回收,被弱引用关联的对象只能生存到下一次垃圾回收发生之前。即如果某WebappClassLoader对象只被某弱引用关联,则它会在下次垃圾回收时被回收,但如果WebappClassLoader对象除了被弱引用关联外还被其他对象强引用,那么WebappClassLoader对象是不会被回收的,根据这些条件就可以判断是否有WebappClassLoader发生内存泄漏了。

    Tomcat的实现是通过WeakHashMap来实现弱引用的,只须将WebappClassLoader对象放到WeakHashMap中,例如weakMap.put(“Loader1”, WebappClassLoader)。当WebappClassLoader及其包含的元素没有被其他任何类加载器中的元素引用到时,JVM发生垃圾回收时则会把WebappClassLoader对象回收。

    使用一个WeakHashMap跟踪WebappClassLoader,在查找内存泄漏之前会先强制调用System.gc()进行一次垃圾回收,保证没问题的WebappClassLoader都被回收。这时如果还有WebappClassLoader的状态是非Started(正常启动的都为Started,关闭的则为非Started)的,则它是未被垃圾回收的WebappClassLoader,发生了内存泄漏。

    在Tomcat中每个Host容器都会对应若干个应用。为了跟踪这些应用是否有内存泄漏,需要将对应Context容器注册到Host容器中的WeakHashMap中,而这里讨论的监听器MemoryLeakTrackingListener就负责Context对应的WebappClassLoader的注册工作。

    7. Wrapper接口

    /**
     * A <b>Wrapper</b> 容器是否表示一个单独的servlet
     * 来自web应用程序部署描述符的定义。它
    *提供了一种方便的机制来使用拦截器来查看每一个
    *请求此定义所表示的servlet。
     * <p>
     *包装器的实现负责管理servlet的生命周期
    *循环,包括调用init()和
    *在适当的时候破坏(),以及尊重的存在
    servlet类本身的SingleThreadModel声明。
     * <p>
    *附加到包装器的父容器通常是
    实现上下文,表示servlet上下文(和
    (因此是web应用程序),servlet在其中执行。
     * <p>
    *子容器不允许在包装器实现,所以
    * addChild()方法应该抛出一个
    * <代码> IllegalArgumentException > < /代码。
     */
    public interface Wrapper extends Container {
    

    Servlet的概念对于我们来说非常熟悉,我们会在它的doGet和doPost等方法上编写逻辑处理代码,而Wrapper则负责调用这些方法的逻辑。一般来说,一个Wrapper对应一个Servlet对象,也就是说,所有处理线程都共用同一个Servlet对象。

    7.1 Servlet机制

    规范提供了一个Servlet接口,接口中包含的重要方法是init、service、destroy等方法。Servlet在初始化时要调用init方法,在销毁时要调用destroy方法,而对客户端请求处理时则调用service方法。对于这些机制,都必须由Tomcat在内部提供支持,具体则由Wrapper容器提供支持。

    当客户端请求到达服务端后,请求被抽象成Request对象后向4个容器进行传递,首先经过Engine容器的管道通过若干阀门,最后通过StandardEngineValve阀门流转到Host容器的管道,处理后继续往下流转,通过StandardHostValve阀门流转到Context容器的管道,继续往下流转,通过StandardContextValve阀门流转到Wrapper容器的管道,而对Servlet的核心处理也正是在StandardWrapperValve阀门中。
    StandardWrapperValve阀门先由Application FilterChain组件执行过滤器,然后调用Servlet的service方法对请求进行处理,然后对客户端响应。

    Servlet工作机制的大致流程是:Request→StandardEngineValve→StandardHostValve→StandardContextValve→StandardWrapperValve→实例化并初始化Servlet对象→由过滤器链执行过滤操作→调用该Servlet对象的service方法→Response。

    7.2 Servlet对象池

    Servlet在不实现SingleThreadModel的情况下以单个实例模式运行,这种情况下,Wrapper容器只会通过反射实例化一个Servlet对象,对应此Servlet的所有客户端请求都会共用此Servlet对象。
    那么:在某个Servlet中使用成员变量累加去统计访问次数,这就存在线程安全问题。

    为了支持一个Servlet对象对应一个线程,Servlet规范提出了一个SingleThreadModel接口, Tomcat容器必须要完成的机制是:如果某个Servlet类实现了SingleThreadModel接口,则要保证一个线程独占一个Servlet对象。假如线程1正在使用Servlet1对象,则线程2不能再使用Servlet1对象,只能用Servlet2对象。

    针对SingleThreadModel模式,Tomcat的Wrapper容器使用了对象池策略,Wrapper容器会有一个Servlet堆,负责保存若干个Servlet对象,当需要Servlet对象时从堆中pop出一个对象,而当用完后则push回堆中。当然获取Servlet的个数有一个上限支持,超过这个上限就只能阻塞等待。

    7.3 过滤器链

    请求通过管道流转到Wrapper容器的管道,经过若干阀门后到达基础阀门StandardWrapperValve,它将创建一个过滤器链Application FilterChain对象。
    在这里插入图片描述

    • 从Context容器中获取所有过滤器的相关信息。
    • 通过URL匹配过滤器,匹配的加入到过滤器链中。
    • 通过Servlet名称匹配过滤器,匹配的加入到过滤器链中。

    7.4 Servlet种类

    根据请求资源的不同种类,可以把Servlet分成三种类别,比如请求可能访问一个普通的Servlet,也可能访问一个JSP页面,也可能访问一个静态资源。

    Servlet路径的匹配规则如下:
    首先,尝试使用精确匹配法匹配精确类型Servlet的路径。然后,尝试使用前缀匹配通配符类型Servlet。接着,尝试使用扩展名匹配通配符类型Servlet。最后,匹配默认Servlet。

    着重讲一下org.apache.catalina.core.StandardWrapper#allocate:
    可以通过该方法获取到相应的Servlet。

        /**
    	*分配一个已初始化的Servlet实例
    	*其service()方法调用。如果servlet类这样做
    	*未实现SingleThreadModel,将(only)初始化
    	实例可能会立即返回。如果servlet类实现了
    	* SingleThreadModel,包装器实现必须保证
    	*这个实例不会再次分配,直到它被a释放
    	*调用deallocate()。
         *
         * @exception 如果servlet init()方法被抛出,则使用ServletException
         * @exception 如果加载错误发生,ServletException
         */
        @Override
        public Servlet allocate() throws ServletException {
    
            //如果我们当前正在卸载这个servlet,抛出一个异常
            if (unloading) {
                throw new ServletException(sm.getString("standardWrapper.unloading", getName()));
            }
    
            boolean newInstance = false;
    
            //如果不是SingleThreadedModel,则每次返回相同的实例
            if (!singleThreadModel) {
                // 如果需要,加载并初始化我们的实例
                if (instance == null || !instanceInitialized) {
                //做一个线程安全锁
                    synchronized (this) {
                        if (instance == null) {
                            try {
                                if (log.isDebugEnabled()) {
                                    log.debug("Allocating non-STM instance");
                                }
    
                                // 注意:我们不知道Servlet是否实现了
                                // 单线程模型,直到我们加载它。
                                instance = loadServlet();
                                newInstance = true;
                                if (!singleThreadModel) {
                                    // 对于非stm,在这里递增以防止竞争
                                    // 条件与卸载。Bug 43683,测试用例
                                    // #3
                                    countAllocated.incrementAndGet();
                                }
                            } catch (ServletException e) {
                                throw e;
                            } catch (Throwable e) {
                                ExceptionUtils.handleThrowable(e);
                                throw new ServletException(sm.getString("standardWrapper.allocate"), e);
                            }
                        }
                        if (!instanceInitialized) {
                            initServlet(instance);
                        }
                    }
                }
    
                if (singleThreadModel) {
                    if (newInstance) {
                        //必须在上面的同步之外做这个来防止a
    					//可能死锁
                        synchronized (instancePool) {
                            instancePool.push(instance);
                            nInstances++;
                        }
                    }
                } else {
                    if (log.isTraceEnabled()) {
                        log.trace("  Returning non-STM instance");
                    }
                    // 对于新实例,计数将在创建时增加
                    
                    if (!newInstance) {
                        countAllocated.incrementAndGet();
                    }
                    return instance;
                }
            }
    
            synchronized (instancePool) {
                while (countAllocated.get() >= nInstances) {
                    // 如果可能,分配一个新实例,否则等待
                    if (nInstances < maxInstances) {
                        try {
                            instancePool.push(loadServlet());
                            nInstances++;
                        } catch (ServletException e) {
                            throw e;
                        } catch (Throwable e) {
                            ExceptionUtils.handleThrowable(e);
                            throw new ServletException(sm.getString("standardWrapper.allocate"), e);
                        }
                    } else {
                        try {
                            instancePool.wait();
                        } catch (InterruptedException e) {
                            // Ignore
                        }
                    }
                }
                if (log.isTraceEnabled()) {
                    log.trace("  Returning allocated STM instance");
                }
                countAllocated.incrementAndGet();
                return instancePool.pop();
            }
        }
    
    
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值