午夜的萤火 發表於 2026-4-22 09:00:00

Tomcat的架构设计和启动过程详解

<p>前面一篇文章,我们手写了了一个mini版的Tomcat,接下来我们从源码和架构的角度来学习Tomcat</p>
<h2 id="引入">引入</h2>
<h3 id="tomcat和catalina是什么关系">Tomcat和Catalina是什么关系?</h3>
<p>Tomcat的前身为Catalina,Catalina又是一个轻量级的Servlet容器。在美国,catalina是一个很美的小岛。所以Tomcat作者的寓意可能是想把Tomcat设计成一个优雅美丽且轻量级的web服务器。Tomcat从4.x版本开始除了作为支持Servlet的容器外,额外加入了很多的功能,比如:jsp、el、naming等等,所以说<strong>Tomcat不仅仅是Catalina</strong>。</p>
<h3 id="什么是servlet">什么是Servlet?</h3>
<blockquote>
<p>所谓Servlet,其实就是Sun为了让Java能实现动态可交互的网页,从而进入Web编程领域而制定的一套标准!</p>
</blockquote>
<p>在互联网兴起之初,当时的Sun公司(后面被Oracle收购)已然看到了这次机遇,于是设计出了Applet来对Web应用的支持。不过事实却并不是预期那么得好,Sun悲催地发现Applet并没有给业界带来多大的影响。经过反思,Sun就想既然机遇出现了,市场前景也非常不错,总不能白白放弃了呀,怎么办呢?于是又投入精力去搞一套规范出来,这时Servlet诞生了!</p>
<p>一个Servlet主要做下面三件事情:</p>
<ul>
<li>创建并填充Request对象,包括:URI、参数、method、请求头信息、请求体信息等</li>
<li>创建Response对象</li>
<li>执行业务逻辑,将结果通过Response的输出流输出到客户端</li>
</ul>
<p><strong>Servlet没有main方法,所以,如果要执行,则需要在一个容器里面才能执行,这个容器就是为了支持Servlet的功能而存在,Tomcat其实就是一个Servlet容器的实现</strong>。</p>
<h2 id="核心架构设计">核心架构设计</h2>
<p>官网:https://tomcat.apache.org/tomcat-8.0-doc/architecture/overview.html</p>
<p>Tomcat 的架构设计以 ‌<strong>模块化、分层、解耦</strong>‌ 为核心,遵循 Java Servlet 规范,同时支持高性能、高扩展的 Web 服务。其整体架构可概括为 ‌<strong>“连接器(Connector)- 容器(Container)” 双层模型</strong>‌,并通过 ‌<strong>Lifecycle 生命周期管理机制</strong>‌ 和 ‌<strong>责任链模式(Pipeline-Valve)</strong>‌ 实现组件协同。</p>
<p>Tomcat的架构呈“套娃式”嵌套:Server → Service → (Connector + Engine) → Host → Context → Wrapper</p>
<p><img src="https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202603081022091.png" alt="" loading="lazy"></p>
<p>核心架构组成:</p>
<ul>
<li>‌<strong>Server</strong>‌:代表整个 Tomcat 实例,是顶级容器,管理多个 Service。</li>
<li>‌<strong>Service</strong>‌:将一个或多个 Connector 与一个 Engine 绑定,构成独立服务单元。
<ul>
<li>Manager:管理器,用于管理会话Session</li>
<li>Logger:日志器,用于管理日志</li>
<li>Loader:加载器,和类加载有关,只会开放给Context所使用</li>
<li>Pipeline:管道组件,配合Valve实现过滤器功能</li>
<li>Valve:阀门组件,配合Pipeline实现过滤器功能</li>
<li>Realm:认证授权组件</li>
</ul>
</li>
<li>‌<strong>Connector</strong>(连接器)‌:负责处理外部 HTTP/AJP 请求,实现网络通信与协议解析。</li>
<li>‌<strong>Container</strong>(容器)‌:负责加载和管理 Servlet,处理业务逻辑,包含四级嵌套容器:
<ul>
<li>‌<strong>Engine</strong>‌:处理所有请求,每个 Service 仅有一个。</li>
<li>‌<strong>Host</strong>‌:虚拟主机,对应一个域名或 IP。</li>
<li>‌<strong>Context</strong>‌:Web 应用上下文,对应一个 WAR 包或目录。</li>
<li>‌<strong>Wrapper</strong>‌:最底层容器,封装单个 Servlet。</li>
</ul>
</li>
</ul>
<h3 id="从webxml配置和模块对应角度">从web.xml配置和模块对应角度</h3>
<p>上述模块的理解不是孤立的,它可以直接映射为Tomcat的web.xml配置,让我们联系起来看</p>
<pre><code class="language-xml">&lt;Server port="8005" shutdown="SHUTDOWN"&gt;
&lt;Listener className="org.apache.catalina.startup.VersionLoggerListener" /&gt;

&lt;Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" /&gt;

&lt;Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" /&gt;
&lt;Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" /&gt;
&lt;Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" /&gt;

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

&lt;Service name="Catalina"&gt;

    &lt;Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" /&gt;
    &lt;Engine name="Catalina" defaultHost="localhost"&gt;
      &lt;Realm className="org.apache.catalina.realm.LockOutRealm"&gt;
      &lt;Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/&gt;
      &lt;/Realm&gt;

      &lt;Host name="localhost"appBase="webapps"
            unpackWARs="true" autoDeploy="true"&gt;
      &lt;Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &amp;quot;%r&amp;quot; %s %b" /&gt;

      &lt;/Host&gt;
    &lt;/Engine&gt;
