tomcat源码分析03:Context启动流程

809 阅读17分钟

注:本文源码分析基于 tomcat 9.0.43,源码的gitee仓库仓库地址:gitee.com/funcy/tomca….

本文是tomcat源码分析的第三篇,本文来分析Context的启动流程。

上tomcat启动流程图:

可以看到, ContextHost启动,在Host#start(...)方法中,会调用Context#init(...)Context#start(...)方法,不过Context#init(...)并没有做什么实质性的工作,我们重点关注Context#start(...).

tomcat中,Context的实现为StandardContext,本文主要也是分析StandardContext的启动流程。

1. StandardContext#startInternal

要分析Context的启动流程,我们直接进入StandardContext#startInternal方法:

protected synchronized void startInternal() throws LifecycleException {
    // 省略了一些代码
    ...

    try {
        if (ok) {
            // 省略了一些代码
            ...
            // 1. 触发 CONFIGURE_START 事件
            fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);

            // 2. 启动 child, context 的 child 为 Wrapper
            for (Container child : findChildren()) {
                if (!child.getState().isAvailable()) {
                    child.start();
                }
            }
        }

        // 省略了一些代码
        ...

        // 3. 处理 ServletContainerInitializers
        for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
            initializers.entrySet()) {
            try {
                // 执行 ServletContainerInitializers#onStartup(...)方法
                entry.getKey().onStartup(entry.getValue(),
                        getServletContext());
            } catch (ServletException e) {
                log.error(sm.getString("standardContext.sciFail"), e);
                ok = false;
                break;
            }
        }

        // 4. 加载 Listener
        if (ok) {
            if (!listenerStart()) {
                log.error(sm.getString("standardContext.listenerFail"));
                ok = false;
            }
        }

        // 5. 加载 filter
        if (ok) {
            if (!filterStart()) {
                log.error(sm.getString("standardContext.filterFail"));
                ok = false;
            }
        }

        // 6. 处理 servlet,加载与初始化,Context 的 child 为 Wrapper,用来存放servlet的
        if (ok) {
            if (!loadOnStartup(findChildren())){
                log.error(sm.getString("standardContext.servletFail"));
                ok = false;
            }
        }
    } finally {
        // 省略了一些代码
        ...
    }

    // 省略了一些代码
    ...
}

StandardContext#startInternal方法非常长,这里我们主要注意与servlet相关的操作,该方法处理流程如下:

  1. 触发 CONFIGURE_START 事件,在这里会解析web.xml
  2. 启动Context的子Container,也就是Wrapper,不过从代码来看,Wrapper初始化与启动时并没有做什么,就不分析了;
  3. 处理 ServletContainerInitializers,也就是执行ServletContainerInitializers实现类的onStartup(...)方法;
  4. 加载servlet组件: Listener
  5. 加载servlet组件:filter
  6. 加载servlet组件:servlet

好了,接下来本文就逐一分析这些流程了。

2. 触发事件:LifecycleBase#fireLifecycleEvent(...)

我们来看看触发CONFIGURE_START事件的操作,代码为:

// 1. 触发 CONFIGURE_START 事件
fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);

我们跟下去:

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

这里会循环所有的LifecycleListener,逐一调用LifecycleListener#lifecycleEvent(...),需要注意的是,这些LifecycleListenertomcat自身提供的,注意区别servlet提供的listener

这个lifecycleListeners都有哪些呢?回忆下,我们在Demo01#test做了这么个操作:

public class Demo01 {

    @Test
    public void test() throws Exception {
        ...
        // 这个 Context 就是 StandardContext
        Context context = tomcat.addContext("", docBase);

        // 得到 lifecycleListener 实际类型为 ContextConfig
        LifecycleListener lifecycleListener = (LifecycleListener)
                Class.forName(tomcat.getHost().getConfigClass())
                        .getDeclaredConstructor().newInstance();

        // 将得到的 lifecycleListener 添加到context中
        context.addLifecycleListener(lifecycleListener);
        ...
    }
}

以上代码中,tomcat.addContext(...)得到的Context就是StandardContext,也就是本文我们要分析的ContextlifecycleListener得到的类型为ContextConfig,可以在StandardHost中看到:

private String configClass =
        "org.apache.catalina.startup.ContextConfig";

所以,这段代码是向StandardContext中添加了一个LifecycleListener,类型为ContextConfig.

之所以要提这个LifecycleListener,是因为ContextConfig的作用十分重要!跟着代码的执行,我们进入ContextConfig#lifecycleEvent(...) 方法:

public void lifecycleEvent(LifecycleEvent event) {
    // Identify the context we are associated with
    try {
        context = (Context) event.getLifecycle();
    } catch (ClassCastException e) {
        log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e);
        return;
    }
    // Process the event that has occurred
    if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
        // 处理启动事件
        configureStart();
    } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
        // 处理启动前的事件
        beforeStart();
    } else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
        // Restore docBase for management tools
        if (originalDocBase != null) {
            context.setDocBase(originalDocBase);
        }
    } else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
        // 处理停止事件
        configureStop();
    } else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
        // 处理初始化事件
        init();
    } else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
        // 处理销毁事件
        destroy();
    }
}

