Tomcat笔记②——生命周期、初始化和启动流程

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_35262405/article/details/101765982

生命周期

在这里插入图片描述
所有的组件都是通过实现上面的接口来完成自己的生命周期的活动

这里这种说明一下Lifecycle的init方法,在下面初始化和启动流程中也很重要,而且和一般的方法调用有所区别。

我们都知道Lifecycle只是一个接口,自然有其的实现类,所以我们需要关注一下LifecycleBase这个抽象类,它实现了init方法,而大多数组件调用的init方法也就是这个方法,我们知道有许多组件都需要通过这个方法完成初始化,而且各个组件初始化的步骤肯定不同,所以这里已经写死了肯定是不行的。

所以这里提供了一个initInternal,它会在init方法中调用,是一个预留方法,需要子类实现,所以其他组件只要实现这个方法就可以完成各自的初始化流程了。
在这里插入图片描述
在这里插入图片描述
同init一样还有一个start方法,真正的逻辑也是在startInternal方法中的,就不再分析了

初始化阶段

整体流程

Bootstrap就是tomcat的入口,启动的入口就是Bootstrap的main方法,但是首先会执行static语句块,下面就是静态语句块,主要就是获取两个环境变量并赋值,这个环境变量是子啊JVM参数中设置的,主要就是告诉tomcat真正项目的是哪一个。

static {
    // Will always be non-null
    String userDir = System.getProperty("user.dir");

    // Home first
    String home = System.getProperty(Globals.CATALINA_HOME_PROP);
    File homeFile = null;

    /**
     * 下面先获取了两个环境变量
     * 并设置了catalinaHomeFile和catalinaBaseFile
     * 通过System.setProperty()方法获取的环境变量,就是子啊启动前设置的JVM参数
     * -Dcatalina.home=catalina-home
     * -Dcatalina.base=catalina-home
     * 在这里获取的
     */

    // 获取tomcat的安装目录
    if (home != null) {
        File f = new File(home);
        try {
            homeFile = f.getCanonicalFile();
        } catch (IOException ioe) {
            homeFile = f.getAbsoluteFile();
        }
    }

    if (homeFile == null) {
        // First fall-back. See if current directory is a bin directory
        // in a normal Tomcat install
        File bootstrapJar = new File(userDir, "bootstrap.jar");

        if (bootstrapJar.exists()) {
            File f = new File(userDir, "..");
            try {
                homeFile = f.getCanonicalFile();
            } catch (IOException ioe) {
                homeFile = f.getAbsoluteFile();
            }
        }
    }

    if (homeFile == null) {
        // Second fall-back. Use current directory
        File f = new File(userDir);
        try {
            homeFile = f.getCanonicalFile();
        } catch (IOException ioe) {
            homeFile = f.getAbsoluteFile();
        }
    }

    // 设置catalinaHomeFile,安装目录
    catalinaHomeFile = homeFile;
    System.setProperty(
            Globals.CATALINA_HOME_PROP, catalinaHomeFile.getPath());

    // Then base
    String base = System.getProperty(Globals.CATALINA_BASE_PROP);
    if (base == null) {
        // 设置catalinaBaseFile,工作目录
        catalinaBaseFile = catalinaHomeFile;
    } else {
        File baseFile = new File(base);
        try {
            baseFile = baseFile.getCanonicalFile();
        } catch (IOException ioe) {
            baseFile = baseFile.getAbsoluteFile();
        }
        catalinaBaseFile = baseFile;
    }
    System.setProperty(
            Globals.CATALINA_BASE_PROP, catalinaBaseFile.getPath());
}

接着解释main方法的执行,首先实例化了一个Bootstrap对象
在这里插入图片描述
接着执行Bootstrap的init方法
在这里插入图片描述
在这个init方法中主要是初始化了类加载器,加载Catalina类并创建实例对象,最后反射执行setParentClassLoader方法

注意这里的initClassLoaders方法,它主要是为了打破jdk传统的双亲委派机制,使得类的加载更加方便

private void initClassLoaders() {
    try {
        // 基本的类加载器,打破jdk原生的双亲委派机制
        commonLoader = createClassLoader("common", null);
        if( commonLoader == null ) {
            // no config file, default to this loader - we might be in a 'single' env.
            commonLoader=this.getClass().getClassLoader();
        }

        // 下面都是附属于commonLoader的类加载器
        // 其中catalinaLoader是标准的类加载器,负责一些通过的类的加载
        // sharedLoader属于每一个webapps单独的类加载器,这样就可以实现加载不同版本的类
        catalinaLoader = createClassLoader("server", commonLoader);
        sharedLoader = createClassLoader("shared", commonLoader);
    } catch (Throwable t) {
        handleThrowable(t);
        log.error("Class loader creation threw exception", t);
        System.exit(1);
    }
}

在这里插入图片描述
接着就执行load方法
在这里插入图片描述
这里是反射调用Catalina的load方法(无参)
在这里插入图片描述
接着会初始化Server,调用init方法,我们已经知道了真正的初始化逻辑其实在initInternal方法中
在这里插入图片描述
在方法中调用了每一个Service的init方法,初始化Service
在这里插入图片描述
在Service的初始化中完成了另外三个组件的初始化
在这里插入图片描述
在connector的init方法中还完成了对适配器和处理器的初始化
在这里插入图片描述
在这里插入图片描述
最后在protocolHandler中完成了对监听器的初始化,同时这也是最后一步初始化过程
在这里插入图片描述
注意Host、Context、Wrapper并没有在这个阶段初始化

初始化流程图

在这里插入图片描述

Catalina初始化

由前面的分析,可知Bootstrap中的load逻辑实际上是交给Catalina去处理的,下面我们对Catalina的初始化过程进行分析。

load(init)

load阶段主要是通过读取conf/server.xml或者server-embed.xml,实例化Server、Service、Connector、Engine、Host等组件,并调用Lifecycle#init()完成初始化动作,以及发出INITIALIZING、INITIALIZED事件。

  • 首先初始化jmx的环境变量
  • 定义解析server.xml的配置,告诉Digester哪个xml标签应该解析成什么类,如果我们要改变server.xml的某个属性值(比如优化tomcat线程池),直接查看对应实现类的setXXX方法即可
  • 解析conf/server.xml或者server-embed.xml,并且实例化对应的组件并且赋值操作,比如Server、Container、Connector等等
  • 为Server设置catalina信息,指定Catalina实例,设置catalina的home、base路径
  • 调用StarndServer#init()方法,完成各个组件的初始化,并且由parent组件初始化child组件,一层套一层,这个设计真心牛逼