&lt;/Service&gt;
&lt;/Server&gt;
</code></pre>
<h3 id="从一个完整请求的角度来看">从一个完整请求的角度来看</h3>
<blockquote>
<p>通过一个完整的HTTP请求,我们还需要把它贯穿起来</p>
</blockquote>
<p>假设来自客户的请求为:http://localhost:8080/test/index.jsp 请求被发送到本机端口8080,被在那里侦听的Coyote HTTP/1.1 Connector,然后</p>
<ul>
<li>Connector把该请求交给它所在的Service的Engine来处理,并等待Engine的回应</li>
<li>Engine获得请求localhost:8080/test/index.jsp,匹配它所有虚拟主机Host</li>
<li>Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机)</li>
<li>localhost Host获得请求/test/index.jsp,匹配它所拥有的所有Context</li>
<li>Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为""的Context去处理)</li>
<li>path="/test"的Context获得请求/index.jsp,在它的mapping table中寻找对应的servlet</li>
<li>Context匹配到URL PATTERN为*.jsp的servlet,对应于JspServlet类,构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet或doPost方法</li>
<li>Context把执行完了之后的HttpServletResponse对象返回给Host</li>
<li>Host把HttpServletResponse对象返回给Engine</li>
<li>Engine把HttpServletResponse对象返回给Connector</li>
<li>Connector把HttpServletResponse对象返回给客户browser</li>
</ul>
<h3 id="从源码的设计角度看">从源码的设计角度看</h3>
<blockquote>
<p>从功能的角度将Tomcat源代码分成5个子模块,分别是:</p>
</blockquote>
<ul>
<li><strong>Jsper模</strong>: 这个子模块负责jsp页面的解析、jsp属性的验证,同时也负责将jsp页面动态转换为java代码并编译成class文件。在Tomcat源代码中,凡是属于org.apache.jasper包及其子包中的源代码都属于这个子模块;</li>
<li><strong>Servlet和Jsp模块</strong>: 这个子模块的源代码属于javax.servlet包及其子包,如我们非常熟悉的javax.servlet.Servlet接口、javax.servet.http.HttpServlet类及javax.servlet.jsp.HttpJspPage就位于这个子模块中;</li>
<li><strong>Catalina模块</strong>: 这个子模块包含了所有以org.apache.catalina开头的java源代码。该子模块的任务是规范了Tomcat的总体架构,定义了Server、Service、Host、Connector、Context、Session及Cluster等关键组件及这些组件的实现,这个子模块大量运用了Composite设计模式。同时也规范了Catalina的启动及停止等事件的执行流程。从代码阅读的角度看,这个子模块应该是我们阅读和学习的重点。</li>
<li><strong>Connector模块</strong>: 如果说上面三个子模块实现了Tomcat应用服务器的话,那么这个子模块就是Web服务器的实现。所谓连接器(Connector)就是一个连接客户和应用服务器的桥梁,它接收用户的请求,并把用户请求包装成标准的Http请求(包含协议名称,请求头Head,请求方法是Get还是Post等等)。同时,这个子模块还按照标准的Http协议,负责给客户端发送响应页面,比如在请求页面未发现时,connector就会给客户端浏览器发送标准的Http 404错误响应页面。</li>
<li><strong>Resource模块</strong>: 这个子模块包含一些资源文件,如Server.xml及Web.xml配置文件。严格说来,这个子模块不包含java源代码,但是它还是Tomcat编译运行所必需的。</li>
</ul>
<h3 id="从后续深入理解的角度">从后续深入理解的角度</h3>
<blockquote>
<p>我们看完上述组件结构后,后续应该重点从哪些角度深入理解Tomcat呢?</p>
</blockquote>
<ul>
<li><strong>基于组件的架构</strong></li>
</ul>
<p>我们知道组成Tomcat的是各种各样的组件,每个组件各司其职,组件与组件之间有明确的职责划分,同时组件与组件之间又通过一定的联系相互通信。Tomcat整体就是一个个组件的堆砌!</p>
<ul>
<li><strong>基于JMX</strong></li>
</ul>
<p>我们在后续阅读Tomcat源码的时候,会发现代码里充斥着大量的类似于下面的代码。</p>
<pre><code>Registry.getRegistry(null, null).invoke(mbeans, "init", false);
Registry.getRegistry(null, null).invoke(mbeans, "start", false);
</code></pre>
<p>而这实际上就是通过JMX来管理相应对象的代码。这儿我们不会详细讲述什么是JMX,我们只是简单地说明一下JMX的概念,参考JMX百度百科。</p>
<blockquote>
<p>JMX(Java Management Extensions,即Java管理扩展)是一个为应用程序、设备、系统等植入管理功能的框架。JMX可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。</p>
</blockquote>
<ul>
<li><strong>基于生命周期</strong></li>
</ul>
<p>如果我们查阅各个组件的源代码,会发现绝大多数组件实现了Lifecycle接口,这也就是我们所说的基于生命周期。生命周期的各个阶段的触发又是基于事件的方式。</p>
<h2 id="启动过程详解">启动过程详解</h2>
<h3 id="总体流程">总体流程</h3>
<p>我们看下整体的初始化和启动的流程,在<strong>理解的时候可以直接和Tomcat架构设计中组件关联上</strong>:</p>
<p><img src="https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202603081035646.png" alt="" loading="lazy"></p>
<h3 id="启动过程代码浅析">启动过程代码浅析</h3>
<p>看了下网上关于Tomcat的文章,很多直接关注在纯代码的分析,这种是很难的;我建议你一定要把代码加载进来自己看一下,然后这里我把它转化为核心的几个问题来帮助你理解。</p>
<h4 id="bootstrap主入口">Bootstrap主入口?</h4>
<p>Tomcat源码就从它的main方法开始。Tomcat的main方法在org.apache.catalina.startup.Bootstrap 里。让我们带着这个为看下Catalina的初始化的</p>
<pre><code class="language-java">/**
* 初始化守护进程
*
* @throws Exception Fatal initialization error
*/
public void init() throws Exception {

    // 初始化classloader(包括catalinaLoader),下文将具体分析
    initClassLoaders();

    // 设置当前的线程的contextClassLoader为catalinaLoader
    Thread.currentThread().setContextClassLoader(catalinaLoader);

    SecurityClassLoad.securityClassLoad(catalinaLoader);

    // 通过catalinaLoader加载Catalina,并初始化startupInstance 对象
    if (log.isDebugEnabled())
      log.debug("Loading startup class");
    Class&lt;?&gt; startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.getConstructor().newInstance();

    // 通过反射调用了setParentClassLoader 方法
    if (log.isDebugEnabled())
      log.debug("Setting startup class properties");
    String methodName = "setParentClassLoader";
    Class&lt;?&gt; paramTypes[] = new Class;
    paramTypes = Class.forName("java.lang.ClassLoader");
    Object paramValues[] = new Object;
    paramValues = sharedLoader;
    Method method =
      startupInstance.getClass().getMethod(methodName, paramTypes);
    method.invoke(startupInstance, paramValues);

    catalinaDaemon = startupInstance;

}
</code></pre>
<p>通过上面几行关键代码的注释,我们就可以看出Catalina是如何初始化的。这里还留下一个问题,tomcat为什么要初始化不同的classloader呢?我们将在下文进行详解。</p>
<h4 id="bootstrap如何初始化catalina的">Bootstrap如何初始化Catalina的?</h4>
<p>我们用<code>Sequence Diagram</code>插件来看main方法的时序图,但是可以发现它并没有帮我们画出Bootstrap初始化Catalina的过程,这和上面的组件初始化不符合?</p>
<p><img src="https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202603081042949.png" alt="" loading="lazy"></p>
<p>让我们带着这个为看下Catalina的初始化的</p>
<pre><code class="language-java">/**
* 初始化守护进程
*
* @throws Exception Fatal initialization error
*/
public void init() throws Exception {

    // 初始化classloader(包括catalinaLoader),下文将具体分析
    initClassLoaders();

    // 设置当前的线程的contextClassLoader为catalinaLoader
    Thread.currentThread().setContextClassLoader(catalinaLoader);

    SecurityClassLoad.securityClassLoad(catalinaLoader);

    // 通过catalinaLoader加载Catalina,并初始化startupInstance 对象
    if (log.isDebugEnabled())
      log.debug("Loading startup class");
    Class&lt;?&gt; startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.getConstructor().newInstance();

    // 通过反射调用了setParentClassLoader 方法
    if (log.isDebugEnabled())
      log.debug("Setting startup class properties");
    String methodName = "setParentClassLoader";
    Class&lt;?&gt; paramTypes[] = new Class;
    paramTypes = Class.forName("java.lang.ClassLoader");
    Object paramValues[] = new Object;
    paramValues = sharedLoader;
    Method method =
      startupInstance.getClass().getMethod(methodName, paramTypes);
    method.invoke(startupInstance, paramValues);

    catalinaDaemon = startupInstance;

}
</code></pre>
<p>通过上面几行关键代码的注释,我们就可以看出Catalina是如何初始化的。这里还留下一个问题,tomcat为什么要初始化不同的classloader呢?我们将在下文进行详解。</p>
<h3 id="启动过程类加载机制详解">启动过程:类加载机制详解</h3>
<h4 id="tomcat初始化了哪些classloader">Tomcat初始化了哪些classloader</h4>
<p>在Bootstrap中我们可以看到有如下三个classloader</p>
<pre><code class="language-java">ClassLoader commonLoader = null;
ClassLoader catalinaLoader = null;
ClassLoader sharedLoader = null;
</code></pre>
<h5 id="如何初始化的呢">如何初始化的呢?</h5>
<pre><code class="language-java">private void initClassLoaders() {
    try {
      // commonLoader初始化
      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初始化, 父classloader是commonLoader
      catalinaLoader = createClassLoader("server", commonLoader);
      // sharedLoader初始化
      sharedLoader = createClassLoader("shared", commonLoader);
    } catch (Throwable t) {
      handleThrowable(t);
      log.error("Class loader creation threw exception", t);
      System.exit(1);
    }
}
</code></pre>
<blockquote>
<p>可以看出,catalinaLoader 和 sharedLoader 的 parentClassLoader 是 commonLoader。</p>
</blockquote>
<h5 id="如何创建classloader的">如何创建classLoader的?</h5>
<p>不妨再看下如何创建的?</p>
<pre><code class="language-java">private ClassLoader createClassLoader(String name, ClassLoader parent)
    throws Exception {

    String value = CatalinaProperties.getProperty(name + ".loader");
    if ((value == null) || (value.equals("")))
      return parent;

    value = replace(value);

    List&lt;Repository&gt; repositories = new ArrayList&lt;&gt;();

    String[] repositoryPaths = getPaths(value);

    for (String repository : repositoryPaths) {
      // Check for a JAR URL repository
      try {
            @SuppressWarnings("unused")
            URL url = new URL(repository);
            repositories.add(new Repository(repository, RepositoryType.URL));
            continue;
      } catch (MalformedURLException e) {
            // Ignore
      }

      // Local repository
      if (repository.endsWith("*.jar")) {
            repository = repository.substring
                (0, repository.length() - "*.jar".length());
            repositories.add(new Repository(repository, RepositoryType.GLOB));
      } else if (repository.endsWith(".jar")) {
            repositories.add(new Repository(repository, RepositoryType.JAR));
      } else {
            repositories.add(new Repository(repository, RepositoryType.DIR));
      }
    }

    return ClassLoaderFactory.createClassLoader(repositories, parent);
}
</code></pre>
<p>方法的逻辑也比较简单就是从 catalina.property文件里找 common.loader, shared.loader, server.loader 对应的值,然后构造成Repository 列表,再将Repository 列表传入ClassLoaderFactory.createClassLoader 方法,ClassLoaderFactory.createClassLoader 返回的是 URLClassLoader,而Repository 列表就是这个URLClassLoader 可以加在的类的路径。 在catalina.property文件里</p>
<pre><code class="language-java">common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=
</code></pre>
<p>其中 shared.loader, server.loader 是没有值的,createClassLoader 方法里如果没有值的话,就返回传入的 parent ClassLoader,也就是说,commonLoader,catalinaLoader,sharedLoader 其实是一个对象。在Tomcat之前的版本里,这三个是不同的URLClassLoader对象。</p>
<pre><code class="language-java">Class&lt;?&gt; startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
      Object startupInstance = startupClass.getConstructor().newInstance();