可以看到,ContextConfig#lifecycleEvent(...)处理的事件非常多,不过我们主要关注CONFIGURE_START_EVENT,跟着ContextConfig#configureStart(...)一路执行,进入ContextConfig#webConfig(...)方法:

    protected void webConfig() {
        // 省略“解析 web.xml”的相关代码
        ...

        // 查找 ServletContainerInitializer 的实现
        if (ok) {
            processServletContainerInitializers();
        }

        if  (!webXml.isMetadataComplete() || typeInitializerMap.size() > 0) {
            // 会在这里处理 @HandlesTypes 注解
            processClasses(webXml, orderedFragments);
        }

        // 省略“合并 web.xml”的代码
        ...

        // 应用配置,将 webXml 中的配置信息应用用Context
        configureContext(webXml);

        // 省略“查找jar包中的静态资源”相关代码
        ...

        // 将 ServletContainerInitializer 添加到Context
        if (ok) {
            for (Map.Entry<ServletContainerInitializer,
                    Set<Class<?>>> entry : initializerClassMap.entrySet()) {
                if (entry.getValue().isEmpty()) {
                    context.addServletContainerInitializer(entry.getKey(), null);
                } else {
                    context.addServletContainerInitializer(
                            entry.getKey(), entry.getValue());
                }
            }
        }
    }

这个方法主要是处理web配置的,主要是解析web.xml文件,不过我们项目中没用上,就不分析了,不过我们项目中用到了ServletContainerInitializer,这个类也是在这里加载的,我们主要来分析它。

2.1 查找ServletContainerInitializer

我们回忆下ServletContainerInitializer的实现,在我们的配置中,我们自定义的ServletContainerInitializer放在了META-INF/services/javax.servlet.ServletContainerInitializer文件中,这个文件的内容为:

org.apache.tomcat.demo.MyServletContainerInitializer

这个就是我们自定义的ServletContainerInitializer包名.类名,我们进入processServletContainerInitializers()看看它是如何查找的:

public class ContextConfig implements LifecycleListener {
    /** 当前类型是由哪个 ServletContainerInitializer 实例引入的 */
    protected final Map<Class<?>, Set<ServletContainerInitializer>> typeInitializerMap =
            new HashMap<>();
    /**  ServletContainerInitializer 实例引入的类型,类型可以有多个 */
    protected final Map<ServletContainerInitializer, Set<Class<?>>> initializerClassMap =
            new LinkedHashMap<>();
    ...

    /**
     * 处理 ServletContainerInitializer 的加载
     */
    protected void processServletContainerInitializers() {

        List<ServletContainerInitializer> detectedScis;
        try {
            WebappServiceLoader<ServletContainerInitializer> loader 
                    = new WebappServiceLoader<>(context);
            // 在这里完成加载的
            detectedScis = loader.load(ServletContainerInitializer.class);
        } catch (IOException e) {
            ...
            ok = false;
            return;
        }

        // 收集 annotation 里的 class
        for (ServletContainerInitializer sci : detectedScis) {
            initializerClassMap.put(sci, new HashSet<>());

            HandlesTypes ht;
            try {
                // 得到 ServletContainerInitializer 上的 @HandlesTypes 注解
                ht = sci.getClass().getAnnotation(HandlesTypes.class);
            } catch (Exception e) {
                ...
                continue;
            }
            ...
            // 得到 @HandlesTypes 指定的类型,可以指定多个
            Class<?>[] types = ht.value();
            ...
            for (Class<?> type : types) {
                // @HandlesTypes 里的类型还可以是注解
                if (type.isAnnotation()) {
                    handlesTypesAnnotations = true;
                } else {
                    handlesTypesNonAnnotations = true;
                }
                // 保存
                Set<ServletContainerInitializer> scis = typeInitializerMap.get(type);
                if (scis == null) {
                    scis = new HashSet<>();
                    typeInitializerMap.put(type, scis);
                }
                scis.add(sci);
            }
        }
    }

    ...
}

这个方法就是用来处理 ServletContainerInitializer 加载的,先是调用WebappServiceLoader#load(...)加载javax.servlet.ServletContainerInitializer并实例化,然后处理 ServletContainerInitializer 类上的@HandlesTypes注解。

1. 加载 ServletContainerInitializer

我们先来看看WebappServiceLoader#load(...)的方法:

public List<T> load(Class<T> serviceType) throws IOException {
    // SERVICES值为“META-INF/services/”,传入的serviceType为“ServletContainerInitializer.class”
    // 因此configFile值为“META-INF/services/javax.servlet.ServletContainerInitializer”
    String configFile = SERVICES + serviceType.getName();

    // 获取classLoader
    ClassLoader loader = context.getParentClassLoader();
    Enumeration<URL> containerResources;
    // 使用classLoader查找 META-INF/services/javax.servlet.ServletContainerInitializer 文件
    if (loader == null) {
        containerResources = ClassLoader.getSystemResources(configFile);
    } else {
        containerResources = loader.getResources(configFile);
    }

    LinkedHashSet<String> containerServiceClassNames = new LinkedHashSet<>();
    Set<URL> containerServiceConfigFiles = new HashSet<>();
    while (containerResources.hasMoreElements()) {
        URL containerServiceConfigFile = containerResources.nextElement();
        containerServiceConfigFiles.add(containerServiceConfigFile);
        // 处理文件,就是读取文件内容
        parseConfigFile(containerServiceClassNames, containerServiceConfigFile);
    }

    // 省略“从jar包中读取”代码
    ...

    // 实例化 serviceClass
    return loadServices(serviceType, containerServiceClassNames);
}

