14:java基础-Tomcat-Web容器

一:Tomcat核心组件及应用架构详解

Web 容器是什么?

  • 让我们先来简单回顾一下 Web 技术的发展历史,可以帮助你理解 Web 容器的由来。
  • 早期的 Web 应用主要用于浏览新闻等静态页面,HTTP 服务器(比如Apache、Nginx)向浏览器返回静态 HTML,浏览器负责解析 HTML,将结果呈现给用户。
  • 随着互联网的发展,我们已经不满足于仅仅浏览静态页面,还希望通过一些交互操作,来获取动态结果,因此也就需要一些扩展机制能够让 HTTP 服务器调用服务端程序。
  • 于是 Sun 公司推出了 Servlet 技术。你可以把 Servlet 简单理解为运行在服务端的 Java 小程序,但是 Servlet 没有 main 方法,不能独立运行,因此必须把它部署到 Servlet 容器中,由容器来实例化并调用 Servlet。
  • 而 Tomcat 就是一个 Servlet 容器。为了方便使用,它们也具有 HTTP 服务器的功能,因此 Tomcat 就是一个“HTTP 服务器 + Servlet 容器”,我们也叫它们 Web 容器。

HTTP 的本质

  • HTTP 协议是浏览器与服务器之间的数据传送协议。作为应用层协议,HTTP 是基于 TCP/IP 协议来传递数据的(HTML 文件、图片、查询结果等),HTTP 协议不涉及数据包(Packet)传输,主要规定了客户端和服务器之间的通信格式。
  • 假如浏览器需要从远程 HTTP 服务器获取一个 HTML 文本,在这个过程中,浏览器实际上要做两件事情。
    1:与服务器建立 Socket 连接。
    2:生成请求数据并通过 Socket 发送出去。
    在这里插入图片描述

HTTP 请求响应实例

  • 用户在登陆页面输入用户名和密码,点击登陆后,浏览器发出了这样的 HTTP
    在这里插入图片描述
  • HTTP 请求数据由三部分组成,分别是请求行、请求报头、请求正文。
  • 当这个HTTP 请求数据到达 Tomcat 后,Tomcat 会把 HTTP 请求数据字节流解析成一个 Request 对象,这个 Request 对象封装了 HTTP 所有的请求信息。
  • 接着Tomcat 把这个 Request 对象交给 Web 应用去处理,处理完后得到一个Response 对象,Tomcat 会把这个 Response 对象转成 HTTP 格式的响应数据并发送给浏览器。
    -
  • HTTP 的响应也是由三部分组成,分别是状态行、响应报头、报文主体。

Cookie 和 Session

cookie

  • 我们知道,HTTP 协议有个特点是无状态,请求与请求之间是没有关系的。这样会出现一个很尴尬的问题:Web 应用不知道你是谁。
  • 因此 HTTP 协议需要一种技术让请求与请求之间建立起联系,并且服务器需要知道这个请求来自哪个用户,于是 Cookie 技术出现了。
  • Cookie 是 HTTP 报文的一个请求头,Web 应用可以将用户的标识信息或者其他一些信息(用户名等)存储在 Cookie 中。
  • 用户经过验证之后,每次 HTTP 请求报文中都包含 Cookie,这样服务器读取这个 Cookie 请求头就知道用户是谁了。
  • Cookie 本质上就是一份存储在用户本地的文件,里面包含了每次请求中都需要传递的信息。
  • 由于 Cookie 以明文的方式存储在本地,而 Cookie 中往往带有用户信息,这样就造成了非常大的安全隐患。

Session

  • 而 Session 的出现解决了这个问题,Session 可以理解为服务器端开辟的存储空间,里面保存了用户的状态,用户信息以Session 的形式存储在服务端。
  • 当用户请求到来时,服务端可以把用户的请求和用户的 Session 对应起来。那么 Session 是怎么和请求对应起来的呢?
  • 答案是通过 Cookie,浏览器在 Cookie 中填充了一个 Session ID 之类的字段用来标识请求。

具体工作过程是这样的:

  • 服务器在创建 Session 的同时,会为该 Session 生成唯一的 Session ID,当浏览器再次发送请求的时候,会将这个 Session ID 带上,
  • 服务器接受到请求之后就会依据 Session ID 找到相应的Session,找到Session 后,就可以在 Session 中获取或者添加内容了。
  • 而这些内容只会保存在服务器中,发到客户端的只有 Session ID,这样相对安全,也节省了网络流量,因为不需要在 Cookie 中存储大量用户信息。

Session 在何时何地创建呢?

  • 在服务器端程序运行的过程中创建的,不同语言实现的应用程序有不同的创建 Session 的方法。在 Java 中,是Web 应用程序在调用 HttpServletRequest 的 getSession 方法时,由 Web 容器(比如 Tomcat)创建的。
  • Tomcat 的 Session 管理器提供了多种持久化方案来存储 Session,通常会采用高性能的存储方式,比如 Redis,并且通过集群部署的方式,防止单点故障,从而提升高可用。
  • 同时,Session 有过期时间,因此 Tomcat 会开启后台线程定期的轮询,如果 Session 过期了就将 Session 失效。

