Tomcat学习(二):启动过程(2)

目录

1.服务器的启动

2.Web应用的加载

3.自动扫描机制

3.1 HostConfig

3.2 ContextConfig


1.服务器的启动

Tomcat学习(一):启动过程(1)中简单介绍了Tomcat的启动类、加载server.xml和初始化部分组件的过程。

在使用load方法初始化组件后,Catalina又调用了Server的start方法,开始了各组件的启动。和init一样,LifecycleBase也在startInternal前后增加了事件发布:

    public final synchronized void start() throws LifecycleException {

        if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) ||
                LifecycleState.STARTED.equals(state)) {
            return;
        }

        if (state.equals(LifecycleState.NEW)) {
            init();
        } else if (state.equals(LifecycleState.FAILED)) {
            stop();
        } else if (!state.equals(LifecycleState.INITIALIZED) &&
                !state.equals(LifecycleState.STOPPED)) {
            invalidTransition(Lifecycle.BEFORE_START_EVENT);
        }

        try {
            setStateInternal(LifecycleState.STARTING_PREP, null, false);
            startInternal();
            if (state.equals(LifecycleState.FAILED)) {
                // This is a 'controlled' failure. The component put itself into the
                // FAILED state so call stop() to complete the clean-up.
                stop();
            } else if (!state.equals(LifecycleState.STARTING)) {
                // Shouldn't be necessary but acts as a check that sub-classes are
                // doing what they are supposed to.
                invalidTransition(Lifecycle.AFTER_START_EVENT);
            } else {
                setStateInternal(LifecycleState.STARTED, null, false);
            }
        } catch (Throwable t) {
            // This is an 'uncontrolled' failure so put the component into the
            // FAILED state and throw an exception.
            handleSubClassException(t, "lifecycleBase.startFail", toString());
        }
    }

对于正在准备启动、正在启动、已启动三个状态,不做重复启动,日志记录后返回;如果还未初始化,则进行初始化;如果初始化失败,则停止启动;正常状态下,应该是已经初始化或者已停止才能启动,否则会抛异常。

然后发布准备启动事件,并执行startInternal方法,最后根据启动状态决定是停机、抛出异常还是发布已启动事件。

进入Server#startInternal,会立即发出CONFIGURE_START_EVENT事件,然后启动JNDI和所有的Service。JNDI的启动只是发布一个事件,没有额外操作,所以主要看Service的。

Service#startInternal和initInternal基本一致,只是调用方法变成了start而已。

1)Engine

StandardEngine的startInternal方法是委托父类ContainerBase完成的,主要工作就是:

  • 启动集群
  • 启动安全组件Realm
  • 启动子容器(即Host,对于Host来说就是Context,以此类推)
  • 启动Pipeline
  • 发布正在启动相关事件

在StandardHost中,首先向Pipeline添加一个ErrorReportValve,然后委托Container继续向下一级启动。这里有一个隐藏的Context加载方式。在Host启动完根据server.xml解析到的Context后,会发出生命周期时间,此时有一个LifecycleListener,名为HostConfig,就会处理该事件,从而到webapps(根据appBase属性配置)目录下扫描Web应用。该逻辑下面单独列出。

在StandardContext中,首先发出一个j2ee.state.starting广播,然后启动所属的JNDI服务。然后尝试加载Web应用,这一段也单独列出。由于加载Context时,会把Servlet一并解析、加载好,所以这次就不需要委托ContainerBase向下一级启动了。与Host类似,也会触发ContextConfig加载Web应用。该逻辑下面单独列出。

Pipeline的启动就简单很多,就是启动所有实现了Lifecycle接口的Valve,默认的几个Valve启动过程都是发布事件。

2)Executor

其实Tomcat的Executor线程池,也是基于JDK的线程池实现的,所以这里创建了任务队列TaskQueue(基于LinkedBlockingQueue)和ThreadPoolExecutor(基于JDK的ThreadPoolExecutor)

    protected void startInternal() throws LifecycleException {

        taskqueue = new TaskQueue(maxQueueSize);
        TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority());
        executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf);
        executor.setThreadRenewalDelay(threadRenewalDelay);
        if (prestartminSpareThreads) {
            executor.prestartAllCoreThreads();
        }
        taskqueue.setParent(executor);

        setState(LifecycleState.STARTING);
    }

3)MapperListener