</code></pre>
<p>初始化完三个ClassLoader对象后,init() 方法就使用 catalinaClassLoader 加载了org.apache.catalina.startup.Catalina 类,并创建了一个对象,然后通过反射调用这个对象的 setParentClassLoader 方法,传入的参数是 sharedClassLoader。最后吧这个 Catania 对象复制给 catalinaDaemon 属性。</p>
<h4 id="深入理解">深入理解</h4>
<p>可以复习下类加载机制的基础:解密类加载机制:深入理解JVM如何加载你的代码</p>
<h5 id="什么是类加载机制">什么是类加载机制</h5>
<p>Java是一门面向对象的语言,而对象又必然依托于类。类要运行,必须首先被加载到内存。我们可以简单地把类分为几类:</p>
<ul>
<li>Java自带的核心类</li>
<li>Java支持的可扩展类</li>
<li>我们自己编写的类</li>
<li><strong>为什么要设计多个类加载器</strong>?</li>
</ul>
<blockquote>
<p>如果所有的类都使用一个类加载器来加载,会出现什么问题呢?</p>
</blockquote>
<p>假如我们自己编写一个类<code>java.util.Object</code>,它的实现可能有一定的危险性或者隐藏的bug。而我们知道Java自带的核心类里面也有<code>java.util.Object</code>,如果JVM启动的时候先行加载的是我们自己编写的<code>java.util.Object</code>,那么就有可能出现安全问题!</p>
<p>所以,Sun(后被Oracle收购)采用了另外一种方式来保证最基本的、也是最核心的功能不会被破坏。你猜的没错,那就是双亲委派模式!</p>
<ul>
<li><strong>什么是双亲委派模型</strong>?</li>
</ul>
<blockquote>
<p>双亲委派模型解决了类错乱加载的问题,也设计得非常精妙。</p>
</blockquote>
<p>双亲委派模式对类加载器定义了层级,每个类加载器都有一个父类加载器。在一个类需要加载的时候,首先委派给父类加载器来加载,而父类加载器又委派给祖父类加载器来加载,以此类推。如果父类及上面的类加载器都加载不了,那么由当前类加载器来加载,并将被加载的类缓存起来。</p>
<p><img src="https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202603081048197.png" alt="" loading="lazy"></p>
<p>所以上述类是这么加载的</p>
<ul>
<li>Java自带的核心类 -- 由启动类加载器加载</li>
<li>Java支持的可扩展类 -- 由扩展类加载器加载</li>
<li>我们自己编写的类 -- 默认由应用程序类加载器或其子类加载</li>
</ul>
<blockquote>
<p>但它也不是万能的,在有些场景也会遇到它解决不了的问题,比如如下场景。</p>
</blockquote>
<h5 id="双亲委派模型问题是如何解决的">双亲委派模型问题是如何解决的?</h5>
<blockquote>
<p>在Java核心类里面有SPI(Service Provider Interface),它由Sun编写规范,第三方来负责实现。SPI需要用到第三方实现类。如果使用双亲委派模型,那么第三方实现类也需要放在Java核心类里面才可以,不然的话第三方实现类将不能被加载使用。但是这显然是不合理的!怎么办呢?</p>
</blockquote>
<p><strong>ContextClassLoader</strong>(上下文类加载器)就来解围了。</p>
<p>在java.lang.Thread里面有两个方法,get/set上下文类加载器</p>
<pre><code class="language-java">public void setContextClassLoader(ClassLoader cl)
public ClassLoader getContextClassLoader()
</code></pre>
<p>我们可以通过在SPI类里面调用getContextClassLoader来获取第三方实现类的类加载器。由第三方实现类通过调用setContextClassLoader来传入自己实现的类加载器, 这样就变相地解决了双亲委派模式遇到的问题。</p>
<h5 id="为什么tomcat的类加载器也不是双亲委派模型">为什么Tomcat的类加载器也不是双亲委派模型</h5>
<blockquote>
<p>我们知道,Java默认的类加载机制是通过双亲委派模型来实现的,而Tomcat实现的方式又和双亲委派模型有所区别。</p>
</blockquote>
<p><strong>原因在于一个Tomcat容器允许同时运行多个Web程序,每个Web程序依赖的类又必须是相互隔离的</strong>。因此,如果Tomcat使用双亲委派模式来加载类的话,将导致Web程序依赖的类变为共享的。</p>
<p>举个例子,假如我们有两个Web程序,一个依赖A库的1.0版本,另一个依赖A库的2.0版本,他们都使用了类xxx.xx.Clazz,其实现的逻辑因类库版本的不同而结构完全不同。那么这两个Web程序的其中一个必然因为加载的Clazz不是所使用的Clazz而出现问题!而这对于开发来说是非常致命的!</p>
<h5 id="tomcat类加载机制是怎么样的呢">Tomcat类加载机制是怎么样的呢</h5>
<blockquote>
<p>既然Tomcat的类加载机器不同于双亲委派模式,那么它又是一种怎样的模式呢?</p>
</blockquote>
<p>我们在这里一定要看下官网提供的类加载的文档</p>
<p><img src="https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202603081048483.png" alt="" loading="lazy"></p>
<p>结合经典的类加载机制,我们完整的看下Tomcat类加载图</p>
<p><img src="https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202603081049372.png" alt="" loading="lazy"></p>
<p>我们在这张图中看到很多类加载器,除了Jdk自带的类加载器,我们尤其关心Tomcat自身持有的类加载器。仔细一点我们很容易发现:Catalina类加载器和Shared类加载器,他们并不是父子关系,而是兄弟关系。为啥这样设计,我们得分析一下每个类加载器的用途,才能知晓。</p>
<ul>
<li><strong>Common类加载器</strong>,负责加载Tomcat和Web应用都复用的类
<ul>
<li><strong>Catalina类加载器</strong>,负责加载Tomcat专用的类,而这些被加载的类在Web应用中将不可见</li>
<li><strong>Shared类加载器</strong>,负责加载Tomcat下所有的Web应用程序都复用的类,而这些被加载的类在Tomcat中将不可见
<ul>
<li><strong>WebApp类加载器</strong>,负责加载具体的某个Web应用程序所使用到的类,而这些被加载的类在Tomcat和其他的Web应用程序都将不可见</li>
<li><strong>Jsp类加载器</strong>,每个jsp页面一个类加载器,不同的jsp页面有不同的类加载器,方便实现jsp页面的热插拔</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>同样的,我们可以看到通过<strong>ContextClassLoader</strong>(上下文类加载器)的<strong>setContextClassLoader</strong>来传入自己实现的类加载器</p>
<pre><code class="language-java">public void init() throws Exception {

initClassLoaders();

// 看这里
Thread.currentThread().setContextClassLoader(catalinaLoader);

SecurityClassLoad.securityClassLoad(catalinaLoader);
...
</code></pre>
<h5 id="webapp类加载器">WebApp类加载器</h5>
<blockquote>
<p>到这儿,我们隐隐感觉到少分析了点什么!没错,就是WebApp类加载器。整个启动过程分析下来,我们仍然没有看到这个类加载器。它又是在哪儿出现的呢?</p>
</blockquote>
<p>我们知道WebApp类加载器是Web应用私有的,而每个Web应用其实算是一个Context,那么我们通过Context的实现类应该可以发现。在Tomcat中,Context的默认实现为StandardContext,我们看看这个类的startInternal()方法,在这儿我们发现了我们感兴趣的WebApp类加载器。</p>
<pre><code class="language-java">protected synchronized void startInternal() throws LifecycleException {
    if (getLoader() == null) {
      WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
      webappLoader.setDelegate(getDelegate());
      setLoader(webappLoader);
    }
}
</code></pre>
<p>入口代码非常简单,就是webappLoader不存在的时候创建一个,并调用setLoader方法。我们接着分析setLoader</p>
<pre><code class="language-java">public void setLoader(Loader loader) {

    Lock writeLock = loaderLock.writeLock();
    writeLock.lock();
    Loader oldLoader = null;
    try {
      // Change components if necessary
      oldLoader = this.loader;
      if (oldLoader == loader)
            return;
      this.loader = loader;

      // Stop the old component if necessary
      if (getState().isAvailable() &amp;&amp; (oldLoader != null) &amp;&amp;
            (oldLoader instanceof Lifecycle)) {
            try {
                ((Lifecycle) oldLoader).stop();
            } catch (LifecycleException e) {
                log.error("StandardContext.setLoader: stop: ", e);
            }
      }

      // Start the new component if necessary
      if (loader != null)
            loader.setContext(this);
      if (getState().isAvailable() &amp;&amp; (loader != null) &amp;&amp;
            (loader instanceof Lifecycle)) {
            try {
                ((Lifecycle) loader).start();
            } catch (LifecycleException e) {
                log.error("StandardContext.setLoader: start: ", e);
            }
      }
    } finally {
      writeLock.unlock();
    }

    // Report this property change to interested listeners
    support.firePropertyChange("loader", oldLoader, loader);
}
</code></pre>
<p>这儿,我们感兴趣的就两行代码:</p>
<pre><code class="language-java">((Lifecycle) oldLoader).stop(); // 旧的加载器停止
((Lifecycle) loader).start(); // 新的加载器启动
</code></pre>
<h3 id="启动过程catalina的加载">启动过程:Catalina的加载</h3>
<h4 id="catalina的引入">Catalina的引入</h4>
<blockquote>
<p>通过前面,我们知道了Tomcat的类加载机制和整体的组件加载流程;我们也知道通过Bootstrap初始化的catalinaClassLoader加载了Catalina,那么进而引入了一个问题就是Catalina是如何加载的呢?加载了什么呢?</p>
</blockquote>
<ul>
<li>先回顾下整个流程,和我们分析的阶段</li>
</ul>
<p><img src="https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202603081050972.png" alt="" loading="lazy"></p>
<ul>
<li>看下Bootstrap中Load的过程</li>
</ul>
<pre><code class="language-java">/**
* 加载守护进程
*/
private void load(String[] arguments) throws Exception {

    // Call the load() method
    String methodName = "load";
    Object param[];
    Class&lt;?&gt; paramTypes[];
    if (arguments==null || arguments.length==0) {
      paramTypes = null;
      param = null;
    } else {
      paramTypes = new Class;
      paramTypes = arguments.getClass();
      param = new Object;
      param = arguments;
    }
    Method method =
      catalinaDaemon.getClass().getMethod(methodName, paramTypes);
    if (log.isDebugEnabled()) {
      log.debug("Calling startup class " + method);
    }
    method.invoke(catalinaDaemon, param);// 本质上就是调用catalina的load方法
}
</code></pre>
<h4 id="catalina的加载">Catalina的加载</h4>
<p>上一步,我们知道catalina load的触发,因为有参数所以是load(String[])方法。我们进而看下这个load方法做了什么?</p>
<ul>
<li>load(String[])本质上还是调用了load方法</li>
</ul>
<pre><code class="language-java">/*
* Load using arguments
*/
public void load(String args[]) {

    try {
      if (arguments(args)) { // 处理命令行的参数
            load();
      }
    } catch (Exception e) {
      e.printStackTrace(System.out);
    }
}
</code></pre>
<ul>
<li>load加载过程本质上是初始化Server的实例</li>
</ul>
<pre><code class="language-java">/**
* Start a new server instance.
*/
public void load() {

    // 如果已经加载则退出
    if (loaded) {
      return;
    }
    loaded = true;

    long t1 = System.nanoTime();

    // (已经弃用)
    initDirs();

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

    // 解析 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();

    // 启动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))));
    }
}
</code></pre>
<p>总体流程如下:</p>
<p><img src="https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202603081051887.png" alt="" loading="lazy"></p>
<h5 id="initdirs">initDirs</h5>
<p>已经弃用了,Tomcat10会删除这个方法。</p>
<pre><code class="language-java">/**
* @deprecated unused. Will be removed in Tomcat 10 onwards.
*/
@Deprecated
protected void initDirs() {
}
</code></pre>
<h5 id="initnaming">initNaming</h5>
<p>设置额外的系统变量</p>
<pre><code class="language-java">protected void initNaming() {
// Setting additional variables
if (!useNaming) {
      log.info(sm.getString("catalina.noNaming"));
      System.setProperty("catalina.useNaming", "false");
} else {
      System.setProperty("catalina.useNaming", "true");
      String value = "org.apache.naming";
      String oldValue =
          System.getProperty(javax.naming.Context.URL_PKG_PREFIXES);
      if (oldValue != null) {
          value = value + ":" + oldValue;
      }
      System.setProperty(javax.naming.Context.URL_PKG_PREFIXES, value);
      if( log.isDebugEnabled() ) {
          log.debug("Setting naming prefix=" + value);
      }
      value = System.getProperty
          (javax.naming.Context.INITIAL_CONTEXT_FACTORY);
      if (value == null) {
          System.setProperty
            (javax.naming.Context.INITIAL_CONTEXT_FACTORY,
                "org.apache.naming.java.javaURLContextFactory");
      } else {
          log.debug("INITIAL_CONTEXT_FACTORY already set " + value );
      }
}
}
</code></pre>
<h5 id="serverxml的解析">Server.xml的解析</h5>
<p>分三大块,下面的代码还是很清晰的:</p>
<pre><code class="language-java">protected void parseServerXml(boolean start) {
    // Set configuration source
    ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(Bootstrap.getCatalinaBaseFile(), getConfigFile()));
    File file = configFile();

    if (useGeneratedCode &amp;&amp; !Digester.isGeneratedCodeLoaderSet()) {
      // Load loader
      String loaderClassName = generatedCodePackage + ".DigesterGeneratedCodeLoader";
      try {
            Digester.GeneratedCodeLoader loader =
                  (Digester.GeneratedCodeLoader) Catalina.class.getClassLoader().loadClass(loaderClassName).newInstance();
            Digester.setGeneratedCodeLoader(loader);
      } catch (Exception e) {
            if (log.isDebugEnabled()) {
                log.info(sm.getString("catalina.noLoader", loaderClassName), e);
            } else {
                log.info(sm.getString("catalina.noLoader", loaderClassName));
            }
            // No loader so don't use generated code
            useGeneratedCode = false;
      }
    }

    // 初始化server.xml的位置
    File serverXmlLocation = null;
    String xmlClassName = null;
    if (generateCode || useGeneratedCode) {
      xmlClassName = start ? generatedCodePackage + ".ServerXml" : generatedCodePackage + ".ServerXmlStop";
    }
    if (generateCode) {
      if (generatedCodeLocationParameter != null) {
            generatedCodeLocation = new File(generatedCodeLocationParameter);
            if (!generatedCodeLocation.isAbsolute()) {
                generatedCodeLocation = new File(Bootstrap.getCatalinaHomeFile(), generatedCodeLocationParameter);
            }
      } else {
            generatedCodeLocation = new File(Bootstrap.getCatalinaHomeFile(), "work");
      }
      serverXmlLocation = new File(generatedCodeLocation, generatedCodePackage);
      if (!serverXmlLocation.isDirectory() &amp;&amp; !serverXmlLocation.mkdirs()) {
            log.warn(sm.getString("catalina.generatedCodeLocationError", generatedCodeLocation.getAbsolutePath()));
            // Disable code generation
            generateCode = false;
      }
    }

    // 用 SAXParser 来解析 xml,解析完了之后,xml 里定义的各种标签就有对应的实现类对象了
    ServerXml serverXml = null;
    if (useGeneratedCode) {
      serverXml = (ServerXml) Digester.loadGeneratedClass(xmlClassName);
    }

    if (serverXml != null) {
      serverXml.load(this);
    } else {
      try (ConfigurationSource.Resource resource = ConfigFileLoader.getSource().getServerXml()) {
            // Create and execute our Digester
            Digester digester = start ? createStartDigester() : createStopDigester();
            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);
            if (generateCode) {
                generateClassFooter(digester);
                try (FileWriter writer = new FileWriter(new File(serverXmlLocation,
                        start ? "ServerXml.java" : "ServerXmlStop.java"))) {
                  writer.write(digester.getGeneratedCode().toString());
                }
                digester.endGeneratingCode();
                Digester.addGeneratedClass(xmlClassName);
            }
      } catch (Exception e) {
            log.warn(sm.getString("catalina.configFail", file.getAbsolutePath()), e);
            if (file.exists() &amp;&amp; !file.canRead()) {
                log.warn(sm.getString("catalina.incorrectPermissions"));
            }
      }
    }
}
</code></pre>
<h5 id="initstreams">initStreams</h5>
<p>替换掉System.out, System.err为自定义的PrintStream</p>
<pre><code class="language-java">protected void initStreams() {
    // Replace System.out and System.err with a custom PrintStream
    System.setOut(new SystemLogHandler(System.out));
    System.setErr(new SystemLogHandler(System.err));
}
</code></pre>
<h4 id="catalina-的启动">Catalina 的启动</h4>
<p>在 load 方法之后,Tomcat 就初始化了一系列的组件,接着就可以调用 start 方法进行启动了。</p>
<pre><code class="language-java">/**
* Start a new server instance.
*/
public void start() {

    if (getServer() == null) {
      load();
    }

    if (getServer() == null) {
      log.fatal(sm.getString("catalina.noServer"));
      return;
    }

    long t1 = System.nanoTime();

    // Start the new server
    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(sm.getString("catalina.startup", Long.valueOf((t2 - t1) / 1000000)));
    }

    // Register shutdown hook
    if (useShutdownHook) {
      if (shutdownHook == null) {
            shutdownHook = new CatalinaShutdownHook();
      }
      Runtime.getRuntime().addShutdownHook(shutdownHook);

      // If JULI is being used, disable JULI's shutdown hook since
      // shutdown hooks run in parallel and log messages may be lost
      // if JULI's hook completes before the CatalinaShutdownHook()
      LogManager logManager = LogManager.getLogManager();
      if (logManager instanceof ClassLoaderLogManager) {
            ((ClassLoaderLogManager) logManager).setUseShutdownHook(
                  false);
      }
    }

    if (await) {
      await();
      stop();
    }
}
</code></pre>
<p>上面这段代码,逻辑非常简单,首先确定 getServer() 方法不为 null ,也就是确定 server 属性不为null,而 server 属性是在 load 方法就初始化了。</p>
<p>整段代码的核心就是 try-catch 里的 getServer().start() 方法了,也就是调用 Server 对象的 start() 方法来启动 Tomcat。本篇文章就先不对 Server 的 start() 方法进行解析了,下篇文章会单独讲。</p>
<h4 id="catalina-的关闭">Catalina 的关闭</h4>
<p>调用完 Server#start 方法之后,注册了一个ShutDownHook,也就是 CatalinaShutdownHook 对象,</p>
<pre><code class="language-java">/**
* Shutdown hook which will perform a clean shutdown of Catalina if needed.
*/
protected class CatalinaShutdownHook extends Thread {

@Override
public void run() {
      try {
          if (getServer() != null) {
            Catalina.this.stop();
          }
      } catch (Throwable ex) {
          ExceptionUtils.handleThrowable(ex);
          log.error(sm.getString("catalina.shutdownHookFail"), ex);
      } finally {
          // If JULI is used, shut JULI down *after* the server shuts down
          // so log messages aren't lost
          LogManager logManager = LogManager.getLogManager();
          if (logManager instanceof ClassLoaderLogManager) {
            ((ClassLoaderLogManager) logManager).shutdown();
          }
      }
}
}
</code></pre>
<p>CatalinaShutdownHook 的逻辑也简单,就是调用 Catalina 对象的 stop 方法来停止 tomcat。</p>
<p>最后就进入 if 语句了,await 是在 Bootstrap 里调用的时候设置为 true 的,也就是本文开头的时候提到的三个方法中的一个。await 方法的作用是停住主线程,等待用户输入shutdown 命令之后,停止等待,之后 main 线程就调用 stop 方法来停止Tomcat。</p>
<pre><code class="language-java">/**
* Stop an existing server instance.
*/
public void stop() {

    try {
      // Remove the ShutdownHook first so that server.stop()
      // doesn't get invoked twice
      if (useShutdownHook) {
            Runtime.getRuntime().removeShutdownHook(shutdownHook);

            // If JULI is being used, re-enable JULI's shutdown to ensure
            // log messages are not lost
            LogManager logManager = LogManager.getLogManager();
            if (logManager instanceof ClassLoaderLogManager) {
                ((ClassLoaderLogManager) logManager).setUseShutdownHook(
                        true);
            }
      }
    } catch (Throwable t) {
      ExceptionUtils.handleThrowable(t);
      // This will fail on JDK 1.2. Ignoring, as Tomcat can run
      // fine without the shutdown hook.
    }

    // Shut down the server
    try {
      Server s = getServer();
      LifecycleState state = s.getState();
      if (LifecycleState.STOPPING_PREP.compareTo(state) &lt;= 0
                &amp;&amp; LifecycleState.DESTROYED.compareTo(state) &gt;= 0) {
            // Nothing to do. stop() was already called
      } else {
            s.stop();
            s.destroy();
      }
    } catch (LifecycleException e) {
      log.error(sm.getString("catalina.stopError"), e);
    }

}
</code></pre>
<p>Catalina 的 stop 方法主要逻辑是调用 Server 对象的 stop 方法。</p>
<h4 id="聊聊关闭钩子">聊聊关闭钩子</h4>
<p>上面我们看到CatalinaShutdownHook, 这里有必要谈谈JVM的关闭钩子。</p>
<pre><code class="language-java">if (shutdownHook == null) {
    shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);
