前文:

《Tomcat源码:启动类Bootstrap与Catalina的加载》

《Tomcat源码:容器的生命周期管理与事件监听》

《Tomcat源码:StandardServer与StandardService》

《Tomcat源码:Container接口》

        写在开头:本文为个人学习笔记,内容比较随意,夹杂个人理解,如有错误,欢迎指正。

前言

        在前文中,我们介绍了容器组件的公共接口Container接口,这个接口的抽象实现类ContainerBase实现了initInternal、startInternal这两个生命周期,规定了子容器中的大部分行为,本文我们就来继续深入到各个子容器中进行源码的分析。

        

目录

前言

一、StandardEngine

        1、initInternal

         2、startInternal

二、StandardHost

三、HostConfig

        生命周期监听器的添加

        生命周期监听器的执行

        start与deployApps

        deployWARs

        HostConfig#deployWAR

四、StandardContext

        1、initInternal

        2、startInternal

        触发事件监听器

        解析web.xml配置

        wrapper的创建

         启动监听器

        loadOnStartup

五、StandardWrapper

        1、startInternal

        2、load


一、StandardEngine

        Engine的实现类为StandardEngine,在前文中我们了解到StandardService在init时调用了engine.init()来初始化engine,StandardEngine没有重写Init方法,但由于StandardEngine继承了ContainerBase从而间接继承了LifecycleBase抽象类,得以复用LifecycleBase中的init方法,start方法也是如此,因此以engine为代表的这些子容器实际上只需要重写initInternal、startInternal即可。

    // LifecycleBase.java
    public final synchronized void init() throws LifecycleException {
        if (!state.equals(LifecycleState.NEW)) {
            invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
        }

        try {
            setStateInternal(LifecycleState.INITIALIZING, null, false);
            // 子类实现initInternal方法完成不同的初始化逻辑
            initInternal();
            setStateInternal(LifecycleState.INITIALIZED, null, false);
        } catch (Throwable t) {
            handleSubClassException(t, "lifecycleBase.initFail", toString());
        }
    }

        1、initInternal

        StandardEngine的initInternal方法中,第一步调用了getRealm() 方法,确保 StandardEngine 的父类 StandardEngine中的 Realm 类型的属性不为空,Realm 是 Tomcat 的特性功能,这里先略过。
        第二步调用了super.initInternal(),也就是ContainerBase的initInternal方法,该方法我们前文中做了介绍,简单来说就是创建一个用于启动子容器的线程池。

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

         ContainerBase的initInternal方法如下

    protected ThreadPoolExecutor startStopExecutor;
    
    protected void initInternal() throws LifecycleException {
        // 创建线程安全的队列
        BlockingQueue<Runnable> startStopQueue = new LinkedBlockingQueue<>();
        // 创建线程池
        startStopExecutor = new ThreadPoolExecutor(getStartStopThreadsInternal(), getStartStopThreadsInternal(), 10,
                TimeUnit.SECONDS, startStopQueue, new StartStopThreadFactory(getName() + "-startStop-"));
        startStopExecutor.allowCoreThreadTimeOut(true);
        // 调用父类LifecycleMBeanBase的初始化方法
        super.initInternal();
    }

         2、startInternal

        startInternal也和initInternal一样,直接调用了父类ContainerBase中的该方法,其作用便是利用initInternal方法中创建的线程池启动以当前容器的子容器(比如Engine中启动Host)。

    protected synchronized void startInternal() throws LifecycleException {
        if (log.isInfoEnabled()) {
            log.info(sm.getString("standardEngine.start", ServerInfo.getServerInfo()));
        }
        super.startInternal();
    }

         startInternal通过线程池异步启动当前容器的子容器。

    // 将要启动的子容器加入到线程池中异步启动
    Container children[] = findChildren();
    List<Future<Void>> results = new ArrayList<>();
    for (Container child : children) {
        results.add(startStopExecutor.submit(new StartChild(child)));
    }
    MultiThrowable multiThrowable = null;
    // 获取子容器启动结果
    for (Future<Void> result : results) {
        try {
            result.get();
           } catch (Throwable e) {
               log.error(sm.getString("containerBase.threadedStartFailed"), e);
               if (multiThrowable == null) {
                  multiThrowable = new MultiThrowable();
               }
               multiThrowable.add(e);
            }
    }
    // ...
    threadStart();

        startInternal启动过程中会调用threadStart方法启动当前容器的backgroundProcess,因为StandardEngine没有重写该方法,因此还是调用的ContainerBase中的实现,前文中已经做了解析,这里就不赘述了。《Tomcat源码:Container接口》