public void load() {
    initDirs();
    // 初始化jmx的环境变量
    initNaming();
    // Create and execute our Digester
    // 定义解析server.xml的配置,告诉Digester哪个xml标签应该解析成什么类
    Digester digester = createStartDigester();
    InputSource inputSource = null;
    InputStream inputStream = null;
    File file = null;
    try {
      // 首先尝试加载conf/server.xml,省略部分代码......
      // 如果不存在conf/server.xml,则加载server-embed.xml(该xml在catalina.jar中),省略部分代码......
      // 如果还是加载不到xml,则直接return,省略部分代码......
      try {
          inputSource.setByteStream(inputStream);
          // 把Catalina作为一个顶级实例
          digester.push(this);
          // 解析过程会实例化各个组件,比如Server、Container、Connector等
          digester.parse(inputSource);
      } catch (SAXParseException spe) {
          // 处理异常......
      }
    } finally {
        // 关闭IO流......
    }
    // 给Server设置catalina信息
    getServer().setCatalina(this);
    getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
    getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
    // Stream redirection
    initStreams();
    // 调用Lifecycle的init阶段
    try {
        getServer().init();
    } catch (LifecycleException e) {
        // ......
    }
    // ......
}
  • Digester利用jdk提供的sax解析功能,将server.xml的配置解析成对应的Bean,并完成注入,比如往Server中注入Service
  • EngineConfig,它是一个LifecycleListener实现,用于配置Engine,但是只会处理START_EVENT和STOP_EVENT事件
  • Connector默认会有两种:HTTP/1.1、AJP,不同的Connector内部持有不同的CoyoteAdapter和ProtocolHandler,在Connector初始化的时候,也会对ProtocolHandler进行初始化,完成端口的监听
  • ProtocolHandler常用的实现有Http11NioProtocol、AjpNioProtocol,还有apr系列的Http11AprProtocol、AjpAprProtocol,apr系列只有在使用apr包的时候才会使用到
  • 在ProtocolHandler调用init初始化的时候,还会去执行AbstractEndpoint的init方法,完成请求端口绑定、初始化NIO等操作,在tomcat7中使用JIoEndpoint阻塞IO,而tomcat8中直接移除了JIoEndpoint,具体信息请查看org.apache.tomcat.util.net这个包

Catalina在load结束之前,会调用Server的init()完成各个组件的初始化,下面我们来分析下各个组件在init初始化过程中都做了哪些操作。

StandardServer是由Catalina进行init初始化的,调用的是LifecycleBase父类的init方法,而StandardServer继承至LifecycleMBeanBase,重写了initInternal方法。关于这块的知识,请参考上面关于生命周期的描述。

StandardServer初始化的时序图如下所示,为了表述清楚,我这里把LifecycleBase、LifecycleMBeanBase拆开了,实际上是同一个StandardServer实例对象,存在继承关系 。
在这里插入图片描述
由上图可以很清晰地看到,StandardServer的初始化过程,先由父类LifecycleBase改变当前的state值并发出事件通知,那么这个时候StandardServer的子容器StandardService内部的state是否会发生改变呢,是否会发出事件通知呢? 当然是不会的,因为这个state值不是LifecycleBase的静态成员变量,StandardServer只能改变自己的值,而StandardService只有在被StandardServer调用init初始化的时候才会改变,二者拥有独立的状态。考虑到有其它线程可能会改变StandardServer的state值,比如利用jmx执行init操作,因此要考虑并发问题,所以LifecycleBase#init()使用了synchronized锁,并且state是volatile修饰的。

LifecycleBase改变state、发出事件通知之后,便会执行StandardServer自身的initInternal,我们来看看这个里面都干嘛了。

protected void initInternal() throws LifecycleException {
    super.initInternal();
    // 往jmx中注册全局的String cache,尽管这个cache是全局听,但是如果在同一个jvm中存在多个Server,
    // 那么则会注册多个不同名字的StringCache,这种情况在内嵌的tomcat中可能会出现
    onameStringCache = register(new StringCache(), "type=StringCache");

    // 注册MBeanFactory,用来管理Server
    MBeanFactory factory = new MBeanFactory();
    factory.setContainer(this);
    onameMBeanFactory = register(factory, "type=MBeanFactory");

    // 往jmx中注册全局的NamingResources
    globalNamingResources.init();

    // Populate the extension validator with JARs from common and shared class loaders
    if (getCatalina() != null) {
        // 忽略ClassLoader操作
    }

    // 初始化内部的Service
    for (int i = 0; i < services.length; i++) {
        services[i].init();
    }
}
  • 先是调用super.initInternal(),把自己注册到jmx
  • 然后注册StringCache和MBeanFactory
  • 初始化NamingResources,就是server.xml中指定的GlobalNamingResources
  • 调用Service子容器的init方法,让Service组件完成初始化,注意:在同一个Server下面,可能存在多个Service组件

Service初始化

StandardService和StandardServer都是继承至LifecycleMBeanBase,因此公共的初始化逻辑都是一样的,这里不做过多介绍,我们直接看下initInternal。

protected void initInternal() throws LifecycleException {

    // 往jmx中注册自己
    super.initInternal();

    // 初始化Engine
    if (engine != null) {
        engine.init();
    }

    // 存在Executor线程池,则进行初始化,默认是没有的
    for (Executor executor : findExecutors()) {
        if (executor instanceof JmxEnabled) {
            ((JmxEnabled) executor).setDomain(getDomain());
        }
        executor.init();
    }

    // 暂时不知道这个MapperListener的作用
    mapperListener.init();

    // 初始化Connector,而Connector又会对ProtocolHandler进行初始化,开启应用端口的监听
    synchronized (connectorsLock) {
        for (Connector connector : connectors) {
            try {
                connector.init();
            } catch (Exception e) {
                // 省略部分代码,logger and throw exception
            }
        }
    }
}
  • 首先,往jmx中注册StandardService
  • 初始化Engine,而Engine初始化过程中会去初始化Realm(权限相关的组件)
  • 如果存在Executor线程池,还会进行init操作,这个Excecutor是tomcat的接口,继承至java.util.concurrent.Executor、org.apache.catalina.Lifecycle
  • 初始化Connector连接器,默认有http1.1、ajp连接器,而这个Connector初始化过程,又会对ProtocolHandler进行初始化,开启应用端口的监听,后面会详细分析

Engine初始化

StandardEngine在init阶段,需要获取Realm,这个Realm是干嘛用的?

Realm(域)是用于对单个用户进行身份验证的底层安全领域的只读外观,并标识与这些用户相关联的安全角色。
域可以在任何容器级别上附加,但是通常只附加到Context,或者更高级别的容器。

StandardEngine初始化的代码如下:

@Override
protected void initInternal() throws LifecycleException {
    getRealm();
    super.initInternal();
}

public Realm getRealm() {
    Realm configured = super.getRealm();
    if (configured == null) {
        configured = new NullRealm();
        this.setRealm(configured);
    }
    return configured;
}

StandardEngine继承至ContainerBase,而ContainerBase重写了initInternal()方法,用于初始化start、stop线程池,默认是1个线程。

private int startStopThreads = 1;
protected ThreadPoolExecutor startStopExecutor;