Servlet规范

  • HTTP 服务器怎么知道要调用哪个 Java 类的哪个方法呢。最直接的做法是在HTTP 服务器代码里写一大堆 if else 逻辑判断:如果是 A 请求就调 X 类的 M1方法,如果是 B 请求就调 Y 类的 M2 方法。但这样做明显有问题,因为 HTTP服务器的代码跟业务逻辑耦合在一起了,如果新加一个业务方法还要改 HTTP服务器的代码。
  • 那该怎么解决这个问题呢,面向接口编程是解决耦合问题的法宝,于是有一伙人就定义了一个接口,各种业务类都必须实现这个接口,这个接口就叫Servlet 接口,有时我们也把实现了 Servlet 接口的业务类叫作 Servlet。
  • 但是这里还有一个问题,对于特定的请求,HTTP 服务器如何知道由哪个Servlet 来处理呢?Servlet 又是由谁来实例化呢?显然 HTTP 服务器不适合做这个工作,否则又和业务类耦合了。
  • 于是,还是那伙人又发明了 Servlet 容器,Servlet 容器用来加载和管理业务类。
  • HTTP 服务器不直接跟业务类打交道,而是把请求交给 Servlet 容器去处理,Servlet 容器会将请求转发到具体的 Servlet,如果这个 Servlet 还没创建,就加载并实例化这个 Servlet,然后调用这个 Servlet 的接口方法。
  • 因此 Servlet接口其实是 Servlet 容器跟具体业务类之间的接口。下面我们通过一张图来加深理解。
    在这里插入图片描述
  • Servlet 接口和 Servlet 容器这一整套规范叫作 Servlet 规范。
  • Tomcat 和 Jetty 都按照Servlet 规范的要求实现了 Servlet 容器,同时它们也具有 HTTP 服务器的功能。
  • 作为 Java程序员,如果我们要实现新的业务功能,只需要实现一个 Servlet,并把它注册到Tomcat(Servlet 容器)中,剩下的事情就由 Tomcat 帮我们处理了。

Servlet 接口定义了下面五个方法:

public interface Servlet {

	public void init(ServletConfig config) throws ServletException;
   
    public ServletConfig getServletConfig();
   
    public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException;
            
    public String getServletInfo();

    public void destroy();
}
service 方法
  • 其中最重要是的 service 方法,具体业务类在这个方法里实现处理逻辑。这个方法有两个参数:
  • ServletRequest 和 ServletResponse。
  • ServletRequest 用来封装请求信息,
  • ServletResponse 用来封装响应信息,
  • 因此本质上这两个类是对通信协议的封装
  • HTTP 协议中的请求和响应就是对应了 HttpServletRequest 和HttpServletResponse 这两个类。你可以通过 HttpServletRequest 来获取所有请求相关的信息,包括请求路径、Cookie、HTTP 头、请求参数等。
  • 此外,我们还可以通过 HttpServletRequest 来创建和获取 Session。而HttpServletResponse 是用来封装 HTTP 响应的。
init 和 destroy
  • 这是一个比较贴心的设计,Servlet 容器在加载 Servlet 类的时候会调用 init 方法,在卸载的时候会调用 destroy 方法。
  • 我们可能会在 init 方法里初始化一些资源,并在destroy 方法里释放这些资源,比如 Spring MVC 中的 DispatcherServlet,就是在 init 方法里创建了自己的 Spring 容器。
ServletConfig
  • ServletConfig 的作用就是封装 Servlet的初始化参数。你可以在web.xml给 Servlet 配置参数,并在程序里通过getServletConfig 方法拿到这些参数。
GenericServlet 抽象类
  • 有接口一般就有抽象类,抽象类用来实现接口和封装通用的逻辑,因此 Servlet 规范提供了 GenericServlet 抽象类,我们可以通过扩展它来实现Servlet。
  • 虽然 Servlet 规范并不在乎通信协议是什么,但是大多数的 Servlet 都是在 HTTP 环境中处理的,因此 Servet 规范还提供了 HttpServlet 来继承GenericServlet,并且加入了 HTTP 特性。
  • 这样我们通过继承 HttpServlet 类来实现自己的 Servlet,只需要重写两个方法:doGet 和 doPost。

Servlet 容器

  • 当客户请求某个资源时,HTTP 服务器会用一个 ServletRequest 对象把客户的请求信息封装起来,然后调用 Servlet 容器的 service 方法,
  • Servlet 容器拿到请求后,根据请求的 URL 和 Servlet 的映射关系,找到相应的 Servlet,如果Servlet 还没有被加载,就用反射机制创建这个 Servlet,并调用 Servlet 的 init方法来完成初始化,
  • 接着调用 Servlet 的 service 方法来处理请求,把ServletResponse 对象返回给 HTTP 服务器,HTTP 服务器会把响应发送给客户端
    在这里插入图片描述

Web 应用

  • Servlet 容器会实例化和调用 Servlet,那 Servlet 是怎么注册到 Servlet 容器中的呢?一般来说,我们是以 Web 应用程序的方式来部署 Servlet 的,而根据Servlet 规范,
  • Web 应用程序有一定的目录结构,在这个目录下分别放置了Servlet 的类文件、配置文件以及静态资源,Servlet 容器通过读取配置文件,就能找到并加载 Servlet。
  • Web 应用的目录结构大概是下面这样的:
MyWebAppWEBINF/web.xml ‐‐ 配置文件,用来配置Servlet等
 ‐ WEBINF/lib/ ‐‐ 存放Web应用所需各种JAR包
 ‐ WEBINF/classes/ ‐‐ 存放你的应用类,比如Servlet类
 ‐ METAINF/ ‐‐ 目录存放工程的一些信息
  • Servlet 规范里定义了 ServletContext 这个接口来对应一个 Web 应用。Web 应用部署好后,Servlet 容器在启动时会加载 Web 应用,并为每个 Web 应用创建唯一的ServletContext 对象。
  • 你可以把 ServletContext 看成是一个全局对象,一个 Web 应用可能有多个 Servlet,这些 Servlet 可以通过全局的 ServletContext 来共享数据,这些数据包括 Web 应用的初始化参数、Web 应用目录下的文件资源等。
  • 由于 ServletContext 持有所有 Servlet 实例,你还可以通过它来实现 Servlet 请求的转发。

