3、tomcat的启动过程

Sep 14, 2016


前言

在上一节我们已经大致了解了tomcat的结构了,本节开始我们来看看tomcat的启动流程。

Bootstrap

在使用maven搭建环境的时候,我们最后运行了org.apache.catalina.startup.Bootstrap的main函数,并指定的启动参数:

-Dcatalina.home=target/classes/ 
-Dcatalina.base=target/classes/ 
-Djava.endorsed.dirs=${catalina.base}endorsed 
-Djava.io.tmpdir=${catalina.base}temp 
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager 
-Djava.util.logging.config.file=${catalina.base}conf/logging.properties

这个类时候tomcat启动的入口类,我们先来看看这个类是如何启动tomcat的。

获取系统变量

org.apache.catalina.startup.Bootstrap这个类中,有一个静态初始化代码块,如下:

static {
    
    String userDir = System.getProperty("user.dir");

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

    // ... 省略计算文件路径的代码

    catalinaHomeFile = homeFile;
    System.setProperty(
            Globals.CATALINA_HOME_PROP, catalinaHomeFile.getPath());

    String base = System.getProperty(Globals.CATALINA_BASE_PROP);
    
    // ... 省略计算文件路径的代码
    
    System.setProperty(
            Globals.CATALINA_BASE_PROP, catalinaBaseFile.getPath());
}

这里的代码我们省略了大部分关于文件路径的计算,这些代码主要是根据系统变量计算最终的两个关键系统变量的:

  • catalinaHomeFile:tomcat应用的根目录
  • catalinaBaseFile:tomcat配置的根目录

一般来说,最终计算的结果都是catalinaHomeFile=catalinaBaseFile=tomcat的根目录,这里的计算也涉及我们传入的启动参数,所以设置启动参数真正的目的就是告诉tomcat最后的配置文件和启动目录在哪里,这个过程我们不做深入解释,有兴趣的同学可以自己读一下源码。

初始化Bootstrap

静态初始化完成之后,在main中,第一步是初始化Bootstrap类,代码如下:

if (daemon == null) {
    Bootstrap bootstrap = new Bootstrap();
    try {
        // 【1】 初始化
        bootstrap.init();
    } catch (Throwable t) {
        handleThrowable(t);
        t.printStackTrace();
        return;
    }
    daemon = bootstrap;
} else {
    Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}

这部门我们可以看到,在注释【1】的位置,对Bootstrap进行了初始化,我们来看一下这个初始化的过程:

public void init() throws Exception {
    // 【1】初始化类加载器
    initClassLoaders();

    Thread.currentThread().setContextClassLoader(catalinaLoader);

    SecurityClassLoad.securityClassLoad(catalinaLoader);

    if (log.isDebugEnabled())
        log.debug("Loading startup class");
        
    // 【2】加载启动类
    Class<?> startupClass =
        catalinaLoader.loadClass
        ("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.newInstance();

    if (log.isDebugEnabled())
        log.debug("Setting startup class properties");
    String methodName = "setParentClassLoader";
    Class<?> paramTypes[] = new Class[1];
    paramTypes[0] = Class.forName("java.lang.ClassLoader");
    Object paramValues[] = new Object[1];
    paramValues[0] = sharedLoader;
    // 【3】设置启动类的父类加载器
    Method method =
        startupInstance.getClass().getMethod(methodName, paramTypes);
    method.invoke(startupInstance, paramValues);

    catalinaDaemon = startupInstance;

}

初始化的过程一共做了三件事:

  • 初始化类加载器
  • 加载启动类(org.apache.catalina.startup.Catalina)
  • 设置启动类的父类加载器

这三件事分别在代码中的【1】【2】【3】标注了。

其中类加载器一共初始化了三个类加载器:

  • commonLoader:公共类加载器,是所有类的默认加载器
  • catalinaLoader:catalina加载器,用于加载catalina类
  • sharedLoader:分享加载器,在catalina启动后用来加载其他类的加载器

实际上默认情况下使用的加载器都是commonLoader,但是我们可以用系统变量指定catalinaLoadersharedLoader使用其他的类加载器。

加载启动类实际上是加载org.apache.catalina.startup.Catalina这个类。

设置启动类的父类加载器实际上就是设置Catalina内部使用的类加载器。

启动Tomcat

Bootstrap初始化完成之后,就会根据当前传入的启动参数决定做什么事情,默认情况不传入启动参数的话,就是start,经过一系列的判断之后,启动执行如下语句:

// 省略其他判断
} else if (command.equals("start")) {
    daemon.setAwait(true);
    // 【1】加载配置
    daemon.load(args);
    // 【2】启动应用
    daemon.start();
}

注意,这里的daemon对象其实就是bootstrap对象。

这里启动的时候做了两件事:

  • 加载配置
  • 启动应用