二、StandardHost

        Host时Engine的子容器,其结构也和StandardEngine类似,但StandardHost没有重写initInternal因此这里只分析startInternal。

        Host#startInternal的逻辑就是看自己的Pipeline对象里是否包含了 org.apache.catalina.valves.ErrorReportValve这个Valve对象,如果没有,就添加一个 org.apache.catalina.valves.ErrorReportValve 到自己的Pipeline 对象里。

        最后调用 ContainerBase 的 startInternal 方法,ContainerBase#startInternal 方法在上篇文章也讲到了,这里就不多讲了。

    protected synchronized void startInternal() throws LifecycleException {

        String errorValve = getErrorReportValveClass();
        if ((errorValve != null) && (!errorValve.equals(""))) {
            try {
                boolean found = false;
                Valve[] valves = getPipeline().getValves();
                for (Valve valve : valves) {
                    if (errorValve.equals(valve.getClass().getName())) {
                        found = true;
                        break;
                    }
                }
                if (!found) {
                    Valve valve = (Valve) Class.forName(errorValve).getConstructor().newInstance();
                    getPipeline().addValve(valve);
                }
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                log.error(sm.getString("standardHost.invalidErrorReportValveClass", errorValve), t);
            }
        }
        super.startInternal();
    }

三、HostConfig

        生命周期监听器的添加

        在Host之前的容器都属于server.xml中的配置,但context并不是其中的配置,因此context的读取方式就和之前的容器不同了,下面我们来介绍下context是如何成为host的child的。

        首先回到Catalina初始化中的createStartDigester方法,其中有这样一行:

digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"));

        这是要向Digester实例中添加多个规则(RuleSet),HostRuleSet继承了RuleSetBase类,并实现了它的抽像方法:addRuleInstances。下面来看这个方法的具体实现:

    public void addRuleInstances(Digester digester) {

        digester.addObjectCreate(prefix + "Host",
                                 "org.apache.catalina.core.StandardHost",
                                 "className");
        digester.addSetProperties(prefix + "Host");
        digester.addRule(prefix + "Host",
                         new CopyParentClassLoaderRule());
        digester.addRule(prefix + "Host",
                         new LifecycleListenerRule
                         ("org.apache.catalina.startup.HostConfig",
                          "hostConfigClass"));
        // 其余代码
        }

        首先创建了LifecycleListenerRule规则,然后在模式" Server/Service/Engine/Hos"下,将这个规则加到Host之上。这个规则正是tomcat的生命周期规则,因此在Host生命周期过程中会触发这个规则。

        以上归纳起来就一句话:HostConfig是一个生命周期监听器类,在tomcat加载配置文件的时候被添加到Host之上。结合我们之前介绍生命周期方法的内容可知,StandardHost启动的过程中,在其父类中改变了生命周期状态,导致这个监听器的触发。

        生命周期监听器的执行

        添加完了监听器,StandardHost在启动时就会触发监听器,下面我们看下监听器执行了哪些操作。

        start与deployApps

        首先是根据状态判断触发start方法。

    public void lifecycleEvent(LifecycleEvent event) {
        // 其余代码
        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状态时触发
            start();
        } else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
            stop();
        }
    }

         继续调用deployApps方法,这个方法里面将会过滤出WEB应用的地址(即webapps),然后对描述符部署、war包部署、文件夹部署这三种不同的部署方式做专门处理。

        注意这里获取WEB应用的webapps使用的时数组,因为一个webapps下面可能有多个web应用。

    // StandardHost.java
   private boolean deployOnStartup = true;    
 
   // HostConfig.java
   public void start() {
        // 其源代码
        if (host.getDeployOnStartup()) {
            deployApps();
        }
    }

    protected void deployApps() {
        // 这里的appBase会读取到StardHost中的 private String appBase = "webapps";
        File appBase = host.getAppBaseFile();
        File configBase = host.getConfigBaseFile();
        // 过滤出应用路径
        String[] filteredAppPaths = filterAppPaths(appBase.list());
        // 针对描述符部署、war包部署、文件夹部署这三种方式
        // Deploy XML descriptors from configBase
        deployDescriptors(configBase, configBase.list());
        // Deploy WARs
        deployWARs(appBase, filteredAppPaths);
        // Deploy expanded folders
        deployDirectories(appBase, filteredAppPaths);
    }

        deployWARs

        我们以最经典的部署包方式为例来深入看下

    protected void deployWARs(File appBase, String[] files) { 
        // initInternal时创建的用于异步启动子容器的线程池
        ExecutorService es = host.getStartStopExecutor();
        List<Future<?>> results = new ArrayList<>();

        for (String file : files) {
            if (file.equalsIgnoreCase("META-INF")) {
                continue;
            }
            if (file.equalsIgnoreCase("WEB-INF")) {
                continue;
            }
            File war = new File(appBase, file);
            if (file.toLowerCase(Locale.ENGLISH).endsWith(".war") && war.isFile() && !invalidWars.contains(file)) {
                ContextName cn = new ContextName(file, true);
                if (tryAddServiced(cn.getName())) {
                    try {
                        // 其余代码
                        // 发布war的任务提交到线程池中
                        results.add(es.submit(new DeployWar(this, cn, war)));
                    } catch (Throwable t) {
                        ExceptionUtils.handleThrowable(t);
                        removeServiced(cn.getName());
                        throw t;
                    }
                }
            }
        }
        for (Future<?> result : results) {
            try {
                result.get();
            } catch (Exception e) {
                log.error(sm.getString("hostConfig.deployWar.threaded.error"), e);
            }
        }
    }

         异步发布war包调用了静态内部类DeployWar中的run方法,可以看到其实际上是调用了外部类的deployWAR。

    private static class DeployWar implements Runnable {
        private HostConfig config;
        private ContextName cn;
        private File war;
        DeployWar(HostConfig config, ContextName cn, File war) {
            this.config = config;
            this.cn = cn;
            this.war = war;
        }
        @Override
        public void run() {
            try {
                // 发布单个war
                config.deployWAR(cn, war);
            } finally {
                config.removeServiced(cn.getName());
            }
        }
    }

        HostConfig#deployWAR

        可以看到这里将当前WEB应用构建为了context子容器作为child加入到了host中,并且给context添加了一个监听器ContextConfig,这个监听器将会在下文有非常重要的作用,具体内容我们会在下文介绍。

   protected void deployWAR(ContextName cn, File war) {
		// 其余代码
        Context context = null;
        boolean deployThisXML = isDeployThisXML(war, cn);
        try {
            // 其余代码
            // 这里读取的StartHost中的configClass变量
            // configClass = "org.apache.catalina.startup.ContextConfig";
            Class<?> clazz = Class.forName(host.getConfigClass());
            LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
            context.addLifecycleListener(listener);
            context.addLifecycleListener(listener);
            context.setName(cn.getName());
            context.setPath(cn.getPath());
            context.setWebappVersion(cn.getVersion());
            context.setDocBase(cn.getBaseName() + ".war");
          	// 添加StandardContext到当前Host中
            host.addChild(context);
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString("hostConfig.deployWar.error", war.getAbsolutePath()), t);
        } 
        // 其余代码
    }

         

四、StandardContext

        1、initInternal

        首先调用了父类的 initInternal 方法,也就是 ContainerBase#initInternal。
然后调用了 namingResources(NamingResourcesImpl类型的属性) 的 init 方法,这个 namingResources 跟 StandardServer 里的 globalNamingResources 类似,只不过 globalNamingResources 是全局的,而这里的 namingResources 只是这个 StandardContext 的。
        最后构造一个 Notification 对象并调用 broadcaster.sendNotification 方法来广播这个通知。broadcaster 是在 StandardContext 对象构造函数里初始化的。

    protected void initInternal() throws LifecycleException {
        super.initInternal();
        if (namingResources != null) {
            namingResources.init();
        }
        if (this.getObjectName() != null) {
            // 发布正在启动的JMX通知,可以通过添加NotificationListener监听Web应用的启动
            Notification notification = new Notification("j2ee.object.created", this.getObjectName(),
                    sequenceNumber.getAndIncrement());
            broadcaster.sendNotification(notification);
        }
    }

    public StandardContext() {
        super();
        pipeline.setBasic(new StandardContextValve());
        broadcaster = new NotificationBroadcasterSupport();
        if (!Globals.STRICT_SERVLET_COMPLIANCE) {
            resourceOnlyServlets.add("jsp");
        }
    }

        2、startInternal

        这段代码超过了三百行,因此这里直接以文字描述的方式简介一下其中的过程