扩展机制

  • 引入了 Servlet 规范后,你不需要关心 Socket 网络通信、不需要关心 HTTP 协议,也不需要关心你的业务类是如何被实例化和调用的,因为这些都被 Servlet规范标准化了,你只要关心怎么实现的你的业务逻辑。
  • 这对于程序员来说是件好事,但也有不方便的一面。所谓规范就是说大家都要遵守,就会千篇一律,但是如果这个规范不能满足你的业务的个性化需求,就有问题了,因此设计一个规范或者一个中间件,要充分考虑到可扩展性。
  • Servlet 规范提供了两种扩展机制:Filter 和 Listener。

Filter

  • Filter 是过滤器,这个接口允许你对请求和响应做一些统一的定制化处理,比如你可以根据请求的频率来限制访问,或者根据国家地区的不同来修改响应内容。
  • 过滤器的工作原理是这样的:Web 应用部署完成后,Servlet 容器需要实例化Filter 并把 Filter 链接成一个 FilterChain。当请求进来时,获取第一个 Filter 并调用 doFilter 方法,doFilter 方法负责调用这个 FilterChain 中的下一个Filter。

Listener

  • Listener 是监听器,这是另一种扩展机制。当 Web 应用在 Servlet 容器中运行时,Servlet 容器内部会不断的发生各种事件,如 Web 应用的启动和停止、用户请求到达等。
  • Servlet 容器提供了一些默认的监听器来监听这些事件,当事件发生时,Servlet 容器会负责调用监听器的方法。当然,你可以定义自己的监听器去监听你感兴趣的事件,将监听器配置在web.xml中。
  • 比如 Spring 就实现了自己的监听器,来监听 ServletContext 的启动事件,目的是当 Servlet 容器启动时,创建并初始化全局的 Spring 容器。
  • Tomcat下载地址:https://tomcat.apache.org/download-80.cgi
    在这里插入图片描述
1 /bin:存放 WindowsLinux 平台上启动和关闭 Tomcat 的脚本文件。
2 /conf:存放 Tomcat 的各种全局配置文件,其中最重要的是server.xml。
3 /lib:存放 Tomcat 以及所有 Web 应用都可以访问的 JAR 文件。
4 /logs:存放 Tomcat 执行时产生的日志文件。
5 /work:存放 JSP 编译后产生的 Class 文件。
6 /webapps:TomcatWeb 应用目录,默认情况下把 Web 应用放在这个目录下。
  • 打开 Tomcat 的日志目录,也就是 Tomcat 安装目录下的 logs 目录。Tomcat的日志信息分为两类 :
  • 一是运行日志,它主要记录运行过程中的一些信息,尤其是一些异常错误日志信息 ;
  • 二是访问日志,它记录访问的时间、IP 地址、访问的路径等相关信息。

Tomcat各组件认知

  • Tomcat是一个基于JAVA的WEB容器,其实现了JAVA EE中的 Servlet 与 jsp 规范,与Nginx apache 服务器不同在于一般用于动态请求处理。
  • 在架构设计上采用面向组件的方式设计。即整体功能是通过组件的方式拼装完成。另外每个组件都可以被替换以保证灵活性。
    在这里插入图片描述

Tomcat 各组件及关系

  • Server 和 Service
  • Connector 连接器
    1:HTTP 1.1
    2:SSL https
    3:AJP( Apache JServ Protocol) apache 私有协议,用于apache 反向代理Tomcat
  • Container
    1:Engine 引擎 catalina
    2:Host 虚拟机 基于域名 分发请求
    3:Context 隔离各个WEB应用 每个Context的ClassLoader都是独立。
  • Component
    1:Manager (管理器)
    2:logger (日志管理)
    3:loader (载入器)
    4:pipeline (管道)
    5:valve (管道中的阀)
    在这里插入图片描述

Tomcat server.xml 配置详解

server

  • root元素:server 的顶级配置 主要属性: port:执行关闭命令的端口号。
  • shutdown:关闭命令
演示shutdown的用法 #基于telent 执行SHUTDOWN 命令即可关闭(必须大写)telnet 127.0.0.1 8005 SHUTDOWN

service

  • 服务:将多个connector 与一个Engine组合成一个服务,可以配置多个服务。

Connector

  • 连接器:用于接收 指定协议下的连接 并指定给唯一的Engine 进行处理。 主要属性:
    1:protocol 监听的协议,默认是http/1.1
    2:port 指定服务器端要创建的端口号
    3:minSpareThreads服务器启动时创建的处理请求的线程数
    4:maxThreads 最大可以创建的处理请求的线程数
    5:enableLookups 如果为true,则可以通过调用request.getRemoteHost()进行DNS查询来得到远程客户端的实际主机名,若为false则不进行DNS查询,而是返回其ip地址。
    6:redirectPort 指定服务器正在处理http请求时收到了一个SSL传输请求后重定向的端口号。
    7:acceptCount 指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理。
    8:connectionTimeout 指定超时的时间数(以毫秒为单位)。
    9:SSLEnabled 是否开启 sll 验证,在Https 访问时需要开启。
  • 配置演示多个connector
<Connector port="8860" protocol="org.apache.coyote.http11.Http11NioProtocol"
           connectionTimeout="20000"
           redirectPort="8862"
           URIEncoding="UTF‐8"
           useBodyEncodingForURI="true"
           compression="on" compressionMinSize="2048"
           compressableMimeType="text/html,text/xml,text/plain,text/javascript,text/css,application/x‐json,application/json,application/x‐javascript"
           maxThreads="1024" minSpareThreads="200"
           acceptCount="800"
           enableLookups="false"