@Override
protected void initInternal() throws LifecycleException {
    BlockingQueue<Runnable> startStopQueue = new LinkedBlockingQueue<>();
    startStopExecutor = new ThreadPoolExecutor(
            getStartStopThreadsInternal(),
            getStartStopThreadsInternal(), 10, TimeUnit.SECONDS,
            startStopQueue,
            new StartStopThreadFactory(getName() + "-startStop-"));
    // 允许core线程超时未获取任务时退出
    startStopExecutor.allowCoreThreadTimeOut(true);
    super.initInternal();
}

private int getStartStopThreadsInternal() {
    int result = getStartStopThreads();

    if (result > 0) {
        return result;
    }
    result = Runtime.getRuntime().availableProcessors() + result;
    if (result < 1) {
        result = 1;
    }
    return result;
}

这个startStopExecutor线程池有什么用呢?

在start的时候,如果发现有子容器,则会把子容器的start操作放在线程池中进行处理
在stop的时候,也会把stop操作放在线程池中处理

在前面的文章中我们介绍了Container组件,StandardEngine作为顶层容器,它的直接子容器是StardandHost,但是对StandardEngine的代码分析,我们并没有发现它会对子容器StardandHost进行初始化操作,StandardEngine不按照套路出牌,而是把初始化过程放在start阶段。个人认为Host、Context、Wrapper这些容器和具体的webapp应用相关联了,初始化过程会更加耗时,因此在start阶段用多线程完成初始化以及start生命周期,否则,像顶层的Server、Service等组件需要等待Host、Context、Wrapper完成初始化才能结束初始化流程,整个初始化过程是具有传递性的。

Connector初始化

Connector也是继承至LifecycleMBeanBase,公共的初始化逻辑都是一样的。我们先来看下Connector的默认配置,大部分属性配置都可以在Connector类中找到,tomcat默认开启了HTTP/1.1、AJP/1.3,其实AJP的用处不大,可以去掉。(在Server.xml中)

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

Connector定义了很多属性,比如port、redirectPort、maxCookieCount、maxPostSize等等,比较有意思的是竟然找不到connectionTimeout的定义,全文搜索后发现使用了属性名映射,估计是为了兼容以前的版本(org.apache.catalina.connector.Connector)。

protected void initInternal() throws LifecycleException {
    // 注册jmx
    super.initInternal();
    // 初始化Coyote适配器,这个适配器是用于Coyote的Request、Response与HttpServlet的Request、Response适配的
    adapter = new CoyoteAdapter(this);
    // protocolHandler需要指定Adapter用于处理请求
    protocolHandler.setAdapter(adapter);
    // Make sure parseBodyMethodsSet has a default
    if (null == parseBodyMethodsSet) {
        setParseBodyMethods(getParseBodyMethods());
    }
    // apr支持,忽略部分代码......
    // 初始化ProtocolHandler,这个init不是Lifecycle定义的init,而是ProtocolHandler接口的init
    try {
        protocolHandler.init();
    } catch (Exception e) {
        throw new LifecycleException(
                sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
    }
}

initInternal过程如下所示:

  • 实例化Coyote适配器,这个适配器是用于Coyote的Request、Response与HttpServlet的Request、Response适配的,后续的博客会进行深入分析
  • 为ProtocolHander指定CoyoteAdapter用于处理请求
  • 初始化ProtocolHander,

ProtocolHandler初始化

首先,我们来认识下ProtocolHandler,它是一个抽象的协议实现,它不同于JNI这样的Jk协议,它是单线程、基于流的协议。ProtocolHandler是一个Cycote连接器实现的主要接口,而Adapter适配器是由一个Coyote Servlet容器实现的主要接口,定义了处理请求的抽象接口。

ProtocolHandler的子类如下所示,AbstractProtocol(org.apache.coyote)是基本的实现,而NIO默认使用的是Http11NioProtocol。
在这里插入图片描述
调用ProtocolHandler的init进行初始化是调用的AbstractProtocol,首先完成jmx的注册,然后对NioEndpoint进行初始化。

public abstract class AbstractProtocol<S> implements ProtocolHandler,
        MBeanRegistration {
    public void init() throws Exception {
        // 完成jmx注册
        if (oname == null) {
            oname = createObjectName();
            if (oname != null) {
                Registry.getRegistry(null, null).registerComponent(this, oname, null);
            }
        }
        if (this.domain != null) {
            rgOname = new ObjectName(domain + ":type=GlobalRequestProcessor,name=" + getName());
            Registry.getRegistry(null, null).registerComponent(
                    getHandler().getGlobal(), rgOname, null);
        }

        String endpointName = getName();
        endpoint.setName(endpointName.substring(1, endpointName.length()-1));
        endpoint.setDomain(domain);

        // 初始化endpoint
        endpoint.init();
    }
}

NioEndpoint初始化过程,最重要的是完成端口和地址的绑定监听工作(org.apache.tomcat.util.net.NioEndpoint)

public class NioEndpoint extends AbstractJsseEndpoint<NioChannel> {
    public void bind() throws Exception {
    // 实例化ServerSocketChannel,并且绑定端口和地址
        serverSock = ServerSocketChannel.open();
        socketProperties.setProperties(serverSock.socket());
        InetSocketAddress addr = (getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort()));
        // 设置最大连接数,原来是在这里设置的
        serverSock.socket().bind(addr,getAcceptCount());
        serverSock.configureBlocking(true); //mimic APR behavior
        // 初始化acceptor、poller线程的数量
        // Initialize thread count defaults for acceptor, poller
        if (acceptorThreadCount == 0) {
            // FIXME: Doesn't seem to work that well with multiple accept threads
            acceptorThreadCount = 1;
        }
        if (pollerThreadCount <= 0) {
            pollerThreadCount = 1;
        }
        setStopLatch(new CountDownLatch(pollerThreadCount));
        // 如果有必要的话初始化ssl
        initialiseSsl();
        // 初始化selector
        selectorPool.open();
    }
}

至此,整个初始化过程便告一段落。整个初始化过程,由parent组件控制child组件的初始化,一层层往下传递,直到最后全部初始化OK。下图描述了整体的传递流程
在这里插入图片描述
默认情况下,Server只有一个Service组件,Service组件先后对Engine、Connector进行初始化。而Engine组件并不会在初始化阶段对子容器进行初始化,Host、Context、Wrapper容器的初始化是在start阶段完成的。tomcat默认会启用HTTP1.1和AJP的Connector连接器,这两种协议默认使用Http11NioProtocol、AJPNioProtocol进行处理。

启动阶段

整体流程

