Tomcat 基础架构与调试环境搭建

2021-02-05 1042点热度 0人点赞 0条评论

概述

Tomcat 主要有两部分核心功能:

  • HTTP 服务器:实现 Socket 通信(TCP/IP),解析 HTTP 报文
  • Servlet 容器:提供一些默认 Servlet 的实现,管理自定义的 Servlet

Servlet 容器是如何工作的

Servlet 容器是一个复杂的系统,但是,它有 3 个基本任务,对每个请求,servlet 容器会为其完成以下 3 个操作::

  • 创建一个 request 对象,用可能会在调用的 Servlet 中使用到的信息填充该 request 对象,如参数、头 cookie 、查询字符串、 URI 等。 reqeust 对象是 javax.servlet.ServletRequest 接口或 javax.ervlet.http.ServletRequest 接口的一个实例;
  • 创建一个调用 Servlet 的 response 对象,用来向 Web 客户端发送响应。 response 对象是 javax.servlet.ServletResponse 接口或 javax servlet.ttp.ServletResponse 接口的一个实例;
  • 调用 Servlet 的 service()方法,将 request 对象和 response 对象作为参数传人。 Servlet 从 request 对象中读取信息,并通过 response 对象发送响应信息。
tomcat_connectors tomcat_architecture

EndPoint 组件和 Processor 组件有一个组合名称:ProtocolHandier 。

  • EndPoint 组件进行 Socket 通信,处理 TCP/IP 协议;
  • Processor 组件解析处理 Http 报文,处理 Http 协议;

从配置文件看总体结构

在 Tomcat 源代码的 conf/server.xml 中,定义了如下结构的节点:

<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">

  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <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 name="Catalina">
    <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 "%r" %s %b" />
      </Host>
    </Engine>
  </Service>
</Server>
  • Server:Server 容器代表了一个 tomcat 实例(Catalina 实例),可以包含一个或多个 Service 容器;
  • Service:Service 是提供具体的对外服务的,一个 Service 容器中可以有多个 Connector 组件(监听不同的端口请求并处理)和一个 Servlet 容器(做具体的业务处理逻辑);
  • Engine 和 Host:Engine 组件是 Servlet 容器的核心,它支持定义多个虚拟主机(Host),虚拟主机允许 Tomcat 引擎在一台机器上配置多个域名;
  • Context:每个虚拟主机下支持部署多个 Web 应用,这就是我们所熟悉的上下文对象。 Context 是使用由 Servlet 规范中制定的 Web 应用程序的格式表示,不论是压缩后的 war 格式还是为压缩的目录结构;
  • Wrapper:在一个 Context 中可以部署多个 Servlet,并且每个 Servlet 都会被一个 Wrapper 所包含。

Tomcat 这样 “套娃式” 的结构架构有以下这些优点:

  • 组件间的关系清晰,组件的生命周期管理方便;
  • 与 XML 配置文件中的标签结构相对应,解读与封装配置类;
  • 子容器可以自然的继承父容器的配置与上下文。

调试环境搭建

获取源码

很多教程喜欢教大家去官网下载某个版本对应的源码的 release 包,但我觉得官网的 release 包更多的应该是一个版本归档的作用。

我个人更喜欢去 Github 上去搜索这个项目的 repo 。用 Github 上的代码进行学习有几个好处。

首先,Github 上的代码拥有完整的 SVN 历史。虽然一开始可能也只是外行粗略的看个热闹,但 SVN 记录给我们提供了极大的便利。

  • 自由的在 branch 之间切换,定位到任意的 commit,比较 diff;
  • 可以在本地拉出自己的 branch,这样自己在代码里添加一些标记,注释,或者调试代码,这些改动都可以很好的被 SVN track 到,一目了然;
  • 可以方便的更新代码,而不需要再去重新手动下载另一个版本。

对于大型的项目,即使开发阶段代码不是直接托管在 Github 上,一般也会在 Github 上 host 一个镜像方便开发者们找到和拉取,比如 JDK 。

因此这里我们从 Tomcat 官方的 Github 上直接 checkout 代码即可:https://github.com/apache/tomcat

添加 Maven 依赖

