Java老司机竟然连 Tomcat 都启动不起来?

221 阅读6分钟

前言

        上一篇文章我们全面的介绍了Tomcat各文件目录的作用、源码各模块功能和组件之间的关系,下面我们从源码的角度去看看Tomcat是如何工作的。

一、启动脚本分析

tomcat为我们提供了启动和停止脚本,在bin目录下:startup.sh/startup.bat;shutdown.sh/shutdown.bat

image.png

下面我们以startup.sh启动脚本为例,看一下Tomcat如何运行的。

# ...省略部分逻辑代码
PRGDIR=`dirname "$PRG"`
EXECUTABLE=catalina.sh
exec "$PRGDIR"/"$EXECUTABLE" start "$@"

由于脚本中存在大量的环境变量获取的逻辑。我们只需要看关键代码即可,我们发现**startup.sh最终是执行了catalina.sh,**由于catalina.sh脚本定义了Tomcat服务器中所有的启动,运行,停止脚本,因此在这里我们只看一下start部分的逻辑即可,大量无关逻辑的先跳过。

# ...省略部分逻辑代码
eval $_NOHUP "\"$_RUNJAVA\"" "\"$CATALINA_LOGGING_CONFIG\"" 
$LOGGING_MANAGER "$JAVA_OPTS" "$CATALINA_OPTS" \
      -D$ENDORSED_PROP="\"$JAVA_ENDORSED_DIRS\"" \
      -classpath "\"$CLASSPATH\"" \
      -Dcatalina.base="\"$CATALINA_BASE\"" \
      -Dcatalina.home="\"$CATALINA_HOME\"" \
      -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
      org.apache.catalina.startup.Bootstrap "$@" start \
      >> "$CATALINA_OUT" 2>&1 "&"

至此我们可以看出来,脚本中设置了一些服务器路径作为环境变量,最终是通过java执行了org.apache.catalina.startup.Bootstrap"$@" start 指令,那我们直接看Bootstrap启动类即可;

二、源码分析

2.1 初探Bootstrap启动类(入口类)