初始化完成后就开始启动tomcat,同样也是从Bootstrap的main方法中开始的
在这里插入图片描述
接着反射调用Catalina的start方法
在这里插入图片描述
接着启动Server
在这里插入图片描述
启动Service
在这里插入图片描述
Service接着启动Engine、线程和Connector
在这里插入图片描述
在StandardEngine中通过父类的startInternal方法启动子组件StandardHost,同时激活监听器HostConfig
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
StandardHost同样是上面所示启动了它的子组件StandardContext
在这里插入图片描述
在StandardContext还激活了CONFIGURE_START_EVENT事件,触发了ContextConfig去处理此事件
在这里插入图片描述
在StandardContext中继续启动子组件wrapper,这也是最小的Contanier组件了
在这里插入图片描述
在Connector中启动ProtocolHandler
在这里插入图片描述
启动监视器
在这里插入图片描述

启动流程图

在这里插入图片描述

Catalina启动

在这里插入图片描述

  1. Bootstrap

启动过程和初始化一样,由Bootstrap反射调用Catalina的start方法

public void start()
    throws Exception {
    if( catalinaDaemon==null ) init();
    Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null);
    method.invoke(catalinaDaemon, (Object [])null);
}
  1. Catalina

主要分为以下三个步骤,其核心逻辑在于Server组件:

  • 调用Server的start方法,启动Server组件
  • 注册jvm关闭的勾子程序,用于安全地关闭Server组件,以及其它组件
  • 开启shutdown端口的监听并阻塞,用于监听关闭指令
public void start() {
    // 省略若干代码......
    // Start the new server
    try {
        getServer().start();
    } catch (LifecycleException e) {
        // 省略......
        return;
    }
    // 注册勾子,用于安全关闭tomcat
    if (useShutdownHook) {
        if (shutdownHook == null) {
            shutdownHook = new CatalinaShutdownHook();
        }
        Runtime.getRuntime().addShutdownHook(shutdownHook);
    }
    // Bootstrap中会设置await为true,其目的在于让tomcat在shutdown端口阻塞监听关闭命令
    if (await) {
        await();
        stop();
    }
}

StandardServer启动

StandardServer中的startInternal方法如下

protected void startInternal() throws LifecycleException {

    fireLifecycleEvent(CONFIGURE_START_EVENT, null);
    setState(LifecycleState.STARTING);

    globalNamingResources.start();

    // Start our defined Services
    synchronized (servicesLock) {
        for (int i = 0; i < services.length; i++) {
            services[i].start();
        }
    }
}

先是由LifecycleBase统一发出STARTING_PREP事件,StandardServer额外还会发出CONFIGURE_START_EVENT、STARTING事件,用于通知LifecycleListener在启动前做一些准备工作,比如NamingContextListener会处理CONFIGURE_START_EVENT事件,实例化tomcat相关的上下文,以及ContextResource资源。

然后,启动内部的NamingResourcesImpl实例,这个类封装了各种各样的数据,比如ContextEnvironment、ContextResource、Container等等,它用于Resource资源的初始化,以及为webapp应用提供相关的数据资源,比如 JNDI 数据源(对应ContextResource)。

接着,启动Service组件,这一块的逻辑将在下面进行详细分析,最后由LifecycleBase发出STARTED事件,完成start。

StandardService启动

StandardService的start代码如下所示

protected void startInternal() throws LifecycleException {
    setState(LifecycleState.STARTING);
    // 启动Engine
    if (engine != null) {
        synchronized (engine) {
            engine.start();
        }
    }
    // 启动Executor线程池
    synchronized (executors) {
        for (Executor executor: executors) {
            executor.start();
        }
    }
    // 启动MapperListener
    mapperListener.start();
    // 启动Connector
    synchronized (connectorsLock) {
        for (Connector connector: connectors) {
            try {
                // If it has already failed, don't try and start it
                if (connector.getState() != LifecycleState.FAILED) {
                    connector.start();
                }
            } catch (Exception e) {
                // logger......
            }
        }
    }
}

主要是做了下面这些事

  • 启动Engine,Engine的child容器都会被启动,webapp的部署会在这个步骤完成
  • 启动Executor,这是tomcat用Lifecycle封装的线程池,继承至java.util.concurrent.Executor以及tomcat的Lifecycle接口
  • 启动Connector组件,由Connector完成Endpoint的启动,这个时候意味着tomcat可以对外提供请求服务了

StandardEngine

在Server调用startInternal启动的时候,首先会调用start启动StandardEngine,而StandardEngine继承至ContainerBase,我们再来回顾下Lifecycle类图,关于Container,我们只需要关注右下角的部分即可。
在这里插入图片描述
StandardEngine、StandardHost、StandardContext、StandardWrapper各个容器存在父子关系,一个父容器包含多个子容器,并且一个子容器对应一个父容器。Engine是顶层父容器,它不存在父容器,关于各个组件的详细介绍,请参考《tomcat框架设计》。

各个组件的包含关系如下图所示,默认情况下,StandardEngine只有一个子容器StandardHost,一个StandardContext对应一个webapp应用,而一个StandardWrapper对应一个webapp里面的一个 Servlet。
在这里插入图片描述
由类图可知,StandardEngine、StandardHost、StandardContext、StandardWrapper都是继承至ContainerBase,各个容器的启动,都是由父容器调用子容器的start方法,也就是说由StandardEngine启动StandardHost,再StandardHost启动StandardContext,以此类推。

由于它们都是继续至ContainerBase,当调用 start 启动Container容器时,首先会执行 ContainerBase 的 start 方法,它会寻找子容器,并且在线程池中启动子容器,StandardEngine也不例外。

下面会详细分析代码执行流程,首先会调用父类的startInternal方法

  1. 调用父类的方法
protected synchronized void startInternal() throws LifecycleException {

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

   // Standard container startup
   // 调用父类的startInternal方法
   // 父类是ContainerBase
   // 以此启动子组件StandardHost
   super.startInternal();
}
  1. ContainerBase

ContainerBase的startInternal方法如下所示,主要分为以下3个步骤:

  • 启动子容器
  • 启动Pipeline,并且发出STARTING事件
  • 如果backgroundProcessorDelay参数 >= 0,则开启ContainerBackgroundProcessor线程,用于调用子容器的backgroundProcess。
protected synchronized void startInternal() throws LifecycleException {
    // 省略若干代码......

    // 把子容器的启动步骤放在线程中处理,默认情况下线程池只有一个线程处理任务队列
    Container children[] = findChildren();
    List<Future<Void>> results = new ArrayList<>();
    for (int i = 0; i < children.length; i++) {
        results.add(startStopExecutor.submit(new StartChild(children[i])));
    }
    // 阻塞当前线程,直到子容器start完成
    boolean fail = false;
    for (Future<Void> result : results) {
        try {
            result.get();
        } catch (Exception e) {
            log.error(sm.getString("containerBase.threadedStartFailed"), e);
            fail = true;
        }
    }
    // 启用Pipeline
    if (pipeline instanceof Lifecycle)
        ((Lifecycle) pipeline).start();
    setState(LifecycleState.STARTING);

    // 开启ContainerBackgroundProcessor线程用于调用子容器的backgroundProcess方法,默认情况下backgroundProcessorDelay=-1,不会启用该线程
    threadStart();
}
  1. 启动子容器