/>

Engine

  • 引擎:用于处理连接的执行器,默认的引擎是catalina。一个service 中只能配置一个Engine。
  • 主要属性:name 引擎名称 defaultHost 默认host。

Host

  • 虚拟机:基于域名匹配至指定虚拟机。类似于nginx 当中的server,默认的虚拟机是localhost.。
  • 主要属性:
  • 演示配置多个Host
<Host name="www.wukong.com" appBase="/usr/www/wukong" unpackWARs="true" autoDeploy="true" a="">
    <Valve className="org.apache.catalina.valves.AccessLogValve"
           directory="logs" prefix="www.wukong.com.access_log"
           suffix=".txt" pattern="%h %l %u %t "%r" %s %b" />
</Host>

Context

  • 应用上下文:一个host 下可以配置多个Context ,每个Context 都有其独立的classPath。相互隔离,以免造成ClassPath 冲突。 主要属性:
<Context path="/testweb" docBase="testweb.war" reloadbale="true"/>

Valve 阀门:

  • 可以理解成过滤器,具体配置要基于具体的Valve 接口的子类。以下即为一个访问日志的Valve
<Valve className="org.apache.catalina.valves.AccessLogValve"
       directory="logs"
       prefix="www.wukong.com.access_log" suffix=".txt"
       pattern="%h %l %u %t "%r" %s %b" />

Tomcat 部署脚本编写

  • 我们平时启动Tomcat过程是怎么样的?
  1. 复制WAR包至Tomcat webapp 目录。
  2. 执行starut.bat 脚本启动。
  3. 启动过程中war 包会被自动解压装载。
  • 但是我们在Eclipse 或idea 中启动WEB项目的时候 也是把War包复杂至webapps目录解压吗?显然不是,
  • 其真正做法是在Tomcat程序文件之外创建了一个部署目录,在一般生产环境中也是这么做的 即:
  • Tomcat 程序目录和部署目录分开。 我们只需要在启动时指定CATALINA_HOME 与CATALINA_BASE 参数即可实现。

二:Tomcat源码之整体架构与启动流程

流程图processOn地址

整体架构

  • 我们想要了解一个框架,首先要了解它是干什么的,Tomcat我们都知道,是用于处理连接过来的Socket请求的。那么Tomcat就会有两个功能:
  • 对外处理连接,将收到的字节流转化为自己想要的Request和Response对象。
  • 对内处理Servlet,将对应的Request请求分发到相应的Servlet中。
  • 那么我们整体的骨架就出来了,Tomcat其实就分为两大部分,一部分是连接器(Connnector)处理对外连接和容器(Container)管理对内的Servelet。
  • 大体的关系图如下:
    -
  • 描述:
  • 最外层的大框就是代表一个Tomcat服务,一个Tomcat服务可以对应多个Service。每个Service都有连接器和容器。
  • 这些对应的关系我们也可以打开在Tomcat目录配置文件中 server.xml中看出来。

Tomcat各个组件对应的实现类

在这里插入图片描述

更多

在这里插入图片描述

Tomcat整体启动流程

  • 蓝线部分是各个组件的初始化,在Catalina对象中创建了server.xml的解析器,一步到位创建出大部分的组件。
  • 红线部分是责任链模式的启动流程。
  • 绿线部分是热加载和热部署的过程(war包的解压和web.xml的解析,解析出listener filter servlet等)。
    -

三:Tomcat源码之线程模型HTTP请求处理与管道线模型

流程图processOn地址

Connector

  • 蓝色框包裹的是nio的线程模型
  • 红色标识的组件是工作线程完成io和业务处理的过程,CoyoteAdapter这个是tomcat唯一的一个适配器负责把请求从Connector发送到Engine进行业务处理。
    -
  • 使用指定IO模型的配置方式: 配置 server.xml 文件当中的 修改即可。 默认配置
  • 8.0protocol=“HTTP/1.1” 8.0 之前是 BIO 8.0 之后是NIO
  • BIO protocol=“org.apache.coyote.http11.Http11Protocol“
  • NIO protocol=”org.apache.coyote.http11.Http11NioProtocol“
  • AIO protocol=”org.apache.coyote.http11.Http11Nio2Protocol“
  • APR protocol=”org.apache.coyote.http11.Http11AprProtocol

容器

  • 只有容器才有管道线,其他组件没有
  • Tomcat中有四个容器,分别是Engine,Host,context,Wrapper。级别是从大到小的。
  • context表示一个Web应用程序,
  • Wrapper表示一个Servlet。一个Web应用程序中可能会有多个Servlet。
  • Host代表的是一个虚拟主机,就好比你的一个Tomcat下面可能有多个Web应用程序。
  • Engine是Host的父类,Host是Context的父类,Context是Wrapper父类。
  • 每层的容器都有自己的管道线。
    在这里插入图片描述

tcp三次握手

  • 客户端–>发送带有SYN标志的数据包(一次握手)–>服务端
  • 服务端–>发送带有 SYN/ACK 标志的数据包(二次握手)–>客户端
  • 客户端–>发送带有带有 ACK 标志的数据包(三次握手)–>服务端
    在这里插入图片描述

为什么要三次握手

  • 第一次握手:Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常
  • 第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:对方发送正常,自己接收正常
  • 第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常

四次挥手

  • 第一次挥手:客户端关闭连接的请求
  • 第二次挥手:服务端收到断开连接的请求后需要对关闭请求进行确认,于是给客户端发送一个消息—(你确定要关闭吗)
  • 第三次挥手:在发送确认消息后再向客户端发送close消息,关闭链接。
    第四次挥手:客户端接收到消息之后,进入到time_wait状态,客户端接收到消息之后。对消息的确认。