1、发布正在启动的JMX通知,这样可以通过添加NotificationListener来监听Web应用的启动。

2、启动当前Context维护的NDI资源。

3、初始化当前Context使用的WebResourceRoot并启动。WebResourceRoot维护了Web应用所有的资源集合(Class文件、Jar包以及其他资源文件),主要用于类加载和按照路径查找资源文件。

4、创建Web应用类加载器(WebappLoader)。WebappLoader继承自LifecycleMBeanBase,在其启动时创建Web应用类加载器(WebappClassLoader)。此外,该类还提供了background-Process,用于Context后台处理。当检测到Web应用的类文件、Jar包发生变更时,重新加载Context。

5、如果没有设置Cookie处理器,则创建默认的Rfc6265CookieProcessor。

6、设置字符集映射(CharsetMapper),该映射主要用于根据Locale获取字符集编码。

7、初始化临时目录,默认为$CATALINA_BASE/work/<Engine名称><Host名称>/<Context名称>

8、Wb应用的依赖检测,主要检测依赖扩展,点完整性。

9、如果当前Context使用JNDI,则为其添加NamingContextListener。

10、启动Web应用类加载器(WebappLoader.start),此时才真正创建WebappClassLoader实例。

11、启动安全组件(Realm)。

12、发布CONFIGURE_START_EVENT事件,ContextConfig监听该事件以完成Servlet的创建.

13、启动Context子节点(Wrapper),在server.xml中配置的Wrapper。

14、启动Context维护的Pipeline。

15、创建会话管理器。如果配置了集群组件,则由集群组件创建,否则使用标准的会话管理器(StandardManager)。在集群环境下,需要将会话管理器注册到集群组件。

16、将Context的Web资源集合(org.apache.catalina.WebResourceRoot)添加到Servlet(ontext属性,属性名为org.apache,catalina.resources

17、创建实例管理器(InstanceManager),用于创建对象实例,如Servlet、Filter等。

18、将Jar包扫描器(JarScanner)添加到ServletContext)属性,属性名为org.apache.tomcat.JarScanner

19、合并ServletContext初始化参数和Context组件中的ApplicationParameter。合并原则:ApplicationParameter配置为可以覆盖,那么只有当ServletContext没有相关参数或者相关参数为空时添加;如果配置为不可覆盖,则强制添加,此时即使ServletContext配置了同名参数也不会生效。

20、启动添加到当前Context的ServletContainerInitializer。该类的实例具体由ContextConfig查找并添加。该类主要用于以可编程的方式添加Wb应用的配置,如Servlet、Filter等。

21、实例化应用监听器(ApplicationListener),分为事件监听器(ServletContextAttributeListener,ServletRequestAttributeListener,ServletRequestListener HttpSessionldListener,HttpSessionAttributeListener)以及生命周期监听(HttpSessionListener、ServletContextListener)。这些监听器可以通过Context部署描述文件、可编程的方式(ServletContainerInitializer)或者Web.xml添加,并且触发ServletContextListener.contextInitialized。

22、检测未覆盖的HTTP方法的安全约束。

23、启动会话管理器。

24、实例化FilterConfig(ApplicationFilterConfig)、Filter,并调用Filter.init初始化e

25、对于loadOnStartup≥O的Wrapper,调用Wrapper.load(),该方法负责实例化Servlet,并调用Servlet.init进行初始化。

26、启动后台定时处理线程。只有当backgroundProcessorDelay>O时启动,用于监控守护文件的变更等。当backgroundProcessorDelay≤O时,表示Context的后台任务由上级容器(Host)调度。

27、发布正在运行的MX通知。

28、调用WebResourceRoot.gc()释放资源(WebResourceRoot加载资源时,为了提高性能会缓存某些信息,该方法用于清理这些资源,如关闭JAR文件)。