Tomcat 项目本来是使用 ant 构建的,对 IDE 调试环境不友好,因此我们把依赖的组件抽到 Maven 中统一管理,省时省力。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>apache-tomcat-8.5.x</artifactId>
    <name>tomcat-8.5</name>
    <version>8.5</version>
    <build>
        <sourceDirectory>java</sourceDirectory>
        <resources>
            <resource>
                <directory>java</directory>
            </resource>
        </resources>
        <testResources>
            <testResource>
                <directory>test</directory>
            </testResource>
        </testResources>
        <plugins>
            <!--引⼊编译插件-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <!--tomcat 依赖的基础包-->
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.easymock</groupId>
            <artifactId>easymock</artifactId>
            <version>4.2</version>
        </dependency>
        <dependency>
            <groupId>wsdl4j</groupId>
            <artifactId>wsdl4j</artifactId>
            <version>1.6.3</version>
        </dependency>
        <dependency>
            <groupId>javax.xml.rpc</groupId>
            <artifactId>javax.xml.rpc-api</artifactId>
            <version>1.1.2</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jdt.core.compiler</groupId>
            <artifactId>ecj</artifactId>
            <version>4.6.1</version>
        </dependency>
        <dependency>
            <groupId>javax.xml.soap</groupId>
            <artifactId>javax.xml.soap-api</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.ant</groupId>
            <artifactId>ant</artifactId>
            <version>1.10.9</version>
        </dependency>
    </dependencies>
</project>

启动类与启动参数

以 Tomcat 8.5.x 代码为例,官网对代码目录结构的描述如下:

These are some of the key tomcat directories:
  • /bin - Startup, shutdown, and other scripts. The *.sh files (for Unix systems) are functional duplicates of the *.bat files (for Windows systems). Since the Win32 command-line lacks certain functionality, there are some additional files in here.
  • /conf - Configuration files and related DTDs. The most important file in here is server.xml. It is the main configuration file for the container.
  • /logs - Log files are here by default.
  • /webapps - This is where your webapps go.

其中最值得关注的两个目录是 confwebapps 。我们在代码根目录下直接运行 Tomcat 也是可以的,这里为了更好的梳理目录结(因为运行时还会额外生成 logswork 等目录),所以我们建立一个 resource 目录,把 confwebapps 放进去。

同时在 VM Options 中指定 CATLINA_HOMECATLINA_BASE

-Dcatalina.home=/home/min/IdeaProjects/tomcat/resource
-Dcatalina.base=/home/min/IdeaProjects/tomcat/resource
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Djava.util.logging.config.file=/home/min/IdeaProjects/tomcat/resource/conf/logging.properties

运行 Main Class 为 org.apache.catalina.startup.Bootstrap 。至于为什么入口是这个类,我们下回详细分解。

初始化 JSP 引擎

从 IDEA 运行 Bootstrap 类,看到日志 Tomcat 已经运行起来了。接着在浏览器中输入 127.0.0.1:8080,看到出现了 500 错误页面:

Exception
org.apache.jasper.JasperException: Unable to compile class for JSP
    org.apache.jasper.JspCompilationContext.compile(JspCompilationContext.java:613)
    org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:399)
    org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:382)
    org.apache.jasper.servlet.JspServlet.service(JspServlet.java:330)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:733)
Root Cause

java.lang.NullPointerException
    org.apache.jasper.compiler.Validator$ValidateVisitor.<init>(Validator.java:523)
    org.apache.jasper.compiler.Validator.validateExDirectives(Validator.java:1855)
    org.apache.jasper.compiler.Compiler.generateJava(Compiler.java:221)
    org.apache.jasper.compiler.Compiler.compile(Compiler.java:375)
    org.apache.jasper.compiler.Compiler.compile(Compiler.java:351)
    org.apache.jasper.compiler.Compiler.compile(Compiler.java:335)
    org.apache.jasper.JspCompilationContext.compile(JspCompilationContext.java:597)
    org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:399)
    org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:382)
    org.apache.jasper.servlet.JspServlet.service(JspServlet.java:330)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:733)

原因是真正的 Tomcat 在运行时,除了 Bootstrap 以外还要加载一些其他的外部组件,包括 JSP 编译引擎,因为 Tomcat 内置的页面也是 JSP 写的。 JSP 引擎没有加载的情况下,访问主页自然无法显示页面。

为了 debug 方便,我们直接把这个逻辑插入到 Bootstrap.java 中,来手动初始化一个 JSP 引擎:

public class ContextConfig implements LifecycleListener {
    /**
     * Process a "contextConfig" event for this Context.
     */
    protected synchronized void configureStart() {
        // ...
        webConfig();

        // Initialize JSP parser engine
        context.addServletContainerInitializer(new JasperInitializer(),null);

        // ...
    }
}

重新启动 Bootstrap Application 并访问页面,Tomcat 主页已经可以正常显示了。至此我们搭建好了 Tomcat 的 debug 环境。

SilverLining

也可能是只程序猿

文章评论