这个方法所做的工作相当清晰,就是从classpathjar包中查到META-INF/services/javax.servlet.ServletContainerInitializer 文件,然后读取内容,最后使用反射实例化,这块就不具体展开了。

2. @HandlesTypes 的功能

让我们回到ContextConfig#processServletContainerInitializers方法,加载完自定义实现的ServletContainerInitializer后,接下来的代码都是围绕@HandlesTypes在进行操作,这么一大段操作最终只是为了得到两个结构:

    /** 当前类型是由哪个 ServletContainerInitializer 实例引入的 */
    Map<Class<?>, Set<ServletContainerInitializer>> typeInitializerMap;
    /**  ServletContainerInitializer 实例引入的类型,类型可以有多个 */
    Map<ServletContainerInitializer, Set<Class<?>>> initializerClassMap;

这块的操作就是一些基本的集合操作,就不过多分析了,关于这两个结构的内容我们后面会用到,在后面具体用到时,知道是在这里装入内容的就行了。

那这个@HandlesTypes究竟是个啥呢?它的代码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface HandlesTypes {

    /**
     * @return array of classes
     */
    Class<?>[] value();

}

这个注解就一个属性,类型为Class数组,它的注释如下:

This annotation is used to declare an array of application classes which are passed to a {@link javax.servlet.ServletContainerInitializer}.

此批注用于声明应用程序类的数组,这些应用程序类将 传递给{@link javax.servlet.ServletContainerInitializer}。

这个注释看得并不是很明白,在我们的代码中没有用上@HandlesTypes,这里我们来看看spring的运用:

// HandlesTypes 指定的类型为 WebApplicationInitializer
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

    /**
     * webAppInitializerClasses:这里传入的就是 WebApplicationInitializer
     */
    @Override
    public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, 
            ServletContext servletContext) throws ServletException {

        List<WebApplicationInitializer> initializers = new LinkedList<>();

        if (webAppInitializerClasses != null) {
            // 这个for循环里处理了一些操作
            for (Class<?> waiClass : webAppInitializerClasses) {
                if (...) {
                    try {
                        // 反射实例化
                        initializers.add((WebApplicationInitializer)
                                ReflectionUtils.accessibleConstructor(waiClass).newInstance());
                    }
                    ...
                }
            }
        }
        ...
        // 遍历调用 WebApplicationInitializer 的方法
        for (WebApplicationInitializer initializer : initializers) {
            initializer.onStartup(servletContext);
        }
    }

}

在spring中,实现ServletContainerInitializer的类为SpringServletContainerInitializer,功能如下:

  1. SpringServletContainerInitializer@HandlesTypes 指定了类型为 WebApplicationInitializer
  2. 这相处理后,onStartup(...)方法的webAppInitializerClasses,传入Set<Class<?>>类型中的Class就为WebApplicationInitializer.class
  3. 使用反射实例化传入的WebApplicationInitializer,调用其onStartup(...)方法;
  4. 因此,在spring中,我们只要实现WebApplicationInitializer,重写其onStartup(...)方法,也能实现servlet的加载,并不需要在META-INF/services文件夹中添加什么文件。

到了这里,应该就明白了@HandlesTypes的作用了吧,它就是用来过滤类的,我们可以使用它来让指定的Class传入到ServletContainerInitializer#onStartup(...)方法中。

2.2 处理类:processClasses(...)

让我们回到ContextConfig#webConfig(...)

    protected void webConfig() {
        ...

        if  (!webXml.isMetadataComplete() || typeInitializerMap.size() > 0) {
            // 会在这里处理 @HandlesTypes 注解
            processClasses(webXml, orderedFragments);
        }
        ...
    }

前面分析了 @HandlesTypes 注解的作用,这就让人不禁有个疑问:这些类是在哪里过滤的呢,要提前进行类加载(即得到类的Class对象,然后判断类型)吗?tomcat 处理 @HandlesTypes的操作在processClasses(...)方法中,我们且看下去。

跟着processClasses(...)一路执行,最终到了ContextConfig#processAnnotationsStream(...)方法:

    protected void processAnnotationsStream(InputStream is, WebXml fragment,
            boolean handlesTypesOnly, Map<String,JavaClassCacheEntry> javaClassCache)
            throws ClassFormatException, IOException {
        // 传入的 is 就是class文件对应的输入流
        ClassParser parser = new ClassParser(is);
        JavaClass clazz = parser.parse();
        // 检查类型,判断是否符合 `@HandlesTypes` 指定的类型
        checkHandlesTypes(clazz, javaClassCache);
        // 如果是 handlesTypesOnly,执行到这里就结束了
        if (handlesTypesOnly) {
            return;
        }
        // 意外之喜:发现了 @WebServlet、@WebFilter、@WebListener 的处理
        processClass(fragment, clazz);
    }