29、设置Context的状态,如果启动成功,设置为STARTING(其父类LifecycleBase会自动将状态转换为STARTED),否则设置为FAILED。

        触发事件监听器

        在启动子容器前会先触发CONFIGURE_START_EVENT事件。

    // 发布CONFIGURE_START_EVENT事件,ContextConfig监听该事件完成Servlet创建
    fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);

        在上文StandardHost的最后我们讲到Host将Context添加为子容器时为其添加了一个监听器ContextConfig,下面我们来看下ContextConfig内部的操作。

        可以看到在CONFIGURE_START_EVENT事件时,会调用configureStart方法,该方法最重要的步骤为webConfig(),正是这个步骤解析了web.xml。

    public void lifecycleEvent(LifecycleEvent event) {
        context = (Context) event.getLifecycle();
        // 其余代码
        if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
            configureStart();
        }
    }

    protected synchronized void configureStart() {
        // 其余代码
        // 开始解析web.xml的配置
        webConfig();
        // 如果未配置忽略应用注解配置,那么对filter,servlet,listener进行Resource注解的搜索
        // Resource配置在类,字段,方法上,会根据资源的类型进行划分资源类型,添加到不同资源集合中,比如环境变量,JNDI等等资源
        if (!context.getIgnoreAnnotations()) {
            applicationAnnotationsConfig();
        }
        if (ok) {
            // 将context约束的角色和wrapper中注解RunAs或配置中设置的角色添加到context容器中,重复的不会被再次添加
            validateSecurityRoles();
        }
        // 配置验证器,如果没有Ralm或者实现了 Authenticator的管道阀,那么就会添加一个默认的NonLoginAuthenticator验证器
        if (ok) {
            authenticatorConfig();
        }
        // 其余代码
        // 如果配置context时没有遇到任何问题,那么就表示配置成功
        if (ok) {
            context.setConfigured(true);
        } else {
            log.error(sm.getString("contextConfig.unavailable"));
            context.setConfigured(false);
        }
    }

        解析web.xml配置

        Tomcat初始化Web容器的过程如下(ContextConfig.webConfig):

  1. 解析默认配置,生成WebXml对象(Tomcat使用该对象表示web.xml的解析结果)。先解析容器级配置,然后再解析Host级配置。这样对于同名配置,Host级将覆盖容器级。为了便于后续过程描述,我们暂且称之为“默认WebXml”。为了提升性能,ContextConfig对默认WebXml进行了缓存,以避免重复解析。
  2. 解析Web应用的web.xml文件。如果StandardContext的altDDName不为空,则将该属性指向的文件作为web,xml,否则使用默认路径,即WEB-NF/web.xml。解析结果同样为WebXml对象(此时创建的对象为主WebXml,其他解析结果均需要合并到该对象上)。暂时将其称为“主WebXml"。
  3. 扫描Web应用所有JAR包,如果包含META-INF/web-fragment.xml,则解析文件并创建WebXml对象。暂时将其称为“片段WebXml”。
  4. 将web-fragment.xml创建的WebXml对象按照Servlet规范进行排序,同时将排序结果对应的JAR文件名列表设置到ServletContext属性中,属性名为javax.servlet.context.orderedLibs。该排序非常重要,因为这决定了Filter等的执行顺序。

        篇幅原因,这里只看下读取web应用读取web.xml的步骤,可以看到首先是调用getContextWebXmlSource方法将web.xml转换为输入流后进行的解析。

    protected void webConfig() {
        WebXmlParser webXmlParser = new WebXmlParser(context.getXmlNamespaceAware(),
                context.getXmlValidation(), context.getXmlBlockExternal());

        Set<WebXml> defaults = new HashSet<>();
        defaults.add(getDefaultWebXmlFragment(webXmlParser));

        WebXml webXml = createWebXml();
        // 读取应用下的web.xml并解析
        InputSource contextWebXml = getContextWebXmlSource();
        if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
            ok = false;
        }
        // 其余代码
        // 添加wrapper为context的子容器
        configureContext(webXml);
    }

        这里将读取当前web应用下的web.xml。

    protected InputSource getContextWebXmlSource() {
        InputStream stream = null;
        InputSource source = null;
        URL url = null;

        String altDDName = null;
        // 获取ServletContext,如没有则创建一个,类别为ApplicationContext
        ServletContext servletContext = context.getServletContext();
        try {
            if (servletContext != null) {
                altDDName = (String)servletContext.getAttribute(Globals.ALT_DD_ATTR);
                // 其余代码
                // ApplicationWebXml = "/WEB-INF/web.xml";
                stream = servletContext.getResourceAsStream
                        (Constants.ApplicationWebXml);
                    try {
                        url = servletContext.getResource(
                                Constants.ApplicationWebXml);
                    } catch (MalformedURLException e) {
                        log.error(sm.getString("contextConfig.applicationUrl"));
                    }
            }
            // 其余代码
            source = new InputSource(url.toExternalForm());
            source.setByteStream(stream);
        } 
        // 其余代码
        return source;
    }

        wrapper的创建

        可以看到这里是将上文中解析完的web.xml中的配置里的servlet读取出来后创建wrapper并设置属性,最后添加到context中作为子容器。

        另外还有一个要注意的就是addApplicationListener,Spring启动中的关键监听器ContextLoaderListener便是这个时候被添加的。

    private void configureContext(WebXml webxml) {
        // 添加web.xml中的监听器
        for (String listener : webxml.getListeners()) {
            context.addApplicationListener(listener);
        }
        // 其余代码
        for (ServletDef servlet : webxml.getServlets().values()) {
            // 获取web.xml中配置的servlet
            Wrapper wrapper = context.createWrapper();
            // 启动项设置
            if (servlet.getLoadOnStartup() != null) {
                wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
            }
            wrapper.setName(servlet.getServletName());
            // 配置servlet初始化参数
            Map<String,String> params = servlet.getParameterMap();
            for (Entry<String, String> entry : params.entrySet()) {
                wrapper.addInitParameter(entry.getKey(), entry.getValue());
            }
            wrapper.setServletClass(servlet.getServletClass());
            // 添加为context的子容器
            context.addChild(wrapper);
        }
        for (Entry<String, String> entry :
                webxml.getServletMappings().entrySet()) {
            context.addServletMappingDecoded(entry.getKey(), entry.getValue());
        }
        // 其余代码
    }

        创建完后回到StandardContext的startInternal中就可以启动子容器了

    // StandardContext#startInternal
    // 启动Context子节点Wrapper(在server.xml中配置的)
    for (Container child : findChildren()) {
        if (!child.getState().isAvailable()) {
            child.start();
        }
    }

         启动监听器

        startInternal下面会调用listenerStart方法实例化并启动应用监听器

    // 实例化并启动应用监听器(ApplicationListener)
    if (ok) {
        if (!listenerStart()) {
            log.error(sm.getString("standardContext.listenerFail"));
            ok = false;
        }
    }

         listenerStart方法内部找出ServletContextListener类型的监听器,调用contextInitialized方法,这个方法就是我们Spring框架的启动点。

    public boolean listenerStart() {
        // 其余代码
        // 遍历所有监听器找出ServletContextListener类别的
        for (Object instance : instances) {
            if (!(instance instanceof ServletContextListener)) {
                continue;
            }
            ServletContextListener listener = (ServletContextListener) instance;
            try {
                fireContainerEvent("beforeContextInitialized", listener);
                // 调用其contextInitialized方法
                if (noPluggabilityListeners.contains(listener)) {
                    listener.contextInitialized(tldEvent);
                } else {
                    listener.contextInitialized(event);
                }
                fireContainerEvent("afterContextInitialized", listener);
            } 
            // 其余代码
        }
    }

         ContextLoaderListener继承了ServletContextListener,其contextInitialized将会启动Spring框架。

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
	public ContextLoaderListener() {}

	public ContextLoaderListener(WebApplicationContext context) {super(context);}

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

	@Override
	public void contextDestroyed(ServletContextEvent event) {
		closeWebApplicationContext(event.getServletContext());
		ContextCleanupListener.cleanupAttributes(event.getServletContext());
	}
}

        loadOnStartup

        在web.xml中我们对于需要启动时加载的servlet会配置<load-on-startup>1</load-on-startup>,这个功能的实现也是在StandardContext#startInternal中完成的。

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

        可以看到内部实现是交给了对应wrapper的load方法完成的,其具体内容我们下文介绍StandardWrapper时介绍。

    public boolean loadOnStartup(Container children[]) {
        // 对于设置了<load-on-startup>1</load-on-startup>的servlet调用wrapper的load方法
        TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
        for (Container child : children) {
            Wrapper wrapper = (Wrapper) child;
            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);
        }

        for (ArrayList<Wrapper> list : map.values()) {
            for (Wrapper wrapper : list) {
                try {
                    wrapper.load();
                } 
            }
        }
        return true;

    }