startStopExecutor是在init阶段创建的线程池,默认情况下 coreSize = maxSize = 1,也就是说默认只有一个线程处理子容器的 start,通过调用 Container.setStartStopThreads(int startStopThreads) 可以改变默认值 1。如果我们有4个webapp,希望能够尽快启动应用,我们只需要设置Host的startStopThreads值即可,如下所示,修改server.xml文件

<Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true" startStopThreads="4">
  <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>

ContainerBase会把StartChild任务丢给线程池处理,得到Future,并且会遍历所有的Future进行阻塞result.get(),这个操作是将异步启动转同步,子容器启动完成才会继续运行。

我们再来看看submit到线程池的StartChild任务,它实现了java.util.concurrent.Callable接口,在call里面完成子容器的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;
    }
}
  1. 启动pipeline

Pipeline是管道组件,用于封装了一组有序的Valve,便于Valve顺序地传递或者处理请求。

Pipeline的接口定义如下,定义了 Valve 的常用操作,以及 Container 的 getter/setter 方法,它的默认实现类是 org.apache.catalina.core.StandardPipeline,同时它也是一个Lifecycle组件

public interface Pipeline {
    public Valve getBasic();
    public void setBasic(Valve valve);
    public void addValve(Valve valve);
    public Valve[] getValves();
    public void removeValve(Valve valve);
    public Valve getFirst();
    public boolean isAsyncSupported();
    public Container getContainer();
    public void setContainer(Container container);
    public void findNonAsyncValves(Set<String> result);
}

Valve 是阀门组件,穿插在 Container 容器中,可以把它理解成请求拦截器,在 tomcat 接收到网络请求与触发 Servlet 之间执行。

Valve的接口如下所示,我们主要关注它的invoke方法,Request、Response分别是HttpServletRequest、HttpServletResponse的实现类。

public interface Valve {
    public Valve getNext();
    public void backgroundProcess();
    public void invoke(Request request, Response response) throws IOException, ServletException;
    public boolean isAsyncSupported();
}

我们再来看看 Pipeline 启动过程,默认使用 StandardPipeline 实现类,它也是一个Lifecycle。在容器启动的时候,StandardPipeline 会遍历 Valve 链表,如果 Valve 是 Lifecycle 的子类,则会调用其 start 方法启动 Valve 组件,代码如下

public class StandardPipeline extends LifecycleBase
        implements Pipeline, Contained {
    // 省略若干代码......
    protected synchronized void startInternal() throws LifecycleException {
        Valve current = first;
        if (current == null) {
            current = basic;
        }
        while (current != null) {
            if (current instanceof Lifecycle)
                ((Lifecycle) current).start();
            current = current.getNext();
        }
        setState(LifecycleState.STARTING);
    }
}

tomcat为我们提供了一系列的Valve

  • AccessLogValve,记录请求日志,默认会开启
  • RemoteAddrValve,可以做访问控制,比如限制IP黑白名单
  • RemoteIpValve,主要用于处理 X-Forwarded-For 请求头,用来识别通过HTTP代理或负载均衡方式连接到Web服务器的客户端最原始的IP地址的HTTP请求头字段

StandardHost启动

前面我们分析了 StandardEngine 的启动逻辑,它会启动其子容器 StandardHost,接下来我们看下 StandardHost 的 start 逻辑。

其实, StandardHost 重写的 startInternal 方法主要是为了查找报告错误的 Valve 阀门。

protected synchronized void startInternal() throws LifecycleException {

    // errorValve默认使用org.apache.catalina.valves.ErrorReportValve
    String errorValve = getErrorReportValveClass();
    if ((errorValve != null) && (!errorValve.equals(""))) {
        try {
            boolean found = false;
            // 如果所有的阀门中已经存在这个实例,则不进行处理,否则添加到  Pipeline 中
            Valve[] valves = getPipeline().getValves();
            for (Valve valve : valves) {
                if (errorValve.equals(valve.getClass().getName())) {
                    found = true;
                    break;
                }
            }
            // 如果未找到则添加到 Pipeline 中,注意是添加到 basic valve 的前面
            // 默认情况下,first valve 是 AccessLogValve,basic 是 StandardHostValve
            if(!found) {
                Valve valve =
                    (Valve) Class.forName(errorValve).getConstructor().newInstance();
                getPipeline().addValve(valve);
            }
        } catch (Throwable t) {
            // 处理异常,省略......
        }
    }
    // 调用父类 ContainerBase,完成统一的启动动作
    super.startInternal();
}

StandardHost Pipeline 包含的 Valve 组件:

  • basic:org.apache.catalina.core.StandardHostValve
  • first:org.apache.catalina.valves.AccessLogValve

需要注意的是,在往 Pipeline 中添加 Valve 阀门时,是添加到 first 后面,basic 前面由上面的代码可知,在 start 的时候,StandardHost 并没有做太多的处理,那么 StandardHost 又是怎么知道它有哪些 child 容器需要启动呢?

tomcat 在这块的逻辑处理有点特殊,使用 HostConfig 加载子容器,而这个 HostConfig 是一个LifecycleListener,它会处理 start、stop 事件通知,并且会在线程池中启动、停止 Context 容器,接下来看下 HostConfig 是如何工作的。

以下是 HostConfig 处理事件通知的代码,我们着重关注下 start 方法,这个方法里面主要是做一些应用部署的准备工作,比如过滤无效的webapp、解压war包等,而主要的逻辑在于 deployDirectories 中,它会往线程池中提交一个 DeployDirectory 任务,并且调用 Future#get() 阻塞当前线程,直到 deploy 工作完成。

public void lifecycleEvent(LifecycleEvent event) {
    // (省略若干代码) 判断事件是否由 Host 发出,并且为 HostConfig 设置属性
    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();
    }
}
public void start() {
    // (省略若干代码)
    if (host.getDeployOnStartup())
        deployApps();
}
protected void deployApps() {
    File appBase = host.getAppBaseFile();
    File configBase = host.getConfigBaseFile();
    // 过滤出 webapp 要部署应用的目录
    String[] filteredAppPaths = filterAppPaths(appBase.list());
    // 部署 xml 描述文件
    deployDescriptors(configBase, configBase.list());
    // 解压 war 包,但是这里还不会去启动应用
    deployWARs(appBase, filteredAppPaths);
    // 处理已经存在的目录,前面解压的 war 包不会再行处理
    deployDirectories(appBase, filteredAppPaths);
}

而这个DeployDirectory任务很简单,只是调用HostConfig#deployDirectory(cn, dir)

private static class DeployDirectory implements Runnable {
    // (省略若干代码)
    @Override
    public void run() {
        config.deployDirectory(cn, dir);
    }
}

