【Java Web编程 十二】深入理解Tomcat系统架构及工作原理(下)

本文涉及的产品
云解析DNS,个人版 1个月
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 【Java Web编程 十二】深入理解Tomcat系统架构及工作原理(下)

Container组件

每个 Service 会包含一个容器。容器由一个引擎可以管理多个虚拟主机。每个虚拟主机可以管理多个 Web 应用。每个 Web 应用会有多个 Servlet 包装器。Engine、Host、Context 和 Wrapper,四个容器之间属于父子关系

四个组件的职责如下:

  • Engine:Servlet 的顶层容器,包含一个或多个Host 子容器
  • Host:虚拟主机,负责 web 应用的部署和 Context 的创建
  • Context:Web 应用上下文,包含多个 Wrapper,负责 web 配置的解析,管理所有的 Web 资源;
  • Wrapper:最底层的容器,是对 Servlet 的封装,负责 Servlet 实例的创建、执行和销毁。

容器的请求处理过程就是在 Engine、Host、Context 和 Wrapper 这四个容器之间层层调用,最后在 Servlet 中执行对应的业务逻辑。各容器都会有一个通道 Pipeline,每个通道上都会有一个 Basic Valve(如StandardEngineValve), 类似一个闸门用来处理 Request 和 Response 。其流程图如下:

请求路径查找流程

了解了两个核心组件后,我们把请求可以简单理解为:连接器的处理流程 + 容器的处理流程 = Tomcat 处理流程。Tomcat 是如何通过请求路径找到对应的虚拟站点?是如何找到对应的 Servlet 呢?我们再来回头看下配置文件里Service的部分:

<!-- Service-->
  <Service name="Catalina">
    <!-- Connector-->
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    <!-- Container-->
    <Engine name="Catalina" defaultHost="localhost">
      <!-- Realm -->
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>
      <!-- Host -->
      <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>

通过配置文件分析tomcat处理一个请求的流程,例如我们请求如下地址:http://localhost:8080/docs/list

  1. 连接器监听的端口是8080。由于请求的端口和监听的端口一致,connector接受了该请求,并将转换好的HttpServletRequest传递给容器
  2. 因为引擎的默认虚拟主机是 localhost,并且虚拟主机的目录是webapps。所以请求找到了 tomcat/webapps 目录。
  3. 解析的 docs是 web 程序的应用名,也就是context。此时请求继续从 webapps 目录下找 docs目录。有的时候我们也会把应用名省略。
  4. 解析的 api 是具体的业务逻辑地址。此时需要从 docs/WEB-INF/web.xml 或者通过注解找映射关系,最后调用具体的函数list进行操作。

以下是一个解析示例:

Tomcat生命周期

Tomcat 为了方便管理组件和容器的生命周期,定义了从创建、启动、到停止、销毁共 12 中状态,tomcat 生命周期管理了内部状态变化的规则控制,组件和容器只需实现相应的生命周期方法即可完成各生命周期内的操作(initInternal、startInternal、stopInternal、 destroyInternal)。

比如执行初始化操作时,会判断当前状态是否 New,如果不是则抛出生命周期异常;是则设置当前状态为 Initializing,并执行 initInternal 方法,由子类实现,方法执行成功则设置当 前状态为 Initialized,执行失败则设置为 Failed 状态。整体生命周期状态变化如下图所示:

Tomcat 的生命周期管理引入了事件机制,在组件或容器的生命周期状态发生变化时会通 知事件监听器,监听器通过判断事件的类型来进行相应的操作。事件监听器的添加可以在 server.xml 文件中进行配置,事实上我们从配置中确实可以看的出一些监听器相关配置:

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

Tomcat 各类容器的配置过程就是通过添加 listener 的方式来进行的,从而达到配置逻辑与容器的解耦。

Tomcat流程管理

Tomcat有两个重要的流程处理,一个是Tomcat的启动流程,一个则是Tomcat的请求处理流程。

Tomcat启动流程

Tomcat的整体启动流程如下,通过如下的方式逐级推动子组件容器的状态变化:

  1. 启动从 Tomcat 提供的 start.sh 脚本开始,shell 脚本会调用 Bootstrap 的 main 方法,实际 调用了 Catalina 相应的 load、start 方法。
  2. load 方法会通过 Digester 进行 config/server.xml 的解析,在解析的过程中会根据 xml 中的关系 和配置信息来创建容器,并设置相关的属性。接着 Catalina 会调用 StandardServer 的 init 、start 方法进行容器的初始化和启动。
  3. StandardServer 完成 init 、 start 方法调用后,会一直监听来自 8005 端口(可配置),如果接收 到 shutdown 命令,则会退出循环监听,执行后续的 stop 和 destroy 方法,完成 Tomcat 容器的 关闭。同时也会调用 JVM 的 Runtime.getRuntime().addShutdownHook 方法,在虚拟机意外退 出的时候来关闭容器。

按照 xml 的配置关系,server 的子元素是 service,service 的子元素是顶层容器 Engine,每层容器有持有自己的子容器,而这些元素都实现了生命周期管理 的各个方法,因此就很容易的完成整个容器的启动、关闭等生命周期的管理。

所有容器都是继承自 ContainerBase,基类中封装了容器中的重复工作,负责启动容器相关的组 件 Loader、Logger、Manager、Cluster、Pipeline,启动子容器(线程池并发启动子容器,通过 线程池 submit 多个线程,调用后返回 Future 对象,线程内部启动子容器,接着调用 Future 对象 的 get 方法来等待执行结果)

Tomcat请求处理流程

