深析Tomcat容器工作流程

深析Tomcat容器工作流程

 

 

 

 

 

必读20遍经典文章:

领悟:https://www.oschina.net/question/12_52027

Tomcat:https://www.ibm.com/developerworks/cn/java/j-lo-tomcat1/

Cookie:https://mp.weixin.qq.com/s/NXrH7R8y2Dqxs9Ekm0u33w?

 

Tomcat下载地址:

http://archive.apache.org/

 

https://mirrors.huaweicloud.com/apache/tomcat/tomcat-7/v7.0.99/

 

强烈声明,本文是读读许令波、红薯等人文章后做的一些整理与思考、探索、总结。

 

底层接口

一段简短的server.xml配置

Server

Service

Executors

Connectors

Containers

Engine

Host

Context

Cluster

其它

Lifecycle

有关其他配置

+自我理解

Tomcat结构

Tomcat工作流程(前半部分)

Tomcat源码导入

Tomcat源码解析-学习整个脉络

Tomcat源码解析深入StandardContext

Tomcat工作流程(后续)

上下文和“场景”

tomcat对静态资源是怎么访问的?

Session和Cookie了解

理解Session和Cookie

测试Session和Cookie

Servlet的编译命令

编译普通的java文件

编译Servlet文件

编译过程编码问题

后期补充

有关Host中的appBase属性和Context中的docBase属性的关系

有关StandardContext和ServletContext、ServletConfig关系

该如何调试Tomcat处理http请求

方法调用序列

接下来要读的是

 

 

 

 

底层接口

 HttpServlet

 ServetConfig

 ServletContext

 Filter

 FilterConfig

 FilterChain

 RequestDispatcher

 HttpServletRequest

 HttpServletResponse

 HttpSession

 一些 Listenser 类

 

1.类关系图

 

 

 

一段简短的server.xml配置

 1 <?xml version='1.0' encoding='utf-8'?>
 2 <Server port="8005" shutdown="SHUTDOWN">
 3   <Service name="Catalina">
 4   
 5     <!--The connectors can use a shared executor, you can define one or more named thread pools-->
 6     <!--
 7     <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
 8         maxThreads="150" minSpareThreads="4"/>
 9     -->
10   
11     <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8"/>
12     
13     <!-- A "Connector" using the shared thread pool-->
14     <!--
15     <Connector executor="tomcatThreadPool"
16                port="8080" protocol="HTTP/1.1"
17                connectionTimeout="20000"
18                redirectPort="8443" />
19     -->
20     
21      <Engine name="Catalina" defaultHost="localhost">
22         <Host name="localhost">
23             <Context path="" docBase="C:\Users\admin\Desktop\ServletBase\ServletDemo\webapp" reloadable="true"/>
24         </Host>
25       </Engine>
26   </Service>
27 </Server>

 

Tomcat有五大组件(Server、Service、Executors、Connectors、Containers),以及一些支持五大组件的扩展组件(Loader、Realm、Manager、Listeners、Resources、SessionIDGenerator、CookieProcessor等)。接下来对这些组件展开介绍。

 

 

Server

Server元素代表整个Servlet容器,因此它必须是所有配置的最外层元素。它的属性代表了整个Servlet容器的特征。

 

Server元素的属性:

 className:要使用的实现的Java类名。这个类必须实现org.apache.catalina.Server接口,如果没有指定类名,将使用标准实现org.apache.catalina.StandardServer

 address:该服务等待关闭命令的TCP/IP地址。如果未指定地址,则使用localhost

 port:该服务等待关闭命令的TCP/IP端口号。设置-1可禁止关闭端口。(注意当使用守护进程(在windows服务器上运行或者在un*xes上与jsvc一起运行)启动Tomcat时,禁用关闭端口可以很好地工作。但是,在使用标准的shell脚本运行Tomcat时不能使用它,因为它会阻止shutdown.bat|sh和catalina.bat|sh优雅地停止)

 shutdown:关闭服务时约定的字符标识。给指定端口号的TCP/IP连接发送该命令字符串可关闭Tomcat(即类似linux上给某服务发送一个信号量,让其关闭)。

 

以下元素可以嵌套在Server元素中:

Service -一个或多个service元素

GlobalNamingResources -为服务器配置全局JDNI资源

结构图:

 

2.StandardServer类关系图

方法:

 

3.Server类方法

从方法中也可以看出,它内部提供了findService用于查找一个或多个Service元素,装填到自身属性的Service数组中。

 

 

Service

Service元素表示共享处理传入请求的单个引擎组件的一个或多个连接器组件的组合,一个或多个Service元素可以嵌套在一个Server元素中。意思大概就是:一个Service里是由一个引擎组件和一个或多个连接器组件组合而成的(一个或多个连接器将共用这一个引擎)。(一个Server中可以有多个Service,一个Service只有一个Engine)。

Service元素的属性:

 className:要使用的实现的Java类型。这个类必须实现org.apache.catalina.Service接口,如果没有指定该属性,将使用标准实现。

 name:这个Service的显示名称,如果您使用标准的Catalina组件,它将包含在日志消息中。注意与每个Service元素里的name必须是唯一的。

 

4.请求处理流程

 

以下元素可以嵌套在Service元素中:

Connector:一个或多个Connector。

Engine:一个Engine,Engine要放在Connector元素之后。

类结构图:

 

 

5.service类关系图

类方法:

 

6.service类方法图

从类方法中,可以看出它直接依赖了Container、Server、Engine,内部提供了查找Conntecor的方法用于查找一个或多个Connector,以及查找Executor方法。

 

Executors

执行器表示可以在Tomcat中的组件之间共享的线程池,从历史上看,每个创建的Connector都有一个线程池,但当配置了执行执行器时,您可以在(主要)连接器之间以及其他组件之间共享线程池。执行器必须实现org.apache.catalina.Executor接口,它是Service元素的嵌套元素,它必须出现在Connector元素之前。(可以配置多个Executor)

Executor相关属性查阅tomcat文档。

类结构图:

 

7.executors类关系图

 

Connectors

HTTP Connector元素代表支持HTTP/1.1协议的Connector组件。除了执行Servlet和JSP页面的功能外,它还使Catalina能够充当独立的WEB服务器。该组件的特定实例侦听服务器上特定的TCP端口号上的连接。可以将一个或多个此类连接器配置为单个Service的一部分,每个连接器都转发到关联的引擎以执行请求处理并创建响应。每个传入的请求在请求期间都需要一个线程。如果接收到的并发请求多于当前可用的请求处理线程所能处理的请求,则将创建额外的线程,直到配置的最大值(max Threads属性的值默认200)。如果同时收到更多的请求,则将它们堆积在连接器创建的服务器套接字中,直到配置的最大值(acceptCount属性值,默认为100),如果继续请求超过了最大值,则这些请求将收到“拒绝连接”的错误信息,直到有足够的资源来处理它们。(可以有多个Connector)

Connector相关属性查阅tomcat文档。

类结构图:

 

8.connector类关系图

Connector直接实现了Lifecycle,它本身就是一个实现类,其下没有子类了。

 

 

Containers

  Containers(容器)是一个对象,可以执行从客户端接收到的请求,并根据这些请求返回响应。容器也可以通过实现Pipeline接口,选择性地支持Valve管道,这些管道按照运行时配置的顺序处理请求。

容器在catalina中的以下几个概念中存在。例如下面几种常见的情况:

 Engine - 表示整个servlet引擎,很可能包含一个或多个子容器,这些子容器要么是Host实现,要么是Context实现,要么是其他自定义组的子容器。

 Host - 包含多个上下文的虚拟主机的表示形式。

 Context - 单个ServletContext的表示形式,它通常包含一个或多个受支持的servlet的包装器。

 Wrapper - 单个servlet定义的表示形式(如果servlet本身实现了SingleThreadModel,那么它可能支持多个servlet实例)

Catalina的给定部署不需要包括上述所有级别的容器。例如,嵌入在网络设备(例如路由器)中的管理应用程序可能只包含一个上下文和几个包装程序,如果应用程序相对较小,甚至可能包含一个包装器。因此,需要对容器实现进行设计,以便在给定部署中没有父容器的情况下能够正确运行。

容器还可以与许多支持组件关联,这些支持组件提供可共享的功能(通过将其附加到父容器)或单独定制的功能。目前可以识别以下支持组件:

 Loader - 类装入器,用于将此容器的新Java类集成到运行Catalina的JVM中。

 Logger - ServletContext接口的log()方法签名的实现。

 Manager - 与此容器关联的会话池的管理器。

 Realm - 安全域的只读接口,用于验证用户身份及其对应角色。

 Resources - JNDI目录上下文支持对静态资源的访问,当Catalina嵌入到更大的服务器中时,支持对现有服务器组件的自定义链接。

 

 

      后期附加图1.Container大家族

  

Engine

Engin元素表示与特定Catalina Service关联的整个请求处理机制。它接收并处理来自一个或多个Connector的所有请求,并将完成的响应返回Connector,以便最终传输回客户端。

在与该Service关联的所有Connector元素之后,必须在Service内嵌套一个Engine元素。Engine的一个标准实现是org.apache.catalina.core.StandardEngine。

Engine相关属性查阅tomcat文档。

Engine的特殊特性:

 Logging:

引擎与org.apache.catalina.core.ContainerBase.[enginename] 日志类别关联,请注意,括号实际上是名称的一部分,请不要忽略。

 Access Logs:

当您允许web服务器时,通常生成的输出文件之一时访问日志,它以标准格式为服务器处理的每一个请求生成一行信息。Catalina包括一个可选的Valve实现,它可以用web服务器创建相同标准格式或任意数量的自定义格式创建访问日志。具体使用见官方说明。

 Lifecycle Listeners:

如果您需要知道引擎何时启动或停止的Java对象,那么可以通过在该元素中嵌套一个侦听器元素来声明它。您指定的类名必须实现org.apache.catalina.LifecycleListener接口,它将被通知相应生命周期事件发生,具体示例见官方说明。

 Request Filters:

您可以要求Catalina针对每一个Enging,Host或Context元素都对每一个请求检查IP地址或域名。这将根据配置的"accpet"和/或“deny”过滤器(使用java.util.regex正则表达式语法定义的)进行过滤,被过滤的请求将收到"禁止"信息。使用方式见官方配置。

类结构图:

参见Host中。

 

Host

Host元素表示一个虚拟主机,它是服务器(例如www.mycompany.com)的网络名称与运行Tomcat的特定服务器的关联。为了使客户端能够使用其网络名称连接到Tomcat服务器,必须在管理您所属的Internet域的域名服务(DNS)服务器中注册该名称(请与网络管理员联系以获取更多信息)。

在许多情况下,系统管理员希望将多个网络名称(如www.mycompany.com和company.com)与相同的虚拟主机和应用程序关联起来。这可以使用下面讨论的主机名别名特性来完成。

一个或多个宿主元素嵌套在引擎元素中。在宿主元素内部,可以为此虚拟主机关联嵌套web应用程序上下文元素。与每个引擎关联的主机中,必须有一个主机的名称与该引擎的defaultHost属性匹配。

客户端通常使用主机名来标识希望连接的服务器。该主机名也包含在HTTP请求头中。Tomcat从HTTP头中读取主机名,并查找具有匹配名称的主机。如果未找到匹配项,则将请求路由到默认主机。默认主机名称不必与DNS名称匹配(尽管可以),因为DNS名称与Host元素的名称不匹配的任何请求都将被路由到默认主机。