五、StandardWrapper

        StandardWrapper 没有重载initInternal 方法,我们从startInternal看起

        1、startInternal

        和其他容器差不多,唯一不同的是多了一个setAvailable。

    protected synchronized void startInternal() throws LifecycleException {
        if (this.getObjectName() != null) {
            Notification notification = new Notification("j2ee.state.starting", this.getObjectName(), sequenceNumber++);
            broadcaster.sendNotification(notification);
        }
        super.startInternal();
        // 方法设置 available 属性的值。
        setAvailable(0L);
        if (this.getObjectName() != null) {
            Notification notification = new Notification("j2ee.state.running", this.getObjectName(), sequenceNumber++);
            broadcaster.sendNotification(notification);
        }

    }

        从 available 的注释可以看出,它的作用是表示 Servlet 的可用时间的。 

    /**
     * The date and time at which this servlet will become available (in milliseconds since the epoch), or zero if the
     * servlet is available. If this value equals Long.MAX_VALUE, the unavailability of this servlet is considered
     * permanent.
     */
    protected long available = 0L;

        2、load

        在上文中,讲到了Context的startInternal 方法中做了一件事情就是调用 Wrapper 的 load 方法(在 StandardContext#loadOnStartup 中调用的)。在 StandardContext#startInternal 中先调用 Wrapper的 start 方法,然后调用 Wrapper 的 load 方法。

        load 方法逻辑很简单,先调用 loadServlet() 获取一个 Servlet 对象,就是通过 servletClass 属性指定的类名,调用 InstanceManager#newInstance(servletClass) 方法来创建一个 Servlet 对象。然后调用 initServlet(instance) 来初始化这个 Servlet 对象,也就是调用这个 Servlet 对象的 init 方法。

        可以看出 Wrapper 里有一个 Servlet 属性,Wrapper 正是对 Servlet 的包装。

    public synchronized void load() throws ServletException {
        // 创建当前Servlet 对象
        instance = loadServlet();
        if (!instanceInitialized) {
            // 调用Servlet对象的init方法来初始化
            initServlet(instance);
        }
        // 其余代码
    }

        至此,整个容器的启动过程就介绍完了,可以看到整个流程是由Server起步直到Wrapper结束。

        其中Server代表的是整个tomcat应用,Service代表的是server.xml中的service节点。而后续的Engine与Host都是service中的子节点。

        再到Context代表了webapps下的每个应用,子容器Wrapper表示web应用中的每个servlet。Context中的start方法中会创建当前web应用的ServletContext,并启动ServletContextListener监听器,启动web应用(典型的如Spring容器的启动点就是ContextLoaderListener这个ServletContextListener的实现类)。并对于需要启动时加载的servlet(loadOnStartup=1)调用其对应wrapper的load方法。

        最后再来回顾下前文StandardService中得Mapper 组件,Tomcat正是通过Mapper 组件来确定请求是由哪个 Wrapper 容器里的 Servlet 来处理的。

        举例来说,假如有一个网购系统,有面向网站管理人员的后台管理系统,还有面向终端客户的在线购物系统。这两个系统跑在同一个 Tomcat 上,为了隔离它们的访问域名,配置了两个虚拟域名:manage.shopping.com和user.shopping.com,网站管理人员通过manage.shopping.com域名访问 Tomcat 去管理用户和商品,而用户管理和商品管理是两个单独的 Web 应用。终端客户通过user.shopping.com域名去搜索商品和下订单,搜索功能和订单管理也是两个独立的 Web 应用。如下所示,演示了 url 应声 Servlet 的处理流程。

         假如有用户访问一个 URL,比如图中的http://user.shopping.com:8080/order/buy,Tomcat 如何将这个 URL 定位到一个 Servlet 呢?

  1. 首先,根据协议和端口号选定 Service 和 Engine。
  2. 然后,根据域名选定 Host。
  3. 之后,根据 URL 路径找到 Context 组件。
  4. 最后,根据 URL 路径找到 Wrapper(Servlet)。
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