上文中我们提到了Tomcat的路径查找方式,请求如何通过配置文件找到对应的处理逻辑,接下来我们完整的来看到一个请求的处理和响应的流程,用一个流程时序图表示如下:

假设来自客户的请求为:http://localhost:8080/Golden/loginServlet

  1. 请求被发送到本机端口 8080,被在那里侦听的 Coyote HTTP/1.1 Connector 获得
  2. Connector 把该请求交给它所在的 Service 的 Engine 来处理,并等待来自 Engine 的回应
  3. Engine 获得请求 localhost:8080/Golden/loginServlet, 匹配到名为 localhost 的 Host(即使匹配不到也把请求交给该 Host 处理,因为该 Host 被定义为该 Engine 的默认主机)
  4. Host 获得请求/Golden/loginServlet,Host 匹配到路径为 /Golden的 Context
  5. Context 获得请求 /loginServlet,匹配它所拥有的Wrapper
  6. Wrapper 构造 HttpServletRequest 对象和 HttpServletResponse 对象,作为参数调用 JspServlet 的 doGetdoPost 方法

返回流程:

  1. Context 把执行完了之后的 HttpServletResponse 对象返回给 Host
  2. Host 把 HttpServletResponse 对象返回给 Engine
  3. Engine 把 Response 对象返回给 Connector
  4. Connector 把 字节流数据返回给客户客户端浏览器。

这样就完成了一次整体的请求和响应。

Tomcat对JSP的处理

我们以上提到的整体处理流程其实是对Servlet的处理流程的概述,实际上大多数场景下我们请求的页面是JSP,而JSP需要被解析为Servlet来执行相关的业务逻辑。

Jasper引擎

客户端访问一个 jsp 文件,最终接收到的响应还是 静态代码,因此 jsp 可以看做是一个运行在服务端的脚本。而 Jasper 的作用就是对 jsp 语法进行解析,生成 servlet 并生成 class 字节码文件,最终将访问的结果直接响应客户端,上文中我们提到的web.xml配置文件中有如下片段,我们可以看到 web.xml 中配置了一个 JspServlet,用于处理所有的 .jsp 或者 .jspx 结尾的请求:

<servlet>
        <servlet-name>jsp</servlet-name>
        <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
        <init-param>
            <param-name>fork</param-name>
            <param-value>false</param-value>
        </init-param>
        <init-param>
            <param-name>xpoweredBy</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>3</load-on-startup>
    </servlet>
    <!-- The mapping for the default servlet -->
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    <!-- The mappings for the JSP servlet -->
    <servlet-mapping>
        <servlet-name>jsp</servlet-name>
        <url-pattern>*.jsp</url-pattern>
        <url-pattern>*.jspx</url-pattern>
    </servlet-mapping>

JSP文件解析流程

我们对JSP的请求在正式转为上述主流程时,前提还有个JSP的解析过程,当我们进行一个jsp请求时,在上述请求主流程的Context容器时,解析之后再继续执行上述主流程的:

最后一步的编译class文件流程详细如下:

总结一下

本篇Blog用较长的篇幅介绍了Tomcat的一些基本原理、组件,以及Tomcat如何处理Servlet请求以及如何解析JSP文件,理解了这些我想对Java Web的全局学习才真正的最终形成了闭环,以后的框架也无非是简化了这流程而已,学习起来应该会更加得心应手。

相关文章
|
8天前
|
SQL 监控 安全
Java Web应用的安全防护与攻防策略
Java Web应用的安全防护与攻防策略
|
8天前
|
设计模式 消息中间件 监控
如何在Java项目中实现可扩展性架构
如何在Java项目中实现可扩展性架构
|
8天前
|
消息中间件 监控 Java
在Java项目中实现事件驱动架构
在Java项目中实现事件驱动架构
|
8天前
编程之路:从代码到架构的心路历程
【7月更文挑战第9天】在数字世界的迷宫中,每一行代码都承载着创造者的梦想与挑战。本文将通过个人技术感悟的镜头,探索编程实践的深层次价值,从最初的代码编写到复杂的系统架构设计,揭示技术成长的内在逻辑和情感变迁。我们将一同穿梭在技术的森林里,寻找那些让代码生动起来的秘密。
12 2
|
12天前
|
消息中间件 设计模式 Java
Java中的消息驱动架构设计
Java中的消息驱动架构设计
|
9天前
|
消息中间件 Java 微服务
构建可扩展的Java Web应用架构
构建可扩展的Java Web应用架构
|
12天前
|
消息中间件 Java 测试技术
Java中的软件架构重构与升级策略
Java中的软件架构重构与升级策略
|
1天前
|
运维 负载均衡 前端开发
深度解析:Python Web前后端分离架构中WebSocket的选型与实现策略
【7月更文挑战第16天】Python Web开发中,前后端分离常见于实时通信场景,WebSocket作为全双工协议,常用于此类应用。选型时考虑性能、功能、易用性、社区支持和成本。Flask-SocketIO是实现WebSocket的一个选项,它简化了与Flask的集成。案例展示了如何用Flask-SocketIO创建一个实时聊天室:后端处理消息广播,前端通过Socket.IO库连接并显示消息。此实现策略演示了在Python中实现实时通信的基本步骤。
8 0
|
7天前
|
监控 Java API
Java面试题:解释微服务架构的概念及其优缺点,讨论微服务拆分的原则。
Java面试题:解释微服务架构的概念及其优缺点,讨论微服务拆分的原则。
11 0
|
12天前
|
前端开发 Java 应用服务中间件
C/S和B/S架构以及Web服务器
C/S和B/S架构以及Web服务器
16 0