从脚本开始:启动的入口

在 Tomcat 的世界里,启动之旅始于启动脚本。对于 Windows 系统,我们使用startup.bat;而在 Linux 系统中,则是startup.sh。这些脚本就像是一把钥匙,开启了 Tomcat 服务器的大门。

startup.sh为例,它首先检测操作系统,然后获取脚本路径,确保catalina.sh文件存在且可执行,最后执行catalina.sh start命令来启动 Tomcat 。其关键代码如下:

eval exec "\"$_RUNJDB\"" "\"$CATALINA_LOGGING_CONFIG\"" $LOGGING_MANAGER "$JAVA_OPTS" "$CATALINA_OPTS" \
        -D$ENDORSED_PROP="$JAVA_ENDORSED_DIRS" \
        -classpath "$CLASSPATH" \
        -sourcepath "$CATALINA_HOME"/../../java \
        -Dcatalina.base="$CATALINA_BASE" \
        -Dcatalina.home="$CATALINA_HOME" \
        -Djava.io.tmpdir="$CATALINA_TMPDIR" \
        org.apache.catalina.startup.Bootstrap "$@" start

可以看到,catalina.sh脚本最终调用 Java 命令来执行 Tomcat 的启动类org.apache.catalina.startup.Bootstrap

Bootstrap 类:初始化的基石

在 Tomcat 启动的漫漫长路中,org.apache.catalina.startup.Bootstrap类堪称是一座坚实的基石。这个类拥有一个public static void main(String args[])方法,它就像是 Tomcat 启动之旅的指南针,引导着整个启动过程的走向 。当我们启动 Tomcat 时,main方法所传入的参数通常是start,这就如同给 Tomcat 下达了出发的指令 。对main方法进行简化后,我们可以清晰地看到其主要完成了三件大事:

public static void main(String args[]) {

    if (daemon == null) {
        Bootstrap bootstrap = new Bootstrap();      
        // 内部会 创建 commonLoader、catalinaLoader、sharedLoader 三层类加载器
        // 然后通过反射实例化 org.apache.catalina.startup.Catalina
        bootstrap.init();
    }

    // 命令行参数解析
    String command = "start";
 
    // 根据命令执行不同操作 startd 后台运行,start 阻塞当前线程
    if (command.equals("start")) {
        daemon.setAwait(true);
        // load和start方法内部都是通过反射获取到catalina对象,然后调用catalina的load和start方法
        // load方法内会解析Server.xml文件,
        // 然后调用org.apache.catalina.core.StandardServer#initInternal,构建 Server、Service、Connector、Engine 等组件树
        // org.apache.tomcat.util.digester.Digester#startElement 方法中会创建server对象
        daemon.load(args);
        // 逐层启动所有组件
        daemon.start();
        if (null == daemon.getServer()) {
            System.exit(1);
        }
    } else if (command.equals("stop")) {
        daemon.stopServer(args);
    } 
}

初始化:在这一阶段,init方法被调用,会初始化类加载器、实例化Catalina类。

加载:完成初始化后,load方法开始发挥作用,它负责解析Server.xml,创建Server对象

响应启动或停止命令:根据main方法传入的参数,Bootstrap类会调用相应的方法,启动或者停止tomcat服务。

(一)初始化类加载器

init方法中,初始化类加载器是至关重要的一环,它就像是为 Tomcat 打造了一把把独特的 “钥匙”,用于打开不同类和资源的大门 。Tomcat 默认会初始化三种特有的类加载器,它们分别是commonLoadercatalinaLoadersharedLoader 。这三种类加载器各司其职,共同构建起了 Tomcat 的类加载体系。

/**
 * 调用入口 org.apache.catalina.startup.Bootstrap#main
 * 该方法内会调用initClassLoaders方法初始化类加载器
 * 然后通过反射实例化Catalina
 */