MapperListener会尝试注册默认Host名,且将自身注册为Engine和所有Host的监听器。然后对于每个Host,将其注册到Mapper。

    public void startInternal() throws LifecycleException {

        setState(LifecycleState.STARTING);

        Engine engine = service.getContainer();
        if (engine == null) {
            return;
        }

        findDefaultHost();

        addListeners(engine);

        Container[] conHosts = engine.findChildren();
        for (Container conHost : conHosts) {
            Host host = (Host) conHost;
            if (!LifecycleState.NEW.equals(host.getState())) {
                // Registering the host will register the context and wrappers
                registerHost(host);
            }
        }
    }

Mapper和MapperListener是完成从请求URL到容器映射的关键类,具体作用会在请求处理中体现。

4)Connector启动了内部的ProtocolHandler,最后又启动了内部的Endpoint。以NioEndpoint为例,它对startInternal进行了实现:

    public void startInternal() throws Exception {

        if (!running) {
            running = true;
            paused = false;

            if (socketProperties.getProcessorCache() != 0) {
                processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                        socketProperties.getProcessorCache());
            }
            if (socketProperties.getEventCache() != 0) {
                eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                        socketProperties.getEventCache());
            }
            if (socketProperties.getBufferPool() != 0) {
                nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                        socketProperties.getBufferPool());
            }

            // Create worker collection
            if (getExecutor() == null) {
                createExecutor();
            }

            initializeConnectionLatch();

            // Start poller thread
            poller = new Poller();
            Thread pollerThread = new Thread(poller, getName() + "-ClientPoller");
            pollerThread.setPriority(threadPriority);
            pollerThread.setDaemon(true);
            pollerThread.start();

            startAcceptorThread();
        }
    }

首先是初始化了Processor、Event和Socket的缓存,也就是说Tomcat为了提高处理效率,是设计了缓存机制的。

接下来对线程池、LimitLatch(限制最大连接数)进行初始化。

下面一段的Poller和初始化时提前启动的Poller实现不同,但也还是根据SelectionKey做处理。

最后启动一个Acceptor线程,负责接收请求,然后包装请求。

Poller和Acceptor线程具体工作在请求处理环节介绍。

2.Web应用的加载

一个典型的Context(Web应用)配置如下:

<Context docBase="testapp" path="/test"/>

docBase代表其在webapps(Host的appBase属性配置的)下的文件夹名,path代表其URL的context部分,例如上面配置的应用,根请求地址是:http://localhost:8080/test。

1)首先是创建工作目录,位于 webapps/work/Engine名称/Host名称/Context名称

2)然后尝试加载WebResourceRoot,实现类为StandardRoot,包括的资源有五种:

private final List<List<WebResourceSet>> allResources =
            new ArrayList<>();
    {
        allResources.add(preResources);
        allResources.add(mainResources);
        allResources.add(classResources);
        allResources.add(jarResources);
        allResources.add(postResources);
    }

其中,mainResources指的是 /WEB-INF/classes/META-INF/resources 目录下的资源文件。pre、jar、post,都是context.xml(是Tomcat的配置文件)中指定的。class指的就是所有class文件了。这一步如果执行成功,就会调用StandardRoot的start方法,对加载到的资源进行初始化。

3)然后创建WebappLoader(就是每个应用私有的ClassLoader)。

4)然后创建Cookie处理器(默认实现为Rfc6265CookieProcessor)、初始化字符集映射、依赖检测、初始化JNDI服务。

5)接下来启动WebappLoader(是的,它也是一个Lifecycle实现类)。启动过程的主要工作有:

  • 设置classpath
  • 设置权限
  • 加载/WEB-INF/classes和/WEB-INF/lib下的资源
  • 发布事件

启动完成后,为它设置一些属性。

6)然后启动安全组件Realm。

7)发布CONFIGURE_START_EVENT事件,从而激活ContextConfig。

8)启动子节点(即StandardWrapper,靠ContextConfig创建)、Pipeline、会话管理器等组件。

9)将加载到的资源,全部传给ServletContext。

10)创建实例管理器,用于实例化Servlet、Filter。

11)创建Jar包扫描器并设置到ServletContext。

12)合并ServletContext和Context组件的参数。

13)调起ServletContainerInitializer对象(Servlet规范3.0版本引入,用于编程式配置Servlet、Filter)。

14)实例化应用监听器,包括SevletContextAttributeListener、ServletRequestAttributeListener、ServletRequestListener、HttpSessionIdListener、HttpSessionAttributeListener,HttpSessionListener、ServletContextListener。Spring MVC就是靠ServletContextListener启动DispatcherServlet的。