下面的描述使用变量名$CATALINA_BASE来引用解析大多数相对路径所依据的基本目录。如果您没有通过设置CATALINA_BASE基目录为多个实例配置Tomcat,那么$CATAINA_BASE将被设置为$CATALINA_HOME的值,即您按照Tomcat的目录。

它的默认实现是org.apache.catalina.core.StandardHost。

Host相关属性查阅tomcat文档。

以下元素可以嵌套在Host元素中:

 Context:一个或多个Context元素,每个Context元素代表与此虚拟主机关联的不同WEB应用程序。

 Realm:最多一个。配置一个域,允许跨嵌套在此主机内的所有上下文共享其用户数据库及其关联角色(除非被较低级别的域配置覆盖)

Host的特殊特性:

 Logging

 Access Logs

 Automatic Application Delpoyment

 自动化部署

 Host Name Aliases

 Lifecycle Listeners

 Request Filters

 Single Sign On

 单点登录

 User Web Application

 Custom context.xml and web.xml

 您可以覆盖$CATALINA_BASE中每个虚拟主机的conf/context.xml和conf/web.xml文件中的默认值。Tomcat将在xmlBase指定的目录中查找名为context.xml.default和web.xml.default的文件,并将这些文件合并为默认文件中的文件。

类结构图:

 

9.host类关系图

 

Context

官方文档说了一句很直白的话:简而言之,上下文就是一个web应用程序。

 

10.官方文档context摘抄

 

Context元素表示一个在特定虚拟主机运行的web应用程序。每个web应用程序都基于web应用程序存档(war)文件,或包含相应解包内容的相应目录,这在Servlet规范(2.2版本或更高)中有描述。有关web应用程序存档的更多信息,可以下载Servlet规范,并查看Tomcat应用程序开发人员指南。

Catalina通过使用最长匹配,把Request URI与每个定义的Context的上下文进行匹配,来选择用于处理每个HTTP请求的Web应用程序。选择之后,该上下文根据Web应用程序部署定义的servlet映射,选择适当的servlet来处理传入的请求。

您可以根据需要定义任意多个Contex元素,每个这样的上下文必须在虚拟主机中具有唯一的上下文名称。上下文路径不必唯一。另外,必须使用等于零长度字符串的上下文路径来提供上下文。此上下文成为该虚拟主机默认web应用程序,并用于处理与任何其他上下文的上下文路径都不匹配的所有请求。

Context代表了运行在Host上的单个Web应用,一个Host可以有多个Context元素,每个Web应用必须有唯一的URL路径,这个URL路径Context中的path属性中设定。

Context相关属性:

 path:指定访问该Web应用的URL入口,web项目的访问路径,不填默认以http://localhost:8080/开始。

 docBase:指定Web应用的文件路径,可以给定绝对路径,也可以给定相对于<Host>的appBase属性的相对路径,如果Web应用采用开放目录结构,则指定Web应用的根目录,如果Web应用是个war文件,则指定war文件的路径,你要让tomcat帮你管理Servlet你总要告诉它Servlet在哪的。

 reloadable:如果这个属性设为true,tomcat服务器在运行状态下会监视在WEB-INF/classes和WEB-INF/lib目录下class文件的改动,如果监测到有class文件被更新的,服务器会自动重新加载Web应用。在开发阶段将reloadable属性设为true,有助于调试servlet和其它的class文件,但这样用加重服务器运行负荷,建议在Web应用的发存阶段将reloadable设为false。

 其他参见官方描述

Context的标准实现是org.apache.catalina.core.StandardContext。

以下元素可以嵌套在Host元素中:

 Cookie Processor - 配置HTTP cookie头的解析和生成。

 Loader - 配置将用于加载此web应用程序的servlet和bean类的web应用程序类加载器。通常,类装载器默认配置就足够了。

 Manager - 配置将用于为此web应用程序创建、销毁和持久化HTTP会话的管理器。通常,会话管理器的默认配置就足够了。

 Realm - 配置一个域,该域将运行其用户数据库及其关联角色仅用于此特定web应用程序,如果未指定,此web应用程序将使用与所属主机或引擎关联的域。

 Resources - 配置用于访问与此web应用程序关联的静态资源管理器。通常,资源管理器的默认配置足够。

 WatchResource - auto deployer将监视web应用程序的指定静态资源获取更新,并在web应用程序被更新时重新加载它。该元素的内容必须是字符串。

 JarScanner - 配置用于扫描web应用程序的Jar文件和类文件目录的Jar扫描程序。它通常在web应用程序开始时用于识别配置文件,如TLDs o web-fragment.xml文件,这些文件必须作为web应用程序初始化的一部分进行处理。通常默认配置足够。

Context元素的特殊特性:

 Logging

 Access Logs

 Automatic Context Configuration

如果使用标准上下文实现,则在启动Catalina或重载此web应用程序将时自动执行以下配置步骤,启用此特性不需要任何特殊配置。

 如果您没有声明自己的Loader元素,那么将配置一个标准的web应用程序类加载器。

 如果您没有声明自己的Manager元素,那么将配置一个标准的会话管理器。

 如果您没有声明自己的Resources元素,那么将配置一个标准的资源管理器。

 conf/web.xml中列出的Web应用程序属性将作为此WEB应用程序的默认处理。这用于建立默认映射(例如,将*.jsp扩展名映射到相应的JSP servlet)以及适用于所有Web应用程序的其他标准功能。

 (如果存在此资源)将处理/WEB-INF/web.xml资源中列出的web应用程序属性。

 如果您的web应用程序指定了可能需要用户身份验证的安全约束,则将配置实现您所选择的登录方法的适当身份验证器。

 Context Parameters

 Environment Entries

 Lifecycle Listeners

 Request Filters

 Resource Definitions

 Resource Links

 Transaction

类结构图:

 

11.context类关系图

 

 

Cluster

集群,参见官方文档描述。

 

 

其它

 

12.总览图

 

 

13.Lifecycle接口方法图

Lifecycle

可以看到,我们tomcat所有组件的标准实现都实现了Lifecycle接口。Lifecycle除了控制生命周期的Start和Stop方法还有一个监听机制,在生命周期开始和结束时做一些额外的操作。这个机制在其他的框架中也被使用,如在Spring中。

Lifecycle接口的方法的实现都在其他组件中,就像前面说的,组件的生命周期包由它的父组件控制,所以它的start方法自然就是调用它下面组件的start方法,stop方法也一样。

 

 

有关其他配置

Tomcat实际上由许多组件组成,包括JSP引擎和各种不同的链接器,但其核心组件叫做Catalina。Catalina提供了Tomcat对servlet规范的实际实现;当你启动你的Tomcat服务器时,你实际上是在启动Catalina。有关Catalina的配置文件在$CATALINA_BASE/conf目录下:

 catalina.policy

 此文件包含Catalina的安全策略,它里面包括系统代码,web应用程序和Catalina本身的权限定义。

 catalina.properties

 此文件是Catalina类的标准Java属性文件。它包含安全包列表和类装入器路径等信息,还可以包含一些字符串缓存设置,你可以调整其内参数以获得最佳Tomcat性能。

 context.xml

 xml配置文件用于定义将在Tomcat的给定实例运行的每个web应用程序上加载的Context信息。

 logging.properties

 此文件配置Catalina内置日志记录,包括阈值和日志位置等内容。

 server.xml

 这是Tomcat的主要配置文件,它使用Servlet规范中指定的分层语法来配置Catalina的初始状态,以定义Tomcat引导和构建其各种组件的顺序。

 tomcat-user.xml

 此文件包含有关给定Tomcat服务器上的各种用户、密码、角色等,当你登录8080时需要。

 web.xml

 此文件配置将应用于加载到给定Tomcat实例中的所有应用程序的选项和值,包括Servlet定义、如缓冲区大小、调试级别、Jasper选项和缺省的欢迎文件。

 

 

 

 

+自我理解

Tomcat结构

1.Tomcat结构理解

 

14.Tomcat容器结构图

在整个Tomcat中,被容器的概念贯穿,四层结构Engine,Host,Context,Weapper,从顶层Engine开始,到Host,再到Context,最后是Wrapper,每一层都是一个容器。每一个Context都代表着一个单独的web项目。在Context中为了解耦,将Servlet封装在Tomcat自身的Wrapper容器里,使得Tomcat与Servlet的关联不在紧密。

Wrapper类关系图:

 

15.wrapper类关系图

 

 

  

16.tomcat下应用程序和全局web.xml关系

 

 

Tomcat工作流程(前半部分)

流程图17.从启动到完成的整个工作流程

 

17.从启动到完成的整个工作流程

2是让我印象最深的一张图,它几乎描述了整个Tomcat的大致工作流程。在理解这张图之前,记得要Tomcat的源码下载下来,因为下一步我们要对照着源码分析这张图,我下载的是Tomcat7.0.99版本的源码,然后将源码中java文件夹下的javax和org分别导入Eclipse中,这样很方便查看。

一般对tomcat的操作如下:点击startup.bat,唤起Tomcat,然后Tomcat在启动(org.apache.catalina.startup.Tomcat)过程中会读取server.xml,然后初始化Server,Service,引擎,还有Connector,Server和Service不必讲也都明白,Engine在这里有必要介绍一下,它的责任就是将用户请求分配给一个虚拟机处理,而Connector的作用就是建立一个连接,一个WEB服务器到Tomcat的连接器,在上图的最下面可以看到在这个Connector容器里初始化Http服务。

Tomcat自身基础搭建好之后,开始针对web应用做文章了。做文章之前先启动了自身的服务然后初始化Host容器,启动Host,在Host启动过程中初始化了一个web应用上下文环境(回看上一章节)即StandardContext,到这你要注意了,当StandardContext的状态变为init时(也就是server.xml中的Context节点被读取并初始化时),ContextConfig作为观察者将被通知,光说不如直接干,我们直接在下一章进行源码分析。

 

Tomcat源码导入

1、解压源码apache-tomcat-7.0.99-src,在源码目录下添加如下pom.xml

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <project xmlns="http://maven.apache.org/POM/4.0.0"
 3          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 5 
 6     <modelVersion>4.0.0</modelVersion>
 7     <groupId>org.apache.tomcat</groupId>
 8     <artifactId>Tomcat7.0</artifactId>
 9     <name>Tomcat7.0</name>