我们再回到 HostConfig,看看 deployDirectory 的具体逻辑,分为以下几个步骤:

  1. 使用 digester,或者反射实例化 StandardContext
  2. 实例化 ContextConfig,并且为 Context 容器注册事件监听器,和 StandardHost 的套路一样,借助 XXXConfig 完成容器的启动、停止工作
  3. 将当前 Context 实例作为子容器添加到 Host 容器中,添加子容器的逻辑在 ContainerBase 中已经实现了,如果当前 Container 的状态是 STARTING_PREP 并且 startChildren 为 true,则还会启动子容器
protected void deployDirectory(ContextName cn, File dir) {

    Context context = null;
    File xml = new File(dir, Constants.ApplicationContextXml);
    File xmlCopy = new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml");

    // 实例化 StandardContext
    if (deployThisXML && xml.exists()) {
        synchronized (digesterLock) {
            // 省略若干异常处理的代码
            context = (Context) digester.parse(xml);
        }

        // (省略)为 Context 设置 configFile
    } else if (!deployThisXML && xml.exists()) {
        // 异常处理
        context = new FailedContext();
    } else {
        context = (Context) Class.forName(contextClass).getConstructor().newInstance();
    }

    // 实例化 ContextConfig,作为 LifecycleListener 添加到 Context 容器中,这和 StandardHost 的套路一样,都是使用 XXXConfig
    Class<?> clazz = Class.forName(host.getConfigClass());
    LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
    context.addLifecycleListener(listener);

    context.setName(cn.getName());
    context.setPath(cn.getPath());
    context.setWebappVersion(cn.getVersion());
    context.setDocBase(cn.getBaseName());

    // 实例化 Context 之后,为 Host 添加子容器
    host.addChild(context);

}

为什么要使用 HostConfig 组件启动 Context 容器呢,不可以直接在 Host 容器中直接启动吗?

HostConfig 不仅仅是启动、停止 Context 容器,还封装了很多应用部署的逻辑,此外,还会对 web.xml、context.xml 文件的改动进行监听,默认情况会重新启动 Context 容器。

而这个 Host 只是负责管理 Context 的生命周期,基于单一职责的原则,tomcat 利用事件通知的方式,很好地解决了藕合问题,Context 容器也是如此,它会对应一个 ContextConfig。

Context 容器又是如何启动的?

前面我们也提到了,HostConfig 将当前 Context 实例作为子容器添加到 Host 容器中(调用 ContainerBase.addChild 方法 ),而 Context 的启动就是在添加的时候调用的,ContainerBase 的关键代码如下所示,Context 启动的时候会解析web.xml,以及启动 Servlet、Listener,Servlet3.0还支持注解配置。

一篇文章中我们分析了 Service、Engine、Host、Pipeline、Valve 组件的启动逻辑,在 HostConfig 中会实例化 StandardContext,并启动 Context 容器,完成 webapp 应用程序的启动,这一块是最贴近我们开发的应用程序。

tomcat 如何支持 servlet3.0 的注解编程,比如对 javax.servlet.annotation.WebListener 注解的支持?

如果 tomcat 利用 ClassLoader 加载 webapp 下面所有的 class,从而分析 Class 对象的注解,这样子肯定会导致很多问题,比如 MetaSpace 出现内存溢出,而且加载了很多不想干的类,我们知道 jvm 卸载 class 的条件非常苛刻,这显然是不可取的。

因此,tomcat 开发了字节码解析的工具类,位于 org.apache.tomcat.util.bcel,bcel 即 :Byte Code Engineering Library,专门用于解析 class 字节码,而不是像我们前面猜测的那样,把类加载到 jvm 中。

假如 webapp 目录有多个应用,使用的开源框架的 jar 版本不尽一致,tomcat 是怎样避免出现类冲突?

不同的 webapp 使用不同的 ClassLoader 实例加载 class,因此 webapp 内部加载的 class 是不同的,自然不会出现类冲突,当然这里要排除 ClassLoader 的 parent 能够加载的 class。

StandardContext启动

首先,我们来看下StandardContext重要的几个属性,包括了我们熟悉的 ServletContext、servlet容器相关的Listener(比如 SessionListener 和 ContextListener)、FilterConfig。

protected ApplicationContext context:即ServletContext上下文
private InstanceManager instanceManager:根据 class 实例化对象,比如 Listener、Filter、Servlet 实例对象
private List<Object> applicationEventListenersList:SessionListener、ContextListner 等集合
private HashMap<String, ApplicationFilterConfig> filterConfigs:filer 名字与 FilterConfig 的映射关系
private Loader loader:用于加载class等资源
private final ReadWriteLock loaderLock:用于对loader的读写操作
protected Manager manager:Session管理器
private final ReadWriteLock managerLock:用于对manager的读写操作
private HashMap<String, String> servletMappings:url与Servlet名字的映射关系
private HashMap<Integer, ErrorPage> statusPages:错误码与错误页的映射
private JarScanner jarScanner:用于扫描jar包资源
private CookieProcessor cookieProcessor:cookies处理器,默认使用Rfc6265CookieProcessor

StandardContext 和其他 Container 一样,也是重写了 startInternal 方法。由于涉及到 webapp 的启动流程,需要很多准备工作,比如使用 WebResourceRoot 加载资源文件、利用 Loader 加载 class、使用 JarScanner 扫描 jar 包,等等。因此StandardContext 的启动逻辑比较复杂,这里描述下几个重要的步骤:

  1. 创建工作目录,比如$CATALINA_HOME\work\Catalina\localhost\examples;实例化 ContextServlet,应用程序拿到的是 ApplicationContext的外观模式
  2. 实例化 WebResourceRoot,默认实现类是 StandardRoot,用于读取 webapp 的文件资源
  3. 实例化 Loader 对象,Loader 是 tomcat 对于 ClassLoader 的封装,用于支持在运行期间热加载 class
  4. 发出 CONFIGURE_START_EVENT 事件,ContextConfig 会处理该事件,主要目的是从 webapp 中读取 servlet 相关的 Listener、Servlet、Filter 等
  5. 实例化 Sesssion 管理器,默认使用 StandardManager
  6. 调用 listenerStart,实例化 servlet 相关的各种 Listener,并且调用
    ServletContextListener
  7. 处理 Filter
  8. 加载 Servlet

下面,将分析下几个重要的步骤

  • 触发 CONFIGURE_START_EVENT 事件

ContextConfig 它是一个 LifycycleListener,它在 Context 启动过程中是承担了一个非常重要的角色。

StandardContext 会发出 CONFIGURE_START_EVENT 事件,而 ContextConfig 会处理该事件,主要目的是通过 web.xml 或者 Servlet3.0 的注解配置,读取 Servlet 相关的配置信息,比如 Filter、Servlet、Listener 等,其核心逻辑在 ContextConfig#webConfig() 方法中实现。下面,我们对 ContextConfig 进行详细分析。