BIO与NIO

在这里插入图片描述

  • BIO:同步阻塞io:数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不多的情况下,这种模型就比较不错。
  • NIO:同步非阻塞io:它支持面向缓冲,基于通道的I/O操作方法。
  • AIO(NIO2):异步非阻塞io:高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

网络请求响应

在这里插入图片描述

阻塞式 I/O 模型(blocking I/O)

在这里插入图片描述

非阻塞式 I/O 模型(non-blocking I/O)

在这里插入图片描述

I/O模型3:I/O 复用模型(I/O multiplexing)

在这里插入图片描述

I/O模型5:异步 I/O 模型(即AIO,全称asynchronous I/O)

在这里插入图片描述

四:Tomcat源码之热加载热部署及类加载器剖析

在这里插入图片描述

热部署与热加载

  • 要在运行的过程中升级 Web 应用,如果你不想重启系统,实现的方式有两种:热加载和热部署。
  • 热加载的实现方式是 Web 容器启动一个后台线程,定期检测类文件的变化,如果有变化,就重新加载类,在这个过程中不会清空 Session ,一般用在开发环境。
  • 热部署原理类似,也是由后台线程定时检测 Web 应用的变化,但它会重新加载整个 Web 应用。这种方式会清空 Session,比热加载更加干净、彻底,一般用在生产环境。
  • Tomcat 就是通过开启后台线程实现:
 String threadName = "ContainerBackgroundProcessor[" + toString() + "]";
 thread = new Thread(new ContainerBackgroundProcessor(), threadName);
 thread.setDaemon(true);
 thread.start();
  • 任务类 ContainerBackgroundProcessor,它是一个 Runnable,同时也是ContainerBase 的内部类,ContainerBase 是所有容器组件的基类,其熟悉爱你原理在此类中,详情请查看热部署原理流程图

Tomcat 热加载

  • 有了 ContainerBase 的周期性任务处理“框架”,作为具体容器子类,只需要实现自己的周期性任务就行。而 Tomcat 的热加载,就是在 Context 容器中实现的。
  • Context 容器的 backgroundProcess 方法是这样实现的:
  • Context 容器通过 WebappLoader 来检查类文件是否有更新,通过 Session 管理器来检查是否有 Session 过期,并且通过资源管理器来检查静态资源是否有更新,最后还调用了父类 ContainerBase 的backgroundProcess 方法。

WebappLoader 是如何实现热加载的

  • 它主要是调用了 Context 容器的reload 方法,而 Context 的 reload 方法比较复杂,总结起来,主要完成了下面这些任务:
  1. 停止和销毁 Context 容器及其所有子容器,子容器其实就是 Wrapper,也就是说Wrapper 里面 Servlet 实例也被销毁了。
  2. 停止和销毁 Context 容器关联的 Listener 和 Filter。
  3. 停止和销毁 Context 下的 Pipeline 和各种 Valve。
  4. 停止和销毁 Context 的类加载器,以及类加载器加载的类文件资源。
  5. 启动 Context 容器,在这个过程中会重新创建前面四步被销毁的资源。
  • 在这个过程中,类加载器发挥着关键作用。一个 Context 容器对应一个类加载器,类加载器在销毁的过程中会把它加载的所有类也全部销毁。Context 容器在启动过程中,会创建一个新的类加载器来加载新的类文件。
  • 在 Context 的 reload 方法里,并没有调用 Session 管理器的 destroy 方法,也就是说这个 Context 关联的 Session 是没有销毁的。
  • 你还需要注意的是,Tomcat 的热加载默认是关闭的,你需要在 conf 目录下的 server.xml 文件中设置 reloadable 参数来开启这个功能,像下面这样:
<Context path="/testweb1" docBase="testweb.war" reloadbale="true"/

Tomcat 热部署

  • 热部署跟热加载的本质区别是,热部署会重新部署 Web 应用,原来的 Context对象会整个被销毁掉,因此这个 Context 所关联的一切资源都会被销毁,包括Session。
  • 那么 Tomcat 热部署又是由哪个容器来实现的呢?应该不是由 Context,因为热部署过程中 Context 容器被销毁了,那么这个重担就落在 Host 身上了,因为它是 Context 的父容器。
  • 跟 Context 不一样,Host 容器并没有在 backgroundProcess 方法中实现周期性检测的任务,而是通过监听器 HostConfig 来实现的,HostConfig 就是前面提到的“周期事件”的监听器。
  • 其实 HostConfig 会检查 webapps 目录下的所有 Web 应用。
  • 如果原来 Web 应用目录被删掉了,就把相应 Context 容器整个销毁掉。
  • 是否有新的 Web 应用目录放进来了,或者有新的 WAR 包放进来了,就部署相应的 Web 应用。

Java 的类加载

  • Java 的类加载,就是把字节码格式“.class”文件加载到 JVM 的方法区,并在JVM 的堆区建立一个java.lang.Class对象的实例,用来封装 Java 类相关的数据和方法。
  • 那 Class 对象又是什么呢?你可以把它理解成业务类的模板,JVM 根据这个模板来创建具体业务类对象实例。
  • JVM 并不是在启动时就把所有的“.class”文件都加载一遍,而是程序在运行过程中用到了这个类才去加载。JVM 类加载是由类加载器来完成的,JDK 提供一个抽象类 ClassLoader,这个抽象类中定义了三个关键方法,理解清楚它们的作用和关系非常重要。
  • JVM 的类加载器是分层次的,它们有父子关系,每个类加载器都持有一个 parent 字段,指向父加载器。