加载配置

我们来看看Bootstrap如何做的:

private void load(String[] arguments)
    throws Exception {

    // Call the load() method
    String methodName = "load";
    Object param[];
    Class<?> paramTypes[];
    if (arguments==null || arguments.length==0) {
        paramTypes = null;
        param = null;
    } else {
        paramTypes = new Class[1];
        paramTypes[0] = arguments.getClass();
        param = new Object[1];
        param[0] = arguments;
    }
    // 【2】使用反射调用Catalina的load方法
    Method method =
        catalinaDaemon.getClass().getMethod(methodName, paramTypes);
    if (log.isDebugEnabled())
        log.debug("Calling startup class " + method);
    method.invoke(catalinaDaemon, param);

}

这里的逻辑很好理解,实际上是使用反射调用了Catalina的load方法,这个方法我们后边再说。

启动应用

接下里看看Bootstrap如何启动应用:

public void start()
    throws Exception {
    if( catalinaDaemon==null ) init();
    // 【1】使用反射调用Catalina的start方法
    Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null);
    method.invoke(catalinaDaemon, (Object [])null);

}

这里的逻辑也很简单,只是使用反射调用了Catalin的start方法。

到这里我们可以看出,Bootstrap这个类其实并没有做太多真正的应用有关的事情,它的作用是把Catalina启动起来,实际上类似于我们操作系统的启动引导程序,而真正的操作系统是Catalina

Catalina

现在我们可以来看看Catalina是如何启动的。

在Bootstrap中我们可以看到,Catalina的启动分两步,loadstart

load()

先看第一步,加载配置。

public void load() {

    long t1 = System.nanoTime();
    // 【1】初始化系统变量
    initDirs();
    initNaming();
    
    // 【2】创建配置解析器
    Digester digester = createStartDigester();

    InputSource inputSource = null;
    InputStream inputStream = null;
    File file = null;
    try {
        
        // ... 省略配置文件对象构造过程
        
        // 【3】解析配置
        try {
            inputSource.setByteStream(inputStream);
            digester.push(this);
            digester.parse(inputSource);
        } catch (SAXParseException spe) {
            log.warn("Catalina.start using " + getConfigFile() + ": " +
                    spe.getMessage());
            return;
        } catch (Exception e) {
            log.warn("Catalina.start using " + getConfigFile() + ": " , e);
            return;
        }
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                // Ignore
            }
        }
    }

    getServer().setCatalina(this);
    getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
    getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());

    initStreams();

    try {
        // 【4】初始化服务器
        getServer().init();
    } catch (LifecycleException e) {
        if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
            throw new java.lang.Error(e);
        } else {
            log.error("Catalina.start", e);
        }
    }

    // ... 省略无关代码
}

上面的代码省略了很多不影响我们理解启动过程的代码,有兴趣的同学自行阅读。

在加载配置的过程,主要做了几件事情:

  • 初始化相关的系统参数
  • 创建配置解析器
  • 解析配置
  • 初始化服务器

初始化相关的系统参数,主要是初始化临时文件目录以及启动过程需要的一些系统变量,然后创建一个配置解析器来解析配置,最后初始化服务器。

这里我们暂时不关系各个环节具体的细节,因为这不影响我们理解tomcat的启动。我们只需要了解两个关键的问题即可。

解析配置

解析配置的过程,实际上就是解析server.xml的过程,这个过程根据解析的结果,创建了前面我们提到的ServerServiceEngineHostContext的所有对象并关联了他们之间的关系。

具体的解析细节我们这里也不做深入的讨论,因为讨论起来的篇幅可能会比较长,而且也不难理解,有兴趣的同学自己看一下代码即可。

初始化服务器

从这一步开始,其实就已经进入到前面我们提到的Tomcat组件的生命周期的初始化阶段了,在LifecycleBase类中,封装了生命周期的状态变化,现在我们来看一下这部分代码:

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

    try {
        setStateInternal(LifecycleState.INITIALIZING, null, false);
        // 【1】执行具体实现类的初始化方法
        initInternal();
        setStateInternal(LifecycleState.INITIALIZED, null, false);
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        setStateInternal(LifecycleState.FAILED, null, false);
        throw new LifecycleException(
                sm.getString("lifecycleBase.initFail",toString()), t);
    }
}

这里我们可以看出,除了维护组件的生命周期状态以外,并没有做其他事情,真正的初始化任务是由子类去完成的,Tomcat服务的默认实现是StandardServer,我们来看一下它的具体实现:

@Override
protected void initInternal() throws LifecycleException {

    super.initInternal();
    // 【1】设置全局缓存和元对象
    onameStringCache = register(new StringCache(), "type=StringCache");

    MBeanFactory factory = new MBeanFactory();
    factory.setContainer(this);
    onameMBeanFactory = register(factory, "type=MBeanFactory");
    // 【2】初始化全局命名资源
    globalNamingResources.init();
    // 【3】加载全局系统资源
    if (getCatalina() != null) {
        ClassLoader cl = getCatalina().getParentClassLoader();
        while (cl != null && cl != ClassLoader.getSystemClassLoader()) {
            if (cl instanceof URLClassLoader) {
                URL[] urls = ((URLClassLoader) cl).getURLs();
                for (URL url : urls) {
                    if (url.getProtocol().equals("file")) {
                        try {
                            File f = new File (url.toURI());
                            if (f.isFile() &&
                                    f.getName().endsWith(".jar")) {
                                ExtensionValidator.addSystemResource(f);
                            }
                        } catch (URISyntaxException e) {
                            // Ignore
                        } catch (IOException e) {
                            // Ignore
                        }
                    }
                }
            }
            cl = cl.getParent();
        }
    }
    // 【4】初始化所有Serrvice
    for (int i = 0; i < services.length; i++) {
        services[i].init();
    }
}

这里关于全局缓存和元对象的作用,我暂时也没弄明白,不过这不影响我们理解tomcat的初始化。 初始化全局命名资源,要是初始化server.xml中配置的全局命名资源,如数据库连接池,线程池等。 加载全局系统资源,主要是把所有类加载器能搜索到的jar包全部设置为系统资源。 最后把当前server所持有的全部service都初始化了。

这里我们可以看到,Server除了自身的初始化,实际上还往下调用了自己的内部对象初始化,就是service,我们来看下service的初始化,tomcat的service默认实现是StandardService:

@Override
protected void initInternal() throws LifecycleException {

    super.initInternal();
    // 【1】初始化容器(Engine)
    if (container != null) {
        container.init();
    }

    // 【2】初始化连接池
    for (Executor executor : findExecutors()) {
        if (executor instanceof JmxEnabled) {
            ((JmxEnabled) executor).setDomain(getDomain());
        }
        executor.init();
    }
    // 【3】初始化监听器
    mapperListener.init();

    // 【4】初始化连接器
    synchronized (connectorsLock) {
        for (Connector connector : connectors) {
            try {
                connector.init();
            } catch (Exception e) {
                String message = sm.getString(
                        "standardService.connector.initFailed", connector);
                log.error(message, e);

                if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"))
                    throw new LifecycleException(message);
            }
        }
    }
}

这里我们可以看到,service一共做了4件事情:

  • 初始化自己的容器(Engine)
  • 初始化连接池
  • 初始化监听器
  • 初始化连接器

注: 前面我们从tomcat的架构中已经知道,service和engine是一对一的关系,这里的container实际上就是Engine,它的实现类是StandardEngine

从这里我们可以看到,tomcat的初始化过程,实际上是从顶层容器一层一层往下调用持有对象的初始化方法来实现的。

其实不仅仅是初始化过程,tomcat中所有生命周期的过程都是一样的,从顶层开始往下逐层调用来完成整个生命周期的。

到这里我们就不在往下看每个容器的初始化过程了,逻辑比较简单,有兴趣的同学继续往下看即可。

start()

接下来我们来看一下Catalina的start方法:

public void start() {

    // ... 省略一些校验代码
    
    // 【1】启动服务
    try {
        getServer().start();
    } catch (LifecycleException e) {
        log.fatal(sm.getString("catalina.serverStartFail"), e);
        try {
            getServer().destroy();
        } catch (LifecycleException e1) {
            log.debug("destroy() failed for failed Server ", e1);
        }
        return;
    }

    long t2 = System.nanoTime();
    if(log.isInfoEnabled()) {
        log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");
    }

    // 【2】创建停止tomcat的钩子
    if (useShutdownHook) {
        if (shutdownHook == null) {
            shutdownHook = new CatalinaShutdownHook();
        }
        Runtime.getRuntime().addShutdownHook(shutdownHook);

        LogManager logManager = LogManager.getLogManager();
        if (logManager instanceof ClassLoaderLogManager) {
            ((ClassLoaderLogManager) logManager).setUseShutdownHook(
                    false);
        }
    }
    // 【3】阻塞线程
    if (await) {
        await();
        stop();
    }
}

Catalina的启动也比较简单,只是把Server启动起来,然后创建钩子等待停止tomcat的通知,最后阻塞线程,然后服务器开始等待请求。

因此主要的初始化任务在StandardServer中,我们看一下Server的启动:

@Override
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();
        }
    }
}

Server的启动逻辑也比较简单,和初始化逻辑一样逐层往下调用,我们再看一下Service的启动:

@Override
protected void startInternal() throws LifecycleException {

    if(log.isInfoEnabled())
        log.info(sm.getString("standardService.start.name", this.name));
    setState(LifecycleState.STARTING);
    
    // 【1】启动容器
    if (container != null) {
        synchronized (container) {
            container.start();
        }
    }
    // 【2】启动线程池
    synchronized (executors) {
        for (Executor executor: executors) {
            executor.start();
        }
    }
    // 【3】映射监听器
    mapperListener.start();

    // 【4】启动连接器
    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) {
                log.error(sm.getString(
                        "standardService.connector.startFailed",
                        connector), e);
            }
        }
    }
}

启动过程和初始化过程基本一致,主要启动了四个组件:

  • 启动容器
  • 启动线程池
  • 启动映射监听器
  • 启动连接器

这里我们不把所有组件的启动全部看完,我们只看连接器的启动,因为这个和我们下一节讲的接收请求关系密切。

连接器的启动

连接器的启动其实依赖于我们在xml中的配置,默认情况下,tomcat有如下两段配置:

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

这个决定了我们启动的时候有两个连接器,由于AJP的连接器我们很少用到,这里我们以HTTP连接器的启动为例来分析。

Connector的start方法实现如下:

@Override
protected void startInternal() throws LifecycleException {
    if (getPort() < 0) {
        throw new LifecycleException(sm.getString(
                "coyoteConnector.invalidPort", Integer.valueOf(getPort())));
    }
    setState(LifecycleState.STARTING);
    try {
        // 【1】启动协议处理器
        protocolHandler.start();
    } catch (Exception e) {
        String errPrefix = "";
        if(this.service != null) {
            errPrefix += "service.getName(): \"" + this.service.getName() + "\"; ";
        }

        throw new LifecycleException
            (errPrefix + " " + sm.getString
             ("coyoteConnector.protocolHandlerStartFailed"), e);
    }
}

这里我们可以看到,connector对象实际上是持有一个协议处理器,并且启动的时候调用了协议处理器的启动方法,这个协议处理器才是真正处理各种协议的对象,connector相当于它的代理类。

默认情况下,http协议连接器使用的协议处理器是org.apache.coyote.http11.Http11NioProtocol这个实现类,我们看一下这个处理器的启动方法:

@Override
public void start() throws Exception {
    if (getLog().isInfoEnabled())
        getLog().info(sm.getString("abstractProtocolHandler.start",
                getName()));
    try {
        endpoint.start();
    } catch (Exception ex) {
        getLog().error(sm.getString("abstractProtocolHandler.startError",
                getName()), ex);
        throw ex;
    }
}

协议处理器实际上也是调用enpoint对象的start()方法,其实就是启动入口对象,目前tomcat一共有四种入口对象:

org.apache.tomcat.util.net.NioEndpoint
org.apache.tomcat.util.net.Nio2Endpoint
org.apache.tomcat.util.net.AprEndpoint
org.apache.tomcat.util.net.JIoEndpoint

关于这四种协议我目前也不算特别了解,可能后续需要单独的章节来讨论,这里我们只看默认的http协议入口:

org.apache.tomcat.util.net.NioEndpoint

这个协议入口的启动方法如下:

@Override
public void startInternal() throws Exception {

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

        processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                socketProperties.getProcessorCache());
        eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                        socketProperties.getEventCache());
        nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                socketProperties.getBufferPool());
        // 【1】创建线程池
        if ( getExecutor() == null ) {
            createExecutor();
        }

        initializeConnectionLatch();

        // 【2】启动辨识线程
        pollers = new Poller[getPollerThreadCount()];
        for (int i=0; i<pollers.length; i++) {
            pollers[i] = new Poller();
            Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
            pollerThread.setPriority(threadPriority);
            pollerThread.setDaemon(true);
            pollerThread.start();
        }
        // 【3】启动接收器线程
        startAcceptorThreads();
    }
}

注: 虽然协议处理器调用的是协议入口的start方法,这个方法实际上在协议入口的抽象类org.apache.tomcat.util.net.AbstractEndpoint中实现了,并且内部调用了startInternal这个方法,所以具体的入口实现类需要实现的是startInternal这个方法。

协议入口的启动主要做了三件事:

  • 创建一个线程池并启动它
  • 启动一个辨识线程
  • 启动接收器线程

接收器线程的任务是接收请求,并把请求放到请求队列中等待响应,辨识线程的作用是从请求队列中取出请求,并把它交给应用处理,这两个线程是我们下节讨论tomcat接收请求的重点。

到这里之后,整个tomcat服务就已经启动完成,接下来就是等待请求了。

总结

本章我们已经看到了tomcat是如何启动的,这个过程的介绍比较简单,限于篇幅,我并没有详细介绍tomcat如何往下加载context,这部分的内容实际上是在配置解析的过程做的,这个过程目前并没有计划再哪一章节讨论,可能会需要独立一个章节来讨论,有兴趣的同学自己先读一下源码吧。