Tomcat架构学习总结

来源:互联网 发布:李连杰电影 知乎 编辑:程序博客网 时间:2024/05/16 17:46

  首先用java编写一个最简单的http服务器我们需要做什么?
  1.创建ServerSocket对象并监听某个端口(假定为8080)
  2.接收请求的字节流,解析并处理http协议内容
  3.编写业务逻辑代码
  4.响应处理结果
  为了执行第三步的业务逻辑,我们需要做大量重复工作,而且网络连接这块情况复杂且容易出现问题,在实际工作中还要面对安全管理、监控管理等问题。Tomcat这样的web服务框架就应运而生了,使我们只需专注于业务逻辑的编写,当业务逻辑庞大到一定程度时,我们又会搭配Spring、SpringMVC等这样的管理框架一起使用。框架的主旨就是帮助我们减少重复工作,屏蔽复杂问题来提高使用者的工作效率,大大缩短业务需求的实现周期。
  Tomcat也是经过一步步的发展和迭代才有了今天如此庞大的规模,想深入了解它的每个细节和实现,工作量是巨大的。好在Tomcat在设计之初就有良好的模块化和扩展性,所以从整体上去弄清楚它的架构,理解其核心组件的设计及作用,也就把握住了Tomcat的核心,至于每个组件是如何实现的,运用了什么设计思想,又是如何巧妙解决问题的,这样的细节学习可以在日常工作中逐个击破。
  首先,我们把第一段描述的4个步骤抽象一下,可以分为两大块:1.处理http网络请求;2.处理业务逻辑;而Tomcat也可以抽象为两大核心组件:Connector和Container。Connector专门负责网络连接,报文解析和返回结果的工作,而Container如其名“容器”,我们的业务逻辑代码就“装”在这里面,把网络请求通过各种中间处理、封装最后传入Request和Response两个对象到你的业务逻辑代码中,当你返回结果时,又一层层向外传递最后交由Connector处理。大体可以如下图所示。
这里写图片描述
  本帖主要分析与我们业务逻辑代码息息相关的Container容器,从它的启动流程,类加载机制,架构组成,生命周期管理,http请求传递机制等方面去了解它的原理。每个模块读者都可以在网上找到相关的结构图,通过直观的视图理解起来事半功倍,我懒就不搬运了。
  1.启动流程
  Tomcat说到底就是一个java程序,那么想启动它,找到并调用它的main函数不就行了,那么它的main函数在哪呢?
顺着这个思路回到我们平时启动Tomcat的方式,执行bin目录下的startup脚本文件,接着在命令行输出一大堆日志后就启动成功了,那这个脚本到底做了什么呢?
  其实它只做了下面三件事:
  (1)检查操作系统环境,找到catalina脚本文件并执行它
  (2)检查Java的运行环境是否可用,调用一些其他脚本文件配置各种参数
  (3)上面的检查都通过后,找到bootstrap的jar包并执行BootStrap类的main函数,同时传递一些指令参数。main函数中首先会初始化容器,在这期间还涉及到两个重点内容:创建自定义的commonLoader类加载器,并执行与传入指令对应的生命周期方法。
  2.类加载机制
  首先jvm的类加载机制是双亲委派模型,因此要实现不同web应用间的资源隔离或者热加载这样的功能是行不通的,Tomcat采取的方式简单来说可以概括如下:
  (1)在BootStrap类初始化时,创建自定义的commonLoader类加载器(本帖研究的版本是Tomcat8),并由该加载器加载整个容器的初始类Catalina
  (2)在加载用户部署的web应用时,创建WebappClassLoader类加载器去加载用户的代码,且每个应用对应一个WebappClassLoader,并覆写类加载机制,优先自己加载类,只有找不到时才由父类加载,以此实现资源隔离,而应用间可以共享的类则由父加载器commonLoader来加载。这样热加载就只需摧毁并回收旧的WebappClassLoader创建一个新的来替代即可,而不用重新加载所有的类,从而缩短部署时间。
  3.架构组成