public static void main(String args[]) {
  synchronized (daemonLock) {
      if (daemon == null) {
          // Don't set daemon until init() has completed
          Bootstrap bootstrap = new Bootstrap();
          try {
              //初始化 catalinaDaemon = Catalina
              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(){
        //...省略部分代码
      }else if (command.equals("start")) {
          daemon.setAwait(true);
          daemon.load(args);
          daemon.start();
          if (null == daemon.getServer()) {
              System.exit(1);
          }
      } 
    // ...省略部分代码
}

启动的时候执行了3个方法

  1. daemon.setAwait(true); //设置服务等待,直到接收到shutdown指令
  2. daemon.load(args);       //执行加载
  3. daemon.start();              //执行开始

下面我们从源码角度分析Tomcat的启动流程(放大看)。

2.2 启动流程

image.png

核心组件说

组件名称说明
Server表示整个Servlet容器,Tomcat运行环境中只有唯一一个Server实例。
ServiceService可以包含多个Connector,这些Connector共享同一个Container来处理请求。在一个Tomcat实例内可以包含任意多个Service实例,它们彼此独立。
Connector链接器,用于监听并转化Socket请求,同时将读取的Socket请求交由Container处理,支持不同的协议以及不同的I/O实现方式。
ContainerContainer表示能够执行客户端请求并返回响应的一类对象。Tomcat中存在不同级别的容器:Engine,Host,Context,Wrapper。
EngineEngine表示整个Servlet引擎。在Tomcat中Engine为最高层级的容器对象。尽管Engine不是直接处理请求的容器,却是获取目标容器的入口。
HostHost作为一类容器,表示Servlet引擎(Engine)中的虚拟机,与一个服务器的网络名有关,如域名等。客户端可以使用这个网络名连接服务器,这个名称必须要在DNS服务器上注册。
ContextContext作为一类容器,用于表示ServletContext,在Servlet规范中,一个ServletContext即表示一个独立的Web应用。
WrapperWrapper作为一类容器,用于表示Web应用中定义的Servlet。
Executor共享线程池

通过阅读源码,我们初步总结Tomcat通过Bootstrap启动类的Main方法为入口,逐级调用Tomcat各组件的init方法及start方法从而完成Tomcat服务器的运行。

2.3 核心配置文件 server.xml

日常配置最多的就是server.xml,仔细看各配置项,所有的组件基本上都包含在了该文件中了,Server,Service,Connector,Engine,Host等,那么Tomcat是如何通过配置文件与各组件之间建立联系的呢?

<!-- Server 代表一个 Tomcat 实例。可以包含一个或多个 Services,其中每个 Service 都有自己的 Engines 和 Connectors。port="8005"指定一个端口,这个端口负责监听关闭 tomcat 的请求 -->
<Server port="8005" shutdown="SHUTDOWN">
  <!-- 监听 -->
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <!-- Security listener. Documentation at /docs/config/listeners.html
  <Listener className="org.apache.catalina.security.SecurityListener" />
  -->
  <!-- APR library loader. Documentation at /docs/apr.html -->
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <!-- 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" />
    <!-- JNDI命名。全局命名资源,定义了 UserDatabase 的一个 JNDI(java 命名和目录接口),通过 pathname 的文件得到一个用户授权的内存数据库 -->
  <GlobalNamingResources>

    <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" />
  </GlobalNamingResources>
    <!-- Service 它包含一个<Engine>元素,以及一个或多个<Connector>,这些 Connector 元素共享用同一个 Engine 元素 -->
  <Service name="Catalina">
        <!-- 链接器 定义一个通过 8080 端口接收 HTTP 请求 -->
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    <!-- 引擎 -->
    <Engine name="Catalina" defaultHost="localhost">
            <!-- 权限 -->
      <Realm className="org.apache.catalina.realm.LockOutRealm">

        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>
            <!-- 虚拟机 -->
      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
        <!-- 日志阀门 -->
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
      </Host>
    </Engine>
  </Service>
</Server>

2.3.1 Xml解析组件Digester

Tomcat在Catalina初始化过程中使用Digester来解析server.xml。并创建出应用服务器。Digester通过流读取XML文件,当识别出特定的XML节点后便会执行特定的动作,或者创建Java对象,或者执行对象的某个方法。

如以下解析server.xml的代码(部分),声明Server类为StandardServer,执行setServer方法。

// 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");

简单一点来说其实就是相当于执行了以下方法

Server server=new StandardServer();  
//... set server properties 
setServer(server);

2.4 生命周期 Lifecycle

Tomcat中所有组件基本上都存在初始化、启动、停止、销毁等动作,以及相应的执行前、执行中、执行后状态。为此抽象出了一个Lifecycle通用接口。

每个生命周期方法可能对应数个状态的转换,生命周期状态由LifecycleBase抽象类自动为我们变更,并且发布各状态变更之后的处理事件通知相关监听处理类进行处理。

image.png

2.4.1 Container

Tomcat使用Container表示容器,Container可以添加并维护子容器,因此Engine、Host、Context、Wrapper均继承自Container。

image.png

此外,Tomcat的Container还有一个很重要的功能,就是后台处理,在很多情况下,Container需要执行一些异步处理,而且是定期执行,如每隔30秒执行一次,Tomcat对于Web应用文件变更的扫描就是通过该机制实现的。

Tomcat针对后台处理,在Container上定义了backgroundProcess()方法,基础抽象类(ContainerBase)确保在启动组件的同时,异步启动后台处理。

因此,在绝大多数情况下,各个容器组件仅需要实现Container的backgroundProcess()方法即可,不必考虑创建异步线程。

2.4.2 Pipeline和Valve

Tomcat采用责任链模式实现客户端的请求处理,它定义了Pipeline(管道)和Valve(阀门)两个接口,前者用于构造职责链,后者代表职责链上的每个处理器。

Pipeline中维护了一个基础的Valve,它始终位于Pipeline的末端(即最后执行),封装了具体的请求处理和输出响应的过程。

然后,通过addValve()方法,可以为Pipeline添加其它的Valve。后添加的Valve位于基础Valve之前,并按照添加顺序执行。Pipeline通过获得首个Valve来启动整个链条的执行。

image.png

比如:默认配置下,Host的实现是StandardHost,它的主要功能交给了StandardHostValve来做。额外配置一个日志的功能就特别方便了,只需要在管道上再加一个日志功能的阀门即可实现。

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

三、小结

Tomcat主要就是通过解析server.xml配置文件,从而构建出各个相关组件,利用生命周期将各个组件之间串联起来,最终提供给我们来使用。

Tomcat作为经久不衰的一款轻量级Java应用服务器,它的设计有很多值得我们借鉴的地方。它的一些优秀的组件比如xml解析的工具Digester我们可以单独拿出来使用。它的思想比如各组件之间松耦合我们可以参考。

它的设计模式的实现比如生命周期的模板方法模式管道阀门组合的责任链模式等也是值得我们去学习的东西。下一节我们将针对应用加载,去分析Tomcat是如何保证作为一个服务器去运行多个应用而相互之间又保持隔离的。