</code></pre>
<p>关闭钩子是指通过<strong>Runtime.addShutdownHook注册的但尚未开始的线程</strong>。这些钩子可以用于<strong>实现服务或者应用程序的清理工作</strong>,例如删除临时文件,或者清除无法由操作系统自动清除的资源。</p>
<p>JVM既可以正常关闭,也可以强行关闭。正常关闭的触发方式有多种,包括:当最后一个“正常(非守护)”线程结束时,或者当调用了System.exit时,或者通过其他特定于平台的方法关闭时(例如发送了SIGINT信号或者键入Ctrl-C)。</p>
<p>在<strong>正常关闭中,JVM首先调用所有已注册的关闭钩子</strong>。JVM并不能保证关闭钩子的调用顺序。在关闭应用程序线程时,如果有(守护或者非守护)线程仍然在执行,那么这些线程接下来将与关闭进程并发执行。当所有的关闭钩子都执行结束时,如果runFinalizersOnExit为true【通过Runtime.runFinalizersOnExit(true)设置】,那么JVM将运行这些Finalizer(对象重写的finalize方法),然后再停止。JVM不会停止或中断任何在关闭时仍然运行的应用程序线程。当JVM最终结束时,这些线程将被强行结束。如果关闭钩子或者Finalizer没有执行完成,那么正常关闭进程“挂起”并且JVM必须被强行关闭。当<strong>JVM被强行关闭时,只是关闭JVM,并不会运行关闭钩子</strong>(举个例子,类似于电源都直接拔了,还怎么做其它动作呢?)。</p>
<p>下面是一个简单的示例:</p>
<pre><code class="language-java">public class T {
        @SuppressWarnings("deprecation")
        public static void main(String[] args) throws Exception {
                //启用退出JVM时执行Finalizer
                Runtime.runFinalizersOnExit(true);
                MyHook hook1 = new MyHook("Hook1");
                MyHook hook2 = new MyHook("Hook2");
                MyHook hook3 = new MyHook("Hook3");
               
                //注册关闭钩子
                Runtime.getRuntime().addShutdownHook(hook1);
                Runtime.getRuntime().addShutdownHook(hook2);
                Runtime.getRuntime().addShutdownHook(hook3);
               
                //移除关闭钩子
                Runtime.getRuntime().removeShutdownHook(hook3);
               
                //Main线程将在执行这句之后退出
                System.out.println("Main Thread Ends.");
        }
}