public void org.apache.catalina.startup.Bootstrap#init() throws Exception {
    // 初始化类加载器
    initClassLoaders();
    // 通过catalinaLoader类加载器加载catalina类
    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    ...
    // 反射创建catalina实例
    method.invoke(startupInstance, paramValues);
}
// 初始化类加载器
private void initClassLoaders() {
    try {
        commonLoader = createClassLoader("common", null);
        if (commonLoader == null) {
            // no config file, default to this loader - we might be in a 'single' env.
            commonLoader = this.getClass().getClassLoader();
        }
        catalinaLoader = createClassLoader("server", commonLoader);
        sharedLoader = createClassLoader("shared", commonLoader);
    } catch (Throwable t) {
        handleThrowable(t);
        log.error("Class loader creation threw exception", t);
        System.exit(1);
    }
}

commonLoader:它是 Tomcat 所有类加载器的父类类加载器,其配置在catalina.properties文件中,默认加载路径为{catalina.base}/lib/*.jar,{catalina.home}/lib/*.jar 。这意味着它能够加载 Tomcat 基础目录和安装目录下lib文件夹中的所有jar文件,为 Tomcat 的核心功能提供了基础的类库支持 。可以说,commonLoader就像是一个大型的资源仓库,存放着 Tomcat 运行所必需的各种 “物资”。

catalinaLoadersharedLoader:在默认配置下,catalinaLoadersharedLoader并没有单独设置加载路径,它们实际上都指向了commonLoader 。这就好比两个助手,在没有特殊任务安排时,都协助commonLoader完成类加载的工作 。不过,在一些特定的场景下,我们也可以对它们进行个性化的配置,让它们承担起更具针对性的类加载任务 。例如,在需要隔离不同 Web 应用所使用的类时,就可以通过配置让catalinaLoadersharedLoader分别加载不同的类库,从而实现类的隔离和共享 。

(二)创建 Catalina 实例

初始化类加载器之后,catalinaLoader就像是一位技艺精湛的工匠,开始着手加载并创建Catalina实例 。这一过程通过反射机制来实现,就像是在一个巨大的知识宝库中,通过特定的索引精准地找到并取出所需的知识 。

Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();

加载类catalinaLoader首先使用loadClass方法加载org.apache.catalina.startup.Catalina类,这个过程就像是在图书馆中根据书名找到对应的书籍 。

创建实例:然后,通过反射调用该类的无参构造函数,使用newInstance方法创建Catalina类的实例,就像是根据书籍中的蓝图制造出相应的产品 。

创建好Catalina实例后,还需要为其设置一些重要的属性,其中设置父类加载器是关键的一步 。通过反射调用CatalinasetParentClassLoader方法,将sharedLoader实例设置为Catalina实例的父级类加载器 。这就好比为一个新出生的婴儿找到了一位可靠的监护人,确保其在成长过程中能够获取到正确的 “营养”(类和资源) 。

String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method =
    startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);

最后,将创建好的Catalina实例赋值给catalinaDaemon,这个catalinaDaemon就像是一个承载着 Tomcat 启动使命的核心载体,后续的加载和启动操作都将围绕它来展开 。可以说,Catalina实例的创建是 Tomcat 启动过程中的一个重要里程碑,它为后续的各种功能实现奠定了坚实的基础 。

Catalina 类:加载与启动的核心

在 Tomcat 的启动之旅中,Catalina类无疑占据着核心地位,它就像是一场盛大演出的导演,掌控着整个启动过程的节奏和流程 。当Bootstrap类完成了初始化和一些前期准备工作后,便将启动的重任交给了Catalina类 。Catalina类主要通过loadstart这两个关键方法,有条不紊地完成 Tomcat 的加载和启动工作,就如同搭建一座高楼,先精心打好地基(加载),再一层一层地向上建造(启动) 。

(一)load 方法:解析与初始化

load方法堪称是Catalina类中的一位 “知识渊博的学者”,它的主要职责是解析Server.xml文件,并对 Tomcat 的核心组件进行初始化,为后续的启动工作筑牢根基 。

public void load() {

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

    long t1 = System.nanoTime();

    initDirs();

    // Before digester - it may be needed
    initNaming();

    // Parse main server.xml
    parseServerXml(true);
    Server s = getServer();
    if (s == null) {
        return;
    }

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

    // Stream redirection
    initStreams();

    // 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(sm.getString("catalina.initError"), e);
        }
    }

    if(log.isInfoEnabled()) {
        log.info(sm.getString("catalina.init", Long.toString(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - t1))));
    }
}

创建 Digester 对象parseServerXml方法首先创建了一个Digester对象,这个Digester对象就像是一位精通各种语言的翻译官,专门负责解析XML文件 。Digester底层采用SAX(Simple API for XML)解析XML文件,这种解析方式由 “事件” 驱动,在识别出特定XML元素时,会执行特定的动作 。例如,当遇到Server标签时,Digester会根据预先设定的规则,创建一个StandardServer对象 。

Digester digester = start ? createStartDigester() : createStopDigester();

createStartDigester方法中,会对Digester进行一系列的配置,比如设置是否进行DTD(Document Type Definition)规则校验、是否进行节点设置规则校验等 。同时,还会为Digester添加各种规则,这些规则定义了如何将XML元素转换为 Java 对象 。例如,添加addObjectCreate规则,用于在遇到特定的XML标签时创建相应的 Java 对象;添加addSetProperties规则,用于设置对象的属性;添加addSetNext规则,用于建立对象之间的关系 。

protected Digester createStartDigester() {
    // Initialize the digester
    Digester digester = new Digester();
    digester.setValidating(false);
    digester.setRulesValidation(true);
    Map<Class<?>, List<String>> fakeAttributes = new HashMap<>();
    // Ignore className on all elements
    List<String> objectAttrs = new ArrayList<>();
    objectAttrs.add("className");
    fakeAttributes.put(Object.class, objectAttrs);
    // Ignore attribute added by Eclipse for its internal tracking
    List<String> contextAttrs = new ArrayList<>();
    contextAttrs.add("source");
    fakeAttributes.put(StandardContext.class, contextAttrs);
    // Ignore Connector attribute used internally but set on Server
    List<String> connectorAttrs = new ArrayList<>();
    connectorAttrs.add("portOffset");
    fakeAttributes.put(Connector.class, connectorAttrs);
    digester.setFakeAttributes(fakeAttributes);
    digester.setUseContextClassLoader(true);

    // Configure the actions we will be using
    digester.addObjectCreate("Server",
                             "org.apache.catalina.core.StandardServer",
                             "className");
    digester.addSetProperties("Server");
    digester.addSetNext("Server",
                        "setServer",
                        "org.apache.catalina.Server");
   // 省略其他规则添加
   return digester;
}

解析 Server.xml 文件:创建好Digester对象后,会获取Server.xml文件的输入流,并将其转换为InputSource对象 。然后,将当前的Catalina实例压入Digester的对象栈顶,这一步就像是给Digester一个 “指引”,让它在解析XML文件时知道如何将创建的对象与Catalina实例关联起来 。接着,调用digester.parse(inputSource)方法开始解析Server.xml文件 。在解析过程中,Digester会根据之前设置的规则,依次创建ServerServiceConnectorEngineHost等核心组件,并为这些组件设置相应的属性 。例如,当解析到Server标签时,会创建一个StandardServer对象,并设置其portshutdown等属性;当解析到Service标签时,会创建一个StandardService对象,并将其添加到StandardServerservices列表中 。

InputStream inputStream = resource.getInputStream();
InputSource inputSource = new InputSource(resource.getURI().toURL().toString());
inputSource.setByteStream(inputStream);
digester.push(this);
if (generateCode) {
    digester.startGeneratingCode();
    generateClassHeader(digester, start);
}
digester.parse(inputSource);

初始化核心组件:完成Server.xml文件的解析后,load方法会获取解析得到的Server组件,并为其设置Catalina实例、Catalina HomeCatalina Base等属性 。然后,调用Server组件的init方法,对Server及其包含的ServiceConnectorEngineHost等组件进行初始化 。在初始化过程中,各个组件会完成一些必要的准备工作,比如创建线程池、初始化网络连接等 。例如,Connector组件会初始化其对应的ProtocolHandlerProtocolHandler负责处理网络通信,它会创建ServerSocket来监听指定的端口,为后续接收客户端请求做好准备 。

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

// Stream redirection
initStreams();

// Start the new server
try {
    // 初始化Service、Connector、Engine
    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(sm.getString("catalina.initError"), e);
    }
}

(二)start 方法:启动服务

load方法完成了组件的加载和初始化后,start方法就像是一位充满激情的指挥官,下达启动的命令,让 Tomcat 的各个组件开始协同工作,对外提供服务 。

public void start() {

    if (getServer() == null) {
        load();
    }
    // Start the new server
    try {
        // 依次启动下层组件 Server → Service → Connector/Engine → Host → Context
        getServer().start();
    } catch (LifecycleException e) {
        getServer().destroy();
        return;
    }

    // 注册关闭钩子,当 JVM 接收到关闭信号(如 Ctrl+C)时,执行资源释放
    if (useShutdownHook) {
        if (shutdownHook == null) {
            shutdownHook = new CatalinaShutdownHook();
        }
        Runtime.getRuntime().addShutdownHook(shutdownHook);
    }

    if (await) {
        await();
        stop();
    }
}

启动 Server 及相关服务start方法首先会检查Server组件是否已经存在,如果不存在,则调用load方法进行加载和初始化 。然后,调用getServer().start()方法启动Server组件 。在Server组件的start方法中,会依次启动其包含的Service组件 。每个Service组件又会启动其内部的Connector组件和Container组件(如EngineHostContextWrapper等) 。以StandardServicestartInternal方法为例,它会先启动EngineEngine的子容器都会被启动,Web 应用的部署也会在这个步骤完成;然后启动Executor线程池;接着启动MapperListener;最后启动Connector组件 。当Connector组件启动完成后,意味着 Tomcat 可以对外提供请求服务了 。

// 依次启动下层组件 Server → Service → Connector/Engine → Host → Context
getServer().start();

注册关闭钩子:在启动过程中,start方法还会注册一个关闭钩子(shutdown hook) 。关闭钩子是一个在 JVM 关闭时会被执行的线程,它的作用是在 Tomcat 服务器关闭时,执行一些清理工作,比如释放资源、关闭连接等,以确保服务器能够安全地关闭 。在 Tomcat 中,关闭钩子是通过CatalinaShutdownHook类实现的,它继承自Thread类 。当 JVM 接收到关闭信号时,会启动所有注册的关闭钩子,CatalinaShutdownHookrun方法会被调用,在这个方法中会调用Serverstop方法,停止 Tomcat 服务器的运行 。

// 注册关闭钩子,当 JVM 接收到关闭信号(如 Ctrl+C)时,执行资源释放
if (useShutdownHook) {
    if (shutdownHook == null) {
        shutdownHook = new CatalinaShutdownHook();
    }
    Runtime.getRuntime().addShutdownHook(shutdownHook);
}

阻塞监听关闭命令:如果在启动时设置了awaittruestart方法会调用await方法,让 Tomcat 在shutdown端口阻塞监听关闭命令 。当接收到关闭命令时,会调用stop方法停止 Tomcat 服务器 。这样可以确保 Tomcat 在接收到关闭信号时,能够有序地停止运行,避免数据丢失或资源泄漏等问题 。

if (await) {
    await();
    stop();
}

Server 与 Service 组件:启动流程的关键环节

(一)Server 组件的启动

在 Tomcat 的架构中,Server组件堪称是整个服务器的 “总指挥”,它就像是一个庞大军队的最高统帅,掌控着 Tomcat 服务器的整体生命周期 。Server组件的启动是 Tomcat 启动流程中的关键步骤,它就像是打响了一场战役的第一枪,为后续的服务启动奠定了基础 。

Server组件实现了Lifecycle接口,这就赋予了它生命周期管理的能力 。当Server组件启动时,会触发一系列的生命周期事件 。在StandardServer类(Server组件的标准实现类)的startInternal方法中,首先会发出CONFIGURE_START_EVENT事件,这个事件就像是一个信号弹,通知相关的监听器开始进行启动前的配置准备工作 。例如,NamingContextListener监听器在接收到这个事件后,会实例化 Tomcat 相关的上下文以及ContextResource资源,为后续的命名服务和资源管理做好准备 。

protected void startInternal() throws LifecycleException {
    // 触发start事件
    fireLifecycleEvent(CONFIGURE_START_EVENT, null);
    setState(LifecycleState.STARTING);

    globalNamingResources.start();

    // Start our defined Services
    synchronized (servicesLock) {
        for (Service service : services) {
            service.start();
        }
    }
}

接着,Server组件会启动globalNamingResources,这一步就像是为整个服务器搭建了一个资源索引库,主要负责管理全局命名资源,比如 JNDI(Java Naming and Directory Interface)资源 。这些资源可以被 Tomcat 服务器中的其他组件共享和访问,为各种服务的正常运行提供了必要的支持 。例如,Web 应用可以通过 JNDI 获取数据库连接池等资源,从而实现与数据库的交互 。

在启动globalNamingResources之后,Server组件会进入一个关键的环节,即启动它所包含的Service组件 。Server组件可以包含多个Service组件,每个Service组件都像是一个独立的作战单元,负责提供特定的服务 。Server组件通过遍历services数组,依次调用每个Service组件的start方法,来启动这些 “作战单元” 。这就好比最高统帅命令各个部队依次进入战斗状态,协同作战,共同完成服务器的启动任务 。在启动每个Service组件时,Server组件会对servicesLock进行同步操作,以确保在多线程环境下,Service组件的启动过程是安全有序的 。

(二)Service 组件的启动

Service组件在 Tomcat 的体系中扮演着 “桥梁” 的角色,它巧妙地将Connector组件和Container组件(如Engine)连接在一起,使得它们能够协同工作,共同为客户端提供服务 。可以说,Service组件就像是一个协调各方的项目经理,确保各个团队(组件)之间的合作顺畅 。

Service组件的启动过程也是围绕着Lifecycle接口展开的 。在StandardService类(Service组件的标准实现类)的startInternal方法中,首先会设置自身的状态为STARTING,这就像是在告诉外界,自己即将开始启动服务 。

protected void startInternal() throws LifecycleException {
    if(log.isInfoEnabled()) {
        log.info(sm.getString("standardService.start.name", this.name));
    }
    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) {
            // If it has already failed, don't try and start it
            if (connector.getState() != LifecycleState.FAILED) {
                connector.start();
            }
        }
    }
}

然后,Service组件会启动Engine容器 。Engine容器是Service组件中的核心容器,它就像是一个精密的大脑,负责处理客户端的请求,并将请求分发给相应的HostContext 。在启动Engine容器时,Service组件会对engine进行同步操作,确保Engine容器的启动过程是线程安全的 。Engine容器启动后,它的子容器(如HostContext等)也会被递归启动,这个过程就像是一颗大树从树干开始,逐渐向树枝和树叶蔓延生长,将整个服务体系搭建起来 。在这个过程中,Web 应用的部署也会同步完成,就像是在搭建好的舞台上摆放好各种道具和布景,为演出做好准备 。

接着,Service组件会启动Executor线程池 。Executor线程池就像是一群勤劳的工人,负责为Connector处理请求提供必要的线程资源 。通过使用线程池,可以有效地提高服务器的并发处理能力,避免在高并发情况下频繁创建和销毁线程带来的性能开销 。Service组件会遍历executors列表,依次启动每个Executor线程池,为后续的请求处理工作做好准备 。

之后,Service组件会启动MapperListenerMapperListener就像是一个智能的导航仪,负责将请求的 URL 映射到对应的容器(如HostContextWrapper等) 。它会监听服务器的状态变化,当有新的请求到来时,能够快速准确地找到处理该请求的容器,确保请求能够被正确地处理 。启动MapperListener就像是为服务器安装了一个精准的导航系统,使得服务器在处理请求时能够更加高效和准确 。

最后,Service组件会启动Connector组件 。Connector组件是 Tomcat 与客户端进行通信的 “大门”,它负责接收客户端的请求,并将请求转换为 Tomcat 内部能够处理的Request对象 。Service组件会遍历connectors数组,依次启动每个Connector组件 。在启动Connector组件时,会先检查其状态,如果状态不是FAILED,才会启动它 。当所有的Connector组件启动完成后,Tomcat 服务器就像是一座灯火通明的城堡,准备好迎接来自客户端的各种请求,正式对外提供服务 。

容器组件的启动:Engine、Host 与 Context

(一)Engine 的启动

Engine容器在 Tomcat 的请求处理流程中扮演着 “导航仪” 的关键角色,它负责将请求精准地路由到对应的Host容器 。当Engine容器启动时,在StandardEngine类的startInternal方法中,首先会记录服务器的标识信息,这就像是给 “导航仪” 贴上了自己的独特标签,方便识别和管理 。

protected synchronized void startInternal() throws LifecycleException {

    // Log our server identification information
    if (log.isInfoEnabled()) {
        log.info(sm.getString("standardEngine.start", ServerInfo.getServerInfo()));
    }

    // Standard container startup
    super.startInternal();
}

然后,Engine容器会调用父类ContainerBasestartInternal方法 。在这个方法中,Engine容器会启动集群(如果存在),集群就像是一个紧密协作的团队,通过集群可以实现负载均衡和高可用性,提升 Tomcat 的整体性能 。同时,还会启动RealmRealm就像是一个安全卫士,负责进行身份验证和授权,确保只有合法的用户才能访问 Tomcat 服务器 。

protected synchronized void startInternal() throws LifecycleException {

    // Start our subordinate components, if any
    logger = null;
    getLogger();
    Cluster cluster = getClusterInternal();
    if (cluster instanceof Lifecycle) {
        ((Lifecycle) cluster).start();
    }
    Realm realm = getRealmInternal();
    if (realm instanceof Lifecycle) {
        ((Lifecycle) realm).start();
    }

    // Start our child containers, if any
    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);
        }

    }
    if (multiThrowable != null) {
        throw new LifecycleException(sm.getString("containerBase.threadedStartFailed"),
                multiThrowable.getThrowable());
    }

    // Start the Valves in our pipeline (including the basic), if any
    if (pipeline instanceof Lifecycle) {
        ((Lifecycle) pipeline).start();
    }

    setState(LifecycleState.STARTING);

    // Start our thread
    if (backgroundProcessorDelay > 0) {
        monitorFuture = Container.getService(ContainerBase.this).getServer()
                .getUtilityExecutor().scheduleWithFixedDelay(
                        new ContainerBackgroundProcessorMonitor(), 0, 60, TimeUnit.SECONDS);
    }
}

接着,Engine容器会启动它的子容器,也就是Host容器 。在这个过程中,Engine容器会将子容器的启动任务委托给startStopExecutor线程池 。startStopExecutor线程池就像是一群勤劳的小蜜蜂,它们会通过调用LifecycleBase#start()方法去启动子容器 。在LifecycleBase#start()方法中,会调用StandardHoststartInternal()方法,从而正式开始启动Host容器 。这个过程就像是一场接力赛,Engine容器将启动的接力棒传递给startStopExecutor线程池,再由线程池传递给StandardHost,确保每个环节都紧密相连,顺利完成Host容器的启动 。

Container children[] = findChildren();
List<Future<Void>> results = new ArrayList<>();
for (Container child : children) {
    results.add(startStopExecutor.submit(new StartChild(child)));
}

除了启动子容器,Engine容器还会启动pipelinepipeline就像是一个管道系统,它负责将请求按照一定的顺序传递给各个Valve进行处理 。在pipeline中,每个Valve都像是管道中的一个关卡,它们依次对请求进行处理,完成各种功能,如日志记录、身份验证等 。当pipeline启动完成后,Engine容器会将自身的状态设置为STARTING,并启动线程,这个线程会在后台执行一些周期性的任务,比如检查类的时间戳、Session 对象的超时时间等,确保Engine容器的稳定运行 。

(二)Host 的启动

Host容器在 Tomcat 中就像是一个 “虚拟站点管理员”,它代表着一个虚拟主机,负责管理多个Context容器,就如同管理员管理着多个不同的区域 。当Host容器启动时,在StandardHost类的startInternal方法中,首先会设置错误报告阀门 。错误报告阀门就像是一个 “问题报告员”,当Host容器出现错误时,它会负责生成错误报告,帮助开发人员快速定位和解决问题 。

protected synchronized void startInternal() throws LifecycleException {

    // Set error report valve
    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 = ErrorReportValve.class.getName().equals(errorValve) ?
                    new ErrorReportValve() :
                    (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();
}

然后,Host容器会调用父类ContainerBasestartInternal方法 。在这个方法中,Host容器会执行与Engine容器启动时类似的操作,比如启动集群(如果存在)、启动Realm(如果存在) 。之后,Host容器会启动它的子容器,也就是Context容器 。同样,Host容器会将子容器的启动任务委托给startStopExecutor线程池,由线程池中的线程调用LifecycleBase#start()方法来启动子容器 。在启动子容器的过程中,Host容器会遍历children数组,为每个Context容器创建一个StartChild任务,并将这些任务提交到startStopExecutor线程池 。StartChild任务会调用Context容器的start方法,从而启动Context容器 。

Container children[] = findChildren();
List<Future<Void>> results = new ArrayList<>();
for (Container child : children) {
    results.add(startStopExecutor.submit(new StartChild(child)));
}

在启动过程中,Host容器还会安装errorReportValue阀门 。这个阀门是专门用于处理错误报告的,它会在Host容器的pipeline中占据一席之地 。当Host容器处理请求时,如果发生错误,errorReportValue阀门就会被触发,它会生成详细的错误报告,包括错误的类型、发生的位置等信息,这些信息对于开发人员诊断和修复问题非常有帮助 。同时,Host容器还会添加一个ErrorDispatcherValve阀门,这个阀门主要负责处理错误的分发,将错误请求转发到合适的处理程序,确保错误能够得到妥善的处理 。

(三)Context 的启动

Context容器在 Tomcat 中可以看作是一个 “Web 应用的舞台”,它代表着一个 Web 应用程序,负责管理 Web 应用的各种资源和组件,就如同舞台上的各种道具和演员 。当Context容器启动时,在StandardContext类的startInternal方法中,会执行一系列复杂而关键的步骤 。

protected synchronized void startInternal() throws LifecycleException {

   // 创建工作目录
   File workDir = new File(System.getProperty("catalina.base"), "work/Catalina/" + getName());
   if (!workDir.exists()) {
       workDir.mkdirs();
   }
   // 实例化WebResourceRoot
   WebResourceRoot resources = new StandardRoot(this);
   setResources(resources);
   // 实例化Loader
   if (getLoader() == null) {
       WebappLoader webappLoader = new WebappLoader();
       webappLoader.setDelegate(getDelegate());
       setLoader(webappLoader);
   }
   // 启动Loader
   Loader loader = getLoader();
   if (loader instanceof Lifecycle) {
       ((Lifecycle) loader).start();
   }
   // 发出CONFIGURE\_START\_EVENT事件
   fireLifecycleEvent(Lifecycle.CONFIGURE\_START\_EVENT, null);
   // 启动子容器
   for (Container child : findChildren()) {
       if (!child.getState().isAvailable()) {
           child.start();
       }
   }
   // 启动pipeline
   if (pipeline instanceof Lifecycle) {
       ((Lifecycle) pipeline).start();
   }
}

首先,Context容器会设置 Web 应用的工作目录 。这个工作目录就像是 Web 应用的 “临时仓库”,用于存储一些临时文件和缓存数据 。例如,JSP 文件在编译后生成的类文件就会存储在这个工作目录中 。Context容器会根据catalina.base系统属性和自身的名称来构建工作目录的路径 。如果这个工作目录不存在,Context容器会自动创建它,确保 Web 应用有一个合适的 “临时仓库” 来存放数据 。

其次,Context容器会指定一个LoaderLoader就像是一个 “知识搬运工”,负责加载 Web 应用的类和资源 。在默认情况下,Context容器会使用WebappLoader作为LoaderWebappLoader会根据 Web 应用的结构,从WEB-INF/classes目录和WEB-INF/lib目录中加载类文件和资源文件 。例如,当 Web 应用需要使用某个类时,WebappLoader会从相应的目录中找到并加载这个类,为 Web 应用的正常运行提供必要的支持 。

然后,Context容器会获取字符编码格式 。字符编码格式就像是一种 “语言规范”,确保 Web 应用在处理文本数据时能够正确地解析和显示字符 。Context容器会从配置文件或者环境变量中获取字符编码格式的设置 。如果没有显式设置,Context容器会使用默认的字符编码格式 。例如,在处理 HTTP 请求中的参数时,Context容器会根据设置的字符编码格式来解析参数值,避免出现乱码问题 。

接着,Context容器会创建临时文件目录 。临时文件目录就像是一个 “临时文件存放区”,用于存储 Web 应用在运行过程中产生的临时文件 。Context容器会在工作目录下创建一个专门的临时文件目录 。例如,当 Web 应用需要上传文件时,上传的文件会先存储在这个临时文件目录中,等待进一步的处理 。

此外,Context容器还会发出CONFIGURE_START_EVENT事件 。这个事件就像是一个 “集结号”,通知相关的监听器开始进行配置和启动工作 。ContextConfig监听器会处理这个事件,它会从 Web 应用的web.xml文件或者 Servlet 3.0 的注解配置中读取 Servlet、Filter、Listener 等相关的配置信息 。例如,ContextConfig监听器会解析web.xml文件,创建Wrapper对象,并将其与当前的Context容器建立关联 。同时,Context容器还会启动它的子容器,也就是Wrapper容器 。每个Wrapper容器代表一个 Servlet,Context容器会遍历children数组,启动那些状态不可用的Wrapper容器 。最后,Context容器会启动pipeline,确保请求能够在pipeline中按照预定的顺序被各个Valve处理 。

总结:Tomcat 启动流程全貌

Tomcat 的启动流程是一个复杂而有序的过程,从启动脚本的执行,到Bootstrap类的初始化,再到Catalina类对核心组件的加载和启动,以及ServerService和容器组件的协同工作,每一个环节都紧密相连,共同构建起了 Tomcat 服务器的运行基础 。在这个过程中,我们深入了解了各个组件的初始化和启动细节,比如类加载器的创建、XML文件的解析、组件之间的依赖关系等 。理解 Tomcat 的启动流程对于我们优化和维护 Tomcat 服务器具有重要的意义 。通过对启动流程的分析,我们可以找到影响启动速度的瓶颈,从而采取针对性的优化措施,比如合理配置类加载器、优化XML文件的解析等 。在服务器的日常维护中,了解启动流程也有助于我们快速定位和解决启动过程中出现的问题 。希望本文能够帮助 Java 开发者们更深入地理解 Tomcat 的启动原理,为开发和运维高效稳定的 Web 应用提供有力的支持 。

文章作者: Z
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 微博客
性能调优
喜欢就支持一下吧