10     <version>7.0</version>
11 
12     <build>
13         <finalName>Tomcat7.0</finalName>
14         <sourceDirectory>java</sourceDirectory>
15         <testSourceDirectory>test</testSourceDirectory>
16         <resources>
17             <resource>
18                 <directory>java</directory>
19             </resource>
20         </resources>
21         <testResources>
22             <testResource>
23                 <directory>test</directory>
24             </testResource>
25         </testResources>
26         <plugins>
27             <plugin>
28                 <groupId>org.apache.maven.plugins</groupId>
29                 <artifactId>maven-compiler-plugin</artifactId>
30                 <version>2.3</version>
31                 <configuration>
32                     <encoding>UTF-8</encoding>
33                     <source>1.8</source>
34                     <target>1.8</target>
35                 </configuration>
36             </plugin>
37         </plugins>
38     </build>
39 
40     <dependencies>
41         <dependency>
42             <groupId>junit</groupId>
43             <artifactId>junit</artifactId>
44             <version>4.12</version>
45             <scope>test</scope>
46         </dependency>
47         <dependency>
48             <groupId>org.easymock</groupId>
49             <artifactId>easymock</artifactId>
50             <version>3.4</version>
51         </dependency>
52         <dependency>
53             <groupId>ant</groupId>
54             <artifactId>ant</artifactId>
55             <version>1.7.0</version>
56         </dependency>
57         <dependency>
58             <groupId>wsdl4j</groupId>
59             <artifactId>wsdl4j</artifactId>
60             <version>1.6.2</version>
61         </dependency>
62         <dependency>
63             <groupId>javax.xml</groupId>
64             <artifactId>jaxrpc</artifactId>
65             <version>1.1</version>
66         </dependency>
67         <dependency>
68             <groupId>org.eclipse.jdt.core.compiler</groupId>
69             <artifactId>ecj</artifactId>
70             <version>4.5.1</version>
71         </dependency>
72     </dependencies>
73 </project>

 

2、找到test.util.TestCookieFilter类文件,将类内的全部内容注释,不注释的化,启动测试时,会需要一个CookieFilter类,你要么自己实现一个CookieFilter,或者选择注释它里面所以内容。

3、找到test.org.apache.catalina.startup.TestTomcat类,找到测试方法:testSingleWebapp方法,以tomcat启动一个web应用的整个流程分析,在其中第一行打好断点。

4、启动测试,会报两个错如下:

java.io.FileNotFoundException: output\build\conf\logging.properties (系统找不到指定的路径。)

[D:\workspace\java\tomcat\apache-tomcat-7.0.99-src\output\build\webapps\examples] does not exist or is not a readable directory

  

原因是我们测试一个webapp首先要需要日志输出,它没找到配置文件,其次没找到webapps目录,我们按照它的指示在output目录下新建build/conf目录和build/webapps目录,然后拷贝conf目录下的logging.properties到build/conf目录,在然后把根目录下的webapps里的examples拷贝到build/webapps下,我们只测试一个example的web应用就够了,没必要把其他的也拷过去。

5、拷贝完后,再次启动测试,报错:

java.lang.ClassNotFoundException: listeners.ContextListener
	at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1951)
	at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1794)

java.lang.ClassNotFoundException: listeners.SessionListener
	at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1951)

java.lang.ClassNotFoundException: async.AsyncStockContextListener
	at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1951)
	at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1794)
	at org.apache.catalina.core.DefaultInstanceManager.loadClass(DefaultInstanceManager.java:536)

这是因为我们拷贝的webapps里的java源码没被编译为.class,所以它找不到,你可以使用eclipse或IDEA进行编译构建,或者你本机里原本就存在tomcat可以找找它目录下webapps目录里的examples拷贝于此(我就是把本机里tomcat8里的examples拷贝于此的),或者是你再去官网下载该源码版本对应的tomcat.bin启动包(即你开发用的tomcat),找到里面的examples复制于此。

6、拷贝完后,再次测试,即可查看到一条期望的绿线。

 

除此之外,如果你不想在测试程序里研究Tomcat的启动流程,而是直接启动Tomcat的main程序,查看整个启动流程,你需要下载和Tomcat源码版本一致的Tomcat.bin启动包,在eclipse或IDEA的启动程序的VM options属性中设置:

-Dcatalina.home="D:\workspace\java\tomcat\apache-tomcat-7.0.99-bin"

  

然后找到org.apache.catalina.startup.Bootstrap点击启动即可启动Tomcat。

 

 

Tomcat源码解析-学习整个脉络

先来看一下org.apache.catalina.startup.TestTomcat#testProgrammatic这段代码【在阅读源码时请打开”其他“章节中的总览图,方便一边对照,一边学习】:

先不看下面我写的注释,直接在断点位置1出打好断点,往下看说明

/**
 * Start tomcat with a single context and one
 * servlet - all programmatic, no server.xml or
 * web.xml used.
 *
 * @throws Exception
 */
@Test
public void testProgrammatic() throws Exception {
    // 创建tomcat实例
    Tomcat tomcat = getTomcatInstance(); // 断点位置1

    // No file system docBase required
    // 在addContext中:1、静默日志输出。2、createContext使用反射创建出
    // StandardContext对象并返回。3、设置contextName、contextPath、dir
    // 4、添加一个生命周期监听器,当上下文被修改时触发。5、把刚创建的
    // StandardContext对象加入到host对象的children属性内(在addChild内给
    // host对象加了一个内存泄漏监听)。最终返回创建的StandardContext对象
    Context ctx = tomcat.addContext("", null);
    // You can customize the context by calling
    // its API

    // 一个干净的StandardContext也有了,但是里面没有任何Servlet,所以下面
    // 往StandardContext内加入了一个servlet,并添加了mapping。
    // addServlet中先是使用ExistingStandardWrapper将servlet包装了起来得
    // 到wrp对象,为的就是和servlet解耦,然后在ctx.addChild(wrp)。
    // 【ExistingStandardWrapper参见"Tomcat结构"章节中的warpper类关系图】
    Tomcat.addServlet(ctx, "myServlet", new HelloWorld());
    // StandardContext中的获取刚才封装的myServlet的wrp,然后以映射pattern作
    // 为key,以wrp作为value封装起来。
    ctx.addServletMapping("/", "myServlet");

    // 一切需要准备的对象都创建好了,但是仅仅只是创建好了,它们还没有经历过
    // 初始化,而且每个环节中的事件(比如说Context启动时监听等)也都没触发呢,
    // 完事开头必须有个引子,这个引子就是server的启动。
    // 这个tomcat对象重写了父类的start方法,进入其中:1、获取server。2、获取
    // server里的service,然后获取service内的Container[即StandardEngine]
    // 3、从engine中获取host,从host中获取Manager,为null一直从父类中获取Manager
    // 都为null,创建一个StandardManager并放入StandardContext中,并把standardcontext
    // 对象赋值给manager的container属性内。最后调用super.start(),super即
    // 指Tomcat类,在Tomcat类的start方法中,getServer和getConnector这些所需返回的
    // 对象之前都已创建过,所以直接开始进入server.start(),这里面的内容比较复杂,我们
    // 单独拿到下面将。
    tomcat.start();

    ByteChunk res = getUrl("http://localhost:" + getPort() + "/");
    Assert.assertEquals("Hello world", res.toString());
}

  

这段测试功能说的很清楚,这段测试代码中,没有添加任何web应用程序,仅仅是tomcat自身基础 + 一个简单的Servlet构成,打好断点,进入getTomcatInstance,可以看到这是TomcatBaseTest抽象类的一个方法,TomcatBaseTest的作用注释上也写的明白:为每个测试提供一个Tomcat实例的基本测试用例-主要是这样我们不必继续编写清理代码。getTomcatInstance直接返回了一个tomcat引用,也就是说这个tomcat引用的实例肯定是在哪被创建了,往下找,可以看到有一个setUp方法,这个方法有个@Before注解,而且这个方法重写了父类的setUp方法,也就是说这个方法将在类其他方法执行前被执行:

直接打好断点位置2,不必看我添加的注释:

/**
 * Make the Tomcat instance available to sub-classes.
 *
 * @return A Tomcat instance without any pre-configured web applications
 */
public Tomcat getTomcatInstance() {
    return tomcat;
}

@Before
@Override
public void setUp() throws Exception {
    // 调用父类LoggingBaseTest的setUp方法,打印一条开始启动测试的日志信息
    super.setUp(); // 断点位置2

    //触发加载catalina.properties,在调用getProperty之前会先执行该类的静态
    // 代码块,执行loadProperyies方法,依次去catalinabase目录下找catalina.properties
    // 没找到,又去Tomcat类目录下找,也没找到,然后创建了一个空的Properties对象。
    // 这里getProperty("foo")只是作为一个引子,去调用loadProperties而已。
    CatalinaProperties.getProperty("foo");

    // 创建一个空的应用程序目录(webapps)
    File appBase = new File(getTemporaryDirectory(), "webapps");
    if (!appBase.exists() && !appBase.mkdir()) {
        Assert.fail("Unable to create appBase for test");
    }

    // TomcatWithFastSessionIDs是一个继承了Tomcat类的内部类,它重写了start方法,
    // 继承了所有Tomcat非private方法和属性。在创建实例时调用了Tomcat类的构造函数。
    // 最终创建了一个tomcat实例
    tomcat = new TomcatWithFastSessionIDs();

    // 反射创建出Http11Protocol对象,注意这个Http11Protocol对象的构造
    // 方法,在它构造方法里有一个重要的对象JIoEndpoint endpoint!这个对象功
    // 能是:处理传入的TCP连接。这个JIoEndpoint类实现了一个简单的服务器模
   // 型【ServerSocket】:一个侦听线程,接收一个套接字,并为每个传入的连
   // 接创建一个新的工作线程。(它是更高级的端点,它将重用线程、使用队列等。)
   
    // 并用该Http11Protocol对象赋值给Conntecor的protocolHandler属性。
    // 最终返回一个connector实例。Http11Protocol是对HTTP1.1协议的封装,用于处理HTTP协议
    String protocol = getProtocol();
    Connector connector = new Connector(protocol);
    // Listen only on localhost
    connector.setAttribute("address",
            InetAddress.getByName("localhost").getHostAddress());
    // Use random free port
    connector.setPort(0);
    // Mainly set to reduce timeouts during async tests
    connector.setAttribute("connectionTimeout", "3000");
    // getService:此时tomcat中server、service、engine、connector、host等全为null,进
    // 入getService:1、initBaseDir由于我们创建appBase时已经创建过,所以在这里
    // 不会做什么动作。2、创建一个StandardServer对象和一个StandardService对象,
    // 赋值给tomcat对象的自身属性server和service,然后调用server.addService(service)把
    // 创建的service对象加入到server内,最后返回service对象。【由之前的类关系图也可
    // 知:StandardService组合了Server对象,查看addService方法,可见其
    // 中调用了service.setServer(this)】
    
    // addConnector:addConnector和addService做的内容类似,只不过换成了connector对象而已
    // connector.setService(this)把service加入到connector对象里,然后又把传入的connector
    // 设置到service自身属性connectors里
    tomcat.getService().addConnector(connector);
    // 把connector设置到tomcat对象内。执行到此,tomcat里server、
    // service、connector都不在为null了。
    tomcat.setConnector(connector);

    // Add AprLifecycleListener if we are using the Apr connector
    if (protocol.contains("Apr")) { // false不执行
        StandardServer server = (StandardServer) tomcat.getServer();
        AprLifecycleListener listener = new AprLifecycleListener();
        listener.setSSLRandomSeed("/dev/urandom");
        server.addLifecycleListener(listener);
        connector.setAttribute("pollerThreadCount", Integer.valueOf(1));
    }

    // 设置tomcat的baseDir
    File catalinaBase = getTemporaryDirectory();
    tomcat.setBaseDir(catalinaBase.getAbsolutePath());//设置tomcat工作目录
    // getHost:tomcat内host属性为null,所以创建了一个StandardHost对象,赋值给自身host,
    // 在getHost内部又调用了getEngine(),tomcat内的engine对象也为null,所以创建
    // 了一个StandardEngine对象赋值给自身engine属性,创建完后又调用了
    // service.setContainer(engine),Engine、Context和Host三个接口都是继承了Container
    // 接口的,StandardEngine实现了Engine接口,setContainer(Container container)
    // 方法的参数engine是Container的实现类。在setContainer将传入的container赋
    // 值给service对象的container属性,然后把service对象设置到engine对象的
    // service属性内【StandardEngine组合了Service】。getEngine()方法结束后,
    // 将刚才创建的StandardHost加入到engine对象的children属性内。最终返回host对象。
    // 此刻tomcat中host和engine对象也不为null了。
    tomcat.getHost().setAppBase(appBase.getAbsolutePath()); // 设置应用程序工作目录

    // 没有启用访问日志功能,所以为false
    accessLogEnabled = Boolean.parseBoolean(
        System.getProperty("tomcat.test.accesslog", "false"));
    if (accessLogEnabled) {
        String accessLogDirectory = System
                .getProperty("tomcat.test.reports");
        if (accessLogDirectory == null) {
            accessLogDirectory = new File(getBuildDirectory(), "logs")
                    .toString();
        }
        AccessLogValve alv = new AccessLogValve();
        alv.setDirectory(accessLogDirectory);
        alv.setPattern("%h %l %u %t \"%r\" %s %b %I %D");
        tomcat.getHost().getPipeline().addValve(alv);
    }

    // Cannot delete the whole tempDir, because logs are there,
    // but delete known subdirectories of it.
    addDeleteOnTearDown(new File(catalinaBase, "webapps")); // 删掉应用程序目录
    addDeleteOnTearDown(new File(catalinaBase, "work")); 
}

  