15)检测未覆盖的HTTP方法的安全约束

16)启动会话管理器

17)实例化FilterConfig、Filter,并调用Filter#init()初始化

18)如果web.xml中,load-on-startup值大于0,此处调用Wrapper的load方法开始实例化

19)启动后台定时处理线程,用来监控文件变更,如果有变化,就需要重新加载or部署

20)发布j2ee.state.running通知

21)释放WebResourceRoot资源

22)发布启动事件

3.自动扫描机制

在Engine启动部分,介绍了基于Tomcat生命周期事件机制设计的HostConfig、ContextConfig存在自动扫描的能力,在这里进行介绍。

3.1 HostConfig

首先看它的事件处理逻辑:

    public void lifecycleEvent(LifecycleEvent event) {

        // Identify the host we are associated with
        try {
            host = (Host) event.getLifecycle();
            if (host instanceof StandardHost) {
                setCopyXML(((StandardHost) host).isCopyXML());
                setDeployXML(((StandardHost) host).isDeployXML());
                setUnpackWARs(((StandardHost) host).isUnpackWARs());
                setContextClass(((StandardHost) host).getContextClass());
            }
        } catch (ClassCastException e) {
            log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
            return;
        }

        // Process the event that has occurred
        if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
            check();
        } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
            beforeStart();
        } else if (event.getType().equals(Lifecycle.START_EVENT)) {
            start();
        } else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
            stop();
        }
    }

如果是StandardHost发出的事件,就更新一下信息,然后处理四种事件:

1)PERIODIC_EVENT

该事件代表Web应用资源发生变化,需要重新加载or部署。check方法如下:

    protected void check() {

        if (host.getAutoDeploy()) {
            // Check for resources modification to trigger redeployment
            DeployedApplication[] apps =
                deployed.values().toArray(new DeployedApplication[0]);
            for (int i = 0; i < apps.length; i++) {
                if (!isServiced(apps[i].name))
                    checkResources(apps[i], false);
            }

            // Check for old versions of applications that can now be undeployed
            if (host.getUndeployOldVersions()) {
                checkUndeploy();
            }

            // Hotdeploy applications
            deployApps();
        }
    }

checkResource方法会检查每个已部署的应用的资源,从代码中可以看到,每个应用都维护了两个列表:redeployResources和reloadResources,记录了资源及其最后修改时间,前一个列表的资源如果发生变更,会导致重新部署,后一个则是重新加载。重新加载和重新部署的区别是,重新加载是Context的重启,重新部署则是重新创建Context。

对于一个资源,有三种情况:

  • 是目录,更新时间即可
  • 是WAR包,且Context的docBase属性结尾不是“.war”,即需要解压,那么就先删除旧目录,再重新加载,否则直接重新加载即可
  • 其他情况,直接重新加载即可

2)BEFORE_START_EVENT

此时Host刚初始化完毕,尚未启动,这一步主要是用来创建需要的目录

3)START_EVENT

该事件会在Host启动时触发,主要工作是完成Web应用的部署。

    protected void deployApps() {

        File appBase = host.getAppBaseFile();
        File configBase = host.getConfigBaseFile();
        String[] filteredAppPaths = filterAppPaths(appBase.list());
        // Deploy XML descriptors from configBase
        deployDescriptors(configBase, configBase.list());
        // Deploy WARs
        deployWARs(appBase, filteredAppPaths);
        // Deploy expanded folders
        deployDirectories(appBase, filteredAppPaths);

    }

可以看到,这里对三种不同形式的应用进行了部署。

  • 根据context描述文件部署

描述文件其实就是把server.xml中,Context元素独立出来,通过Host元素的xmlBase属性指定描述文件所在目录,未配置则默认是 $CATALINA_BASE/conf/Engine名称/Host名称。部署时,扫描配置文件目录,通过线程池提交一个解析任务。

解析任务中又使用了Digester对xml文件进行解析,然后构造一个Context对象,并利用Host的addChild方法,将Context添加到Host。在ContainerBase中,addChildInternal方法会判断当前容器是否已经启动,是的话就将新加入的子容器也一起启动了。最后还会添加对描述文件、Web应用目录、web.xml等的监视,一旦文件发生变更,就会触发重新加载或部署。

  • 部署WAR包

对于appBase目录下所有的WAR包(文件名不能是META-INF或WEB-INF),类似根据context描述文件部署,也是利用线程池提交任务来加载的。在提交前,如果配置了需要解压,会把WAR包展开成目录再操作。

