Tomcat Structure and Start-up process


Tomcat 顶层结构及启动过程

1、Tomcat 顶层结构

Tomcat 中最顶层的容器叫做 Server,代表整个服务器,Server 中包含至少一个 Service,用于提供具体服务。Service 主要包含两部分:Connector 和 Container。Connector 用于处理连接相关的事情,并提供 Socket 与 request、response 的转换,Container 用于封装和管理 Servlet,以及具体处理 request 请求。

一个 Tomcat 中只有一个 Server,一个 Server 可以包含多个 Service,一个 Service 只有一个 Container,但可以有多个 Connector(因为一个服务可以有多个连接,如同时提供 http 和 https 连接,也可以提供相同协议不同端口的连接),结构如下:

关于这一点,我们看看 Tomcat 的 server.xml 文件就可以看出个大概,下面是该文件的部分内容:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Note:  A "Server" is not itself a "Container", so you may not
     define subcomponents such as "Valves" at this level.
     Documentation at /docs/config/server.html
 -->
<Server port="8005" shutdown="SHUTDOWN">
  <!-- 先加载启动日志服务 -->
  <Listener className="org.apache.catalina.startup.VersionLoggerListener"/>
  <!-- 然后加载安全安全服务和 apr(Apache Portable Runtime) 环境 -->
  <Listener SSLEngine="on" className="org.apache.catalina.core.AprLifecycleListener"/>
  <!-- Prevent memory leaks due to use of particular java/javax APIs-->
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"/>
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener"/>
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/>

  <GlobalNamingResources>
    <Resource auth="Container" description="User database that can be updated and saved" factory="org.apache.catalina.users.MemoryUserDatabaseFactory" name="UserDatabase" pathname="conf/tomcat-users.xml" type="org.apache.catalina.UserDatabase"/>
  </GlobalNamingResources>

  <Service name="Catalina">

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

    <Engine defaultHost="localhost" name="Catalina">
        
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
      </Realm>

      <Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" pattern="%h %l %u %t &quot;%r&quot; %s %b" prefix="localhost_access_log" suffix=".txt"/>
      	<Context docBase="E:\apache-tomcat-9.0.35\webapps\Java_Web_Experiment" path="/Java_Web_Experiment" reloadable="true" source="org.eclipse.jst.jee.server:Java_Web_Experiment"/>
       </Host>
    </Engine>
  </Service>
</Server>

总结:一个 Server 下有多个 Service 共享服务器上的 JVM 环境,每个 Service 中有多个 Connector 和 一个 Container


Tomcat 里的 Server 由 org.apache.catalina.startup.Catalina 来管理,Catalina 是整个 Tomcat 的管理类,它里面的三个方法:load、start、stop 分别用来管理整个服务器的生命周期:

  • load 方法用于根据 conf/server.xml 文件创建 Server 并调用 Server 的 init 方法进行初始化
  • start 方法用于启动服务器
  • stop 方法用于停止服务器

start 和 stop 方法在内部分别调用了 Server 的 start 和 stop 方法,load 方法内部调用了 Server 的 init 方法,这三个方法都会按容器的结构逐层调用相应的方法,比如 Server 的 start 方法中会调用所有 Service 中的 start 方法,Service 中的 start 方法会调用所有包含的 Connectors 和 Container 中的 start 方法,这样整个服务器就启动了,init 和 stop 也是一样,这就是 Tomcat 生命周期的管理方式。

Catalina 还有个方法也很重要,就是 await 方法,Catalina 中的 await 方法直接调用 Server 的 await 方法,这个方法的作用是进入一个循环,让主线程不会退出。

不过 Tomcat 的入口 main 方法不在 Catalina 类里,而是在 org.apache.catalina.startup.Bootstrap 中。Bootstrap 的作用类似于一个 CatalinaAdaptor适配器模式),具体处理过程还是使用 Catalina 来完成的,这么做的好处是可以把启动的入口和具体的管理类分开,从而可以很方便的创建出多种启动方式,每种启动方式只需要写一个相应的 CatalinaAdaptor 就可以了。

2、Bootstrap 启动过程

Bootstrap 是 Tomcat 的入口,正常情况下启动 Tomcat 就是调用 Bootstrap 的 main 方法,代码如下:

// org.apache.catalina.startup.Bootstrap

// Daemon object used by main.
private static final Object daemonLock = new Object();
private static volatile Bootstrap daemon = null;

// Daemon reference
private Object catalinaDaemon = null;