首先,是通过 WebXmlParser 对 web.xml 进行解析,如果存在 web.xml 文件,则会把文件中定义的 Servlet、Filter、Listener 注册到 WebXml 实例中

protected void webConfig() {
	WebXmlParser webXmlParser = new WebXmlParser(context.getXmlNamespaceAware(),
	            context.getXmlValidation(), context.getXmlBlockExternal());
	Set<WebXml> defaults = new HashSet<>();
	defaults.add(getDefaultWebXmlFragment(webXmlParser));
	
	// 创建 WebXml实例,并解析 web.xml 文件
	WebXml webXml = createWebXml();
	InputSource contextWebXml = getContextWebXmlSource();
	if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
	    ok = false;
	}
}

如果没有 web.xml 文件,tomcat 会先扫描 WEB-INF/classes 目录下面的 class 文件,然后扫描 WEB-INF/lib 目录下面的 jar 包,解析字节码读取 servlet 相关的注解配置类,这里不得不吐槽下 serlvet3.0 注解,对 servlet 注解的处理相当重量级。tomcat 不会预先把该 class 加载到 jvm 中,而是通过解析字节码文件,获取对应类的一些信息,比如注解、实现的接口等,核心代码如下所示:

protected void processAnnotationsStream(InputStream is, WebXml fragment,
            boolean handlesTypesOnly, Map<String,JavaClassCacheEntry> javaClassCache)
            throws ClassFormatException, IOException {
    // is 即 class 字节码文件的 IO 流
    ClassParser parser = new ClassParser(is);

    // 使用 JavaClass 封装 class 相关的信息
    JavaClass clazz = parser.parse();
    checkHandlesTypes(clazz, javaClassCache);

    if (handlesTypesOnly) {
        return;
    }

    AnnotationEntry[] annotationsEntries = clazz.getAnnotationEntries();
    if (annotationsEntries != null) {
        String className = clazz.getClassName();
        for (AnnotationEntry ae : annotationsEntries) {
            String type = ae.getAnnotationType();
            if ("Ljavax/servlet/annotation/WebServlet;".equals(type)) {
                processAnnotationWebServlet(className, ae, fragment);
            }else if ("Ljavax/servlet/annotation/WebFilter;".equals(type)) {
                processAnnotationWebFilter(className, ae, fragment);
            }else if ("Ljavax/servlet/annotation/WebListener;".equals(type)) {
                fragment.addListener(className);
            } else {
                // Unknown annotation - ignore
            }
        }
    }
}

Tomcat 使用自己的工具类 ClassParser 通过对字节码文件进行解析,获取其注解,并把WebServlet、WebFilter、WebListener 注解的类添加到 WebXml 实例中,统一由它对 ServletContext 进行参数配置。tomcat 对字节码的处理是由org.apache.tomcat.util.bcel 包完成的,bcel 即 Byte Code Engineering Library,其实现比较繁锁,需要对字节码结构有一定的了解。

配置信息读取完毕之后,会把 WebXml 装载的配置赋值给 ServletContext,在这个时候,ContextConfig 会往 StardardContext 容器中添加子容器(即 Wrapper 容器),部分代码如下所示:

private void configureContext(WebXml webxml) {
    // 设置 Filter 定义
    for (FilterDef filter : webxml.getFilters().values()) {
        if (filter.getAsyncSupported() == null) {
            filter.setAsyncSupported("false");
        }
        context.addFilterDef(filter);
    }
    // 设置 FilterMapping,即 Filter 的 URL 映射 
    for (FilterMap filterMap : webxml.getFilterMappings()) {
        context.addFilterMap(filterMap);
    }
    // 往 Context 中添加子容器 Wrapper,即 Servlet
    for (ServletDef servlet : webxml.getServlets().values()) {
        Wrapper wrapper = context.createWrapper();
        // 省略若干代码。。。
        wrapper.setOverridable(servlet.isOverridable());
        context.addChild(wrapper);
    }
    // ......
}

tomcat 还会加载 WEB-INF/classes/META-INF/resources/、WEB-INF/lib/xxx.jar/META-INF/resources/ 的静态资源,这一块的作用暂时不清楚,关键代码如下所示

protected void processResourceJARs(Set<WebXml> fragments) {
    for (WebXml fragment : fragments) {
        URL url = fragment.getURL();
        if ("jar".equals(url.getProtocol()) || url.toString().endsWith(".jar")) {
            try (Jar jar = JarFactory.newInstance(url)) {
                jar.nextEntry();
                String entryName = jar.getEntryName();
                while (entryName != null) {
                    if (entryName.startsWith("META-INF/resources/")) {
                        context.getResources().createWebResourceSet(
                                WebResourceRoot.ResourceSetType.RESOURCE_JAR,
                                "/", url, "/META-INF/resources");
                        break;
                    }
                    jar.nextEntry();
                    entryName = jar.getEntryName();
                }
            }
        } else if ("file".equals(url.getProtocol())) {
            File file = new File(url.toURI());
            File resources = new File(file, "META-INF/resources/");
            if (resources.isDirectory()) {
                context.getResources().createWebResourceSet(
                        WebResourceRoot.ResourceSetType.RESOURCE_JAR,
                        "/", resources.getAbsolutePath(), null, "/");
            }
        }
    }
}
  • 启动Wrapper容器

ContextConfig 把 Wrapper 子容器添加到 StandardContext 容器中之后,便会挨个启动 Wrapper 子容器。但是实际上,由于 StandardContext 至 ContainerBase,在添加子容器的时候,便会调用 start 方法启动 Wrapper。

for (Container child : findChildren()) {
    if (!child.getState().isAvailable()) {
        child.start();
    }
}
  • 调用ServletContainerInitializer

在初始化 Servlet、Listener 之前,便会先调用 ServletContainerInitializer,进行额外的初始化处理。

注意ServletContainerInitializer 需要的是 Class 对象,而不是具体的实例对象,这个时候 servlet 相关的 Listener 并没有被实例化,因此不会产生矛盾。

// 指定 ServletContext 的相关参数
mergeParameters();

// 调用 ServletContainerInitializer#onStartup()
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
    initializers.entrySet()) {
    try {
        entry.getKey().onStartup(entry.getValue(),
                getServletContext());
    } catch (ServletException e) {
        log.error(sm.getString("standardContext.sciFail"), e);
        ok = false;
        break;
    }
}
  • 启动Servlet相关的Listener

WebConfig 加载 Listener 时,只是保存了 className,实例化动作由 StandardContext 触发。前面在介绍 StandardContext 的时候提到了 InstanceManager,创建实例的逻辑由 InstanceManager 完成。

Listener 监听器分为 Event、Lifecycle 监听器,WebConfig 在加载 Listener 的时候是不会区分的,实例化之后才会分开存储。在完成 Listener 实例化之后,tomcat 容器便启动 OK 了。

