【Tomcat】第七篇:Tomcat启动流程源码分析(下)start启动组件

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

public void start()
        throws Exception {
    
    
        if( catalinaDaemon==null ) init();
    	// 调用CatAlina的Start方法
        Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null);
        method.invoke(catalinaDaemon, (Object [])null);
}

Catalina

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

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

1.启动Server

StandardSever

  1. 先是由LifecycleBase统一发出STARTING_PREP事件,StandardServer额外还会发出CONFIGURE_START_EVENT、STARTING事件,用于通知LifecycleListener在启动前做一些准备工作,比如NamingContextListener会处理CONFIGURE_START_EVENT事件,实例化tomcat相关的上下文,以及ContextResource资源.
  2. 然后,启动内部的NamingResourcesImpl实例,这个类封装了各种各样的数据,比如ContextEnvironment、ContextResource、Container等等,它用于Resource资源的初始化,以及为webapp应用提供相关的数据资源,比如 JNDI 数据源(对应ContextResource).
  3. 接着,启动Service组件,这一块的逻辑将在下面进行详细分析,最后由LifecycleBase发出STARTED事件,完成start.
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();
        }
    }
}

2.启动Service

tandardService

  1. 启动Engine,Engine的child容器都会被启动,webapp的部署会在这个步骤完成;
  2. 启动Executor,这是tomcat用Lifecycle封装的线程池,继承至java.util.concurrent.Executor以及tomcat的Lifecycle接口
  3. 启动MapperListener
  4. 启动Connector组件,由Connector完成Endpoint的启动,这个时候意味着tomcat可以对外提供请求服务了。
protected void startInternal() throws LifecycleException {
    
    
    setState(LifecycleState.STARTING);
    // 1.启动Engine
    if (engine != null) {
    
    
        synchronized (engine) {
    
    
            engine.start();
        }
    }
    // 2.启动Executor线程池
    synchronized (executors) {
    
    
        for (Executor executor: executors) {
    
    
            executor.start();
        }
    }
    // 3.启动MapperListener
    mapperListener.start();
    // 4.启动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......
            }
        }
    }
}

3.启动Engine

StandardEngine

Engine主要是调用调用父类ContainerBase的startInternal

protected synchronized void startInternal() throws LifecycleException {
    
    

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

    // 调用父类ContainerBase的startInternal方法
    super.startInternal();
}

ContainerBase

  1. 启动子容器,通过线程池(默认只有一个线程,可以在配置文件中设置(startStopThread))

    <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>
    
  2. 启动Pipeline,并且发出STARTING事件

  3. 如果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])));
    }
    // 1.阻塞当前线程,直到子容器start完成
    boolean fail = false;
    for (Future<Void> result : results) {
    
    
        try {
    
    
            result.get();
        } catch (Exception e) {
    
    
            log.error(sm.getString("containerBase.threadedStartFailed"), e);
            fail = true;
        }
    }
    // 2.启用Pipeline
    if (pipeline instanceof Lifecycle)
        ((Lifecycle) pipeline).start();
    setState(LifecycleState.STARTING);

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

4.启动Pipeline

**StandPipeline **

我们来看 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);
    }
}

5.启动Host

StandardHost

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 容器需要启动呢?

HostConfig

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

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

-start()

public void start() {
    
    
    // (省略若干代码)
    if (host.getDeployOnStartup())
        deployApps();
}

–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

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

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

----deployDirectory()

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 容器中,
    // S这和 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);

}

现在有两个疑问:

  1. 为什么要使用HostConfig 组件启动 Context 容器呢,不可以直接在Host 容器中直接启动吗?
    • HostConfig 不仅仅是启动、停止Context 容器,还封装了很多应用部署的逻辑
    • 此外,还会对 web.xml、context.xml文件的改动进行监听,默认情况会重新启动 Context 容器。
    • 而这个Host 只是负责管理 Context 的生命周期,基于单一职责的原则,tomcat 利用事件通知的方式,很好地解决了藕合问题,Context 容器也是如此,它会对应一个 ContextConfig
  2. Context 容器又是如何启动的?
    • 前面我们也提到了,HostConfig将当前 Context 实例作为子容器添加到Host 容器中(调用 ContainerBase.addChild 方法 ),而Context 的启动就是在添加的时候调用的
    • ContainerBase 的关键代码如下所示,Context启动的时候会解析web.xml,以及启动Servlet、Listener,Servlet3.0还支持注解配置,等等这一系列逻辑将在下一篇文章进行分析

6.启动Context

首先我们思考两个问题:

  1. 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 中。
  2. 假如 webapp 目录有多个应用,使用的开源框架的 jar 版本不尽一致,tomcat 是怎样避免出现类冲突?

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

StandardContext的属性主要有:

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

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

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

6.1 ContextConfig读取web.xml

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 还会加载 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, "/");
            }
        }
    }
}

6.2 启动Wrapper容器

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

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

6.3 初始化处理

在初始化 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;
    }
}

6.4 启动Servlet 相关的 Listener

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

Listener 监听器分为Event、Lifecycle 监听器,WebConfig在加载 Listener 的时候是不会区分的,实例化之后才会分开存储。在完成 Listener 实例化之后,tomcat容器便启动 OK 了。此时,tomcat 需要通知应用程序定义的ServletContextListener,方便应用程序完成自己的初始化逻辑,它会遍历 ServletContextListener 实例,并调用其contextInitialized 方法,比如 spring 的ContextLoaderListener

  • 有以下 Event 监听器,主要是针对事件通知:
    • ServletContextAttributeListener
    • ServletRequestAttributeListener
    • ServletRequestListener
    • HttpSessionIdListener
    • HttpSessionAttributeListener
  • 有以下两种 Lifecycle 监听器,主要是针对ServletContext、HttpSession 的生命周期管理,比如创建、销毁等
    • ServletContextListener
    • HttpSessionListener

6.5 初始化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;
}

6.6 处理Wrapper 容器

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

// 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;
}

7.启动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 的启动逻辑

7.1 启动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);
    }
}

7.2 加载 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
    }
}

总结

tomcat 实现了javax.servlet.ServletContext 接口,在 Context 启动的时候会实例化该对象。由 Context 容器通过 web.xml 或者 扫描class 字节码读取 servlet3.0 的注解配置,从而加载webapp 定义的 Listener、Servlet、Filter等 servlet 组件,但是并不会立即实例化对象。全部加载完毕之后,依次对 Listener、Filter、Servlet进行实例化、并且调用其初始化方法,比如ServletContextListener#contextInitialized()、Flter#init() 等。

初始化和启动的的时序图

可以用浏览器打开看大图

猜你喜欢

转载自blog.csdn.net/weixin_43935927/article/details/108673621