public static void main(String args[]) {
	// 先创建一个 Bootstrap 实例
    synchronized (daemonLock) {
        if (daemon == null) {
            // Don't set daemon until init() has completed
            Bootstrap bootstrap = new Bootstrap();
            try {
                // 初始化了 ClassLoader,并用 ClassLoader 创建了 Catalina 实例,赋给 catalinaDaemon 变量
                bootstrap.init();
            } catch (Throwable t) {
                handleThrowable(t);
                t.printStackTrace();
                return;
            }
            daemon = bootstrap;
        } else {
            // When running as a service the call to stop will be on a new
            // thread so make sure the correct class loader is used to
            // prevent a range of class not found exceptions.
            Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
        }
    }

    try {
        String command = "start";
        if (args.length > 0) {
            command = args[args.length - 1];
        }

        if (command.equals("startd")) {
            args[args.length - 1] = "start";
            daemon.load(args);
            daemon.start();
        } else if (command.equals("stopd")) {
            args[args.length - 1] = "stop";
            daemon.stop();
        } else if (command.equals("start")) {
            daemon.setAwait(true);
            daemon.load(args);
            daemon.start();
            if (null == daemon.getServer()) {
                System.exit(1);
            }
        } else if (command.equals("stop")) {
            daemon.stopServer(args);
        } else if (command.equals("configtest")) {
            daemon.load(args);
            if (null == daemon.getServer()) {
                System.exit(1);
            }
            System.exit(0);
        } else {
            log.warn("Bootstrap: command \"" + command + "\" does not exist.");
        }
    } catch (Throwable t) {
        // Unwrap the Exception for clearer error reporting
        if (t instanceof InvocationTargetException &&
            t.getCause() != null) {
            t = t.getCause();
        }
        handleThrowable(t);
        t.printStackTrace();
        System.exit(1);
    }
}

main 方法非常简单,就做了两件事:首先实例化了 Bootstrap,然后执行 init 方法初始化 Catalina 实例;最后处理 main 方法传入的命令,如果 args 参数为空,默认执行 start。

在 init 方法中初始化了 ClassLoader,并用 ClassLoader 创建了 Catalina (org.apache.catalina.startup.Catalina)实例,然后赋给 CatalinaDaemon 变量,后面对命令的操作都需要使用 catalinaDaemon 来具体执行。

start 命令的处理调用了三个方法:

  • setAwait(true)
  • load(args)
  • start()

这三个方法内部都调用了 Catalina 的相应方法进行具体操作,只不过是用反射来调用的。这里以 start 方法为例(其余两个类似):

// org.apache.catalina.startup.Bootstrap
// Start the Catalina daemon
public void start() throws Exception {
    // 如果在 main 方法中没有成功调用 init() 初始化 CatalinaDaemon,这里再次调用
    if (catalinaDaemon == null) {
        init();
    }
	
    // 使用反射调用目标对象的方法
    Method method = catalinaDaemon.getClass().getMethod("start", (Class [])null);
    method.invoke(catalinaDaemon, (Object [])null);
}

Java 反射和注解

Method 对象代表目标 class 对象的一个方法,这里获取的是没有参数的名为 start 的方法,invoke 是执行该方法的意思。

3、Catalina 启动过程

从前面的内容(main 方法)可知,Catatlina 启动主要是调用 setAwait、load、start 方法来完成的,setAwait 方法用于设置 Server 启动完成后是否进入等待状态的标志,如果为 true 则进入,否则不进入;load 方法用于加载配置文件,创建并初始化 Server;start 方法用于启动服务器。

setAwait 方法

// org.apache.catalina.startup.Bootstrap#main()
} else if (command.equals("start")) {
    // 设置 await 为 true
    daemon.setAwait(true);
    // 创建 server
    daemon.load(args);
    // 启动服务器
    daemon.start();
    if (null == daemon.getServer()) {
        System.exit(1);
    }
}

// org.apache.catalina.startup.Bootstrap#setAwait(boolean await)
public void setAwait(boolean await)
    throws Exception {

    Class<?> paramTypes[] = new Class[1];
    paramTypes[0] = Boolean.TYPE;
    Object paramValues[] = new Object[1];
    paramValues[0] = Boolean.valueOf(await);
    Method method =
        catalinaDaemon.getClass().getMethod("setAwait", paramTypes);
    method.invoke(catalinaDaemon, paramValues);
}

// org.apache.catalina.startup.Catalina#setAwait(boolean b)
public void setAwait(boolean b) {
    await = b;
}

这个方法就是设置 await 属性的值,await 属性会在 start 方法中的服务器启动完之后使用它来判断是否进入等待状态。

init() 方法

Catalina 的 load 方法根据 conf/server.xml 创建了 Server 对象,并赋值给 server 属性(具体解析 xml 是由开源项目 Digester 完成的),然后调用了 server 的 init 方法,代码如下:

// org.apache.catalina.startup.Catalina
public void load() {

    if (loaded) {
        return;
    }
    loaded = true;

    long t1 = System.nanoTime();

    // 中间省略使用 Degister 解析 xml 创建 server 的代码

    // Start the new server
    try {
        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);
        }
    }

    long t2 = System.nanoTime();
    if(log.isInfoEnabled()) {
        // 启动过程中,控制台可以看到
        log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms");
    }
}

Catalina 的 start 方法主要调用了 server 的 start 方法启动服务器,并根据 await 属性判断是否让程序进入等待状态:

// org.apache.catalina.startup.Catalina
public void start() {

    // 两次检查 server 实例是否被创建
    if (getServer() == null) {
        load();
    }

    if (getServer() == null) {
        log.fatal("Cannot start server. Server instance is not configured.");
        return;
    }

    long t1 = System.nanoTime();

    // Start the new server
    try {
        // 调用 Server 的 start 方法启动服务器
        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");
    }

    // 省略了注册关闭钩子的代码

    // 进入等待状态
    if (await) {
        await();
        stop();
    }
}

这里首先判断 Server 是否已经存在了,如果不存在就调用 load 方法来初始化 Server,然后调用 Server 的 start 方法来启动服务器,最后注册了关闭钩子并根据 await 属性判断是否进入了等待状态,之前我们已经将 await 设置为 true,所以需要进入等待状态。进入等待状态会调用 await 和 stop 方法,await 方法直接调用 Server 的 await 方法,Server 的 await 方法内部会执行一个 while 循环,这样程序就停到了 await 方法,当 await 方法里的 while 循环退出时,将会执行 stop 方法,从而关闭服务器。

4、Server 启动过程

  • org.apache.catalina.Server
public interface Server extends Lifecycle {}

Server 是一个接口,代表整个 Catalina Servlet 容器。

Server 接口中提供 addService(Service service)removeService(Service service) 来添加和删除 Service,Server 的 init 方法和 start 方法分别循环调用了每个 Service 的 init 方法和 start 方法来启动所有 Service。

Server 的默认实现是 org.apache.catalina.core.StandardServer,StandardServer 继承自 LifecycleMBeanBase,LifecycleMBeanBase 又继承自 LifecycleBase,init 和 start 方法就定义在了 LifecycleBase 中,LifecycleBase 里的 init 方法和 start 方法又调用了 initInternal 方法和 startInternal 方法,这两个方法都是模板方法(模板方法模式),由子类具体实现,所以调用 StandardServer 的 init 和 start 方法会执行 StandardServer 自己的 initInternal 和 startInternal 方法,这就是 Tomcat 生命周期的管理方式。

StandardServer 中的 initInternal 和 startInternal 方法分别循环调用了每一个 Service 的 start 和 init 方法。

// org.apache.catalina.core.StandardServer
@Override
protected void startInternal() throws LifecycleException {
	
    // Start our defined Services
    synchronized (servicesLock) {
        for (Service service : services) {
            service.start();
        }
    }
}

@Override
protected void initInternal() throws LifecycleException {
	
    // ......
    // Initialize our defined Services
    for (Service service : services) {
        service.init();
    }
}

除了 startInternal 和 initInternal 方法,StandardServer 中还实现了 await 方法,Catalina 中就是调用它让服务器进入等待状态的,其核心代码如下:

@Override
public void await() {
    // 如果端口为 -2 表示不进入循环,Tomcat 为嵌入式的或者不需要使用端口
    if (port == -2) {
        // undocumented yet - for embedding apps that are around, alive.
        return;
    }
    // 如果端口为 -1 则进入循环,而且无法通过网络退出
    if (port==-1) {
        try {
            awaitThread = Thread.currentThread();
            while(!stopAwait) {
                try {
                    Thread.sleep( 10000 );
                } catch( InterruptedException ex ) {
                    // continue and check the flag
                }
            }
        } finally {
            awaitThread = null;
        }
        return;
    }

    // 如果端口不是 -1 或 -2(应该大于0),则会新建一个监听关闭命令的 ServerSocket	
    try {
        awaitSocket = new ServerSocket(port, 1,
                                       InetAddress.getByName(address));
    } 

    try {
        awaitThread = Thread.currentThread();

        // Loop waiting for a connection and a valid command
        while (!stopAwait) {
            ServerSocket serverSocket = awaitSocket;
            if (serverSocket == null) {
                break;
            }

            // Wait for the next connection
            Socket socket = null;
            StringBuilder command = new StringBuilder();
            try {
                InputStream stream;
                long acceptStartTime = System.currentTimeMillis();
                try {
                    socket = serverSocket.accept();
                    socket.setSoTimeout(10 * 1000);  // Ten seconds
                    stream = socket.getInputStream();
                } // ......
                
            }
            // 检查在指定端口接收到的命令是否和 shutdown 命令匹配
            boolean match = command.toString().equals(shutdown);
            if (match) {
                // 如果匹配就跳出循环
                log.info(sm.getString("standardServer.shutdownViaPort"));
                break;
            } else {
                log.warn(sm.getString("standardServer.invalidShutdownCommand", command.toString()));
            }
        }
    }
}