以上两个断点位置打好后就可以debug启动测试该方法了,启动后首先进入断点位置2(现在可以跟着debug一点点看注释了),当setUp执行完,tomcat实例中,五大组件对象基本都已创建完毕(没有Executor,因为我们这里只有一个Connector组件,所以根本用不到共享线程池)。然后进入断点1位置(继续跟踪断点看我写的注释)。

在以上两端代码都跟踪debug走过一遍之后,最后就到了server.start()这里,StandardServer本身没有重写父类的start方法,在父类LifecycleBase中找到了start方法,可以看到StandardServer、StandardService、StandardEngine、StandardHost、StandardContext都继承了LifecycleBase类,之前说了Lifecycle除了控制生命周期的Start和Stop方法还有一个监听机制,在生命周期开始和结束时做一些额外的操作,注意:所有的标准组件都没有重写start/stop/init方法,也就是说这三个方法都在父类LifecycleBase里,而真正 初始化/启动/终止/销毁 各个组件的方法initInternal/startInternal/stopInternal/destoryInternal在LifecycleBase都是抽象的,也就是说对于各个组件自身的初始化、启动、终止、销毁都是交给每个组件自己实现的

# LifecycleBase#start

@Override
public final synchronized void start() throws LifecycleException {

    if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) ||
            LifecycleState.STARTED.equals(state)) {

        if (log.isDebugEnabled()) {
            Exception e = new LifecycleException();
            log.debug(sm.getString("lifecycleBase.alreadyStarted", toString()), e);
        } else if (log.isInfoEnabled()) {
            log.info(sm.getString("lifecycleBase.alreadyStarted", toString()));
        }

        return;
    }

    if (state.equals(LifecycleState.NEW)) {
        init();
    } else if (state.equals(LifecycleState.FAILED)) {
        stop();
    } else if (!state.equals(LifecycleState.INITIALIZED) &&
            !state.equals(LifecycleState.STOPPED)) {
        invalidTransition(Lifecycle.BEFORE_START_EVENT);
    }

    try {
        setStateInternal(LifecycleState.STARTING_PREP, null, false);
        startInternal();
        if (state.equals(LifecycleState.FAILED)) {
            // This is a 'controlled' failure. The component put itself into the
            // FAILED state so call stop() to complete the clean-up.
            stop();
        } else if (!state.equals(LifecycleState.STARTING)) {
            // Shouldn't be necessary but acts as a check that sub-classes are
            // doing what they are supposed to.
            invalidTransition(Lifecycle.AFTER_START_EVENT);
        } else {
            setStateInternal(LifecycleState.STARTED, null, false);
        }
    } catch (Throwable t) {
        // This is an 'uncontrolled' failure so put the component into the
        // FAILED state and throw an exception.
        handleSubClassException(t, "lifecycleBase.startFail", toString());
    }
}

@Override
public final synchronized void init() throws LifecycleException {
    if (!state.equals(LifecycleState.NEW)) {
        invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
    }

    try {
        setStateInternal(LifecycleState.INITIALIZING, null, false);
        initInternal();
        setStateInternal(LifecycleState.INITIALIZED, null, false);
    } catch (Throwable t) {
        handleSubClassException(t, "lifecycleBase.initFail", toString());
    }
}

  

现在开始进入server.start,在这里启动时会不断的对一系列标准组件执行类似如下状态切换操作:

NEW(start()->init():改变状态)、INITIALIZING(init()->initInternal():内部标准组件初始化)、INITIALIZED、STARTING_PREP、STARTING

首当其冲的就是server,进入start方法的init:

 server,server的初始状态为NEW,然后进入init方法【所有标准组件的init方法都是调用LifecycleBase#init】中调用setStateInternal方法将server的状态切换为INITIALIZING,然后获取server内部初始化前置生命周期的事件即before_init,如果有就注册监听器。在这里我们server里没有,setStateInternal方法执行完成。进入initInternal(),这是每个标准组件都必须实现的方法,因为父类LifecycleBase的initInternal是个抽象方法,在StandardServer#initInternal才是真正启动server做的事情。1、调用super.initInternal(),进入的是LifecycleMBeanBase的initInternal方法,在这个方法里初始化了两个对象:MBeanServer和ObjectName,它们主要是用于缓存注册过的组件的。2、注册一个缓存对象,由缓存名和缓存组件组成。3、不知道大家还记得不记得有两大元素可以嵌套在Server元素中,第一就是Service(一个或多个),第二就是GlobalNamingResources(一个)。

 刚才我们进入了server的init方法,现在要进入globalNamingResources的init方法了,它的流程也和server一样,先是调用LifecycleBase#init,然后调用setStateInternal方法globalNamingResources由NEW状态变为INITIALIZING,获取globalNamingResources初始化前置事件before_init,它里面也没有任何需要监听的事件,然后globalNamingResources#initInternal方法,这个方法里初始化globalNamingResources,首先它把该组件缓存了一下,由于我们没有配置任何命名资源所以里面啥也没干,所以它执行完毕后,接着再次调用setStateInternal方法将globalNamingResources状态切换为INITIALIZED状态,获取其初始化后置事件after_init,没有后置事件,至此globalNamingResources的一个流程NEW->INITIALIZING->INITIALIZED走完了。我们继续回到server的initInternal方法里。

 globalNamingResources已经初始化完了,Server里的Service还没初始化,紧接着,遍历所有的service调用它们的init,我们这里只有一个service,所以直接调用它的init,即又回到了LifecycleBase#init方法,又开始重复那一套了,先是切换service状态由NEW变为INITIALIZING,然后获取初始化前置事件,这里没有前置事件,然后进入StandardService#initInternal,首先缓存该组件,其次又开始像Server一样去找能嵌套在Service下的元素,Service里可以包含哪些元素?Executor、Connector、Engine(Conainer)。一个一个来吧:

 先是执行container.init(),即engine的init()方法,又回到了LifecycleBase#init,把engine的状态由NEW变为INITIALIZING,查找它的初始化前置事件before_init,没有前置事件,然后进入StandardEngine#initInternal,在它里面getRealm()确保在初始化engine之前有一个域,如果没有就创建一个默认的域。然后又调用了super.initInternal(),即ContainerBase#initInternal,在这里创建了一个线程池startstopExecutor,注意这个startStopExecutor的说明:用于处理与此容器关联的任何子组件启动和停止事件的线程数。然后把该组件缓存一下。到此engine#initInternal执行完毕,接下来就是执行它的setStateInternal把INITIALIZING状态变为INITIALIZED,查找初始化后置事件after_init,没有后置事件,结束。

 查找service下所有Executor,由于我们没用到共享线程池,所以没有,也不用为它执行init了

 获取所有Connector,由于我们只有一个,调用connector.init(),即LifecycleBase#init,将connector状态由INIT变成INITIALIZING,没有初始化前置事件before_init,然后进入Connector#initInternal,首先缓存这个组件,然后初始化适配器,把之前添加到Connector里的Http11Protocol做个适配,设置解析http请求body的方式为POST。

 然后调用Http11Protocol的init方法,去初始化Http11Protocal,这个init就不在是LifecycleBase的init方法了,首先也是缓存该组件,接下来就是初始化Http11Protocal,然后初始化它里面的endpoint(JIoEndpoint),创建serversocket工厂,获取serversocket,设置最大请求数、默认接收请求线程数,绑定端口和address等。

 在然后调用mapperListener.init(),调用的还是LifecycleBase#init,把mapperListener状态由INIT变为INITIALIZING,没有前置事件,它没有重写initInternal方法,所以调用父类的initInternal把该组件缓存一下,最后再次调用setStateInternal把状态由INITIALIZING切换为INITIALIZED,没有初始化后置事件after_init,结束。

 connector再次调用setStateInternal,把状态由INITIALIZING切换为INITIALIZED,没有初始化后置事件after_init,结束。

 然后再次调用service的setStateInternal把状态由INITIALIZING切换为INITIALIZED,没有初始化后置事件after_init,结束。service初始化结束后,我们又回到了server的initInternal里,也就意味着server的initInternal到此也结束了,然后再次调用server的setStateInternal,把状态由INITIALIZING切换为INITIALIZED,没有初始化后置事件after_init,结束。现在我们再次回到了最初刚刚进入server.init的位置,即上面代码行的第20行。到目前位置所有需要的组件都初始化完毕了,接下来还要像上面这个对每个组件进行启动!

 