此时,tomcat 需要通知应用程序定义的 ServletContextListener,方便应用程序完成自己的初始化逻辑,它会遍历 ServletContextListener 实例,并调用其 contextInitialized 方法,比如 spring 的ContextLoaderListener。

有以下 Event 监听器,主要是针对事件通知:

  1. ServletContextAttributeListener
  2. ServletRequestAttributeListener
  3. ServletRequestListener
  4. HttpSessionIdListener
  5. HttpSessionAttributeListener

有以下两种 Lifecycle 监听器,主要是针对 ServletContext、HttpSession 的生命周期管理,比如创建、销毁等

  1. ServletContextListener
  2. HttpSessionListener
  • 初始化Filter

ContextConfig 在处理 CONFIGURE_START_EVENT 事件的时候,会使用 FilterDef 保存 Filter 信息。而 StandardContext 会把 FilterDef 转化成 ApplicationFilterConfig,在 ApplicationFilterConfig 构造方法中完成 Filter 的实例化,并且调用 Filter 接口的 init 方法,完成 Filter 的初始化。

ApplicationFilterConfig 是 javax.servlet.FilterConfig 接口的实现类

public boolean filterStart() {
    boolean ok = true;
    synchronized (filterConfigs) {
        filterConfigs.clear();
        for (Entry<String,FilterDef> entry : filterDefs.entrySet()) {
            String name = entry.getKey();
            try {
                // 在构造方法中完成 Filter 的实例化,并且调用 Filter 接口的 init 方法,完成 Filter 的初始化
                ApplicationFilterConfig filterConfig =
                        new ApplicationFilterConfig(this, entry.getValue());
                filterConfigs.put(name, filterConfig);
            } catch (Throwable t) {
                // 省略 logger 处理
                ok = false;
            }
        }
    }
    return ok;
}
  • 处理Wrapper容器

Servlet 对应 tomcat 的 Wrapper 容器,完成 Filter 初始化之后便会对 Wrapper 容器进行处理,如果 Servlet 的 loadOnStartup >= 0,便会在这一阶段完成 Servlet 的加载,并且值越小越先被加载,否则在接受到请求的时候才会加载 Servlet。

加载过程,主要是完成 Servlet 的实例化,并且调用 Servlet 接口的 init 方法,具体的逻辑将在下文进行详细分析。

// Load and initialize all "load on startup" servlets
//  对Wrapper进行处理,主要是判断是否需要懒加载并且根据配置排序
// StandardWrapper 实例化并且启动 Servlet,由于 Servlet 存在 loadOnStartup 属性
// 因此使用了 TreeMap,根据 loadOnStartup 值 对 Wrapper 容器进行排序,然后依次启动 Servlet
if (ok) {
    if (!loadOnStartup(findChildren())){
        log.error(sm.getString("standardContext.servletFail"));
        ok = false;
    }
}

loadOnStartup 方法使用 TreeMap 对 Wrapper 进行排序,loadOnStartup 值越小越靠前,值相同的 Wrapper 放在同一个 List 中,代码如下所示

public boolean loadOnStartup(Container children[]) {

    // 使用 TreeMap 对 Wrapper 进行排序,loadOnStartup 值越小越靠前,值相同的 Wrapper 放在同一个 List 中
    TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
    for (int i = 0; i < children.length; i++) {
        Wrapper wrapper = (Wrapper) children[i];
        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);
    }

    // 根据 loadOnStartup 值有序加载 Wrapper 容器
    for (ArrayList<Wrapper> list : map.values()) {
        for (Wrapper wrapper : list) {
            try {
                wrapper.load();
            } catch (ServletException e) {
                if(getComputedFailCtxIfServletStartFails()) {
                    return false;
                }
            }
        }
    }
    return true;
}
  • Wrapper容器

Wrapper 容器是 tomcat 所有容器中最底层子容器,它没有子容器,并且父容器是 Context。

默认实现是 StandardWrapper,我们先来看看类定义,它继承至 ContainBase,实现了 servlet 的 ServletConfig 接口,以及 tomcat 的 Wrapper 接口,说明 StandardWrapper 不仅仅是一个 Wrapper 容器实现,还是 ServletConfig 实现,部分代码如下所示

public class StandardWrapper extends ContainerBase
    implements ServletConfig, Wrapper, NotificationEmitter {

    // Wrapper 的门面模式,调用 Servlet 的 init 方法传入的是该对象
    protected final StandardWrapperFacade facade = new StandardWrapperFacade(this);    
    protected volatile Servlet instance = null; // Servlet 实例对象
    protected int loadOnStartup = -1;   // 默认值为 -1,不立即启动 Servlet
    protected String servletClass = null;
    public StandardWrapper() {
        super();
        swValve=new StandardWrapperValve();
        pipeline.setBasic(swValve);
        broadcaster = new NotificationBroadcasterSupport();
    }
}

由前面对 Context 的分析可知,StandardContext 在启动的时候会发出CONFIGURE_START_EVENT 事件,ContextConfig 会处理该事件,通过解析 web.xml 或者读取注解信息获取 Wrapper 子容器,并且会添加到 Context 容器中。

由于 StandardContext 继承至 ContainerBase,在调用 addChild 的时候默认会启动 child 容器(即 Wrapper),我们来看看 StandardWrapper 的启动逻辑。

  • 启动Wrapper容器

StandardWrapper 没有子容器,启动逻辑相对比较简单清晰,它重写了 startInternal 方法,主要是完成了 jmx 的事件通知,先后向 jmx 发出 starting、running 事件,代码如下所示:

protected synchronized void startInternal() throws LifecycleException {
    // 发出 j2ee.state.starting 事件通知
    if (this.getObjectName() != null) {
        Notification notification = 
            new Notification("j2ee.state.starting", this.getObjectName(), sequenceNumber++);
        broadcaster.sendNotification(notification);
    }

    // ConainerBase 的启动逻辑
    super.startInternal();
    setAvailable(0L);

    // 发出 j2ee.state.running 事件通知
    if (this.getObjectName() != null) {
        Notification notification =
            new Notification("j2ee.state.running", this.getObjectName(), sequenceNumber++);
        broadcaster.sendNotification(notification);
    }
}
  • 加载Wrapper

由前面对 Context 容器的分析可知,Context 完成 Filter 初始化之后,如果 loadOnStartup >= 0 便会调用 load 方法加载 Wrapper 容器。

StandardWrapper 使用 InstanceManager 实例化 Servlet,并且调用 Servlet 的 init 方法进行初始化,传入的 ServletConfig 是 StandardWrapperFacade 对象。
在这里插入图片描述

public synchronized void load() throws ServletException {
    // 实例化 Servlet,并且调用 init 方法完成初始化
    instance = loadServlet();
    if (!instanceInitialized) {
        initServlet(instance);
    }
    if (isJspServlet) {
        // 处理 jsp Servlet
    }
}

猜你喜欢

转载自blog.csdn.net/qq_35262405/article/details/101765982