这里写图片描述
  如果熟悉并配置过conf目录下的server.xml文件,就已经对Tomcat的核心组件及其关系有了一定的了解。
Server
  都说Server组件是Tomcat整个容器的服务抽象,因为Tomcat的组件之间大都是包含关系,而Server是处于最顶层的存在,从资源流向的角度看,Server就像一条河流的主干,往内的组件如细分的河流,后面讲到管道Pipeline时就可以有更为形象的感受。
  这里不得不提一下前面讲到的Catalina类,这个类到底做了什么事情呢,一句话来说就是初始化资源并启动、关闭Server实例。其中初始化资源最为核心的是按照server.xml中的配置,实例化相关对象并组织成一个资源对象放入容器的Context中供后续使用。Tomcat的一大特色就是可配置化,从配置文件到内存对象的转换由内置开源框架 Digester来完成,有兴趣的读者可以去了解下该框架的设计方式。
  什么是配置化?Tomcat采取的方式是在指定位置给你一个配置文件,你在指定的位置写上你的类名和参数,这些类必须实现了统一的接口,然后Tomcat会在一些关键位置加上钩子方法,这样就可以通过你配置的类名及参数实现一定程度上的灵活改变。
  回到Server组件,它包含了两大块:GlobalNamingResources全局命名资源和各种Listener。关于第一块,是JNDI标准的一种实现,在容器初始化期间,由NamingContextListener完成全局命名上下文(globalNamingContext)的创建与绑定;NamingResources对象的内容就是你在Server.xml中Resource标签下的配置。而各种监听器的作用就是在Server的不同生命周期阶段做相应的一些事情,比如上面的NamingContextListener,后面会讲到生命周期。

Service
  该组件中包含了三个重要对象:Connector,Engine和Executor,Connector是专门用来监听并接收客户端消息的,Engine组件是Service的下一级容器,Connector拿到请求后会主动从Service中拿到Engine的pipeline对象并触发第一级Valve的调用,这块责任链模式的应用后面会单独讲到。因此可以说到了Engine这里,才算是真正开始处理客户端的请求了,前面都是资源初始化、加载,构件上下文等准备工作,而到Service这一级连接器和处理容器才产生了交集。Executor看名字就知道是一个线程池,Service下的所有组件都可以共用该线程池,但默认情况下Connector使用的是自己创建的线程池。

Engine
  该组件直译是引擎的意思,就是说这个容器开始包含了一整套处理方案,你把请求数据给我就行了, 我会返回给你处理好的结果,也可以理解为它包含了完成的请求处理流水线,后面讲到pipeline时,会有更加形象的理解。

Host
  Host表示虚拟主机的意思,比如http://helloworld.com/index 这样一个网址,helloworld.com就表示一个虚拟主机的地址,也就是说你可以给自己的应用配置多个Host来分别对应不同的域名。它还包含了多个Context组件,一个Context组件中包含了我们Web应用所有需要的东西,因此是一个较为复杂的组件,而Context的创建是由ContextConfig监听器根据生命周期创建的。