进入start方法的setStateInternal(上面代码29行):

 server的状态由INITIALIZED切换为STARTING_PREP,查看是否有启动前置事件before_start,没有。然后开始进入StandsrdServer的startInternal,注意是startInternal不再是initInternal了。首先做的是触发configure_start事件,然后把状态由STARTING_PREP切换为STARTING,查找start事件,没有。

 调用globalnamingResources.start(),走的是LifecycleBase#start,调用setStateInternal把globalnamingResources的状态由INITIALIZED切换为STARTING_PREP,然后获取启动前置事件before_start,没有,然后调用NamingResources#startInternal,触发configure_start事件,然后把状态由STARTING_PREP切换为STARTING,然后查找start事件,没有。然后再次调用setStateInternal把STARTING状态切换为STARTED,查找启动后置事件after_start,也没有,至此globalnamingResources启动完成。

 遍历所有service,调用它们的start,我们这只有一个,进入LifecycleBase#start,先是调用setStateInternal把service的状态由INITIALIZED切换为STARTING_PREP,查找启动前置事件before_start,没有。进入StandardService#startInternal,首先把状态由STARD_PREP切换为STARTING,查找start事件,没有。

 调用container.start(),将engine状态由INITIALIZED切换为STARTING_PREP,查找启动前置事件before_start,没有。进入super.startInternal,即Containerbase#startInternal,在这里做的东西比较多,

 如果你配置了manager则启动manager,如果你配置了集群则启动集群,这两个我们都没配置。

 如果你配置了realm,虽然我们没配置,但是在之前init时创建了一个默认的realm,现在要realm.start(),再次进入LifecycleBase的start()方法,由于之前这个realm组件没有被init过,也就是说它的状态还是NEW,所以让它走一遍我们之前的逻辑,先是NEW->INITIALIZING->INITIALIZED,然后在让他变为STARTING_PREP->STARTING->STARTED。realm启动完成。

 紧接着查找engine下有子组件StandardHost,将其用StartChild封装起来,注意StartChild实现了Callable而且实现了call方法,在call方法里调用了child.start()即要启动StandardHost,这个call回调方法不是立即执行的。封装到StartChild后然后丢到startStopExecutor线程池里,它将为StandardHost的启动单独创建一个线程去执行。紧接着for循环所有的Future事件,调用get时触发call,此刻StandardHost.start被线程池里某一个线程调用:

 StandardHost调用Lifecycle#start,进入start方法内的init方法状态由NEW变为STARTING,它没有before_init事件,然后在调用initInternal方法,它本身没有重写initInternal方法,使用的是父类ContainerBase的initInternal,即ContainerBase#initInternal,然后相似的一幕出现了!在这里又创建另一个startStopExecutor,刚才初始化engine的时候,不知道你是否记得过这一幕,也就是说接下来的结果可想而知,这是一个不断的递归获取子组件初始化、启动子组件的过程。在初始化完StandardHost之后在启动StandardHost时,虽然StandardHost重写了startInternal但是在该方法的末尾又调用了super.startInternal即ContainerBase#startInternal,它将再次获取StandardHost下的子组件StandardContext,然后初始化StandardContext,StandardContext状态由NEW-...>STARTED(StandardContext下再无递归子元素。),StandardHost的状态也由NEW->STARTED。

 在然后启动pipeline.start(),它也和realm一样由NEW-...>STARTED。

 然后把engine的状态变为STARTING,查找start事件,没有。

 然后threadStart启动后台线程,该线程将定期检查会话是否超时。最后把engine的状态由于STARTING变为STARTED,检查启动后置事件after_start,没有。

 由于没有executor对象,所以也不会执行executor.start()方法。

 调用connnector.start(),进入LifecycleBase#start方法,首先将connector的状态由INITIALIZED变为STARTING_PREP,然后查找before_start事件,没有。然后执行Connector#startInternal,先把状态由STARTING_PREP变为STARTING,查找start事件,没有。然后

 protocolHandler.start(),调用内部相关start方法,这个start不再是LifecycleBase#start方法了,它内部又调用了endpoint.start(),第一步把running标志设置为true,由于之前serversocket已经创建了,但是还没有调用accept方法,也就是说还没有启动监听。第二步,创建ThreadPoolExecutor线程池。第三步设置Acceptor线程数目为1,只能有一个接收器。第四步startAcceptThreads,for循环创建指定数据的Acceptor接收器,由于上面设置了一个,所以也就只创建了一个Acceptor【Acceptor实现了AprEndpoint.Acceptor,重写了run方法,在run方法里不断的while循环,监听接收serversocket即将到来的请求连接,当连接来时交给线程池里适当的线程处理它。】。然后启动acceptor线程,执行了它的run方法,至此ServerSocker启动了。最后启动异步超时线程用于计算请求是否超时。

 mapperListener.start(),进入LifecycleBase#start(),把mapperListener状态由INITIALIZED变为START_PREP,查找是否存在before_start事件,没有。进入MapperListener#startInternal,把mapperListener状态由START_PREP变为STARTING,查找是否存在start事件,没有。然后设置mapper默认host为StandardHost,然后给engine及其子节点host、context、wrapper都添加一个ContainerListener事件和一个生命周期事件LifecycleListener。在然后registerHost(),把host加入到mapper内,然后registerContext(engine),设置contextPath,获取welcome Files,查找context下的所有wrapper,然后给每个wrapper注册映射信息。在然后addContextVersion()像现有host主机添加新的上下文(即包装了engine、host、context的新上下文)。最后调用setStateInternal把mapperListener状态由STARTING切换为STARTED,查找after_start事件,没有。

 最后调用setStateInternal把connector的状态由于STARTING变为STARTED,查找after_start,没有。

 service的startInternal执行完毕,然后调用了setStateInternal把service的状态由于STARTING变为STARTED,查找after_start事件,没有。

 server的startInternal方法执行完毕,然后调用了setStateInternal把server的状态由于STARTING变为STARTED,查找after_start事件,没有。到此server.start()方法执行完后,相应的组件也都start了,也就是说Tomcat启动成功了。

最后的测试结果,向http://localhost:8080/发送请求。其实调用的是HelloWorld的doGet方法返回byte类型的”Hello world“,最后做断言为true【注意此时如果用浏览器访问该url是没有返回结果的】,然后就是调用父类TomcatBaseTest的teardown对各个资源的关闭操作了。至此tomcat的一个基本的启动案例源码分析就结束了。

从整个流程上看下来,都是围绕着各个组件的初始化和启动来进行的,除了protocolHandler的初始化和start和其他组件不太一样外,其他的流程都类似,可以多看看protocolHandler。

接下来在看看tomcat是怎么加载我们webapp的源码。

 

 

Tomcat源码解析深入StandardContext

我们来看一下org.apache.catalina.startup.TestTomcat#testSingleWebapp这段代码:

// 这是手动启动Tomcat下面examples项目的例子,真实的启动Tomcat也大致类似
@Test
public void testSingleWebapp() throws Exception {
     // 初始化Server、Service、Connector、Engine、Host等组件
    Tomcat tomcat = getTomcatInstance(); // 断点位置

    File appDir = new File(getBuildDirectory(), "webapps/examples");
    // app dir is relative to server home
    tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath());

    tomcat.start();

    ByteChunk res = getUrl("http://localhost:" + getPort() +
            "/examples/servlets/servlet/HelloWorldExample");
    String text = res.toString();
    Assert.assertTrue(text, text.indexOf("<a href=\"../helloworld.html\">") > 0);
}

  

上面只是一段从创建Tomcat实例,到调用addWebapp()的简短过程,我们现在的主要关注点在addWebapp里干了什么

public Context addWebapp(Host host, String contextPath, String docBase) {
    LifecycleListener listener = null;
    try {
        // 使用反射获取class org.apache.catalina.startup.ContextConfig类
        //  ContextConfig类是继承了LifecycleListener接口的。
        Class<?> clazz = Class.forName(getHost().getConfigClass());
        // 这里调用ContextConfig类的无参构造函数,返回一个ContextConfig实例。
        // ContextConfig是Context启动事件侦听器,用于配置该Context的属性以及
        // 关联定义的servlet。
        listener = (LifecycleListener) clazz.newInstance();
    } catch (Exception e) {
        // Wrap in IAE since we can't easily change the method signature to
        // to throw the specific checked exceptions
        throw new IllegalArgumentException(e);
    }

    return addWebapp(host, contextPath, docBase, listener);
}

// 可以看到addWebapp和上一章节里分析的addContext十分类似
public Context addWebapp(Host host, String contextPath, String docBase,
            LifecycleListener config) {
    // 静默日志输出
    silence(host, contextPath);
    // 创建StandardContext实例
    Context ctx = createContext(host, contextPath);
    ctx.setPath(contextPath);
    ctx.setDocBase(docBase);
    // getDefaultWebXmlListener注释:返回一个用于配置JSP处理的侦听器对象,该
    // 侦听器提供了JSP处理所需的配置项。配置项来自tomcat全局的web.xml。
    // 将这个对象递给Context.addLifecycleListener(LifecycleListener),然后将
    // noDefaultWebXmlPath()结果传递给ContextConfig.setDefaultWebXml(String)。
    
    // DefaultWebXmlListener重写了父接口LifecycleListener的lifecycleEvent方法
    // 最终把该监听器添加到ctx中待用。
    ctx.addLifecycleListener(getDefaultWebXmlListener());
    // getWebappConfigFile:获取webapps目录下的应用程序里的context.xml,如果是
    // 个应用程序目录直接从目录里获取,如果是个war包从war包里获取。我们的examples
    // 应用程序没有添加context.xml所以这里返回的是个null
    ctx.setConfigFile(getWebappConfigFile(docBase, contextPath));
    // 为一个StandardContext添加一个监听器,当StandardContext启动时触发该监听器
    ctx.addLifecycleListener(config);

    if (config instanceof ContextConfig) {
        // prevent it from looking ( if it finds one - it'll have dup error )
        // 设置默认描述符的位置
        ((ContextConfig) config).setDefaultWebXml(noDefaultWebXmlPath());
    }

    if (host == null) {
        getHost().addChild(ctx);
    } else {
        host.addChild(ctx);
    }

    return ctx;
}

  

我们上一章节里创建的是一个空的StandardContext里面加了一个Helloworld servlet,现在则是以应用程序目录为基准创建了一个StandardContext,并添加了两个启动监听事件:1、ContextConfig启动时配置。2、DefaultWebXmlListener用于解析默认web.xml里面的DefaultServlet和JspServlet等。

既然这些事件都在context启动时执行,那接下来开始进入tomcat.start中看这个context启动时做了什么。

流程回顾:

 首先server.start()

 server.init()状态由NEW变INITIALIZING,没有before_init事件,然后执行initInternal,开始调用service.init()

 service.init()

 状态由NEW变为INITIALIZING,没有before_inti事件,开始执行initInternal,开始调用

 engine.init(),状态由NEW到INITIALIZING再到INITIALIZED

 connector.init(),protocolHandler.init(),mapperListener.init()不在多说了。状态由NEW到INITIALIZING再到INITIALIZED。

 service状态由NEW到INITIALIZING再到INITIALIZED。

 server状态由NEW到INITIALIZING再到INITIALIZED。

 server状态由INITIALIZED到STARTING_PREP,执行startInternal

 service.start()

 引擎start

 realm.start()

 创建线程池startStopExecutor启动独立线程对engine子组件StandardHost初始化、启动

 standardhost启动

 创建线程池startStopExecutor启动独立线程对StandardHost子组件StandardContext初始化

 StandardContext初始化时,状态由NEW变成INITIALIZING,有4个启动前置处理事件before_start,分别是:

 

18.StandardContext生命周期事件

 然后依次触发这四个事件,第一个DefaultWebXmlListener#lifecycleEvent,看如下代码:

public LifecycleListener getDefaultWebXmlListener() {
    return new DefaultWebXmlListener();
}

public static class DefaultWebXmlListener implements LifecycleListener {
    @Override
    public void lifecycleEvent(LifecycleEvent event) {
        if (Lifecycle.BEFORE_START_EVENT.equals(event.getType())) {
            initWebappDefaults((Context) event.getLifecycle());
        }
    }
}

  

 由于在if判断里只允许在before_start时才执行initWebappDefaults,所以等StandardContext由STARTING变为STARTED时这个initWebappDefaults方法才会被执行。我们先不管它,继续看第二个事件ContextConfig#lifecycleEvent事件,在这个事件中执行了如下代码的init方法:

@Override
public void lifecycleEvent(LifecycleEvent event) {

    // Identify the context we are associated with
    try {
        context = (Context) event.getLifecycle();
    } catch (ClassCastException e) {
        log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e);
        return;
    }

    // Process the event that has occurred
    if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
        configureStart();
    } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
        // 第二次进入,before_start
        // 1、扫描应用程序下的所有war,解包,配置真正的应用程序所在文件位置(
        // 解过包后的。)
        beforeStart();
    } else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
        // Restore docBase for management tools
        if (originalDocBase != null) {
            context.setDocBase(originalDocBase);
        }
    } else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
        configureStop();
    } else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
        // 第一次进入before_init
        // 1、创建并返回配置为处理应用程序上下文配置描述符的Digester(context.xml
        // 文件的摘要器)。2、创建一个SAXParser,将使用它来解析输入流。3、设置正
        // 确配置标志位。4、使用SAXParser处理默认配置文件(如果存在)5、创建并
        // 返回配置为处理web应用程序部署描述符(web.xml)的Digester
        init();
    } else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
        destroy();
    }

}

  

 剩下两个事件我们不在说明。紧接着当状态再次切换为STARTING时,再次对这四个事件调用自身的lifecycleEvent,此刻第一个事件DefaultWebXmlListener得以执行initWebappDefaults方法

// org.apache.catalina.startup.Tomcat.java

/**
* initWebappDefaults做的事情就是初始化默认的Servlet和JSP
* 在Tomcat启动时 加载编译JspServlet和DefaultServlet(只有
* 当load-on-startup的value值>0时才会被加载编译)。
*/
public static void initWebappDefaults(Context ctx) {
    // Default servlet,它的load-on-startup值为1
    // 使用Wrapper包装,创建StandardWrapper,然后初始化这个StandardWrapper,它
    // 也是走那一套逻辑,状态由NEW-...>STARTED,中途如果有绑定到StandardWrapper的
    // 事件也会被触发。最后用Warapper把默认的Servlet包装好
    // 和welcome files。
    Wrapper servlet = addServlet(
            ctx, "default", "org.apache.catalina.servlets.DefaultServlet");
    servlet.setLoadOnStartup(1);
    servlet.setOverridable(true);

    // JSP servlet (by class name - to avoid loading all deps),它的load-on-startup值为3
    // 这边也一样,创建StandardWrapper,继续那一套流程,
    // 初始化StandardWrapper,最后包装Jsp
    servlet = addServlet(
            ctx, "jsp", "org.apache.jasper.servlet.JspServlet");
    servlet.addInitParameter("fork", "false");
    servlet.setLoadOnStartup(3);
    servlet.setOverridable(true);

    // Servlet mappings
    // 设置好映射
    ctx.addServletMapping("/", "default");
    ctx.addServletMapping("*.jsp", "jsp");
    ctx.addServletMapping("*.jspx", "jsp");

    // Sessions
    ctx.setSessionTimeout(30);

    // MIME mappings
    for (int i = 0; i < DEFAULT_MIME_MAPPINGS.length;) {
        ctx.addMimeMapping(DEFAULT_MIME_MAPPINGS[i++],
                DEFAULT_MIME_MAPPINGS[i++]);
    }

    // Welcome files
    // 添加welcome file
    ctx.addWelcomeFile("index.html");
    ctx.addWelcomeFile("index.htm");
    ctx.addWelcomeFile("index.jsp");
}

  

 到此我们默认web.xml下的默认servlet和jsp已经用wrapper包装完成也配置好映射了。接下来继续for循环事件调用ContextConfig的before_start事件,这就调用了它里面的beforeStart方法(参见上面代码),在状态由INITIALIZED标为STARTING且before_start事件也都触发后,进入StandardContext#startInternal方法,这里面代码相当的长,主要分为以下几个步骤:

 获取应用程序目录examples

 resourcesStart方法,配置资源目录。

 setLoader设置与此容器关联的装载程序。

 getCharsetMapper初始化字符集映射器

 postWorkDirectory为我们的工作目录设置适当的上下文(即ServletContext)属性。在他里面创建了一个ServletContext对象【ApplicationContextFacade门面类】,然后将该对象赋值给StandardContext的context属性。

 bindThread,在上下文的启动、关闭、和重载期间,为CL用途和JNDI ENC支持而绑定当前线程。

 启动附属组件WebappLoader,还是走那一套流程状态由NEW-...>STARTED。

 解绑后再绑定。由于加载程序刚刚启动,因此现在创建了webapp类加载程序。通过连续调用unbindThread和bindThread,我们将当前的Thread CCL设置为webapp类加载器。

 然后调用fireLifecycleEvent,还是那四个事件,第一个DefaultWebXmlListener已经执行过,且状态不再是before_start就不在执行了,再次进入ContextConfig#lifecycleEvent方法,开始调用configureStart方法,在它里面调用webConfig,【在该方法内做了如下操作:1、扫描适用于web应用程序的web.xml文件,并使用规范中定义的规则合并它们。对于配置重复的全局web.xml,以最具体的级别为准。也就是说,应用程序的web.xml优先于主机级别或全局web.xml文件。2、对于web.fragment也如第一条。3、使用SPI思想扫描所有jar包下ServletContainerInitializer的实现类,存储到initializerClassMap和typeInitializerMap。4、如果web.xml配置的有jsp servlet则对其进行转换。5、使用web.xml配置StandardContext。在这里将把 环境、errorPages、filters、filtersMapping、listeners、roles、servlets(每个都用warapper封装起来)、servletMappings、sessionConfig、jsp的taglibs、welcome Files等配置到StandardContext中。到此在应用程序里配置的所有内容都被加载到StandardContext里了。继续往下把web.xml文件内容读取成字符串存储在ServletContext里。又把fragment文件中定义的jar包存储到resourceJars中,然后扫描所有jar里的包是否是用于配置应用程序静态资源的文件,如果是就把这个jar包也加载到StandardContext内。最后,获取所有ServletContainerInitializer的实现类把它们也加入到StandardContext内。】webConfig方法执行完后,又设置了一些校验和认证器。到此configureStart方法执行完了。关于configure_start剩下两个事件略过。

 for循环遍历所有的wrapper没start的都start一下。

 获取刚才放入StandardContext内的ServletContainerInitializer的实现类,并依次调用它们的onStartup方法(入参是刚才创建的ServletContext的门面类)。

 context配置web.xml里注册的listeners,并启动。

 启动StandardManager。

 启动filters

 启动后台线程用于检测session是否超时。

 最后StandardContext状态变为STARTING,再次对它的after_start事件做出回调,最后把状态变为STARTED

 stanardhost启动完成...剩下的启动不在多说了。

整个tomcat的StandardContext源码我们也分析完了,用下面一张简陋流程图概述一下ContextConfig做的事:

 

19.ContextConfig执行流程图

上图描述的是当StandardContext状态变为init后通知ContextConfig后,ContextConfig做的一系列事情,可以看出从解析所有的xml到将xml数据存储在StandardContext里,然后就开始包装Servlet了,获取xml里有关Servlet或Jsp的配置,创建Wrapper并设置各种属性包括ServletClass,如果是Jsp就会先去访问一次让其编译成Servlet然后在设置到Wrapper,在将Wrapper加入StandardContext中。然后在将xml配置里的其他信息如servletMapping也设置到Context容器里。都添加完毕了,要想使用Servlet还要去init Servlet(反射获取)【loadOnStartup>0的】,也就是去调用Servlet的init方法初始化Servlet。到此从通知ContextConfig到ContextConfig把web.xml解析创建Wrapper,使用InstanceManager反射原理获取Servlet对象,初始化Servlet并封装StandWrapper(即Wrapper门面类)都完成了。

 

 

 

Tomcat工作流程(后续)

这时候ContextConfig的任务完成,也就是到了流程图17里的12,13,14步骤已完成。然后在流程图17的16,17,18,19步骤是创建一个Connector连接器启动http服务初始化MapperListener,当socket连接上服务器后,一个Http请求过来被Connector连接到Tomcat,MapperListener被触发,它会读取这个Http请求的URL地址,这个MapperListener中含有上下文所有的信息,看下图:

20.MapperListener为什么会有上下文所有信息

  

20.MapperListener为什么会有上下文所有信息

既然MapperListener含有上下文所有的信息,自然也知道Mapper和Wrapper,自然也能知道这个URL请求的是哪个web服务的哪个Servlet。看下图21,了解请求过程。

 

 

21.请求过程图

 

看图22了解从Http请求过来到被MapperListener触发,获取URL信息找到映射的Mapping和Wrapper(Servlet)并封装成MappingData向后传递。经过引擎找到Java虚拟主机,getHost获取虚拟主机(简单理解:虚拟主机是空间 就是我们做网站时候存放网站程序的地方),getContext拿到上下文,找到对应的Wrapper。

22.映射URL到交给Servlet处理业务

 

22.映射URL到交给Servlet处理业务

 

 

 

上下文和“场景”

在此处插入一段许令波大佬一段文字和代码,一段让我深有感触的地方:

 

23.Servlet 顶层类关联图

 

23..Servlet 顶层类关联图

从上图23可以看出 Servlet 规范就是基于这几个类运转的,与 Servlet 主动关联的是三个类,分别是 ServletConfig、ServletRequest 和 ServletResponse。这三个类都是通过容器传递给 Servlet 的,其中 ServletConfig 是在 Servlet 初始化时就传给 Servlet 了,而后两个是在请求达到时调用 Servlet 时传递过来的。我们很清楚 ServletRequest 和 ServletResponse 在 Servlet 运行的意义,但是 ServletConfig 和 ServletContext 对 Servlet 有何价值?仔细查看 ServletConfig 接口中声明的方法发现,这些方法都是为了获取这个 Servlet 的一些配置属性,而这些配置属性可能在 Servlet 运行时被用到。而 ServletContext 又是干什么的呢? Servlet 的运行模式是一个典型的“握手型的交互式”运行模式。所谓“握手型的交互式”就是两个模块为了交换数据通常都会准备一个交易场景,这个场景一直跟随个这个交易过程直到这个交易完成为止。这个交易场景的初始化是根据这次交易对象指定的参数来定制的,这些指定参数通常就会是一个配置类。所以对号入座,交易场景就由 ServletContext 来描述,而定制的参数集合就由 ServletConfig 来描述。而 ServletRequest 和 ServletResponse 就是要交互的具体对象了,它们通常都是作为运输工具来传递交互结果。【自己对比J2EE API查看ServletContext】

附上一段简单代码,体验上面所说的"场景":

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ServletToJsp extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Override
    public void doGet (HttpServletRequest request,
            HttpServletResponse response) {

       try {
           // Set the attribute and Forward to hello.jsp
           request.setAttribute ("servletName", "servletToJsp");
           getServletConfig().getServletContext().getRequestDispatcher(
                   "/jsp/jsptoserv/hello.jsp").forward(request, response);
       } catch (Exception ex) {
           ex.printStackTrace ();
       }
    }
}

  

