前言
本篇文章 会针对tomcat的实现原理,以及servlet Tomcat容器与运行机制,利用servlet的规范实现的一套web服务器,而spring mvc 也是 spring实现了servlet 的web应用程序。 tomcat这么流行 的Servlet Web容器,具有大量的配置可以扩展 良好的运行效果,在开发中不应该只是会用,更重要的是对原理以及实现方式的理解,也许最后你也能写出很好的一个tomcat框架,也是为什么去研究他的原因。
Tomcat介绍
Tomcat 实现了 Java Servlet , JavaServer Page , Java 表达式语言和 Java 的 WebSocket 技术的一个开源实 现。说白了就是一个非常流行的Servlet Web 容器。
Apache Tomcat®软件是Jakarta Servlet,Jakarta Server Pages,Jakarta Expression Language,Jakarta WebSocket,Jakarta Annotations和Jakarta Authentication规范的开源实现。这些规范是 Jakarta EE 平台的一部分.
Jakarta EE 平台是 Java EE 平台的演变。Tomcat 10 及更高版本实现了作为 Jakarta EE 的一部分开发的规范。Tomcat 9 及更早版本实现了作为 Java EE 的一部分开发的规范。
在9.0.40 根据 不同的安装包去下载不同的安装包。
下载过后 的关键目录
1. lib
Tomcat 依赖的 jar 包
2. logs
catalina-xxx.log
localhost-xxx.log
3. webapps
web 应用部署目录, eclipse 中的配置演示
在bin目录中 包括的 bat 文件启动的 对应的 win版本的启动以及 linux的启动方式。
我们要开发中间件 都参考这种形式去开发就可以了。
config目录 这里面 的web.xml 以及 catalina.policy 设置运行jvm的 权限 等参数 都放到 这里面。
在 policy 中 包括了 shutdown 以及 juc日志等。
配置文件
jconsol 调试参数,观察参数变化带来的影响
1. server.xml
调整连接池的大小
设置 I/O 模式 (BIO 与 NIO)
去掉 AJP 的 Connector
移除 access-log 日志
server 里面设置监听器等等。
以及 端口号 以及转发端口号
执行器 线程池,重新定义 数量等等。
对应的默认 host 主机名称等 都在server.xml中配置。
server.xml 中可以 设置的。
阈值 需要处理的事情。
区分多个应用 还是有个 上下文 context 上下文 在 context里面 最终进行映射 ,这里可以使用这个 作为访问 到 其他目录下面的 路径。 默认 是 root 访问到 工作目录下 ,然后可以根据配置 可以 访问到其他目录下面 其实这里是可以配置到 其他目录下面的 访问的。 包括 f盘或者其他的。
2. web.xml
servlet 标签: DefaultServlet 、 JspServlet
servlet-mapping 标签: servlet 的访问路径
mime-mapping 标签,支持的内容类型: json 、 xml 、 html 、 jpg 等
对于jsp 的请求访问 处理 方式等。
在 web.xml中可以 设置的json,能够解读 对应的映射地方 在 tomcat中 自动给我们设置好的。
tomcat-users.xml 配置相关的权限等。配置方式 在 文件上 都有一个例子给我们使用。
然后以及经常使用的地方也就是 webapps 启动 到 对应的 项目上
在使用上可以配置 对应 的访问地址,这都是tomcat提供给我们进行项目地址
jmx是jdk给我们提供的管理插件的功能,
部署方式
显式部署
1. 添加 context 元素方式 (server.xml)
2. 创建 xml 的方式
在 conf/Catalina/localhost 目录创建的 xml 文件
能访问到其他目录下面。
隐式部署
将一个 war 文件或者整个应用程序复制到 Tomcat 的 webapps
Tomcat源码下载与构建
IDE 装载 Tomcat 项目源码
Apache Tomcat 9 (9.0.60) - Building Tomcat
需要添加 环境变量这些。
Tomcat 体系结构
Apache Tomcat 9 Architecture (9.0.60) - Table of Contents
Server
在 Tomcat 中, Server 代表整个容器。
Service
服务是一个中间组件,它在服务器内部,将一个或多个连接器连接到一个引擎。
Engine
Engine 也就是以前版本中的 Container 。引擎表示特定服务的请求处理管道。 服务可能有多个连接器, 引擎接收并处理来自这些连接器的所有请求,将响应返回到适当的连接器以传输到客户端。 引擎接口可 以实现,以提供自定义引擎,尽管这是不常见的。引擎可以通过jvm 路由参数用于 Tomcat 服务器集群。
Engine 包含: Host 、 Context 、 Wrapper 这几个容器,他们都是 Container 子类型。
Connerctor
Connector 负责把接收到的请求解析出来然后封装成 request 和 response 对象然后交给 Container 处理。 目前Connector 支持 http 和 ajp 协议。 连接器处理与客户端的通信。 Tomcat 有多个可用的连接器。 其中包括 HTTP 连接器、 AJP 连接器。当将 Tomcat作为独立服务器运行时使用 HTTP 协议的 HTTP 连接器;当将 Tomcat 连接到 Apache HTTPD 服务器等Web 服务器时使用的 AJP 协议的 AJP 连接器。 还可以创建自定义连接器。
Host
主机是网络名称的关联。 例如: www.yourcompany.com 能访问到 Tomcat 服务器。引擎可能包含多个主机,主机元素还支持网络别名,如yourcompany.com 和 abc.yourcompany.com 。 用户很少创建自定义主机,因为标准主机实现提供了重要的附加功能。
Host 说白了就是我们所理解的虚拟主机。
Context
上下文表示 Web 应用程序。 主机可能包含多个上下文,每个上下文具有唯一的路径。 上下文接口可以 实现创建自定义上下文,但这种情况很少,因为标准上下文提供了重要的附加功能。
Context 就是我们所部属的具体 Web 应用的上下文,每个请求都在是相应的上下文里处理的。
Wrapper
Wrapper 是针对每个 Servlet 的 Container ,每个 Servlet 都有相应的 Wrapper 来管理。
Tomcat容器与运行机制
Tomcat 启动步骤
Tomcat 的启动方式有下面几种方式:
1. 通过命令行启动
2. 通过 Java 程序,作为一个内嵌服务启动
3. 作为 windows 服务自动启动
bootstrap 作为启动类,
赋值catlina.bat 调用格式, start参数, 其实 start.bat 调用时,都是调用的catalina.bat,这个可以在脚本中就可以看出来
这里对于主调用的 启动类就是 bootstrap
在tomcat中 的启动类
对于参数来说, 传入的参数 第一个参数就是 start
启动 最终底层 都调用到了bootstarp
做了初始化的操作 init
继续 加载 catalina
紧接着 继续下去
根据不同的 命令 选择 不同的方式进行方式。
然后继续下去 就是 初始化 命名 ,
设置命名空间等等。 初始化的factroy
解析 对应的serverxml 并设置 home 的file
创建 摘要器等 初始化部分。
Bootstrap
Tomcat 启动类, Tomcat 启动都是通过 org.apache.catalina.startup.Bootstrap 类的 main 方法来进行启 动的。它所做的工作包括:
commonLoader (common)-> System Loader sharedLoader (shared)-> commonLoader ->
System Loader catalinaLoader(server) -> commonLoader -> System Loader
默认, commonLoader 用于 sharedLoader 和 catalinaLoader
org.apache.catalina.startup.Catalina setParentClassloader -> sharedLoader
Thread.contextClassloader -> catalinaLoader
- 3. Bootstrap的守护线程初始化方法执行完成
步骤 2 :处理命令行参数
Bootstrap 处理 start 、 stop 这样的启动命令参数,这里以 start 参数解释启动步骤。工作流程如下:
1. Catalina.setAwait(true)
2. Catalina.load()
catalina.home = D:/tomcat
catalina.base == catalina.home
setProperty ( javax . naming . Context . INITIAL_CONTEXT_FACTORY ,
org . apache . naming . java . javaURLContextFactory -
> default )
- 3. createStartDigester()
- 4. 加载server.xml并解析
自动的使用 digester 进行 server.xml 的解析
XML 对象映射工具,它将创建 server.xml
实际上此时容器的启动尚未开始。
将 System.out 、 System.err 绑定到 SystemLogHandler 类上
- 6. 调用所有的组件初始化方法,确保每个对象都注册到了JMX代理上
在这个过程中,连接器也初始化适配器。适配器是执行请求预处理的组件。典型的适配器是
HTTP1.1 (如果没有指定协议,则默认为, org.apache.coyote.http11.Http11NioProtocol )
1. 启动NamingContext绑定所有JNDI引用到NamingContext中
2. 启动标签下的service
3. 通过Service启动StandardHost
- 配置ErrorReportValve以对不同的HTTP错误代码执行正确的HTML输出
- 启动管道中的阀门Valve(至少是ErrorReportValve)
- 配置StandardHostValve
这个阀门将Webapp类加载器绑定到线程上下文
它还会找到请求的会话
并调用上下文管道
此组件部署所有webapps , (webapps & conf/Catalina/localhost/*.xml)
HostConfig将为你的上下文创建一个 Digester 摘要器,这个 Digester 将调研
ContextConfig.start()方法
ContextConfig.start()将会处理默认的 web.xml(conf/web.xml) ,然后再处理应用的
web.xml (WEB-INF/web.xml)
在容器( StandardEngine )的生存期内,有一个后台线程不断检查上下文是否已更改。如果
上下文发生变化( war 包的时间戳, context.xml 文件, web.xml 文件 ) ,然后发出重新加载
( stop/remove/deploy/start )
4. Tomcat 在HTTP端口上开始接受请求
5. Servlet类调用
Tomcat处理请求的过程
- 1. 用户点击网页内容,请求被发送到本机端口8080,被在那里监听的Coyote HTTP/1.1 Connector获得。
- 2. Connector把该请求交给它所在的Service的Engine来处理,并等待Engine的回应。
- 3. Engine获得请求localhost/test/index.jsp,匹配所有的虚拟主机Host。
- 4. Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机),名为localhost的Host获得请求/test/index.jsp,匹配它所拥有的所有的 Context。Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为“ ”的Context 去处理)。 path=“/test”的Context获得请求/index.jsp,在它的mapping table中寻找出对应的 Servlet。Context匹配到URL PATTERN为*.jsp的Servlet,对应于JspServlet类。
- 5. 构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet() 或doPost().执行业务逻辑、数据存储等程序。
- 6. Context把执行完之后的HttpServletResponse对象返回给Host。
- 7. Host把HttpServletResponse对象返回给Engine。
- 8. Engine把HttpServletResponse对象返回Connector。
- 9. 1Connector把HttpServletResponse对象返回给客户Browser。
Tomcat中的组件
JVM 类加载器及双亲委派
JVM 类加载器结构
public class Main {
public static void main(String[] args) {
Main main = new Main();
System.out.println(main.getClass().getClassLoader());
System.out.println(main.getClass().getClassLoader().getParent());
System.out.println(main.getClass().getClassLoader().getParent().getParent());
}
}
双亲委派机制
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
双亲委派优势
Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重
复加载,当父亲已经加载了该类时,子 ClassLoader 就没有必要再加载一次。
java 核心 api 中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.Integer 的类,通过
双亲委托模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字的类,发现该类
已被加载,并不会重新加载网络传递的过来的 java.lang.Integer ,而直接返回已加载过的
Integer.class ,这样便可以防止核心 API 库被随意篡改。
由于 jre\lib\ext 中存在 java.lang.String 类,当加载该类的时候,根据全限定名进行查找,找到后
由启动类加载器加载,发现 String 类中不包含 main() 方法,因此程序出错。
Tomcat 的类加载器
Apache Tomcat 9 (9.0.62) - Introduction
Tomcat 的类加载器结构
Catalina 类加载器和 Shared 类加载器,他们并不是父子关系,而是兄弟关系。
1. 保证应用程序各自类库的独立隔离 ,一个 Web 容器可能需要部署多个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
2. 共享相同版本类库 ,部署在同一个 Web 容器中相同的类库相同的版本可以共享。否则,如果服务器有10 个应用程序,那么要有 10 份相同的类库加载进虚拟机,显然不合适。
3. 安全 - 隔离 Web 容器与应用程序类库 , Web 容器也有自己依赖的类库,不能于应用程序的类库混
淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
4. 支持 JSP 热修改 , Web 容器要支持 jsp 的修改, jsp 文件最终编译成 class 文件才能在虚拟机中运行,在程序运行后修改jsp 是常见的事,否则要你何用? 所以, web 容器需要支持 jsp 修改后不用重启。
打破双亲委派机制
WebAppClassLoader 与 JasperLoader 查找类则不会先往上查找,而是直接在自己类加载器中进行查找,不遵循双亲委派机制。
为什么不遵循双亲委派机制 ?
1. 如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管 你是什么版本的,只在乎你的全限定类名,并且只有一份。tomcat 为了实现隔离性,没有遵守双亲委派机制.
2. 每个 webappClassLoader 加载自己的目录下的 class 文件,不会传递给父类加载器。
3. 每个 jsp 文件对应一个唯一的类加载器,当一个 jsp 文件修改了,就直接卸载这个 jsp 类加载器。重新创建类加载器,重新加载jsp 文件。双亲委派机制无法做到。
所以双亲委派机制不能满足 Tomcat 的需求。
如果 Tomcat 的 CommonClassLoader 想加载 WebAppClassLoader 中的类,该怎么办?
可以使用线程上下文类加载器实现,使用线程上下文加载器,让父类加载器请求子类加载器去完成类加载的动作。
Catalina
Catalina 是 Tomcat 的核心组件,是 Servlet 容器, Catalina 包含了所有的容器组件,其他模块均为
Catalina 提供支撑。通过 Coyote 模块提供连接通信, Jasper 模块提供 JSP 引擎, Naming 提供 JNDI 服务,Juli提供日志服务。结构如下:
主要的功能包括接收请求,处理请求,返回结果,这些具体的实现是在catalina里面的子容器里面。
Lifecycle
Tomcat 的卡特琳娜( Catalina )由许多组件构成。当 Catalina 启动的时候,这些组件也要跟着一起启动,并且当Catalina 关闭的时候,这些组件也要同时关闭,并且要进行必要的清理操作,释放资源。例如,当容器关闭的时候,需要调用已加载的servlet 对象的 destroy 方法, session 对象也要持久化到secondary storage(二级存储,通常指的就是硬盘)。这就要求所有 Component 有一致的方法,可以统一处理。