如果deployXML属性为true且其内部包含META-INF/context.xml,则使用该文件创建Context对象,如果deployXML为false,但包含META-INF/context.xml,则创建FailedContext,表示部署失败。其他情况下,都是根据contextClass属性来反射创建Context对象,默认是StandardContext。

如果使用WAR包包含的描述文件,且copyXML属性为true,还会将描述文件复制到 $CATALINA_BASE/conf/Engine名称/Host名称 下,文件名和WAR包名称相同。

最后对Context对象设置一些属性,比如生命周期监听器、名称、路径、版本、docBase等,然后通过addChild方法添加到Host。

  • Web应用目录部署

基本上和WAR包的一样,可以理解成WAR包多了一步解压操作

4)STOP_EVENT

解除Host作为MBean的注册

3.2 ContextConfig

ContextConfig也是一个生命周期事件监听器,会处理6类事件,不过只有3个和Web应用加载关系比较大,所以只看这三个。

1)CONFIGURE_START_EVENT

负责解析web.xml,将Servlet包装为Wrapper,并创建Filter、ServletContextListener等Web容器相关的对象。

  • 初始化Web容器
  • 如果配置了ignoreAnnotations属性为false,则解析注解配置,并添加JNDI资源引用
  • 验证安全角色名称
  • 设置安全认证

第一步中,会加载 默认配置(即 Tomcat目录下conf/web.xml)、Web应用的 WEB-INF/web.xml、JAR包中的 META-INF/web-fragment.xml 和 META-INF/services/javax.servlet.ServletContainerInitializer。然后将从web-fragment.xml解析得到的WebXml对象进行排序,并将排序后各WebXml对应的来源JAR包名存入ServletContext的javax.servlet.context.orderedLibs属性。

对于这个orderedLibs,如果不为空,那么就会检查这些JAR包包含的ServletContainerInitializer实现,封装为initializerClassMap。再解析其HandlesTypes注解指定的Class列表,封装成typeInitializerMap(key为Class,值为ServletContainerInitializer集合)。

如果typeInitializerMap不为空,就会据此来处理/WEB-INF/classes下的类,查找@WebServlet、@WebFilter、@WebListener的等注解的内容,合并进WebXml对象。

然后,将所有的WebXml对象合并。

接下来就是配置JspServlet,以及用合并后的WebXml配置StandardContext,包括Servlet、Filter等Servlet规范规定的组件,还有欢迎文件(welcome-file-list配置)、postConstructMethod和preDestroyMethod等配置。在for (ServletDef servlet : webxml.getServlets().values()) {...}循环中,完成了Servlet封装为Wrapper的过程,并通过addChild添加到Context中。

接下来还会查找 WEB-INF/classes/META-INF/resources 下的静态资源,静态资源和上面解析出的ServletContainerInitializer信息,最后都会被设置到StandardContext中。

2)BEFORE_START_EVENT

该事件用于处理docBase配置,并且解决目录锁问题。

先看docBase的处理。首先获取了Host的appBase属性和Context的docBase属性,从而计算出docBase的绝对路径。如果docBase配置的是个WAR包,且需要解压,则解压之,并且将docBase属性更新为解包后的目录。否则就不用更新。

如果docBase是目录,但是有一个同名WAR包,也需要解压部署,则重新解压。

如果docBase配置的目录不存在,但存在同名WAR包,且需要解压部署,则解压并更新docBase属性。
然后看目录锁的处理。首先也是获取了Host的appBase属性和Context的docBase属性,计算出docBase的绝对路径。然后计算出工作目录下Web应用的目录名或WAR包名,并将源文件复制到工作目录,最后更新docBase属性为工作目录下的目录名 or WAR包名。

这一步的好处是不会对源文件加锁,这样就可以边运行边做修改。

3)AFTER_INIT_EVENT

这一步首先初始化了Digester,用来后续解析conf/context.xml、web.xml用。

然后解析了conf/context.xml,用来将Tomcat提供的默认配置更新到StandardContext对象中。并解析了conf/Engine名称/Host名称/context.xml.default文件(Host级的默认配置),同样进行更新。最后解析Context的configFile配置的context.xml文件,再次更新属性。

 

实际上在逻辑中,上面三个发生的顺序,应该是 3 - 2 - 1 才对。

 

至此,Tomcat就完成了所有组件的启动,并且开启了Socket监听,后面就是等待请求到达并处理了。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值