为了能够深入理解tomcat,所以我找了一本叫《深入剖析Tomcat》的书籍,这篇相当于类似读书笔记。
一些说明:
- 该书使用的tomcat版本为tomcat4和tomcat5,而我在读这本书的时候自己使用的tomcat版本为9.0,所以会有一些出入,且文中说的“最新版本”其实指的就是tomcat 9.0。
- tomcat也是面向接口编程的,基本就是一个接口X,然后有一个XBase的抽象类来实现这个接口,接下来就是一个StandardX来继承这个抽象类。
全书脉络
- 第一章介绍了一下最简单的web知识,并且手动实现了一个web server。
- 第二章展示了什么是servlet容器
- 第三章实现了最简单的连接器
- 第四章详细讲解了连接器
- 第五章简单介绍了容器
- 第六章介绍了Lifecycle,几乎所有的类都实现了这个接口
- 第七章介绍了日志记录器,非常非常过时
- 第八章对载入器进行了介绍,比较重要的一个模块
- 第九章介绍了session管理器,也比较重要
- 第十章介绍了安全机制,不过似乎我没见到过实际中存在这类机制
- 第十一章详细介绍了Wrapper
- 第十二章详细介绍了Context
- 第十三章介绍了Host和Engine
- 第十四章介绍了服务器组件和服务组件
- 第十五章介绍了如何使用Digester来从配置文件中生成所需要的对象而不是硬编码
- 第十六章介绍了关闭钩子
- 第十七章介绍如何启动和关闭tomcat,还额外介绍了一下bash和bat的部分知识
- 第十八章介绍了部署器
- 第十九章介绍了如何使用一个特殊的servlet来对tomcat进行处理
- 第二十章介绍了JMX来对tomcat进行管理
第一章
开篇需要理解一下http协议中最简单的包的结构,即http请求和响应的结构。
然后是手动实现一个http server,能够实现最最简单的功能。除了server类之外,我们还需要抽象两个概念,一个是request,另外一个是response,用来作为接受请求和对请求做出相应。
默认情况下,服务器只需要一启动就会永远监听8080端口,只有当满足特定的条件(这个之后再讨论)才会退出。然后底层是通过服务器socket的accept方法返回一个和客户端进行通信的socket,并且通过基础的io流进行通信,有了这些,就可以得到下面的代码了。
1 | public class SimpleHttpServer { |
接下来我们先去抽象出我们需要的request和response,方便我们之后使用。
我们都知道http请求头部形如这样:GET /index.html HTTP/1.1
,而在第一章,我们也仅仅对这个感兴趣,只要获取了第一行,就可以知道这个请求需要的文件地址了,就可以去对应的地方找到这个文件并读取了。
1 | 4j |
其实上面的代码就一个目的:获取uri的值。其余都是附加的。
接下来是response的代码,response原书里面是没有HTTP/1.1 200 OK
这段的,实测不加这段的chrome是无法解析的,所以我就加上了。
1 | 4j |
也是很简单的代码,只需要获取了request想要的资源的信息,就可以通过output发回去了。
第二章
Servlet
在这一部分,将添加servlet的部分。servlet本身有一个java的接口,所以我们只需要实现它们就可以了。
这个接口本身有五个方法,我们基本上只需要实现service方法即可。
Servlet容器
然后是站在servlet容器(也就是Tomcat的角度)来思考,我们需要为servlet做些什么?
- 创建该servlet,并且调用它的init方法。
- 每次收到请求,都需要创建出对应的request和response实例,这部分之后会独立出去
- 然后调用servlet的service方法,并将创建出来的request和response实例传过去。
- 最后调用destory方法,并且释放内存。
接下来分别对各个部分加以改造,使其支持上面说的功能。
HttpServer
server需要根据获取得到的包的第一行,来判断究竟应该调用谁来完成任务。这里先规定,假设uri里面以/servlet/
作为开头的就调用servlet,否则就和之前一样认为是静态资源。
Request
之前说了servlet的service()方法中就会有request和response对象传入,所以我们需要在之前的request类的基础上,让它继承自javax.servlet.ServletRequest
。当然这些方法我们目前并不需要实现,留空即可。
Response
和request同理,新增一个接口的实现,但是需要注意这里需要多实现一个方法(主要是为了能够让servlet调用这个方法来进行输出,便于显示结果):
1 | private PrintWriter writer; |
ServletProcesser
现在唯一缺的,就是我们如何对servlet进行处理了。思路也是非常简单,我们仅仅需要知道servlet被放在服务器的哪个位置,然后使用类加载器去加载它,并且将其强转为servlet,并调用它的service方法即可。
1 | 4j |
这里遇到和书里不一样的地方导致我类加载不了,因为这里我的类是直接在当前路径下,然后需要类的全限定类名。
还有一个问题就是,我这里server发送response似乎是需要断开之后(或者超时之后)才会将响应送达到浏览器,这里的解决方法(可能不一定完美)就是当发送结束之后,立刻断开input和output。
第三章
这一章主要是写连接器。连接器的主要功能是,为每个收到的HTTP请求创建request对象和response对象,并交给容器。从这里开始,我们的tomcat正式分成了两个部分——连接器和servlet容器。
HttpRequest
这个类就是用来读取请求,所以可以大致归为这五个部分:
- 读取套接字的输入流
- 解析请求行
- 解析请求头
- 解析cookie(cookie也可以算在请求头里)
- 获取参数
下面分部分来实现。
读取套接字
书中使用SocketInputStream,其实用原生的就行。
解析请求行
这里指的注意的是,如果禁用了cookie,那么jsessionid也可以通过请求参数传递过来,这里可以获取到。
解析请求头
请求头可以抽象一个HttpHeader来代替它。本质上就是通过一个hashmap来对其进行操作。
解析cookie
cookie本身就是请求头的一项,只不过由于它比较重要,所以这里可以抽象一个Cookie类来对其进行操作。
获取参数
如果是get请求,那么只需在请求行里获取即可。如果是POST请求,则需要去请求体里面看看。
HttpResponse
和之前的一样,判断一下是静态的资源还是servlet即可。
第四章
这一章将详细讲述连接器的作用。tomcat的连接器作为一个独立的模块,所以实际中我们可以进行更换。连接器必须要满足以下几个要求:
- 实现了org.apache.catalina.Connector接口
- 能够创建实现了org.apache.catalina.Request接口的request对象
- 能够创建实现了org.apache.catalina.Response接口的response对象
官方实现的连接器进行了一些优化,比如使用对象池技术来避免频繁创建对象的性能损耗,使用字符数组来代替字符串提高效率。
HTTP 1.1新特性
- 持久连接。除了我们请求的资源,服务器发回的响应体里面往往还会有一些图片啦、js文件等,需要我们再去找服务器索要,如果使用传统的每次都建立一个http连接,那么开销过于巨大。所以有了这个持久连接的特性,只需要在请求头部加上
connection:keep-alive
即可。 - 块编码。有的时候不知道响应块的大小,就需要用到这种方法来分块传送响应内容。
- 状态码100.当客户端需要发送一个超大的请求的时候,由于不确定服务器是否会接受,所以可以发送一个请求头
Expect:100-continue
,服务器如果愿意接收,则可以发送HTTP/1.1 100 Continue
给浏览器。
Connector接口
该接口位于org.apache.catalina.Connector
,里面最重要的方法是(我在最新的版本找过了,已经没有下面这些方法了):
- setContainer() 将连接器与某个servlet容器关联。
- getContainer() 获取当前连接器相关联的某个servlet容器。
- createRequest() 和 createResponse() 见名知意。
HttpConnector类
该类实现了Connector接口,并且实现了Lifecycle接口,使得可以调用一些方法来完成生命周期的管理,它的主要功能是:
- 创建服务器套接字
- 维护一个HttpProcessor的实例池,这样就可以同时处理多个连接了。所以其实是把socket传给HttpProcessor,让其进行加工。
HttpProcessor
之前实现的HttpConnector,运行在自己的线程里,而且它必须等待当前的HTTP请求返回,才可以处理下一个HTTP请求,这样效率显然非常的低下。
所以,HttpProcessor本身实现了Runnable接口,这样,每次当Connector给Processor派送任务的时候,任务就可以在Processor新的线程里面被执行了,而这些线程就叫“处理器线程”。这么说起来可能有点拗口,其实就是一个多线程的服务器……
下面是连接器线程的assign()方法的伪代码:
1 | public void assign(){ |
下面是Processor的await()方法的伪代码:
1 | public void await(){ |
具体流程是这样的:当Connector刚刚启动的时候,available这个值是false,这里就意味着,Processor里面的await方法必须等待。而连接器一旦有了新的socket,就会调用assign方法,此时该方法并不会进入到while循环内,而是会得到这个socket,并且将available置为true,同时调用notifyAll()来唤醒Processor线程,这样Processor就可以得到这个socket,并且
第五章
tomcat中,一共有4种servlet容器,分别是Engine、Host、Context和Wrapper,它们按照范围从大到小排列的。
Container接口
其下分别有四个接口,Engine、Host、Context和Wrapper,而这些接口可以由大到小去包含,比如一个Context可以包含0个或者多个Wrapper。所有的servlet容器必须实现这个接口。
管道任务
类似过滤器一样(注意!只是机制类似,它和过滤器完完全全不是一个东西),每当一个request需要传递过去的时候,需要经过一根管道,而管道里面则有一个一个的阀(valve),每一个阀代表了一个任务,最后一个阀是基础阀。
当连接器调用了容器的invoke方法之后,容器会接着调用管道的invoke方法。也就是说现在执行的大任落到了管道身上,管道通过其内部类——ValveContext的invokeNext方法,来一次调用每一个阀,并且将自身传递给下一个阀,让下一个阀继续调用。
Wrapper
作为最低级的容器,wrapper的功能就是调用servlet的init()、service()、destory()方法。
应用程序实现
已知Wrapper接口继承了Container接口,这里创建一个SimpleWrapper类来实现Wrapper接口,并且在这个SimpleWrapper类同时也实现了Pipeline,用来实现上面说的管道任务。
由于是servlet容器,所以必然需要一个类加载器来加载servlet。而且上面也说了Wrapper是最低级的servlet容器,所以它还有一个成员变量parent(当然可以是Null),然后在获取类加载的时候,先判断自己有没有,然后再判断自己的parent有没有。
接下来可以通过实现Valve接口来自己定义一个又一个的阀。
最后可以通过简单的包装使用,来实现一个只有一个servlet的服务器(最简单的tomcat):
1 | HttpConnector httpConnector = new HttpConnector(); |
Context
上面已经学会了仅有一个servlet的wrapper是如何工作的,但是实际中我们几乎不可能仅仅使用一个servlet来完成工作的,大部分都需要多个servlet,所以实际中最常见的是context。
而之前也提到过,context可以包含多个wrapper,所以我们要做的就是组合这些wrapper,然后抽象出一个新的组件——Mapper,针对获得的不同的request,来匹配应该用哪个servlet——路径匹配就这么诞生了。
第六章
Lifecycle
1 | public interface Lifecycle { |
生命周期除了上面这个,还有实现事件的LifecycleEvent类、相关的监听器接口和LifecycleSupport类。
只要我们的组件实现了这个接口,就可以有良好的生命周期管理。
第七章
日志器,没啥好说的。而且相关代码超级老旧……这里建议直接跳过。
第八章
这里开始介绍载入器,它就是一个类加载器,但是之前实现的类加载器,可以加载任意的类,显然安全性不高,这里就自己动手写一个类加载器,原因如下:
- 为了在载入类中指定某些规则。
- 为了缓存已经载入的类。
- 为了实现类的预载入。
Loader
tomcat有一个Loader接口来规定,有一个WebappLoader的默认实现类。下面是核心代码:
1 | this.classLoader = this.createClassLoader(); |
从上面的代码中可以发现,第一行在创建类加载器,该方法本质上是通过loaderClass
这个String类型的成员变量来控制的。当然并不是随意指定一个String就可以用来当做类加载器的,最后还需要经过检查(强制类型转换)的。
第二行是在指定这个“类加载器”能够去哪里获取到这些类。
之后就是设置类路径和设置访问权限了。
第九章
这一章介绍Session管理器,Session管理器需要且必须要和一个Context容器进行关联,它负责创建、更新和销毁session。
Session对象
在servlet中,Session对象通过javax.servlet.HttpSession接口来表示,而在catalina中则是位于org.apache.catalina.session包下面,StandardSession类,则是既实现了servlet中的session接口,又实现了catalina包下的session接口。
SessionManager
所有的session都是通过SessionManager来进行管理的,比如session的过期、session的存储和加载等。
对于每一个context,都会有一个session manager,而对于request,也可以通过getContext的方法来获取这个context,相当于request就可以直接获取到session manager,自然而然也可以获取到session。
饶了这么一大个弯就是说,request -> context -> session manager -> session,然后其实request本身就贴心的封装好了,只需要request.getSession即可。
分布式
这里不得不提一句,tomcat中是有一个叫DistributedManager类来进行多台tomcat服务器之间session管理的,但是每一台服务器都必须存储所有的session,庞大臃肿速度慢,现在没人用这个,还不如忘记tomcat支持分布式的session存储。
第十章
有不少内容是需要用户登录了之后才能查看的,在Tomcat中,是通过一个叫“验证器”的阀来支持的。但是说实话,用到现在都没看到过这种登录方式的….
领域
领域对象是用来对用户进行身份认证的,通常一个领域对象和一个context相关联,而一个context只能有一个领域对象。在tomcat中,领域对象是一个Realm接口的实例,其下有很多实现类,默认是MemoryRealm类。
GenericPrincipal类
该类用来保存用户的账号和密码。
接下来的略,真的完全没用到过……
第十一章
从这里开始我们详细深入了解一下StandardWrapper。
调用链
对于每一个http请求,连接器都会调用相关联的servlet容器的invoke()方法,然后servlet容器则会调用子容器的invoke()方法,接下来就是对应的管道和管道里面的阀调用。详细的过程如下:
- 连接器会接受用户发来的请求,并创建request和response
- 连接器调用了StandardContext的invoke()方法,其实是去调用父类ContainerBase的invoke()方法,但是这里认为是StandardContext的invoke()方法也没问题。
- servlet容器会调用相关管道对象的invoke()方法,管道中必然存在一个基础阀,所以基础阀的invoke()方法会被调用
- 基础阀的invoke方法会调用对应的Wrapper实例的invoke()方法
- 同样的Wrapper也有管道,也有基础阀。基础阀里面会有一个allocate()能够获取到servlet实例。
- allocate()能够调用load()方法来载入对应的servlet类,如果存在则不需要重复载入。
- load()方法会调用servlet的init()方法
- 最后就是servlet的service()方法被调用
SingleThreadModel
servlet可以实现该接口已经被弃用。javax.servlet.SingleThreadModel
接口,该接口从名字中也可以看出就是为了保证servlet一次只处理一个请求。可以保证绝对不会有两个线程同时执行该类的service()方法。
StandardWrapper
从上面调用链可知,StandardWrapper的主要任务就是载入对应的servlet类,进行实例化。注意它并不能调用servlet的service()方法,该方法是由对应的阀来完成的。实例化之后,StandardWrapperValve的对象就能够通过allocate()来获取servlet实例,并调用service()方法。
如果类没有实现上面的SingleThreadModel接口,则StandardWrapper就只会载入一次,之后对于它的任何请求都返回同一个实例(单例模式),线程安全由程序员来进行负责。而如果实现了接口,那么就需要来位于一个实例池。
下面是allocate()的逻辑伪代码:
1 | if(!singleThreadModel){ |
接下来就是通过loadServlet()方法来载入servlet并执行init()方法了,主要逻辑就是先判断下当前的类名,判断下当前是不是JSP页面的servlet类。然后就是获取载入器,并且获得类加载器,有了类加载器就可以载入类并对该类进行实例化操作了。有了实例之后,就可以调用它的init()方法了。
StandardWrapperValve
这个阀主要完成两个任务:执行与servlet关联的全部过滤器(看好了过滤器是在这里才开始的)和调用servlet的service()方法。为了实现这两个方法,会执行以下步骤:
- 获取servlet实例(就是上一步中通过allocate()方法得到的)
- 调用私有方法createFilterChain()来创建过滤器链
- 调用过滤器链的doFilter()方法,其中包含了调用servlet的service()方法(最后一个被调用)
- 释放过滤器链
- 调用Wrapper的deallocate()方法
- 如果该servlet不会再被用到,则调用unload()方法
为了能够了解过滤器链,首先需要介绍一下过滤器定义类——FilterDef类,我在这里稍微抽象了一下它有哪些属性:
1 | private String description; // 过滤器的描述信息 |
有了这个类之后,可以通过ApplicationFilterConfig类来构建一个过滤器。
第十二章
这里开始详细聊一聊StandardContext类。
构造函数里面最重要的一点:this.pipeline.setBasic(new StandardContextValve());
为当前的管道设置一个基本阀。
StandardContext类有两个对象属性,分别是available(目前最新版是在state这个枚举类里面,不过原理是一模一样的,姑且认为有这么一个成员变量吧)和configured(最新版还在)。其中的available指的是当前的context是否可用,而configured则指的是当前的context是否配置完成。
context的启动流程如下所示:
- 触发lifecycle的BEFORE_START事件。
- 将available设置成false
- 将configured设置成false
- 配置相关资源
- 设置loader
- 设置session manager
- 初始化字符集映射器
- 启动与context相关联的组件
- 启动子容器(Wrapper)
- 启动管道对象
- 启动session manager
- 触发START事件。在这里会执行一些配置操作,成功则会将configured置为true
- 如果configured为true,则说明上一步成功,那么调用postWelcomePages()方法,载入Wrapper,并将available设置成true;如果configured为false,那么调用stop()方法结束。
- 触发AFTER_START事件
StandardContextMapper
我们都知道context作为一个容器,必然是实现了Container接口的,而其实实际上是去继承一个抽象类ContainerBase,在这个ContainerBase里面有一个addDefaultMapper的方法(目前已经没得了),通过一个String类型的全限定类名来构造一个mapper并将其加入到当前的类中,而这个String,默认就是”org.apache.catalina.core.StandardContextMapper”(目前也已经消失了)。
这个mapper最重要的功能,就是通过传入一个request来判断应该使用哪一个container来处理它,这个功能就是map()方法。更具体的说,当StandardContextValve需要对每一个HTTP请求作出判断的时候,它会使用容器(map方法实际定义在ContainerBase中)的map()方法来找到需要的container。当然有相应的规则来匹配找到对应的wrapper,而这些规则其实就是我们自己之前定义好并且传递给容器的。
目前这个mapper已经灰飞烟灭了,实际功能被转移到了StandardContextValve中,当调用它的invoke()方法的时候,里面有一句:Wrapper wrapper = request.getWrapper();
,即只需要request的信息就可以得到wrapper了。
backgroundProcess()
从名字就知道这个方法是新开一个线程并且在后台运行的,用来做一些诸如周期性检查的事务等,比如session管理器需要检查session是否过期等,而如果为了每个后台都需要开启一个线程,那么会比较浪费,所以在tomcat 5就把它们聚合在了一起,而这一方法被定义在了Container接口中,并且真正的实现在ContainerBase中。
然后在ContainerBase中还有一个内部类是ContainerBackgroundProcessor,该类实现了Runnable接口,并且在内部的run方法中,它会周期调用processChildren()方法,该方法调用自己和子容器的backgroundProcess()方法。
重载功能
这里的重载说的是当某些文件被重新编译的时候,应用程序可以及时感知到并且更新应用。这个功能当然应该交给Loader来实现了。
第十三章
之前提到了容器一共有四类,分别是Engine、Host、Context和Wrapper,之前已经详细了解了Context和wrapper,这里开始聊聊Host和Engine。
StandardHost
该类作为Host接口的标准实现,和之前的那两个标准容器一样,该类的构造函数也会添加一个默认的基础阀;与它们不同的是,当调用start()方法的时候,会额外引入两个阀,分别是ErrorReportValve(新版本里还在)和ErrorDispatcherValve(新版本里面已经没得了),至于invoke()方法,则是和之前的流程一样。
其它的流程与context几乎一模一样,这里就不多做介绍了。
Host是用来干嘛的
从上面的说明我们可以知道,host和context几乎是一模一样的东西,我们为什么还需要搞出host这个东西呢。之前说到context的时候提到,一个context是需要ContextConfig的对象来进行设置,而这个ContextConfig对象需要知道web.xml文件的位置并且读取它里面的内容。也就是如果需要使用ContextConfig对象来设置context的话,外部必须要有一个Host容器,除非你自己实现了ContextConfig。(书这里我想是不是翻译的问题啊,这理由很牵强,我个人的理解就是需要有一个抽象的概念来代表一台服务器)
Engine
Engine是一个顶层的接口,如果希望tomcat能够支持多个虚拟机的话,就需要使用engine容器。engine容器可以设置一个默认host容器或者是一个默认的context容器。engine容器作为顶层容器,并不能给它在设置父容器了,与此同时,engine容器的子容器如果存在,则必须是Host容器。
第十四章
服务器组件Server
Server接口,表示catalina的整个servlet引擎,当启动服务器组件的时候,它会启动所有的组件,并且开始等待关闭命令;如果想要关闭系统,那么就可以向指定的端口发送一条命令,这样服务器组件就会关闭所有的组件,它的存在为我们提供了一种优雅的方式来启动和关闭tomcat
而服务器组件使用了服务组件(Service)来包含其他的组件。一个服务器组件可以有0个或者多个服务组件。标准实现类是StandardServer类,该类有四个生命周期,分别是initialize(),start(),stop()和await()方法。
initialize()方法新版本更名为initInternal()
,最主要的实现就是对于每一个服务组件,都调用它们的init()方法。
而start()方法则更名为startInternal(),用来调用所有的service的start方法。同理stop就不赘述了。
await方法没有改名,而且它的代码我们非常熟悉,就是之前的连接器的功能,创建一个serversocket,并且监听对应的端口(该端口默认情况下是正常关闭端口加一个偏移,正常关闭端口是8005,偏移是0,所以最后默认监听的关闭服务器端口就是8005),如果收到对应的关闭命令,就关闭。
服务组件Service
一个服务组件可以有一个servlet容器和多个连接器,可以自由的把连接器放进去,然后连接器就会自动和这个servlet容器发生关系了。由于有多个连接器,这也就意味着可以同时处理http请求和https请求。同样它也有对应的生命周期方法,包括initialize(),start(),stop()。
initialize方法就是执行所有连接器的initialize方法;start方法需要启动连接器和servlet容器,注意要先启动servlet然后启动连接器;stop方法则会停止所有的连接器和servlet容器,先关闭连接器,再关闭servlet容器。
第十五章
可以发现之前的代码中,我们都是通过硬编码来完成这些组件之间的耦合关系的,这样子不够优雅,实际中我们是通过配置文件来对Tomcat进行配置的,而Tomcat能够实现这一功能的秘诀就在于,它使用了Digester这个开源库来完成将xml对象转换成java对象这一艰巨的任务的。
这个第三方库最重要的就是Digester类,对于XML文档中的每一个元素,它都会检查它是否要做预先定义好的事件,然后在遇到对应的XML元素的时候,就执行相对应的动作。
稍微介绍一下,根元素的模式就是它自己的名字,然后根下面的一级元素的模式是根元素+"/"+子元素
。然后是规则,规则就是找到了对应的元素之后应该进行的动作,规则的接口是Rule,而存储这些规则和模式的,则是一个叫Rules的东西。另外,Rule会有begin和end方法,就是当匹配到的时候触发begin方法,而结束的时候触发end方法。
当然已经有不少现成的规则供我们直接拿来使用。下面简单介绍几种:
- 当遇到特定的模式的时候,需要创建对象,则调用addObjectCreate()方法。比如你想当遇到
<employee>
这个标签的时候(这里假设模式和标签名相同),那么就可以用digester.addObjectCreate("employee",cn.clp.Employee.class);
,创建的这个对象会被压入到一个内部的栈中。 - 当创建为类之后,我们一般还会在xml中标注好对应的属性值,可以通过addSetProperties来自动读取xml的值并且为创建好的对象进行属性赋值。类的属性名字和xml中的属性名字必须要能对的上。
- 我们还可以通过规则自动调用一个类的方法,通过addCallMethod可以调用内部栈最顶端对象的某个方法。
- 当然我们创建的对象之间也可能会有关系,所以可以用addSetNext()方法来调用相关的方法并且将对象传入。
当调用上面的这些方法的时候,都会间接地调用Digester类的addRule()方法,该方法会把Rule和对应的模式加入到Rules集合中。
ContextConfig
StandardContext实例必须要有一个监听器,这个监听器会配置好StandardContext实例,并将configured置为true。之前的ContextConfig仅仅是简简单单的将configured置为true,好让程序可以继续跑下去,但是实际上它应该要去读取web.xml文件(在CATALINE_HOME的conf目录下面),并且利用上面说过的digester将对应的xml元素变成java对象。
web.xml大概长这样:
1 | <servlet> |
可以看到有了这个可以轻松构造出一个默认的servlet,并且将下面的两个参数传递给它。这样就可以创建出对应的StandardWrapper类。所以,在启动类中,我们需要创建好这个Config类,然后将它作为监听器添加到Context中。这样只要StandContext触发了什么事件,就可以通过相应的方法来进行处理了。
第十六章
有的时候,或者说大多数时候我们其实不是按照正常流程那样,给指定端口发送关闭命令来关闭tomcat的(其实我甚至都不知道关闭端口是啥…),而是直接简单的退出。而这个时候就需要通过关闭钩子来生效。
在java中,当用户调用System.exit()方法或者最后一个非守护进程的线程退出时,应用程序正常退出,此时按照tomcat的逻辑肯定已经都释放资源、关闭一切了;也可能是通过用户强制中断java虚拟机(最常见的就是ctrl+C)来退出。但是就算你是用关闭jvm的方式来进行关闭,也要经历两个过程:
- 虚拟机启动所有已经注册的关闭钩子,所有的关闭钩子都会执行。
- 虚拟机根据情况调用所有没有被调用过的终结器(finalizer)。
而我们这里显然把关注点放到了第一阶段,通过创建关闭钩子来确保安全的关闭。方法很简单,关闭钩子本身也是一个线程,所以只需要继承一下Thread,重写run方法,并且通过Runtime.getRuntime().addShutdownHook(你的钩子);
即可。
tomcat中的钩子
emmm 就是一个叫CatalinaShutdownHook
的内部类,然后它会确保Catalina.this.stop();
这句话被执行,然后就没了….
第十七章
这一章主要是为了讲述启动Tomcat的。Tomcat有启动的时候会用到两个类,分别是Bootstrap类和Catalina类。Catalina类是用来启动和关闭server对象,并且解析web.xml文件的。而Bootstrap则是为了创建Catalina的,所以其实两者可以合二为一,但是为了更好的扩展性和更多的运行方式,还是分开了。而且Tomcat还提供了脚本,我们仅仅需要运行这些脚本就可以执行了。
catalina本身持有一个Digester对象,能够方便对web.xml进行解析,同时有一个Server对象。
接下来开始流程分析。
在用户传入参数的时候,它会通过protected boolean arguments(String args[])
来判断是否能够处理用户的命令,如果通过则返回true。
然后是通过创建一个Digester对象来解析server.xml文件,解析完成之后,server变量就能够引用一个Server对象,并且调用server的initialize和start方法,接下来就让server进入await方法,等着shutdown命令的到来。
Bootstrap
这个类里面有main方法,所以它才是真正的入口,脚本其实就是调用了它的main方法了实现的。
这个类本身有三个类加载器,分别是:
1 | ClassLoader commonLoader = null; // 对应common/classes目录下,common/endorsed目录下,common/lib目录下 |
这里指的注意的是,与context相关联的类载入器的父类都是sharedLoader,但是这些类载入器并不能访问catalina的内部类以及CLASSPATH环境变量中的类(为了安全考虑)。而在最新版的代码中是这个样子的:
1 | try { |
可以看到和上面的描述是一致的。搞定了这三个类加载器之后,就会通过catalinaLoader加载一个新的catalina,并且将这个新的catalina的父类加载器置成sharedLoader,然后执行这个对象的process()方法。
接下来介绍了Windows平台下和linux平台下启动脚本,这里我跳过了。
第十八章
实际部署的时候,我们通常会将我们的web项目打包成war文件,放到Webapp目录下面进行部署。
而在tomcat中,则是使用一个部署器的玩意儿来进行部署。部署器的接口是Deployer,且部署器与Host实例相关联,部署器会启动一个StandardContext实例,并将该实例放入到Host中。
在实际中,会有一个HostConfig类型的生命周期监听器,当我们调用StandardHost的start()方法的时候,HostConfig会进行响应,并且它自身有一个start()方法,该方法就可以逐个部署并且安装所有Web程序。
在Host实例发出START事件之后,HostConfig首先会进行下面的操作:
1 | setCopyXML(((StandardHost) host).isCopyXML()); |
经过了上面的检查之后,就会执行该类的start()方法,抽象一下代码可以得出其实就是一句函数:deployApps();
,也就是最关键的部署其实奥秘就在这个函数里,下面来看看这个函数长什么样子:
1 | protected void deployApps() { |
可以看到这个方法又调用了其他三个方法,只要了解这三个方法,我们就可以了解tomcat的部署过程了。
deployDescriptors:一个Context可以使用一个xml文件来进行描述。该方法用来部署好Context所对应的xml文件。
deployWARs:部署war文件。
deployDirectories:部署目录,就是把整个目录复制到webapps目录下面。
Deployer接口
很遗憾,在tomcat9.0版本中已经不存在了,所以下面的只能是全靠书中怎么讲了。
StandardHost类本身就实现了Deployer接口,所以它本身也是一个部署器。然后它将接受到的任务委托给StandardHostDeployer类来进行处理。
安装一个描述符
这个本身是非常简单的,因为只需要通过文件的输入输出流读取到对应的文件,并且使用Digester对象来解析这个文件即可。
安装一个war
根据war的不同,可以得到它的url,然后就可以对其进行载入操作,利用Class.forName()等操作。
第十九章
1 | <servlet> |
这就是本章索要描述的对象,Manager对象,上面的代码应该很熟悉,就是当你访问http://localhost:8080/text/*
的时候,其实会去调用ManagerServlet这个类来完成相应的工作。
ManagerServlet
一个servlet对象通常由一个StandardWrapper实例来表示,在我们第一次调用servlet的时候,会先调用StandardWrapper的loadServlet方法,加载完成之后才是对应servlet的init()方法。而这个servlet则有那么一丝丝特殊,在加载它的时候,会执行setWrapper方法,相当于它可以持有对Wrapper的引用。
由于它持有了Wrapper,导致它可以调用一系列的方法,所以这也就是它Manager的命名的由来,我们可以通过访问它来执行一些特殊的命令,比如localhost:8080/manager/html
,输入账号密码之后就可以查看了。更多的功能可自行探索。
第二十章
上一章介绍了使用ManagerServlet类来作为tomcat的管理器,接下来介绍Java Management Extensions(JMX)来进行管理tomcat。
JMX
JMX提供了比上面一章的servlet更加详细的管理方式。可以看到类名中带MBean
的基本上都是可以被JMX管理的对象,而这些MBean则可以用来管理对应的java对象。
MBean统一被MBean Server所管理,接下来介绍这些不同种类的MBean。
JMX API
MBean Server可以由对应的工厂来进行创建,同时具有一个注册MBean的方法,也有查询指定MBean,或者是执行特定MBean的方法,亦或是为MBean设置属性的方法。
由于每一个MBean都注册到了MBean Server中,而MBean Server则是通过一个叫ObjectName的东西来唯一区别这些MBean的。
标准MBean
这是最为简单的一种MBean,可以通过以下步骤来创建一个标准MBean来管理对象:
- 为你想要控制的java对象创建一个接口,名字规范是
你要管理的类+MBean
,比如你要管理的类是Car,那么就请取名为CarMBean
- 让之前的Car类实现新创建的CarMBean接口
- 创建一个代理,代理中必须包含一个MBeanServer——其实就是一个普通的类,然后该类有一个成员变量是MBeanServer,且该MBean必须要有工厂来进行创建。
- 给MBean创建ObjectName实例(为了能够唯一区分)
- 实例化MBeanServer并将MBean注册到其中。
显然最大的问题就是在第二步,要是我们自己的类其实还好说,但是如果是他人已经实现的类呢?那就无法进行修改了呀。而且是现有类,然后才有接口,接口中还需要写好你想要管理该类的方法(当然一般推荐是全写上)
模型MBean
上面标准MBean的缺点是缺少灵活性,所以有了这个MBean。而所有的MBean的任务就是用来管理,所以又有了ModelMBeanInfo这个对象来完成对托管资源的属性/方法暴露给代理。
- 实例化一个RequiredMBean类,并且创建出对应的代理类,代理类里有对应的MBeanServer。
- 在代理类里面编写好我们需要管理的类(Car)所需要的各种信息,比如各类方法,属性等。
- 直接组合即可。
第三方库
显然我们不想把精力都花在编写需要管理类的信息上面,就有了Commons Modeler这个库。而这个库的作用就是把上面那些繁琐的属性抽象成了一个文件,我们只需要去对应的文件(mbean.xml,最新的里面叫mbeans.descriptors.xml)中写好对应的xml语句,即可自动创建出对应的ModelMBeanInfo对象了。长得像这样子:
1 | <mbean name="StandardEngine" |
也就是一个mbean标签,下面带了很多组attribute和operation,然后operation元素里面又有很多的parameter。
有了这个描述文件,就可以通过Registry这个类来加载xml文件,并进行MBean的创建了。
重要的MBean
知道了如何创建这些mbean,这里就需要介绍一下在catalina中很重要的三个MBean,了解了它们可以很好的了解MBean的工作机制,同时也能够掌握如何管理tomcat。
ClassNameMBean
见名知意,获取托管资源的类名。
StandardServerMBean
用来管理StandardServer,是一种模型MBean。
MBeanFactory
用来创建tomcat中所有的MBean。
最后实现了一个能够查看所有MBean的MBean,有兴趣的可以自己去实现下。
全书完。