class MyHook extends Thread {
        private String name;
        public MyHook (String name) {
                this.name = name;
                setName(name);
        }
        public void run() {
                System.out.println(name + " Ends.");
        }
        //重写Finalizer,将在关闭钩子后调用
        protected void finalize() throws Throwable {
                System.out.println(name + " Finalize.");
        }
}
</code></pre>
<p>和(可能的)执行结果(因为JVM不保证关闭钩子的调用顺序,因此结果中的第二、三行可能出现相反的顺序):</p>
<pre><code class="language-java">Main Thread Ends.
Hook2 Ends.
Hook1 Ends.
Hook3 Finalize.
Hook2 Finalize.
Hook1 Finalize.
</code></pre>
<p>可以看到,main函数执行完成,首先输出的是Main Thread Ends,接下来执行关闭钩子,输出Hook2 Ends和Hook1 Ends。这两行也可以证实:JVM确实不是以注册的顺序来调用关闭钩子的。而由于hook3在调用了addShutdownHook后,接着对其调用了removeShutdownHook将其移除,于是hook3在JVM退出时没有执行,因此没有输出Hook3 Ends。</p>
<p>另外,由于MyHook类实现了finalize方法,而main函数中第一行又通过Runtime.runFinalizersOnExit(true)打开了退出JVM时执行Finalizer的开关,于是3个hook对象的finalize方法被调用,输出了3行Finalize。</p>
<p>注意,多次调用addShutdownHook来注册同一个关闭钩子将会抛出IllegalArgumentException:</p>
<pre><code class="language-java">Exception in thread "main" java.lang.IllegalArgumentException: Hook previously registered
        at java.lang.ApplicationShutdownHooks.add(ApplicationShutdownHooks.java:72)
        at java.lang.Runtime.addShutdownHook(Runtime.java:211)
        at T.main(T.java:12)