在理解“场景”的同时,更要把场景和Tomcat体系联系起来。看下图24,Servlet被包装成StandardWrapper,而StandardWrapperFacade又是StandardWrapper的门面类,这二者都继承了ServletConfig,在这个"场景"(ServletContext)中使用的不是ServletConfig而是StandardWrapperFacade门面类。在看看ServletContext和StandardContext,一个是提供一个一次交互的场景,一个是上下文环境,在上下文环境里依赖这某一次交互的"场景"。也就是说,当一次交互过来时,在上下文环境中准备一个"场景"即ServletContext,在这个上下文的"场景"中含有着StandardWrapperFacade(一些Servlet的配置和处理逻辑的Servlet),在这个"场景"里交互的对象即是ServletRequest 和 ServletResponse,它们通常都是作为运输工具来传递交互结果。

 

24.Servlet体系和Tomcat体系的系列图

 

24.Servlet体系和Tomcat体系的系列图

 

下图25描述的是request和response在不同模块中会有不同封装。我们在service方法中通常使用的是HttpServletRequest和HttpServletResponse,这二者在"场景"中是ServletRequest 和 ServletResponse,由下图25即可知道为什么service方法可以使用HttpServletRequest和HttpServletResponse。

 

25

 

25.请求响应类关系图

 

 

tomcat对静态资源是怎么访问的?

tomcat访问所有的资源,都是用Servlet来处理的。三种资源划分:静态资源(js,css,png,jpg),Servlet,JSP。

 对于静态资源交给org.apache.catalina.servlets.DefaultServlet来处理(就是全局web.xml里的servlet),在全局web.xml的default servlet上面有这么一句话:

<!-- The default servlet for all web applications, that serves static     -->
<!-- resources.  It processes all requests that are not mapped to other   -->
<!-- servlets with servlet mappings (defined either here or in your own   -->
<!-- web.xml file). -->
<!-- 所有的Web应用程序的默认servlet,用于处理静态资源。-->
<!-- 它处理所有未映射到其他带有servlet映射的servlet(在此处或在您的定义中)。-->

<servlet>
        <servlet-name>default</servlet-name>
        <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
        <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>listings</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

  

从代码标红的那句话来看,也就是说处理逻辑最后才是DefaultServlet。

 

 对于JSP,Tomcat最后会交给全局web.xml里的org.apache.jasper.servlet.JspServlet来处理。

<!-- The JSP page compiler and execution servlet, which is the mechanism  -->
  <!-- used by Tomcat to support JSP pages.  Traditionally, this servlet    -->
  <!-- is mapped to the URL pattern "*.jsp".  This servlet supports the     -->
  <!-- following initialization parameters (default values are in square    -->
  <!-- brackets): -->
  <!-- JSP页面编译器和执行servlet,这是由Tomcat用于支持JSP页面的机制。 -->
  <!-- 传统上,这个servlet映射到URL模式“*.jsp -->
  <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>

  

 对于Servlet,Tomcat最后会交给一个叫做InvokerServlet的类来处理。在tomcat7以前web.xml里有这么一段被注释的配置:

<-- The default servlet-invoking servlet for most web applications, -->
used to serve requests to servlets that have not been registered in -->
<!-- the web application deployment descriptor.-->
<!-- 为大多数Web应用程序调用servlet的默认servlet,用于向尚未在Web应用程序 -->
<!-- 部署描述符中注册的servlet提供请求。 -->
<!--
    <servlet>
        <servlet-name>invoker</servlet-name>
        <servlet-class>
          org.apache.catalina.servlets.InvokerServlet
        </servlet-class>
        <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
        </init-param>
        <load-on-startup>2</load-on-startup>
    </servlet>
-->
<!--
    <servlet-mapping>
        <servlet-name>invoker</servlet-name>
        <url-pattern>/servlet/*</url-pattern>
    </servlet-mapping>
-->

  

注释:Web Application Server提供了一种默认的访问servlet的方式,即通过http://myWebApp/mypackage.MyServlet的方式直接访问,而不需要定义<servlet>和<servlet-mapping>,这种功能称为Invoker Servlet,但是现在的App Server一般都默认禁用的这个功能。

从上面的学习中并没有出现过Invoker Servlet的影子了,对于web.xml中配置的<servlet>和<servlet-mapping>被读取封装成wrapper放在容器中,在请求时通过Mapping Data去找到某个Wrapper运行,这都是由StandardContext容器来处理的。在tomcat7及其以后的版本里上面这段代码都被移除了【在tomcat源码里已经找不到org.apache.catalina.servlets.InvokerServlet类了】。

 

26.请求流程到tomcat工作流程图

 

 

Mapper对资源调用的七大规则

打开tomcat源码org.apache.tomcat.util.http.mapper.Mapper搜索internalMapWrapper方法,即可看到在该方法内定义匹配的七大顺序:

 Rule 1 -- Exact Match 精确匹配           Servlet

 Rule 2 -- Prefix Match 前缀匹配          JSP

 Rule 3 -- Extension Match 扩展匹配

 Rule 4a -- Welcome resources processing for exact macth

 Rule 4b -- Welcome resources processing for prefix match

 Rule 4c -- Welcome resources processing for physical folder

 Rule 7 -- Default servlet                       DefaultServlet--静态资源

 

可见最后匹配的才是DefaultServlet,也就是说当一个HTTP请求过来后先去找Mapping Data中的path去匹配Servlet的url,没有才去按规则2匹配下一个,就这样一直到最后DefaultServlet,如果到最后还没匹配到怎么办?返回前台404,资源未找到。

 

 

 

 

Session和Cookie了解

理解Session和Cookie

 

流程图27.sessioncookie工作流程

Session 与 Cookie 的作用都是为了保持访问用户与后端服务器的交互状态。它们有各自的优点也有各自的缺陷。然而具有讽刺意味的是它们优点和它们的使用场景又是矛盾的,例如使用 Cookie 来传递信息时,随着 Cookie 个数的增多和访问量的增加,它占用的网络带宽也很大,试想假如 Cookie 占用 200 个字节,如果一天的 PV 有几亿的时候,它要占用多少带宽。所以大访问量的时候希望用 Session,但是 Session 的致命弱点是不容易在多台服务器之间共享,所以这也限制了 Session 的使用。

在理解web项目中的Session和cookie时,谨记以下点:

1、Session与Cookie的作用都是为了保持访问用户与后台服务器的交互状态

2、Session并不是在有客户端访问时被创建的,而是在服务器端调用了HttpServletRequest.getSession(true)时才被创建的,如果他访问的是一个Servlet而且这个Servlet返回的不是Jsp,而是Html或其他格式的页面,那么就需要request.getSession()才会生成Session,如果Servlet返回的是一个Jsp或者直接访问的就是一个Jsp,那么你要知道HttpSession是Jsp的内置对象,当这个Jsp被编译称Servlet时就已经被创建了。总结来说就是Session不是主动生成的,而是需要后端调用getSession()方法时才生成Session。

3、Session是基于Cookie工作的。

有三种方式可以让Session正常工作:

 基于 URL Path Parameter,默认就支持

 基于 Cookie,如果你没有修改 Context 容器个 cookies 标识的话,默认也是支持的

 基于 SSL,默认不支持,只有 connector.getAttribute("SSLEnabled") 为 TRUE 时才支持

 

 当浏览器不支持Cookie功能时,浏览器会将用户的SessionCookieName重写到用户请求的URL参数中,他的传递格式如/path/Servlet;name=value;name2=value2?name3=value3,其中"Servlet;"后面的K-V就,就是要传递的Path Parameters,服务器会从这个Path Parameters中拿到用户配置的SessionCookieName。关于这个SessionCookieName,如果你在web.xml中配置了session-config配置项的话,其 cookie-config 下的 name 属性就是这个 SessionCookieName 值,如果你没有配置 session-config 配置项,默认的 SessionCookieName 就是大家熟悉的“JSESSIONID”。接着 Request 根据这个 SessionCookieName 到 Parameters 拿到 Session ID 并设置到 request.setRequestedSessionId 中。

 请注意如果客户端也支持 Cookie 的话,Tomcat 仍然会解析 Cookie 中的 session id,并会覆盖 URL 中的 Session ID。

 如果是第三种情况的话将会根据 javax.servlet.request.ssl_session 属性值设置 Session ID。

有了SessionId服务器就可以创建HttpSession对象了,第一次触发是通过request.getSession()方法,如果当前的Session Id还没有对应的HttpSession对象那么就创建一个新的,并将这个对象添加到org.apache.catalina. Manager 的 sessions 容器中保存,Manager 类将管理所有 Session 的生命周期,Session 过期将被回收,服务器关闭,Session 将被序列化到磁盘等。只要这个 HttpSession 对象存在,用户就可以根据 Session ID 来获取到这个对象,也就达到了状态的保持。

 

测试Session和Cookie

 先配置一下初始环境,Tomcat下的server.xml

<?xml version='1.0' encoding='utf-8'?>
<Server port="8005" shutdown="SHUTDOWN">
  <Service name="Catalina">
    <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8"/>
    <Engine name="Catalina" defaultHost="localhost">
      <Host name="localhost">
		<Context path="" docBase="C:\Users\admin\Desktop\ServletDemo\webapp" reloadable="true"/>
      </Host>
    </Engine>
  </Service>
</Server>

  

 项目结构

-- ServletDemo

-- src

-- demo

-- HelloFilter.java

-- HelloServlet.java

-- servlet-api.jar

-- webapp

-- WEB-INF

-- classes

-- demo

-- HelloFilter.class

-- HelloServlet.calss

-- web.xml

 项目基本文件

// web.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" 
						 "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>

	<filter>
		<filter-name>helloFilter</filter-name>
		<filter-class>demo.HelloFilter</filter-class>
	</filter>

	<filter-mapping>
		<filter-name>helloFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	
	<servlet>
		<servlet-name>hello_world</servlet-name>
		<servlet-class>demo.HelloServlet</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>hello_world</servlet-name>
		<url-pattern>/hello</url-pattern>
	</servlet-mapping>

</web-app>

  

package demo;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 最简单的Servlet
 * @author Winter Lau
 */
public class HelloServlet extends HttpServlet {

	@Override
	protected void service(HttpServletRequest req, HttpServletResponse res)
			throws ServletException, IOException {
		res.getWriter().println("Hello World!");
	}

}

  

package demo;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

public class HelloFilter implements Filter {

	@Override
	public void init(FilterConfig arg0) throws ServletException {
		System.out.println("Filter 初始化");
	}

	@Override
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		System.out.println("拦截 URI=" + request.getRequestURI());
		chain.doFilter(req, res);
	}

	@Override
	public void destroy() {
		System.out.println("Filter 结束");
	}
}

  

 在上面一开始的我们是没有使用Jsp的,也没有在service()里使用request.getSession(),编译一下将生成的class文件覆盖WEB-INF下demo里的class文件,然后启动Tomcat

# 编译命令
javac -encoding utf-8 -classpath C:\Users\admin\Desktop\ServletDemo\src\demo\servlet-api.jar C:\Users\admin\Desktop\ServletDemo\src\demo\*.java

  

 

28.测试session1

 修改service()方法,在其内部添加

req.getSession();

  

重新编译,将编译后的class文件覆盖WEB-INF下的class文件,清空浏览器缓存,再次启动Tomcat。

 

29.测试session2

对比以上两张图,得出结论,JSESSIONID是服务器调用getSession()才生成的。

要想了解更多,可以看看最开始推荐的那篇关于Cookie的文章。

 

 

最后附上一张Servlet中的Listener图:

30.Servlet中的Listener

 

 

Servlet的编译命令

编译普通的java文件