defineClass

  • defineClass 是个工具方法,它的职责是调用 native 方法把 Java 类的字节码解析成一个 Class 对象,所谓的 native 方法就是由 C 语言实现的方法,Java 通过 JNI 机制调用。

findClass

  • findClass 方法的主要职责就是找到“.class”文件,可能来自文件系统或者网络,找到后把“.class”文件读到内存得到字节码数组,然后调用defineClass 方法得到 Class 对象。

loadClass

  • loadClass 是个 public 方法,说明它才是对外提供服务的接口,具体实现也比较清晰:
  • 首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则交给父加载器去加载。
  • 请你注意,这是一个递归调用,也就是说子加载器持有父加载器的引用,当一个类加载器需要加载一个 Java类时,会先委托父加载器去加载,然后父加载器在自己的加载路径中搜索Java 类,当父加载器在自己的加载范围内找不到时,才会交还给子加载器加载,这就是双亲委托机制。

实现自定义类加载器

  • 类加载器的父子关系不是通过继承来实现的,比如 AppClassLoader 并不是 ExtClassLoader 的子类,而是说 AppClassLoader 的 parent 成员变量指向 ExtClassLoader 对象。
  • 同样的道理,如果你要自定义类加载器,不去继承 AppClassLoader,而是继承 ClassLoader 抽象类,再重写 findClass 和 loadClass 方法即可,
  • Tomcat 就是通过自定义类加载器来实现自己的类加载逻辑。不知道你发现没有,如果你要打破双亲委托机制,就需要重写 loadClass 方法,因为 loadClass 的默认实现就是双亲委托机制。

Tomcat 的类加载器

  • Tomcat 的自定义类加载器 WebAppClassLoader 打破了双亲委托机制,它首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的是优先加载 Web 应用自己定义的类。
  • 具体实现就是重写 ClassLoader 的两个方法:findClass 和 loadClass。

findClass

public Class<?> findClass(String name) throws ClassNotFoundException {
    ...
    
    Class<?> clazz = null;
    try {
            //1. 先在Web应用目录下查找类 
            clazz = findClassInternal(name);
    }  catch (RuntimeException e) {
           throw e;
       }
    
    if (clazz == null) {
    try {
            //2. 如果在本地目录没有找到,交给父加载器去查找
            clazz = super.findClass(name);
    }  catch (RuntimeException e) {
           throw e;
       }
    
    //3. 如果父类也没找到,抛出ClassNotFoundException
    if (clazz == null) {
        throw new ClassNotFoundException(name);
     }

    return clazz;
}
  • 在 findClass 方法里,主要有三个步骤:
    1:先在 Web 应用本地目录下查找要加载的类。
    2:如果没有找到,交给父加载器去查找,它的父加载器就是上面提到的系统类加载器AppClassLoader。
    3:如果父加载器也没找到这个类,抛出 ClassNotFound 异常。

loadClass

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

    synchronized (getClassLoadingLock(name)) {
 
        Class<?> clazz = null;

        //1. 先在本地cache查找该类是否已经加载过
        clazz = findLoadedClass0(name);
        if (clazz != null) {
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }

        //2. 从系统类加载器的cache中查找是否加载过
        clazz = findLoadedClass(name);
        if (clazz != null) {
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }

        // 3. 尝试用ExtClassLoader类加载器类加载,为什么?
        ClassLoader javaseLoader = getJavaseClassLoader();
        try {
            clazz = javaseLoader.loadClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }

        // 4. 尝试在本地目录搜索class并加载
        try {
            clazz = findClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }

        // 5. 尝试用系统类加载器(也就是AppClassLoader)来加载
            try {
                clazz = Class.forName(name, false, parent);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
       }
    
    //6. 上述过程都加载失败,抛出异常
    throw new ClassNotFoundException(name);
}
  • loadClass 方法稍微复杂一点,主要有六个步骤:
    1:先在本地 Cache 查找该类是否已经加载过,也就是说 Tomcat 的类加载器是否已经加载过这个类。
    2:如果 Tomcat 类加载器没有加载过这个类,再看看系统类加载器是否加载过。
    3:如果都没有,就让 ExtClassLoader 去加载,这一步比较关键,目的防止 Web 应用自己的类覆盖 JRE 的核心类。因为 Tomcat 需要打破双亲委托机制,假如 Web 应用里自定义了一个叫 Object 的类,如果先加载这个 Object 类,就会覆盖 JRE 里面的那个 Object 类,这就是为什么 Tomcat 的类加载器会优先尝试用 ExtClassLoader 去加载,因为 ExtClassLoader 会委托给 BootstrapClassLoader 去加载,BootstrapClassLoader 发现自己已经加载了 Object 类,直接返回给 Tomcat 的类加载器,这样 Tomcat 的类加载器就不会去加载 Web 应用下的 Object 类了,也就避免了覆盖 JRE 核心类的问题。
    4:如果 ExtClassLoader 加载器加载失败,也就是说 JRE 核心类中没有这类,那么就在本地 Web 应用目录下查找并加载。
    5:如果本地目录下没有这个类,说明不是 Web 应用自己定义的类,那么由系统类加载器去加载。这里请你注意,Web 应用是通过Class.forName调用交给系统类加载器的,因为Class.forName的默认加载器就是系统类加载器。
    6:如果上述加载过程全部失败,抛出 ClassNotFound 异常。

Tomcat 类加载器的层次结构

  • 假如我们在 Tomcat 中运行了两个 Web 应用程序,两个 Web 应用中有同名的 Servlet,但是功能不同,Tomcat 需要同时加载和管理这两个同名的 Servlet 类,保证它们不会冲突,因此 Web 应用之间的类需要隔离。
  • 假如两个 Web 应用都依赖同一个第三方的 JAR 包,比如 Spring,那 Spring 的 JAR 包被加载到内存后,Tomcat 要保证这两个 Web 应用能够共享,也就是说 Spring 的 JAR 包只被加载一次,否则随着依赖的第三方 JAR 包增多,JVM 的内存会膨胀。
  • 跟 JVM 一样,我们需要隔离 Tomcat 本身的类和 Web 应用的类。
    在这里插入图片描述

Session

  • 我们可以通过 Request 对象的 getSession 方法来获取 Session,并通过 Session 对象来读取和写入属性值。
  • 而 Session 的管理是由 Web 容器来完成的,主要是对 Session 的创建和销毁,除此之外 Web 容器还需要将 Session 状态的变化通知给监听者。
  • 当然 Session 管理还可以交给 Spring 来做,好处是与特定的 Web 容器解耦,Spring Session 的核心原理是通过 Filter 拦截 Servlet 请求,将标准的 ServletRequest 包装一下,换成 Spring 的 Request 对象,这样当我们调用 Request 对象的 getSession 方法时,Spring 在背后为我们创建和管理 Session。

Session 的创建

  • Tomcat 中主要由每个 Context 容器内的一个 Manager 对象来管理 Session。默认实现类为StandardManager。下面我们通过它的接口来了解一下 StandardManager 的功能:
public interface Manager {
    public Context getContext();
    public void setContext(Context context);
    public SessionIdGenerator getSessionIdGenerator();
    public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator);
    public long getSessionCounter();
    public void setSessionCounter(long sessionCounter);
    public int getMaxActive();
    public void setMaxActive(int maxActive);
    public int getActiveSessions();
    public long getExpiredSessions();
    public void setExpiredSessions(long expiredSessions);
    public int getRejectedSessions();
    public int getSessionMaxAliveTime();
    public void setSessionMaxAliveTime(int sessionMaxAliveTime);
    public int getSessionAverageAliveTime();
    public int getSessionCreateRate();
    public int getSessionExpireRate();
    public void add(Session session);
    public void changeSessionId(Session session);
    public void changeSessionId(Session session, String newId);
    public Session createEmptySession();
    public Session createSession(String sessionId);
    public Session findSession(String id) throws IOException;
    public Session[] findSessions();
    public void load() throws ClassNotFoundException, IOException;
    public void remove(Session session);
    public void remove(Session session, boolean update);
    public void addPropertyChangeListener(PropertyChangeListener listener)
    public void removePropertyChangeListener(PropertyChangeListener listener);
    public void unload() throws IOException;
    public void backgroundProcess();
    public boolean willAttributeDistribute(String name, Object value);
}
  • 不出意外我们在接口中看到了添加和删除 Session 的方法;另外还有 load 和 unload 方法,它们的作用是分别是将 Session 持久化到存储介质和从存储介质加载 Session。
  • 当我们调用HttpServletRequest.getSession(true)时,这个参数 true 的意思是“如果当前请求还没有 Session,就创建一个新的”。那 Tomcat 在背后为我们做了些什么呢?
  • HttpServletRequest 是一个接口,Tomcat 实现了这个接口,具体实现类是:
    org.apache.catalina.connector.Request。
  • 但这并不是我们拿到的 Request,Tomcat 为了避免把一些实现细节暴露出来,还有基于安全上的考虑,定义了 Request 的包装类,叫作 RequestFacade,我们可以通过代码来理解一下:
public class Request implements HttpServletRequest {}

public class RequestFacade implements HttpServletRequest {
  protected Request request = null;
  
  public HttpSession getSession(boolean create) {
     return request.getSession(create);
  }
}
  • 因此我们拿到的 Request 类其实是 RequestFacade,RequestFacade 的 getSession 方法调用的是 Request 类的 getSession 方法,我们继续来看 Session 具体是如何创建的:
Context context = getContext();
if (context == null) {
    return null;
}

Manager manager = context.getManager();
if (manager == null) {
    return null;      
}

session = manager.createSession(sessionId);
session.access();
  • 从上面的代码可以看出,Request 对象中持有 Context 容器对象,而 Context 容器持有 Session 管理器 Manager,这样通过 Context 组件就能拿到 Manager 组件,最后由 Manager 组件来创建 Session。
  • 因此最后还是到了 StandardManager,StandardManager 的父类叫 ManagerBase,这个 createSession 方法定义在 ManagerBase 中,StandardManager 直接重用这个方法。
  • 接着我们来看 ManagerBase 的 createSession 是如何实现的:
@Override
public Session createSession(String sessionId) {
    //首先判断Session数量是不是到了最大值,最大Session数可以通过参数设置
    if ((maxActiveSessions >= 0) &&
            (getActiveSessions() >= maxActiveSessions)) {
        rejectedSessions++;
        throw new TooManyActiveSessionsException(
                sm.getString("managerBase.createSession.ise"),
                maxActiveSessions);
    }

    // 重用或者创建一个新的Session对象,请注意在Tomcat中就是StandardSession
    // 它是HttpSession的具体实现类,而HttpSession是Servlet规范中定义的接口
    Session session = createEmptySession();


    // 初始化新Session的值
    session.setNew(true);
    session.setValid(true);
    session.setCreationTime(System.currentTimeMillis());
    session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
    String id = sessionId;
    if (id == null) {
        id = generateSessionId();
    }
    session.setId(id);// 这里会将Session添加到ConcurrentHashMap中
    sessionCounter++;
    
    //将创建时间添加到LinkedList中,并且把最先添加的时间移除
    //主要还是方便清理过期Session
    SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
    synchronized (sessionCreationTiming) {
        sessionCreationTiming.add(timing);
        sessionCreationTiming.poll();
    }
    return session
}
  • 到此我们明白了 Session 是如何创建出来的,创建出来后 Session 会被保存到一个 ConcurrentHashMap 中:
protected Map<String, Session> sessions = new ConcurrentHashMap<>();
  • 请注意 Session 的具体实现类是 StandardSession,StandardSession 同时实现了javax.servlet.http.HttpSession和org.apache.catalina.Session接口,并且对程序员暴露的是 StandardSessionFacade 外观类,保证了 StandardSession 的安全,避免了程序员调用其内部方法进行不当操作。
  • StandardSession 的核心成员变量如下:
public class StandardSession implements HttpSession, Session, Serializable {
    protected ConcurrentMap<String, Object> attributes = new ConcurrentHashMap<>();
    protected long creationTime = 0L;
    protected transient volatile boolean expiring = false;
    protected transient StandardSessionFacade facade = null;
    protected String id = null;
    protected volatile long lastAccessedTime = creationTime;
    protected transient ArrayList<SessionListener> listeners = new ArrayList<>();
    protected transient Manager manager = null;
    protected volatile int maxInactiveInterval = -1;
    protected volatile boolean isNew = false;
    protected volatile boolean isValid = false;
    protected transient Map<String, Object> notes = new Hashtable<>();
    protected transient Principal principal = null;
}
  • 我讲到容器组件会开启一个 ContainerBackgroundProcessor 后台线程,调用自己以及子容器的 backgroundProcess 进行一些后台逻辑的处理,和 Lifecycle 一样,这个动作也是具有传递性的,也就是说子容器还会把这个动作传递给自己的子容器。
  • 你可以参考下图来理解这个过程。
    在这里插入图片描述
  • 其中父容器会遍历所有的子容器并调用其 backgroundProcess 方法,而 StandardContext 重写了该方法,它会调用 StandardManager 的 backgroundProcess 进而完成 Session 的清理工作,
  • 下面是 StandardManager 的 backgroundProcess 方法的代码:
public void backgroundProcess() {
    // processExpiresFrequency 默认值为6,而backgroundProcess默认每隔10s调用一次,也就是说除了任务执行的耗时,每隔 60s 执行一次
    count = (count + 1) % processExpiresFrequency;
    if (count == 0) // 默认每隔 60s 执行一次 Session 清理
        processExpires();
}

/**
 * 单线程处理,不存在线程安全问题
 */
public void processExpires() {
 
    // 获取所有的 Session
    Session sessions[] = findSessions();   
    int expireHere = 0 ;
    for (int i = 0; i < sessions.length; i++) {
        // Session 的过期是在isValid()方法里处理的
        if (sessions[i]!=null && !sessions[i].isValid()) {
            expireHere++;
        }
    }
}
  • backgroundProcess 由 Tomcat 后台线程调用,默认是每隔 10 秒调用一次,但是 Session 的清理动作不能太频繁,因为需要遍历 Session 列表,会耗费 CPU 资源,
  • 所以在 backgroundProcess 方法中做了取模处理,backgroundProcess 调用 6 次,才执行一次 Session 清理,也就是说 Session 清理每 60 秒执行一次。

Session 事件通知

  • 按照 Servlet 规范,在 Session 的生命周期过程中,要将事件通知监听者,Servlet 规范定义了 Session 的监听器接口:
public interface HttpSessionListener extends EventListener {
    //Session创建时调用
    public default void sessionCreated(HttpSessionEvent se) {
    }
    
    //Session销毁时调用
    public default void sessionDestroyed(HttpSessionEvent se) {
    }
}
  • 注意到这两个方法的参数都是 HttpSessionEvent,所以 Tomcat 需要先创建 HttpSessionEvent 对象,然后遍历 Context 内部的 LifecycleListener,并且判断是否为 HttpSessionListener 实例,
  • 如果是的话则调用 HttpSessionListener 的 sessionCreated 方法进行事件通知。这些事情都是在 Session 的 setId 方法中完成的:
session.setId(id);

@Override
public void setId(String id, boolean notify) {
    //如果这个id已经存在,先从Manager中删除
    if ((this.id != null) && (manager != null))
        manager.remove(this);

    this.id = id;

    //添加新的Session
    if (manager != null)
        manager.add(this);

    //这里面完成了HttpSessionListener事件通知
    if (notify) {
        tellNew();
    }
}
  • 从代码我们看到 setId 方法调用了 tellNew 方法,那 tellNew 又是如何实现的呢?
public void tellNew() {

    // 通知org.apache.catalina.SessionListener
    fireSessionEvent(Session.SESSION_CREATED_EVENT, null);

    // 获取Context内部的LifecycleListener并判断是否为HttpSessionListener
    Context context = manager.getContext();
    Object listeners[] = context.getApplicationLifecycleListeners();
    if (listeners != null && listeners.length > 0) {
    
        //创建HttpSessionEvent
        HttpSessionEvent event = new HttpSessionEvent(getSession());
        for (int i = 0; i < listeners.length; i++) {
            //判断是否是HttpSessionListener
            if (!(listeners[i] instanceof HttpSessionListener))
                continue;
                
            HttpSessionListener listener = (HttpSessionListener) listeners[i];
            //注意这是容器内部事件
            context.fireContainerEvent("beforeSessionCreated", listener);   
            //触发Session Created 事件
            listener.sessionCreated(event);
            
            //注意这也是容器内部事件
            context.fireContainerEvent("afterSessionCreated", listener);
            
        }
    }
}

在这里插入图片描述

  • 9
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值