Context
  这里简单介绍了Context组件包含的几个核心组件:
  (1)过滤器组件,这个组件的作用大家应该都很熟悉,我们配置的过滤器会在这被创建
  (2)Context级别的NamingResource组件,针对JNDI标准的实现。
  (3)ApplicationContext对象,它实现了Servlet规范中ServletContext接口的所有的方法,而该对象中的资源都是从Context组件中获取的,我们在业务代码中使用的Context对象正是ApplicationContext的门面对象ApplicationContextFacade,它保证了资源访问的安全性。
  (4)WebappLoader组件,用于加载每个应用各自的class和jar包,实现资源隔离
  (5)Wrapper组件,Context中包含了多个Wrapper组件,每个Wrapper组件中的内容就是我们配置的Servlet
  (6)Mapper请求路由组件,根据Servlet的路径配置,将请求分发到对应的Servlet中处理。

  4.生命周期管理
  Tomcat的这些容器组件都实现了Container接口,而Container又继承了Lifecycle接口,这个接口定义了Tomcat所有容器统一的生命周期状态及方法,定义这样一个接口有哪些好处?首先Tomcat的容器都是包含关系,除了Service包含了Connector和Engine组件外,其他都是一层层包含,可以说整个系统的架构都是由容器组合起来的,那么容器间的调用和状态控制就非常重要了。Tomat拥有非常多的细分状态,如初始化前、初始化后、启动前等等,为什么需要如此多的细分状态,而不是直接分为初始化,启动,停止,结束这4个大状态呢,答案就是生命周期监听器,除http请求处理这一条主线外,其他大部分的工作都是交由各个容器的监听器去处理的,因此更多的状态意味着细粒度的事件触发。比如Server容器初始化时,就会调用Service的init方法(实际执行的是每个容器的initInternal方法),而Serivce会继续调用下一级容器的初始化方法,通过这样一级启动一级的方式,就可以把生命周期的状态事件一级级的传递下去,监听器收到各自监听容器的状态事件,就会开始做各自的工作,就如多米诺骨牌一样,只需触发一个起点即可。这样的设计方式,非常好的解耦了各个容器,当你需要增加或删除容器时,只需修改下一级容器的指向即可,遵循了开闭原则,同时这也是状态模式的一种实现,LifecycleState枚举类中定义了Tomcat整个生命周期的所有状态,比如执行init方法后,状态就由NEW转为INITIALIZING,当初始化完毕后又转为INITIALIZED。

  5.PipeLine
  上面讲到的是容器间的状态管理,而PipeLine就是负责容器间的调用了,它是一个典型的责任链模式应用。
  我们在日常开发中,经常会碰到逻辑很复杂的需求,最简单粗暴的方式就是把他们写在一起,但是这样的代码当你需要修改部分逻辑或者在中间插入一些新的处理代码时,就很痛苦了。那我们很自然的一个优化思路就是按照业务和流程把他们拆分成相互独立且更小粒度的模块,模块间以处理的结果作为下一个模块的输入参数,这样不但降低了修改的风险,同时模块间的调用顺序也很容易根据业务进行修改。此外,粒度小的模块可重用性也会大大提高,就好比一块砖,哪里需要就往哪里搬,要是这块砖不用了,另外造一块顶替它就行了,其他也不受影响,多干脆。
  绕回来继续说Tomcat的管道模式,它就是采用了上述的方式,将整个复杂的请求处理流程,拆解到了各个容器中,大家可以去百度搜一下相关的管道图,看着图就很好理解了。首先有两个名词要确定,管道与阀门。我们把这个模式想象成家里的水管,管道就是指那一截一截的管子,阀门(Valve)则是每个水管的接合处。处理请求的容器一共有四个级别,其默认实现为:StandardEngine、StandardHost、StandardContext和StandardWrapper,请求对象在这四个容器中一级级往下传递。比如Engine管道的处理流程大体如下:
  请求对象到了StandardEngine,这时StandardEngine会检查是否有配置自定义的阀门,通过StandardPipeline定义组装阀门,并在最后放入自己的StandardEngineValve对象。也就是说用户定义的阀门先执行,容器默认的阀门把最后一道关,把该做的事情做完,最后调用下级容器的Pipeline对象。通过这样的设计方式,Tomcat对请求的处理就非常灵活了,我们可以给不同的容器添加自定义的阀门,只需在Server.xml对应的容器标签下添加即可。

  最后总结一下,首先我们得弄清楚每个容器的作用域以及它们之间的关系。其次是对生命周期状态的理解,当Tomcat处于不同状态时,容器们又做了哪些事,接着是管道与阀门的灵活运用。另外一个非常重要的地方,就是Tomcat对网络连接的处理,而这些处理都在Connector中。此外,Tomcat的类加载机制、各容器MBean的实现以便对JMX的支持、日志打印的管理和session管理等,我们都可以从中学到很多很好的代码技巧及设计思路。