概述
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 对象发送响应信息。
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.
其中最值得关注的两个目录是 conf
和 webapps
。我们在代码根目录下直接运行 Tomcat 也是可以的,这里为了更好的梳理目录结(因为运行时还会额外生成 logs
,work
等目录),所以我们建立一个 resource
目录,把 conf
和 webapps
放进去。
同时在 VM Options 中指定 CATLINA_HOME
和 CATLINA_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 环境。
文章评论