我们来细细分析这个方法的执行过程。

1. ClassParser

该方法的一开始,创建了ClassParser来解析class文件对应的输入流,我们来看看ClassParser

/**
 * Wrapper class that parses a given Java .class file. The method <A
 * href ="#parse">parse</A> returns a <A href ="JavaClass.html">
 * JavaClass</A> object on success. When an I/O error or an
 * inconsistency occurs an appropriate exception is propagated back to
 * the caller.
 *
 * 解析给定Java .class文件的包装器类。成功时,方法parse返回
 * JavaClass对象。发生IO错误或不一致时,
 * 会将适当的异常传播回调用方。
 *
 * The structure and the names comply, except for a few conveniences,
 * exactly with the <A href="http://docs.oracle.com/javase/specs/">
 * JVM specification 1.0</a>. See this paper for
 * further details about the structure of a bytecode file.
 *
 * 除了一些方便之外,结构和名称
 * 完全符合JVM规范1.0。有关字节码文件结构的更多详细信息,
 * 请参见本文。
 */
public final class ClassParser {
    ...
}

从它的描述来看,就是另外实现了一个class解析器,如果我们直接使用java提供的Class对象,只需两步就能完成类型判断:Class.forname(...)进行加载、Class.isAssignableFrom(...)进行判断,tomcat为何要搞了个ClassParser,想了想,其主要目的,应该是避免提前进行类加载,使用Class对象,必须要先加载类,而这样操作,就把所有的类都加载了,有些类在整个jvm生命都可能用不到,因此造成了额外的内存浪费。

为避免类的提前加载,tomcat自己实现了jvm规范,搞了个ClassParserJavaClass

2. checkHandlesTypes(...)

这个方法就是用来检查当前类是否与@HandlesTypes指定的类相匹配的,它的注释如下:

For classes packaged with the web application, the class and each super class needs to be checked for a match with {@link HandlesTypes} or for an annotation that matches {@link HandlesTypes}.

对于与Web应用程序打包在一起的类,需要检查该类和每个 超类是否与 @HandlesTypes 指定的类相匹配, 或是否与 @HandlesTypes 指定的注解相匹配。

关于这个方法的执行过程,就不分析了。

3. handlesTypesOnly

ContextConfig#processAnnotationsStream(...)方法运行到这里,接着就是判断handlesTypesOnly的值了,如果为true就直接返回了,剧透下,这里的handlesTypesOnly的值就是为true,那么这个值有何玄机呢?

它的赋值在ContextConfig#scanWebXmlFragment(...)方法,代码如下:

// Only need to scan for @HandlesTypes matches if any of the
// following are true:
// - it has already been determined only @HandlesTypes is required
//   (e.g. main web.xml has metadata-complete="true"
// - this fragment is for a container JAR (Servlet 3.1 section 8.1)
// - this fragment has metadata-complete="true"
boolean htOnly = handlesTypesOnly || !fragment.getWebappJar() ||
        fragment.isMetadataComplete();

上面的翻译是说,这个属性是表示仅仅只处理@HandlesTypes,在以下几点情况下为true

  1. 仅仅只需要处理@HandlesTypes,意思是可以在外面设置handlesTypesOnly的值;
  2. 以 jar 包运行方式,应该是指内嵌的方式运行,区别于以往运行时都要搞个webapp文件夹、要web.xml文件;
  3. 在web.xml文件中指定metadata-complete="true".

本系列的示例demo中没有webapp文件夹,也没有web.xml文件,handlesTypesOnly的值也是true.

4. processClass(...)

虽然ContextConfig#processAnnotationsStream(...)方法运行到if (handlesTypesOnly) 就返回了,但我们还是来看看processClass(...)干了些什么:

    protected void processClass(WebXml fragment, JavaClass clazz) {
        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
                }
            }
        }
    }

如果对servlet 3.0 比较熟悉,就会发现这是3个熟悉的面孔:

  • @WebServlet:用来标记 servlet 类,标记了该注解的类会被注册为Servlet
  • @WebFilter:用来标记 filter 类,标记了该注解的类会被注册为Filter
  • @WebListener:用来标记 listener 类,标记了该注解的类会被注册为Listener

servlet 3.0 时代,我们注册servletfilterlistener,只需要使用对应的注解就可以了,十分方便!不过,这个“方便”是有前提的,前面刚 分析过handlesTypesOnly为true时,processClass(...)方法是不会执行的!不过springboot框架为我们解决了这个问题,在springboot中,我们依然可以大胆地使用以上3个注解,关于它的解决办法,可以参考springboot web应用之servlet组件的注册流程