只需要cd 到jdk的bin目录下,然后执行下面代码即可,它会在和Test.java的同目录下生成Test.calss文件。

javac C:\Users\ServletDemo\src\demo\Test.java

  

编译Servlet文件

编译Servlet文件,由于Servlet文件依赖servlet-api.jar包,你没有这个包,编译的时候会报错,因为它不认识Servlet.java里的HttpServletRequest等对象,所以,你要想编译的话,必须将servlet-api.jar的路径配置在CLASSPATH的最前面:

,;%TOMCAT_HOME%\lib\servlet-api.jar

  

然后才能想编译普通文件那样编译Servlet文件

 

编译过程编码问题

Linux下为UTF-8编码,javac编译gbk编码的java文件时,报错:编码UTF8的不可映射字符,解决办法:

javac -encoding gbk ServletTest.java

  

Windows下为GBK编码,javac编译utf-8编码的java文件时,报错:编码GBK的不可映射字符,解决办法:

javac -encoding utf-8 ServletTest.java

  

如果没把servlet-api.jar放在classpath里你也可以这样写:

javac -encoding utf-8 -classpath C:\Users\src\demo\servlet-api.jar C:\Users\src\demo\*.java

  

31.java源码执行

 

 

 

后期补充

有关Host中的appBase属性和Context中的docBase属性的关系

appBase表示应用程序目录,但是我们的一个大型的应用程序可能包含多个子功能模块,这些子功能模块被看作是一个个文档应用程序(提供页面服务的)。

appBase目录下的所有子目录都会被当做是一个个文档应用,我们可以使用appBase指定一个应用程序目录,例如像tomcat中conf/server.xml里的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 "%r" %s %b" />

      </Host>

  

它指定了应用程序目录为根目录下的webapps,则webapps目录下的所有子目录都会被当做文档应用程序加载,也就是说webapps目录下的doc、examples、host-manager、manager等这些文档应用程序都会被加载。

如果我们只想加载examples文档应用怎么办呢?如果我把server.xml中appBase改为"webapps/examples",启动tomcat,则examples下的所有子目录都会被当做文档应用程序,但是examples目录下的子目录并没有含有web应用描述符,所以即使加载了也访问不到任何东西。

只想加载examples文档应用我们需要使用Context元素了,并在它的docBase里配置上examples的绝对路径,或者是相对于appBase路径的相对路径,这样tomcat启动就只会加载我们单独的文档应用程序examples了。

 

 

有关StandardContext和ServletContext、ServletConfig关系

上面说了:"Context是单个ServletContext的表示形式,它通常包含一个或多个受支持的servlet的包装器。"

 有关StandardContext注释文档如下:

上下文接口【指的是Context】的标准实现。每个子容器【指的是StandardWrapper】必须是包装器实现,才能处理指向特定servlet的请求。

 

 有关上下文接口Context注释文档如下:

Context是一个容器,代表Catalina Servlet引擎中的servlet上下文,因此代表单个Web应用程序【指的是文档应用程序】,因此在Catalina的几乎所有部署中都非常有用(即使链接器连接到Web上也如此)服务器(例如Apache)使用Web服务器的功能来标识适当的Wrapper来处理此请求,它还提供了一种方便的机制来使用拦截器,该拦截器查看此特定Web应用程序处理的每个请求。

管理到上下文父容器的通常是主机(Host),但可以是其他的一些实现,也可以在不需要时将其省略。

附加到上下文的子容器通常是包装器的实现(表示单个Servlet的定义)。

 

 有关ServletContext注释文档如下:

定义servlet用于与其servlet容器进行通信的一组方法,例如,用于获取文件的MIME类型,调度请求或写入日志文件。

每个Java虚拟机的每个"Web应用程序"【这里指的是文档应用程序】都有一个上下文。(”Web应用程序“是Servlet和内容的集合,这些Servlet和内容安装在服务器URL名称空间的特定子集(例如/catalog)下,并且可能通过.war文件安装。)

如果Web应用程序的部署描述符中标记为”distributed“,则每个虚拟机都有一个上下文实例。在这种情况下,上下文不能用作共享全局信息的位置(因为该信息不是真正的全局信息)。请改为外部资源,例如数据库。

ServletContext对象包含在ServletConfig对象中,该对象由Web服务器在servlet初始化时提供servlet。

 

 有关ServletConfig注释文档如下:

ServletConfig是servlet容器用来配置Servlet的配置对象,它用于在初始化Servlet期间(即调用Servlet的init方法时)将配置信息传递给servlet。

 

总结:

首先StandardContext代表着单个文档应用程序,它是Servlet的上下文。而ServletContext同样也是Servlet的上下文,ServletContext提供了用于与其Servlet容器进行通信的一组方法。StandardContex中依赖了ServletContext【其实现类ApplicationContext】,所以StandardContext不仅能完成ServletContext所做的事情,它还为了防止tomcat过于依赖Servlet而把Servlet包装成一个个StandardWrapper。

说白了ServletContext是约定,而StandardContext是tomcat自己安装约定自己又做的一层包装。

画个图理解如下:

 

后期附加图2StandardContextServletContext

 

最后用debug验证:

验证方式:不再是使用在测试代码中测试了,因为测试代码中只能测试单个文档应用程序(即testSingleWebapp里测试examples),这次我们要测试tomcat启动时加载多个文档应用程序(即加载webapps下的所有应用程序。),下载tomcat.bin,在启动命令行参数中加入:

-Dcatalina.home="D:\workspace\java\tomcat\apache-tomcat-7.0.99"

  

我们这次使用tomcat启动程序org.apache.catalina.startup.Bootstrap启动的,搜索StandardService类,在类中的startInternal方法的synchronized(executors)处打上断点,查看this.container.children可以看到只有一个元素即StandardHost:

 

继续查看这个StandardHost里的内容,value.children,可以看到里面有5个StandardContext,每个StandardContext代表一个文档应用程序。

 

 

  

我们随便点开一个StandardContext(例如examples文档应用程序的),继续查看,便能找到它里面的context(即ServletContext的子类ApplicationContext)、servletMappings、welcomeFiles。

 

 

 

继续查看StandardContext的children,可以看到有20个元素:

 

继续随便点开一个StandardWrapper,你能看到它封装的是哪个servlet,映射的mappings是什么

最后列出关于debug验证总结:

一个StandardContxt对应一个文档应用程序。

一个StandardContext对应一个ServletContext,用于提供一个"Servlet和Servlet容器交互"的 环境。

一个StandardWrappper对应一个Servlet,用于包装该Servlet。

一个Servlet有一个ServletConfig,用于配置该Servlet。

 

 

该如何调试Tomcat处理http请求

在上面的讲解中,我一直对一件事有点不确定,那就是tomcat启动后,我的页面向后台发送请求时,虽然知道在AprEndpoint里内部类Acceptor的run方法内,不断地循环的获取socket连接,然后从线程池中创建一个子线程执行请求,子线程没法在开发工具中进行调试了。虽然上面也简短的描述了怎么执行,但是总感觉自己在学的过程中错过了什么。于时从网上搜索到一本书《深入刨析Tomcat》在第11章StandardWrapper里找到了一点答案。

 

方法调用序列

对于每个引入的HTTP请求,连接器都会调用其关联的servlet容器的invoke()方法。然后,servlet容器会调用其所有子容器的invoke()方法。例如,若连接器直接与一个StandardContext实例相关联(没有配置Engine、Host),则连接器会调用StandardContext实例的invoke()方法,而StandardContext实例会调用其所有子容器的invoke()方法(在测试例子中是StandardWrapper的invoke()方法)。下图展示了连接器收到HTTP请求后的方法调用的协作图。(回忆一下第5章的内容,servlet容器包括一条管道和一个或多个阀(有关管道和阀我单独在另一篇笔记中研究))。

 

具体过程如下:

 连接器创建request和response对象;

 连接器调用StandardContext实例的invoke()方法;

 接着,StandardContext实例的invoke()方法调用其管道对象的invoke()方法。StandardContext中管道对象的基础阀是StandardContextValve类的实例,因此,StandardContext的管道对象会调用StandardContextValve实例的invoke()方法;

 StandardContextValve实例的invoke()方法获取相应的Wrapper实例处理Http请求,调用Wrapper实例的invoke()方法;

 StandardWrapper类是Wrapper接口的标准实现,StandardWrapper实例的invoke方法会调用其管道对象的invoke()方法;

 StandardWrapper的管道对象中的基础阀是StandardWrapperValve类的实例,因此,会调用StandardWrapperValve的invoke()方法,StandardWrapperValve的invoke()方法调用Wrapper实例的allocate()方法获取servlet实例;

 allocate()方法调用load()方法载入相应的servlet类,若已经载入,则无需重复载入;

 load()方法调用servlet实例的init()方法;

 StandardWrapperValve调用servlet实例的service方法。

注意:

StandardContext类的构造函数会设置StandardContextValve类的一个实例作为其基础阀:

public StandardContext() {

    super();
    pipeline.setBasic(new StandardContextValve());
    broadcaster = new NotificationBroadcasterSupport();
    // Set defaults
    if (!Globals.STRICT_SERVLET_COMPLIANCE) {
        // Strict servlet compliance requires all extension mapped servlets
        // to be checked against welcome files
        resourceOnlyServlets.add("jsp");
    }
}

 

读懂了上面这些,我们现在来用debug验证Tomcat接收并处理请求的行为,我们依旧使用tomcat的Bootstarp作为启动类,这次我们不在打任何关系关于tomcat启动时的断点,而是打它处理请求的断点,在做这个之前,我们先看看StandardContextValve这个类的父类ValveBase,看看在Tomcat中到底封装了哪些标准的Valve:

 

搜索ValveBase实现类中Standard开头的,只有上图中的那四个,我们知道它们的优先级是顺序是:Engine、Host、Context、Wrapper。那处理请求肯定也是StandardEngineValve先处理,那我们就先把断点打在StandardEngineValve中的invoke()里。重新启动tomcat,开启浏览器标签页,输入http://localhost:8080/,此刻会进入断点内,这次我们可以成功测试到Tomcat处理请求的流程了。

  

 1 public final void invoke(Request request, Response response)
 2     throws IOException, ServletException {
 3 
 4     // Select the Host to be used for this Request
 5     Host host = request.getHost(); // 断点位置
 6     if (host == null) {
 7         response.sendError
 8             (HttpServletResponse.SC_BAD_REQUEST,
 9              sm.getString("standardEngine.noHost",
10                           request.getServerName()));
11         return;
12     }
13     if (request.isAsyncSupported()) {
14         request.setAsyncSupported(host.getPipeline().isAsyncSupported());
15     }
16 
17     // Ask this Host to process this request
18     // host.getPipeline().getFirst()获取的是AccessLogValve
19     host.getPipeline().getFirst().invoke(request, response);
20 
21 }

 

 

跟踪断点,走过了AccessLogValve->ErrorReportValve->StandardHostValve->AuthentictorBase->StandardContextValve->StandardWrapperValve。具体流程,在此就先不跟了,我先恶补一下自己的管道和阀的概念吧!

 

 

 

接下来要读的是

Tomcat管道与阀、Servlet深入、Session、cookie

 

 

posted @ 2020-12-02 22:13  刘呆哗  阅读(431)  评论(0编辑  收藏  举报