由于该方法比较长,所以省略了一些处理异常、关闭 Socket 以及对接收到数据处理的代码。

处理的逻辑大概是首先判断端口号 port,然后根据 port 的值分为三种处理方法:

  • port 为 -2,直接退出,不进入循环
  • port 为 -1,进入一个 while(!stopAwait)的循环,并且在内部没有 break 跳出的语句,stopAwait 标志只有调用了 stop 方法才会设置为 true,所以 port 为 -1 时只有外部调用 stop 方法才会退出循环
  • port 为其他值,也会进入一个 while(!stopAwait)的循环,不过同时会在 port 所在端口启动一个 ServerSocket 监听关闭命令,如果接收到则会使用 break 跳出循环。

这里的端口 port 和关闭命令 shutdown 是在 conf/server.xml 文件中配置 Server 时设置的,默认配置如下:

<Server port="8005" shutdown="SHUTDOWN">

这时会在 8005 端口监听 “SHUTDOWN” 命令,如果接收到了就会关闭 Tomcat,如果不想使用网络命令来关闭服务器可以将端口设置为 -1,另外 await 方法中从端口接收到数据后还会进行简单处理,如果接收到的数据中有 ASCII 码小于 32 的(ASCII 中 32 以下的为控制符)则会从小于 32 的那个字符截断并丢弃后面的数据。

5、Service 启动过程

Service 是一个接口,主要用于处理请求,内部包含一个或一组 Connectors 和一个 Conatiner。

它的标准实现是:org.apache.catalina.core.StandardService,StandardService 也继承自 LifecycleMBeanBase 类,所以 init 和 start 方法最终也会调用 initInternal 和 startInternal 方法。

@Override
protected void initInternal() throws LifecycleException {

    super.initInternal();

    if (engine != null) {
        engine.init();
    }

    // Initialize any Executors
    for (Executor executor : findExecutors()) {
        if (executor instanceof JmxEnabled) {
            ((JmxEnabled) executor).setDomain(getDomain());
        }
        executor.init();
    }

    // Initialize mapper listener
    mapperListener.init();

    // Initialize our defined Connectors
    synchronized (connectorsLock) {
        for (Connector connector : connectors) {
            try {
                connector.init();
            }
        }
    }
}

@Override
protected void startInternal() throws LifecycleException {

    setState(LifecycleState.STARTING);

    // Start our defined Container first
    if (engine != null) {
        synchronized (engine) {
            engine.start();
        }
    }

    synchronized (executors) {
        for (Executor executor: executors) {
            executor.start();
        }
    }

    mapperListener.start();

    // Start our defined Connectors second
    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();
                }
            }
        }
    }
}

可以看出 StandardService 中的 initInternal 和 startInternal 方法主要调用 container、executors、mapperListener、connectors 的 init 和 start 方法。

这里的 connectors 和 container 之前已经介绍过:

  • mapperListener:是 Mapper 的监听器,可以监听 container 容器的变化
  • executors:是用在 connectors 中管理线程的线程池,在 server.xml 配置文件中有参考用法,不过默认是注释起来的,打开注释就可以看到其使用方法
<Service name="Catalina">

    <!--The connectors can use a shared executor, you can define one or more named thread pools-->
    <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
        maxThreads="150" minSpareThreads="4"/>
    
    <!-- A "Connector" represents an endpoint by which requests are received
         and responses are returned. Documentation at :
         Java HTTP Connector: /docs/config/http.html
         Java AJP  Connector: /docs/config/ajp.html
         APR (HTTP/AJP) Connector: /docs/apr.html
         Define a non-SSL/TLS HTTP/1.1 Connector on port 8080
    -->
    <Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>
    
    <!-- A "Connector" using the shared thread pool-->
    <Connector executor="tomcatThreadPool"
               port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
</Service>

这样 Connector 就配置了一个叫做 tomcatThreadPool 的线程池,最多可以同时启动 150 个线程,最少要有 4 个可用线程,现在整个 Tomcat 就启动了,整个启动流程如下:

6、Tomcat 启动流程时序图


Author: NaiveKyo
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source NaiveKyo !
  TOC