最后,我们再来看看它最终把servletfilterlistener注解到了哪里:

  • servletWebXml#servlets,类型为Map<String,ServletDef>
  • filterWebXml#filters,类型为Map<String,FilterDef>
  • listenerWebXml#listeners,类型为Set<String>

这些位置还是比较重要的,后面我们处理servletfilterlistener时,就是从这里拿到的。

2.3 配置ContextContextConfig#configureContext

让我们再回到ContextConfig#webConfig(...)方法:

    protected void webConfig() {
        ...

        // 应用配置,将 webXml 中的配置信息应用用Context
        configureContext(webXml);
        ...
    }

虽然我们的项目中并没有web.xml文件,但tomcat依然会生成webXml对象,该对象用来保存项目中的配置,如上面得到的servletfilterlistener,在这之后会调用ContextConfig#configureContext方法将这些配置应用到context,这里我们主要关注servlet三大组件的应用,进入ContextConfig#configureContext方法:

    private void configureContext(WebXml webxml) {
        ...
        for (FilterDef filter : webxml.getFilters().values()) {
            if (filter.getAsyncSupported() == null) {
                filter.setAsyncSupported("false");
            }
            context.addFilterDef(filter);
        }
        for (String listener : webxml.getListeners()) {
            context.addApplicationListener(listener);
        }
        for (ServletDef servlet : webxml.getServlets().values()) {
            Wrapper wrapper = context.createWrapper();
            // 省略了好多属性配置
            ...
            wrapper.setName(servlet.getServletName());
            wrapper.setServletClass(servlet.getServletClass());
            // 添加到 Context 子 容器
            context.addChild(wrapper);
        }
        ...
    }

这个方法非常长,我这里仅列出了servletfilterlistener三大组件的处理,最终这些组件都会添加到Context中,后面我们会看到这些组件是如果拿出来使用。

2.4 保存ServletContainerInitializer

让我们再回到ContextConfig#webConfig(...)方法:

    protected void webConfig() {
        ...

        // 将 ServletContainerInitializer 添加到Context
        if (ok) {
            for (Map.Entry<ServletContainerInitializer,
                    Set<Class<?>>> entry : initializerClassMap.entrySet()) {
                if (entry.getValue().isEmpty()) {
                    context.addServletContainerInitializer(entry.getKey(), null);
                } else {
                    context.addServletContainerInitializer(entry.getKey(), entry.getValue());
                }
            }
        }
        ...
    }

获得了ServletContainerInitializer,接下来就是找个地方把它们保存起来了,我们来看看它保存到哪里了,进入StandardContext#addServletContainerInitializer方法:

    private Map<ServletContainerInitializer,Set<Class<?>>> initializers =
        new LinkedHashMap<>();

    /**
     * 添加到 StandardContext#initializers 结构中
     */
    public void addServletContainerInitializer(
            ServletContainerInitializer sci, Set<Class<?>> classes) {
        initializers.put(sci, classes);
    }

可以看到,最终是添加到了StandardContext#initializers中了。

到此,ServletContainerInitializer的加载就算完成了。

3. 执行ServletContainerInitializers#onStartup(...)方法

我们再回到StandardContext#startInternal()方法,继续进行下面的分析:

protected synchronized void startInternal() throws LifecycleException {
    // 省略了一些代码
    ...

    // 3. 处理 ServletContainerInitializers
    for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
        initializers.entrySet()) {
        try {
            // 执行 ServletContainerInitializers#onStartup(...)方法
            entry.getKey().onStartup(entry.getValue(), getServletContext());
        } catch (ServletException e) {
            ...
        }
    }
    ...
}

这块代码所做的工作就是执行ServletContainerInitializers#onStartup(...)方法,上一 节中,费了老大劲终于集齐了所有的ServletContainerInitializer,保存的结构为StandardContext#initializers

    /**
     * 保存 ServletContainerInitializer 的实例类,
     * key:ServletContainerInitializer 实例,也就是
     *         `META-INF/services/xxx.ServletContainerInitializer`指定类的实例
     * value:引入类,就是在 ServletContainerInitializer 上使用 @HandlesTypes 指定的类 
     *
     */
    private Map<ServletContainerInitializer, Set<Class<?>>> initializers =
        new LinkedHashMap<>();

我们来看下ServletContainerInitializers#onStartup(...)的参数就明白了:

/**
 * @param c:Class 集合,由 @HandlesTypes 指定的类
 * @param ctx:ServletContext,由 servlet 提供
 */
void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;

前面分析了Set<Class<?>> c参数的获取,接着我们来看看ServletContext ctx的获取,也就是以下代码中,getServletContext()所做的工作:

// 执行 ServletContainerInitializers#onStartup(...)方法
// getServletContext():获取 ServletContext
entry.getKey().onStartup(entry.getValue(), getServletContext());

3.1 StandardContext#getServletContext()

StandardContext#getServletContext()方法如下:

    protected ApplicationContext context = null;

    /**
     * 获取 ServletContext
     */
    @Override
    public ServletContext getServletContext() {
        // 这个 context 指的是 ApplicationContext
        if (context == null) {
            context = new ApplicationContext(this);
            if (altDDName != null)
                context.setAttribute(Globals.ALT_DD_ATTR,altDDName);
        }
        return context.getFacade();
    }