</code></pre>
<p>另外,从JavaDoc中得知:<strong>一旦JVM关闭流程开始,就只能通过调用halt方法来停止该流程,也不可能再注册或移除关闭钩子了,这些操作将导致抛出IllegalStateException</strong>。</p>
<p>如果在关闭钩子中关闭应用程序的公共的组件,如日志服务,或者数据库连接等,像下面这样:</p>
<pre><code class="language-java">Runtime.getRuntime().addShutdownHook(new Thread() {
        public void run() {
                try {
                        LogService.this.stop();
                } catch (InterruptedException ignored){
                        //ignored
                }
        }
});
</code></pre>
<p>由于<strong>关闭钩子将并发执行,因此在关闭日志时可能导致其他需要日志服务的关闭钩子产生问题</strong>。<strong>为了避免这种情况,可以使关闭钩子不依赖那些可能被应用程序或其他关闭钩子关闭的服务</strong>。实现这种功能的一种方式是对所有服务使用同一个关闭钩子(而不是每个服务使用一个不同的关闭钩子),并且在该关闭钩子中执行一系列的关闭操作。这确保了关闭操作在单个线程中串行执行,从而避免了在关闭操作之前出现竞态条件或死锁等问题。</p>
<h5 id="使用场景">使用场景</h5>
<p>通过Hook实现临时文件清理</p>
<pre><code class="language-java">public class test {

public static void main(String[] args) {
      try {
          Thread.sleep(20000);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }

      Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
          public void run() {
            System.out.println("auto clean temporary file");
          }
      }));
}
}
</code></pre>
<h4 id="小结">小结</h4>
<p>Catalina 类承接了 Bootstrap 类的 load 和 start 方法,然后根据配置初始化了 Tomcat 的组件,并调用了 Server 类的 init 和 start 方法来启动 Tomcat。</p>


</div>
<div id="MySignature" role="contentinfo">
    <p>本文来自在线网站:seven的菜鸟成长之路,作者:seven,转载请注明原文链接:www.seven97.top</p><br><br>
来源:https://www.cnblogs.com/sevencoding/p/19887168
頁: [1]
查看完整版本: Tomcat的架构设计和启动过程详解