最终返回的ServletContext是调用context.getFacade()返回的,我们来看看ApplicationContext#getFacade()的实现:

/**
 * ApplicationContext 实现了 ServletContext
 */
public class ApplicationContext implements ServletContext {

    /**
     * 提供对外访问的类 ServletContext
     */
    private final ServletContext facade = new ApplicationContextFacade(this);

    /**
     * 获取 ServletContext
     */
    protected ServletContext getFacade() {
        return this.facade;
    }

    ...
}

ApplicationContext中有一个成员变量facade,类型为ApplicationContextFacadeApplicationContext#getFacade()就是用来返回这个facade的,继续查看ApplicationContextFacade的构造方法:

public class ApplicationContextFacade implements ServletContext {

    /**
     * 这个 context 就是前面的 ApplicationContext
     */
    private final ApplicationContext context;

    /**
     * 构造方法
     */
    public ApplicationContextFacade(ApplicationContext context) {
        ...
        this.context = context;
        ...
    }

    ...

}

这是个典型的门面模式,在ApplicationContextFacade方法中,可以看到它实例调用的是ApplicationContext提供的方法:

public class ApplicationContextFacade implements ServletContext {

    ...

    @Override
    public int getMajorVersion() {
        return context.getMajorVersion();
    }


    @Override
    public int getMinorVersion() {
        return context.getMinorVersion();
    }

    ...
}

其他方法也是这么调用的,就不一一列举了。

3.2 注册servletServletContext#addServlet(...)

我们回到MyServletContainerInitializer类,执行ServletContainerInitializers#onStartup(...)方法方法后,我们自定义的MyServletContainerInitializer#onStartup会被调用到:

public void onStartup(Set<Class<?>> clsSet, ServletContext servletContext)
        throws ServletException {
    MyHttpServlet servlet = new MyHttpServlet();
    ServletRegistration.Dynamic registration = servletContext.addServlet("servlet", servlet);
    // loadOnStartup 设置成 -1 时,只有在第一次请求时,才会调用 init 方法
    registration.setLoadOnStartup(-1);
    registration.addMapping("/*");
}

可以看到,注册servlet是通过ServletContext#addServlet(...)方法进行的,我们跟进去,进入的是ApplicationContextFacade#addServlet(...)

@Override
public ServletRegistration.Dynamic addServlet(String servletName,
        Servlet servlet) {
    if (SecurityUtil.isPackageProtectionEnabled()) {
        return (ServletRegistration.Dynamic) doPrivileged("addServlet",
                new Class[]{String.class, Servlet.class},
                new Object[]{servletName, servlet});
    } else {
        // 这里会被执行
        return context.addServlet(servletName, servlet);
    }
}

这个context就是ApplicationContext,我们继续,进入ApplicationContext#addServlet(...)方法:

private ServletRegistration.Dynamic addServlet(String servletName, String servletClass,
            Servlet servlet, Map<String,String> initParams) throws IllegalStateException {

        ...

        // 获取 wrapper
        Wrapper wrapper = (Wrapper) context.findChild(servletName);

        // 如果servletName对应的wrapper不存在,就会将servlet包装为wrapper,并添加到Context的子容器中,
        // 这里的wrapper具体实现为StandardWrapper,Context的子容器为Wrapper
        if (wrapper == null) {
            wrapper = context.createWrapper();
            wrapper.setName(servletName);
            // 这个 context 为 StandardContext
            context.addChild(wrapper);
        } else {
            ...
        }

        // 设置 servlet 的一些参数,设置到 wrapper
        ...

        if (initParams != null) {
            for (Map.Entry<String, String> initParam: initParams.entrySet()) {
                wrapper.addInitParameter(initParam.getKey(), initParam.getValue());
            }
        }
        // 返回注册对象
        ServletRegistration.Dynamic registration =
                new ApplicationServletRegistration(wrapper, context);
        ...
        return registration;
    }

添加servlet的操作还是比较简单的,流程如下:根据servletName查找wapper,如果wapper不存在,就将的servlet包装为Wrapper对象,添加到Context的子容器中,最后返回该servlet的注册对象registration

这个registration是个啥呢,为何对它操作可以影响到servlet?我们进入ApplicationServletRegistration

public class ApplicationServletRegistration implements ServletRegistration.Dynamic {

    /** servlet 的包装类,实现为 StandardWrapper */
    private final Wrapper wrapper;

    /** 实现类为 StandardContext */
    private final Context context;

    /**
     * 赋值操作
     */
    public ApplicationServletRegistration(Wrapper wrapper, Context context) {
        this.wrapper = wrapper;
        this.context = context;

    }

    /**
     * 设置 loadOnStartup 属性
     */
    @Override
    public void setLoadOnStartup(int loadOnStartup) {
        wrapper.setLoadOnStartup(loadOnStartup);
    }

    ...
}

ApplicationServletRegistration实现了ServletRegistration.DynamicServletRegistration.Dynamic是servlet提供的,用来处理servlet注册的,ApplicationServletRegistration中有两个成员变量:wrappercontext,其中wrapperservlet 的包装类,实现为 StandardWrapper

我们在调用ServletRegistration.Dynamic#setLoadOnStartup(...)方法时,最终会调用Wrapper#setLoadOnStartup(...)方法,其他属性设置的方式也是类似的操作,最终都会调用Wrapper相关的方法,这也就是registration能处理servlet属性的原因了。

4. 加载servlet组件

让我们回到StandardContext#startInternal方法:

protected synchronized void startInternal() throws LifecycleException {
    // 省略了一些代码
    ...


    // 4. 加载 Listener
    if (ok) {
        if (!listenerStart()) {
            log.error(sm.getString("standardContext.listenerFail"));
            ok = false;
        }
    }

    // 5. 加载 filter
    if (ok) {
        if (!filterStart()) {
            log.error(sm.getString("standardContext.filterFail"));
            ok = false;
        }
    }

    // 6. 处理 servlet,加载与初始化,Context 的 child 为 Wrapper,用来存放servlet的
    if (ok) {
        if (!loadOnStartup(findChildren())){
            log.error(sm.getString("standardContext.servletFail"));
            ok = false;
        }
    }

    ...
}

前面做了那么多的准备工作,终于开始加载servlet的三大组件了!

4.1 加载Listener

Listener的加载操作如下:

// 4. 加载 Listener
if (ok) {
    if (!listenerStart()) {
        log.error(sm.getString("standardContext.listenerFail"));
        ok = false;
    }
}

我们进入StandardContext#listenerStart方法:

    public boolean listenerStart() {

        // findApplicationListeners:获取 listeners,
        // 里面的内容就是前面在 ContextConfig#configureContext 添加的
        String listeners[] = findApplicationListeners();
        Object results[] = new Object[listeners.length];
        boolean ok = true;
        for (int i = 0; i < results.length; i++) {
            try {
                String listener = listeners[i];
                // 创建监听器实例
                results[i] = getInstanceManager().newInstance(listener);
            } catch (Throwable t) {
                ...
            }
        }

        // 省略了一些代码
        ...

        // 处理各种 listener,真想不到 servlet 竟然提供了这么多类型的 Listener
        List<Object> eventListeners = new ArrayList<>();
        List<Object> lifecycleListeners = new ArrayList<>();
        for (Object result : results) {
            if ((result instanceof ServletContextAttributeListener)
                    || (result instanceof ServletRequestAttributeListener)
                    || (result instanceof ServletRequestListener)
                    || (result instanceof HttpSessionIdListener)
                    || (result instanceof HttpSessionAttributeListener)) {
                eventListeners.add(result);
            }
            if ((result instanceof ServletContextListener)
                    || (result instanceof HttpSessionListener)) {
                lifecycleListeners.add(result);
            }
        }
        // 省略了一些 代码
        ...
    }

这个方法还是挺清晰的,大致流程如下:

  1. 获取所有的listener,在ContextConfig#configureContext方法中,会将 listener 添加到 Context中,现在findApplicationListeners(...)方法来获取了;
  2. 获取到listener后,使用反射进行实例化;
  3. 接下来就是对Listener进行分类了,想不到servlet提供的listener类型竟然有这么多。

得到的这些listener在哪里运行呢?由于listener类型的不同,监听的事件也不同,因此他们执行的时机也各不相同,只能根据具体的listener类型来分析了。

这里我们还是以spring为例,看看spring提供的一个重要listnerContextLoaderListener,它实现了ServletContextListener

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent event) {
        ...
    }

    ...
}

这个ServletContextListener#contextInitialized方法是在哪里执行的呢?还是在StandardContext#listenerStart方法:

public boolean listenerStart() {
    // 省略了好多代码
    ...

    // 处理 ServletContextListener
    for (Object instance : instances) {
        if (!(instance instanceof ServletContextListener)) {
            continue;
        }
        // 调用 ServletContextListener#contextInitialized(...)
        ServletContextListener listener = (ServletContextListener) instance;
        try {
            fireContainerEvent("beforeContextInitialized", listener);
            // 在这里调用的
            if (noPluggabilityListeners.contains(listener)) {
                listener.contextInitialized(tldEvent);
            } else {
                listener.contextInitialized(event);
            }
            fireContainerEvent("afterContextInitialized", listener);
        } catch (Throwable t) {
            ...
        }
    }
    return ok;
}

StandardContext#listenerStart方法的最后,会遍历所有的listener,找到ServletContextListener类型的,然后一一执行ServletContextListener#contextInitialized方法。

4.2 加载filter

处理 filter 加载的代码如下:

if (!filterStart()) {
    log.error(sm.getString("standardContext.filterFail"));
    ok = false;
}

我们进入StandardContext#filterStart方法:

/** 保存 ApplicationFilterConfig */
private Map<String, ApplicationFilterConfig> filterConfigs = new HashMap<>();

/**
 * 处理 filter
 */
public boolean filterStart() {
    boolean ok = true;
    synchronized (filterConfigs) {
        filterConfigs.clear();
        for (Entry<String,FilterDef> entry : filterDefs.entrySet()) {
            String name = entry.getKey();
            try {
                // 生成 filterConfig 对象,然后保存 filter
                ApplicationFilterConfig filterConfig =
                        new ApplicationFilterConfig(this, entry.getValue());
                filterConfigs.put(name, filterConfig);
            } catch (Throwable t) {
                ...
            }
        }
    }
    return ok;
}

这个方法只是将filter转换为ApplicationFilterConfig,然后保存到StandardContext#filterConfigs中,看来在这一步还没用上filter

4.3 加载servlet

处理servlet加载的代码如下:

if (!loadOnStartup(findChildren())){
    log.error(sm.getString("standardContext.servletFail"));
    ok = false;
}

loadOnStartup(findChildren())包含两个方法:

  1. findChildren():查找当前子容器,通过前面的分析可知,Context存放的是Wrapper,这个Wrapper又是servlet的包装类;
  2. loadOnStartup:处理 servlet 的加载的方法;

我们进入StandardContext#loadOnStartup方法:

public boolean loadOnStartup(Container children[]) {
    TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
    // 根据 loadOnStartup 排序
    for (Container child : children) {
        Wrapper wrapper = (Wrapper) child;
        int loadOnStartup = wrapper.getLoadOnStartup();
        // 如果 loadOnStartUp < 1,则不会在启动时加载
        if (loadOnStartup < 0) {
            continue;
        }
        // 处理排序操作,TreeMap 本身就是有序的,相同的 loadOnStartup 值放入同一个 ArrayList 中
        Integer key = Integer.valueOf(loadOnStartup);
        ArrayList<Wrapper> list = map.get(key);
        if (list == null) {
            list = new ArrayList<>();
            map.put(key, list);
        }
        list.add(wrapper);
    }

    // Load the collected "load on startup" servlets
    for (ArrayList<Wrapper> list : map.values()) {
        for (Wrapper wrapper : list) {
            try {
                // 加载
                wrapper.load();
            } catch (ServletException e) {
                ...
            }
        }
    }
    return true;
}

这个方法做了两件事:

  1. 排序:根据loadOnStartup的值排序,值小于0,则不进行处理,值相同则放入同一个ArrayList
  2. 加载:遍历排好序的wrapper,逐一调用其Wrapper#load()方法

我们来看看StandardWrapper#load做了什么:

/**
 * wrapper 的加载操作
 */
public synchronized void load() throws ServletException {
    // 加载 servlet,就是进行实例化的操作
    instance = loadServlet();
    if (!instanceInitialized) {
        // 初始化servlet,就是执行 Servlet#init(...) 方法
        initServlet(instance);
    }
    // 其他省略
    ...
}

这个方法先是调用loadServlet()方法得到servlet实例,然后再调用initServlet(...)方法进行初始化,我们先看loadServlet()方法,核心代码如下:

/**
 * 加载 serlvet,得到的是 servlet实例
 */
public synchronized Servlet loadServlet() throws ServletException {
    // 当前 servlet 已实例化了,直接返回
    if (!singleThreadModel && (instance != null))
        return instance;
    Servlet servlet;
    ...
    try {
        // 实例化 servlet
        servlet = (Servlet) instanceManager.newInstance(servletClass);
    } catch (ClassCastException e) {
        ...
    }
    ...
    return servlet;
}

这个方法就只是处理servlet对象的获取,当对象不存在时,就调用反射实例化了。

继续看看initServlet(...)方法,核心代码如下:

private synchronized void initServlet(Servlet servlet) throws ServletException {
    // 判断是否已初始化
    if (instanceInitialized && !singleThreadModel) return;
    try {
        // 调用 Servlet#init 方法
        if( Globals.IS_SECURITY_ENABLED) {
            boolean success = false;
            try {
                Object[] args = new Object[] { facade };
                SecurityUtil.doAsPrivilege("init", servlet, classType, args);
                success = true;
            } finally {
                if (!success) {
                    SecurityUtil.remove(servlet);
                }
            }
        } else {
            servlet.init(facade);
        }
        instanceInitialized = true;
    } catch (UnavailableException f) {
        ...
    }
}

这个方法就是用来执行servlet#init方法的。

5. 总结

本文分析了Context的启动流程,总结如下:

  1. 处理servlet 3.0规范,也就是加载ServletContainerInitializer
  2. 解析web配置,不仅是指web.xml,还有@HandlersTypes指定的类,以及@WebServlet@WebFilter@WebListener注解
  3. 将解析到的配置应用于Context,在这一步会把解析到的servletfilterlistener添加到Context
  4. 执行ServletContainerInitializer#onStartUp方法,将配置内容加入到Context后,接着就是应用这些配置了
  5. 加载servlet三大组件:servletfilterlistener

限于作者个人水平,文中难免有错误之处,欢迎指正!原创不易,商业转载请联系作者获得授权,非商业转载请注明出处。

本文首发于微信公众号 Java技术探秘,如果您喜欢本文,欢迎关注该公众号,让我们一起在技术的世界里探秘吧!