走进JavaWeb技术世界6Tomcat5总体架构剖析

37次阅读

共计 6079 个字符,预计需要花费 16 分钟才能阅读完成。

本系列文章将整理到我在 GitHub 上的《Java 面试指南》仓库,更多精彩内容请到我的仓库里查看

https://github.com/h2pl/Java-…

喜欢的话麻烦点下 Star 哈

文章首发于我的个人博客:

www.how2playlife.com

本文是微信公众号【Java 技术江湖】的《走进 JavaWeb 技术世界》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。

该系列博文会告诉你如何从入门到进阶,从 servlet 到框架,从 ssm 再到 SpringBoot,一步步地学习 JavaWeb 基础知识,并上手进行实战,接着了解 JavaWeb 项目中经常要使用的技术和组件,包括日志组件、Maven、Junit,等等内容,以便让你更完整地了解整个 JavaWeb 技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。

如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java 技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。

文末赠送 8000G 的 Java 架构师学习资料,需要的朋友可以到文末了解领取方式,资料包括 Java 基础、进阶、项目和架构师等免费学习资料,更有数据库、分布式、微服务等热门技术学习视频,内容丰富,兼顾原理和实践,另外也将赠送作者原创的 Java 学习指南、Java 程序员面试指南等干货资源)
<!– more –>

jsp 作为 Servlet 技术的扩展,经常会有人将 jsp 和 Servlet 搞混。本文,将为大家带来 servlet 和 jsp 的区别,希望对大家有所帮助。

servlet 和 jsp 的区别

1、Servlet 在 Java 代码中可以通过 HttpServletResponse 对象动态输出 HTML 内容。

2、JSP 是在静态 HTML 内容中嵌入 Java 代码,然后 Java 代码在被动态执行后生成 HTML 内容。

servlet 和 jsp 各自的特点

1、Servlet 虽然能够很好地组织业务逻辑代码,但是在 Java 源文件中,因为是通过字符串拼接的方式生成动态 HTML 内容,这样就容易导致代码维护困难、可读性差。

2、JSP 虽然规避了 Servlet 在生成 HTML 内容方面的劣势,但是在 HTML 中混入大量、复杂的业务逻辑。

通过 MVC 双剑合璧

JSP 和 Servlet 都有自身的适用环境,那么有没有什么办法能够让它们发挥各自的优势呢?答案是肯有的,MVC 模式就能够完美解决这一问题。

MVC 模式,是 Model-View-Controller 的简称,是软件工程中的一种软件架构模式,分为三个基本部分,分别是:模型(Model)、视图(View)和控制器(Controller):

Controller——负责转发请求,对请求进行处理

View——负责界面显示

Model——业务功能编写(例如算法实现)、数据库设计以及数据存取操作实现

在 JSP/Servlet 开发的软件系统中,这三个部分的描述如下所示:

1、Web 浏览器发送 HTTP 请求到服务端,然后被 Controller(Servlet)获取并进行处理(例如参数解析、请求转发)

2、Controller(Servlet)调用核心业务逻辑——Model 部分,获得结果

3、Controller(Servlet)将逻辑处理结果交给 View(JSP),动态输出 HTML 内容

4、动态生成的 HTML 内容返回到浏览器显示

MVC 模式在 Web 开发中有很大的优势,它完美规避了 JSP 与 Servlet 各自的缺点,让 Servlet 只负责业务逻辑部分,而不会生成 HTML 代码;同时 JSP 中也不会充斥着大量的业务代码,这样能大提高了代码的可读性和可维护性。

JavaWeb 基础知识

一、Servlet 是什么?

Java Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。

使用 Servlet,您可以收集来自网页表单的用户输入,呈现来自数据库或者其他源的记录,还可以动态创建网页。

Java Servlet 通常情况下与使用 CGI(Common Gateway Interface,公共网关接口)实现的程序可以达到异曲同工的效果。但是相比于 CGI,Servlet 有以下几点优势:

  • 1、性能明显更好。
  • 2、Servlet 在 Web 服务器的地址空间内执行。这样它就没有必要再创建一个单独的进程来处理每个客户端请求。
  • 3、Servlet 是独立于平台的,因为它们是用 Java 编写的。
  • 4、服务器上的 Java 安全管理器执行了一系列限制,以保护服务器计算机上的资源。因此,Servlet 是可信的。
  • 5、Java 类库的全部功能对 Servlet 来说都是可用的。它可以通过 sockets 和 RMI 机制与 applets、数据库或其他软件进行交互。

二、Servlet 的生命周期

Servlet 生命周期可被定义为从创建直到毁灭的整个过程。以下是 Servlet 遵循的过程:

  • 1、Servlet 通过调用 init () 方法进行初始化。
  • 2、Servlet 调用 service() 方法来处理客户端的请求。
  • 3、Servlet 通过调用 destroy() 方法终止(结束)。
  • 4、最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。

init() 方法

init 方法被设计成只调用一次。它在第一次创建 Servlet 时被调用,在后续每次用户请求时不再调用。因此,它是用于一次性初始化,就像 Applet 的 init 方法一样。

Servlet 创建于用户第一次调用对应于该 Servlet 的 URL 时,但是您也可以指定 Servlet 在服务器第一次启动时被加载。

service() 方法

service() 方法是执行实际任务的主要方法。Servlet 容器(即 Web 服务器)调用 service() 方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。

每次服务器接收到一个 Servlet 请求时,服务器会产生一个新的线程并调用服务。service() 方法检查 HTTP 请求类型(GET、POST、PUT、DELETE 等),并在适当的时候调用 doGet、doPost、doPut,doDelete 等方法。

 destroy() 方法

destroy() 方法只会被调用一次,在 Servlet 生命周期结束时被调用。destroy() 方法可以让您的 Servlet 关闭数据库连接、停止后台线程、把 Cookie 列表或点击计数器写入到磁盘,并执行其他类似的清理活动。

在调用 destroy() 方法之后,servlet 对象被标记为垃圾回收。

 示例

执行后:

以后继续请求时:

可见,就绪请求时只有 service()方法执行!

相关面试题

怎样理解 Servlet 的单实例多线程?**

不同的用户同时对同一个业务(如注册)发出请求,那这个时候容器里产生的有是几个 servlet 实例呢?

答案是:只有一个 servlet 实例。一个 servlet 是在第一次被访问时加载到内存并实例化的。同样的业务请求共享一个 servlet 实例。不同的业务请求一般对应不同的 servlet。

由于 Servlet/JSP 默认是以多线程模式执行的,所以,在编写代码时需要非常细致地考虑多线程的安全性问题。

JSP 的中存在的多线程问题:

当客户端第一次请求某一个 JSP 文件时,服务端把该 JSP 编译成一个 CLASS 文件,并创建一个该类的实例,然后创建一个线程处理 CLIENT 端的请求。如果有多个客户端同时请求该 JSP 文件,则服务端会创建多个线程。每个客户端请求对应一个线程。以多线程方式执行可大大降低对系统的资源需求, 提高系统的并发量及响应时间。

对 JSP 中可能用的的变量说明如下:

实例变量 : 实例变量是在堆中分配的, 并被属于该实例的所有线程共享,所以 不是线程安全的。

JSP 系统提供的 8 个类变量

JSP 中用到的OUT,REQUEST,RESPONSE,SESSION,CONFIG,PAGE,PAGECONXT 是线程安全的(因为每个线程对应的 request,respone 对象都是不一样的,不存在共享问题),APPLICATION 在整个系统内被使用, 所以不是线程安全的。

局部变量 : 局部变量在堆栈中分配, 因为每个线程都有它自己的堆栈空间, 所以 是线程安全的

静态类 : 静态类不用被实例化, 就可直接使用, 也 不是线程安全的

外部资源: 在程序中可能会有多个线程或进程同时操作同一个资源(如: 多个线程或进程同时对一个文件进行写操作). 此时也要注意同步问题.

Servlet 单实例多线程机制:

Servlet 采用多线程来处理多个请求同时访问。servlet 依赖于一个线程池来服务请求。线程池实际上是一系列的工作者线程集合。Servlet 使用一个调度线程来管理工作者线程。

当容器收到一个 Servlet 请求,调度线程从线程池中选出一个工作者线程, 将请求传递给该工作者线程,然后由该线程来执行 Servlet 的 service 方法。

当这个线程正在执行的时候, 容器收到另外一个请求, 调度线程同样从线程池中选出另一个工作者线程来服务新的请求, 容器并不关心这个请求是否访问的是同一个 Servlet. 当容器同时收到对同一个 Servlet 的多个请求的时候,那么这个 Servlet 的 service()方法将在多线程中并发执行。

Servlet 容器默认采用单实例多线程的方式来处理请求,这样减少产生 Servlet 实例的开销,提升了对请求的响应时间,对于 Tomcat 可以在 server.xml 中通过 <Connector> 元素设置线程池中线程的数目。

如何开发线程安全的 Servlet

1、实现 SingleThreadModel 接口

该接口指定了系统如何处理对同一个 Servlet 的调用。如果一个 Servlet 被这个接口指定, 那么在这个 Servlet 中的 service 方法将不会有两个线程被同时执行,当然也就不存在线程安全的问题。这种方法只要将前面的 Concurrent Test 类的类头定义更改为:

<pre>Public class Concurrent Test extends HttpServlet implements SingleThreadModel {
…………
} </pre>

同步对共享数据的操作

使用 synchronized 关键字能保证一次只有一个线程可以访问被保护的区段

避免使用实例变量

本实例中的线程安全问题是由实例变量造成的,只要在 Servlet 里面的任何方法里面都不使用实例变量,那么该 Servlet 就是线程安全的。

1) Struts2 的 Action 是原型,非单实例的;会对每一个请求, 产生一个 Action 的实例来处理

Struts1 Action 是单实例的

mvc 的 controller 也是如此。因此开发时要求必须是线程安全的,因为仅有 Action 的一个实例来处理所有的请求。单例策略限制了 Struts1 Action 能作的事,并且要在开发时特别小心。Action 资源必须是线程安全的或同步的。

2) Struts1 的 Action,Spring 的 Ioc 容器管理的 bean 默认是单实例的.

Spring 的 Ioc 容器管理的 bean 默认是单实例的。

Struts2 Action 对象为每一个请求产生一个实例,因此没有线程安全问题。(实际上,servlet 容器给每个请求产生许多可丢弃的对象,并且不会导致性能和垃圾回收问题)。

当 Spring 管理 Struts2 的 Action 时,bean 默认是单实例的,可以通过配置参数将其设置为原型。(scope=”prototype)

五、servlet 与 jsp 的区别

1.jsp 经编译后就变成了 Servlet.(JSP 的本质就是 Servlet,JVM 只能识别 java 的类,不能识别 JSP 的代码,Web 容器将 JSP 的代码编译成 JVM 能够识别的 java 类)

2.jsp 更擅长表现于页面显示,servlet 更擅长于逻辑控制.

3.Servlet 中没有内置对象,内置对象都是必须通过 HttpServletRequest 对象,HttpServletResponse 对象以及 HttpServlet 对象得到.Jsp 是 Servlet 的一种简化,使用 Jsp 只需要完成程序员需要输出到客户端的内容,Jsp 中的 Java 脚本如何镶嵌到一个类中,由 Jsp 容器完成。而 Servlet 则是个完整的 Java 类,这个类的 Service 方法用于生成对客户端的响应。

4. 对于静态 HTML 标签,Servlet 都必须使用页面输出流逐行输出

参考文章

https://www.w3cschool.cn/serv…
https://blog.csdn.net/qq_1978…
https://blog.csdn.net/qiuhuan…
https://blog.csdn.net/zt15732…
https://blog.csdn.net/android…

微信公众号

个人公众号:黄小斜

黄小斜是跨考软件工程的 985 硕士,自学 Java 两年,拿到了 BAT 等近十家大厂 offer,从技术小白成长为阿里工程师。

作者专注于 JAVA 后端技术栈,热衷于分享程序员干货、学习经验、求职心得和程序人生,目前黄小斜的 CSDN 博客有百万 + 访问量,知乎粉丝 2W+,全网已有 10W+ 读者。

黄小斜是一个斜杠青年,坚持学习和写作,相信终身学习的力量,希望和更多的程序员交朋友,一起进步和成长!

原创电子书:
关注危险公众号【黄小斜】后回复【原创电子书】即可领取我原创的电子书《菜鸟程序员修炼手册:从技术小白到阿里巴巴 Java 工程师》这份电子书总结了我 2 年的 Java 学习之路,包括学习方法、技术总结、求职经验和面试技巧等内容,已经帮助很多的程序员拿到了心仪的 offer!

程序员 3T 技术学习资源: 一些程序员学习技术的资源大礼包,关注公众号后,后台回复关键字 “资料” 即可免费无套路获取,包括 Java、python、C++、大数据、机器学习、前端、移动端等方向的技术资料。

技术公众号:Java 技术江湖

如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的微信公众号【Java 技术江湖】

这是一位阿里 Java 工程师的技术小站。作者黄小斜,专注 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点 Docker、ELK,同时也分享技术干货和学习经验,致力于 Java 全栈开发!

(关注公众号后回复”Java“即可领取 Java 基础、进阶、项目和架构师等免费学习资料,更有数据库、分布式、微服务等热门技术学习视频,内容丰富,兼顾原理和实践,另外也将赠送作者原创的 Java 学习指南、Java 程序员面试指南等干货资源)

Java 工程师必备学习资源: 一些 Java 工程师常用学习资源,关注公众号后,后台回复关键字 “Java” 即可免费无套路获取。

正文完
 0

走进JavaWeb技术世界6Tomcat5总体架构剖析

37次阅读

共计 16916 个字符,预计需要花费 43 分钟才能阅读完成。

微信公众号【Java 技术江湖】一位阿里 Java 工程师的技术小站。(关注公众号后回复”Java“即可领取 Java 基础、进阶、项目和架构师等免费学习资料,更有数据库、分布式、微服务等热门技术学习视频,内容丰富,兼顾原理和实践,另外也将赠送作者原创的 Java 学习指南、Java 程序员面试指南等干货资源)

本文以 Tomcat 5 为基础,也兼顾最新的 Tomcat 6 和 Tomcat 4。Tomcat 的基本设计思路和架构是具有一定连续性的。

Tomcat 总体结构

Tomcat 的结构很复杂,但是 Tomcat 也非常的模块化,找到了 Tomcat 最核心的模块,您就抓住了 Tomcat 的“七寸”。下面是 Tomcat 的总体结构图:

图 1.Tomcat 的总体结构

从上图中可以看出 Tomcat 的心脏是两个组件:Connector 和 Container,关于这两个组件将在后面详细介绍。Connector 组件是可以被替换,这样可以提供给服务器设计者更多的选择,因为这个组件是如此重要,不仅跟服务器的设计的本身,而且和不同的应用场景也十分相关,所以一个 Container 可以选择对应多个 Connector。多个 Connector 和一个 Container 就形成了一个 Service,Service 的概念大家都很熟悉了,有了 Service 就可以对外提供服务了,但是 Service 还要一个生存的环境,必须要有人能够给她生命、掌握其生死大权,那就非 Server 莫属了。所以整个 Tomcat 的生命周期由 Server 控制。

以 Service 作为“婚姻”

我们将 Tomcat 中 Connector、Container 作为一个整体比作一对情侣的话,Connector 主要负责对外交流,可以比作为 Boy,Container 主要处理 Connector 接受的请求,主要是处理内部事务,可以比作为 Girl。那么这个 Service 就是连接这对男女的结婚证了。是 Service 将它们连接在一起,共同组成一个家庭。当然要组成一个家庭还要很多其它的元素。

说白了,Service 只是在 Connector 和 Container 外面多包一层,把它们组装在一起,向外面提供服务,一个 Service 可以设置多个 Connector,但是只能有一个 Container 容器。这个 Service 接口的方法列表如下:

图 2. Service 接口

从 Service 接口中定义的方法中可以看出,它主要是为了关联 Connector 和 Container,同时会初始化它下面的其它组件,注意接口中它并没有规定一定要控制它下面的组件的生命周期。所有组件的生命周期在一个 Lifecycle 的接口中控制,这里用到了一个重要的设计模式,关于这个接口将在后面介绍。

Tomcat 中 Service 接口的标准实现类是 StandardService 它不仅实现了 Service 借口同时还实现了 Lifecycle 接口,这样它就可以控制它下面的组件的生命周期了。StandardService 类结构图如下:

图 3. StandardService 的类结构图

从上图中可以看出除了 Service 接口的方法的实现以及控制组件生命周期的 Lifecycle 接口的实现,还有几个方法是用于在事件监听的方法的实现,不仅是这个 Service 组件,Tomcat 中其它组件也同样有这几个方法,这也是一个典型的设计模式,将在后面介绍。

下面看一下 StandardService 中主要的几个方法实现的代码,下面是 setContainer 和 addConnector 方法的源码:

清单 1. StandardService. SetContainer

123456789101112131415161718192021222324252627public void setContainer(Container container) {`Container oldContainer = this.container;if ((oldContainer != null) && (oldContainer instanceof Engine))((Engine) oldContainer).setService(null);this.container = container;if ((this.container != null) && (this.container instanceof Engine))((Engine) this.container).setService(this);if (started && (this.container != null) && (this.container instanceof Lifecycle)) {try {((Lifecycle) this.container).start();} catch (LifecycleException e) {;}}synchronized (connectors) {for (int i = 0; i < connectors.length; i++)connectors[i].setContainer(this.container);}if (started && (oldContainer != null) && (oldContainer instanceof Lifecycle)) {try {((Lifecycle) oldContainer).stop();} catch (LifecycleException e) {;}}support.firePropertyChange(“container”, oldContainer, this.container);`}

这段代码很简单,其实就是先判断当前的这个 Service 有没有已经关联了 Container,如果已经关联了,那么去掉这个关联关系—— oldContainer.setService(null)。如果这个 oldContainer 已经被启动了,结束它的生命周期。然后再替换新的关联、再初始化并开始这个新的 Container 的生命周期。最后将这个过程通知感兴趣的事件监听程序。这里值得注意的地方就是,修改 Container 时要将新的 Container 关联到每个 Connector,还好 Container 和 Connector 没有双向关联,不然这个关联关系将会很难维护。

清单 2. StandardService. addConnector

12345678910111213141516171819202122232425public void addConnector(Connector connector) {`synchronized (connectors) {connector.setContainer(this.container);connector.setService(this);Connector results[] = new Connector[connectors.length + 1];System.arraycopy(connectors, 0, results, 0, connectors.length);results[connectors.length] = connector;connectors = results;if (initialized) {try {connector.initialize();} catch (LifecycleException e) {e.printStackTrace(System.err);}}if (started && (connector instanceof Lifecycle)) {try {((Lifecycle) connector).start();} catch (LifecycleException e) {;}}support.firePropertyChange("connector", null, connector);}`}

上面是 addConnector 方法,这个方法也很简单,首先是设置关联关系,然后是初始化工作,开始新的生命周期。这里值得一提的是,注意 Connector 用的是数组而不是 List 集合,这个从性能角度考虑可以理解,有趣的是这里用了数组但是并没有向我们平常那样,一开始就分配一个固定大小的数组,它这里的实现机制是:重新创建一个当前大小的数组对象,然后将原来的数组对象 copy 到新的数组中,这种方式实现了类似的动态数组的功能,这种实现方式,值得我们以后拿来借鉴。

最新的 Tomcat6 中 StandardService 也基本没有变化,但是从 Tomcat5 开始 Service、Server 和容器类都继承了 MBeanRegistration 接口,Mbeans 的管理更加合理。

以 Server 为“居”

前面说一对情侣因为 Service 而成为一对夫妻,有了能够组成一个家庭的基本条件,但是它们还要有个实体的家,这是它们在社会上生存之本,有了家它们就可以安心的为人民服务了,一起为社会创造财富。

Server 要完成的任务很简单,就是要能够提供一个接口让其它程序能够访问到这个 Service 集合、同时要维护它所包含的所有 Service 的生命周期,包括如何初始化、如何结束服务、如何找到别人要访问的 Service。还有其它的一些次要的任务,如您住在这个地方要向当地政府去登记啊、可能还有要配合当地公安机关日常的安全检查什么的。

Server 的类结构图如下:

图 4. Server 的类结构图

它的标准实现类 StandardServer 实现了上面这些方法,同时也实现了 Lifecycle、MbeanRegistration 两个接口的所有方法,下面主要看一下 StandardServer 重要的一个方法 addService 的实现:

清单 3. StandardServer.addService

123456789101112131415161718192021222324public void addService(Service service) {`service.setServer(this);synchronized (services) {Service results[] = new Service[services.length + 1];System.arraycopy(services, 0, results, 0, services.length);results[services.length] = service;services = results;if (initialized) {try {service.initialize();} catch (LifecycleException e) {e.printStackTrace(System.err);}}if (started && (service instanceof Lifecycle)) {try {((Lifecycle) service).start();} catch (LifecycleException e) {;}}support.firePropertyChange(“service”, null, service);}}`

从上面第一句就知道了 Service 和 Server 是相互关联的,Server 也是和 Service 管理 Connector 一样管理它,也是将 Service 放在一个数组中,后面部分的代码也是管理这个新加进来的 Service 的生命周期。Tomcat6 中也是没有什么变化的。

组件的生命线“Lifecycle”

前面一直在说 Service 和 Server 管理它下面组件的生命周期,那它们是如何管理的呢?

Tomcat 中组件的生命周期是通过 Lifecycle 接口来控制的,组件只要继承这个接口并实现其中的方法就可以统一被拥有它的组件控制了,这样一层一层的直到一个最高级的组件就可以控制 Tomcat 中所有组件的生命周期,这个最高的组件就是 Server,而控制 Server 的是 Startup,也就是您启动和关闭 Tomcat。

下面是 Lifecycle 接口的类结构图:

图 5. Lifecycle 类结构图

除了控制生命周期的 Start 和 Stop 方法外还有一个监听机制,在生命周期开始和结束的时候做一些额外的操作。这个机制在其它的框架中也被使用,如在 Spring 中。关于这个设计模式会在后面介绍。

Lifecycle 接口的方法的实现都在其它组件中,就像前面中说的,组件的生命周期由包含它的父组件控制,所以它的 Start 方法自然就是调用它下面的组件的 Start 方法,Stop 方法也是一样。如在 Server 中 Start 方法就会调用 Service 组件的 Start 方法,Server 的 Start 方法代码如下:

清单 4. StandardServer.Start

12345678910111213141516public void start() throws LifecycleException {`if (started) {log.debug(sm.getString("standardServer.start.started"));return;}lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);lifecycle.fireLifecycleEvent(START_EVENT, null);started = true;synchronized (services) {for (int i = 0; i < services.length; i++) {if (services[i] instanceof Lifecycle)((Lifecycle) services[i]).start();}}lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null);}`

监听的代码会包围 Service 组件的启动过程,就是简单的循环启动所有 Service 组件的 Start 方法,但是所有 Service 必须要实现 Lifecycle 接口,这样做会更加灵活。

Server 的 Stop 方法代码如下:

清单 5. StandardServer.Stop

123456789101112public void stop() throws LifecycleException {`if (!started)return;lifecycle.fireLifecycleEvent(BEFORE_STOP_EVENT, null);lifecycle.fireLifecycleEvent(STOP_EVENT, null);started = false;for (int i = 0; i < services.length; i++) {if (services[i] instanceof Lifecycle)((Lifecycle) services[i]).stop();}lifecycle.fireLifecycleEvent(AFTER_STOP_EVENT, null);}`

它所要做的事情也和 Start 方法差不多。

Connector 组件

Connector 组件是 Tomcat 中两个核心组件之一,它的主要任务是负责接收浏览器的发过来的 tcp 连接请求,创建一个 Request 和 Response 对象分别用于和请求端交换数据,然后会产生一个线程来处理这个请求并把产生的 Request 和 Response 对象传给处理这个请求的线程,处理这个请求的线程就是 Container 组件要做的事了。

由于这个过程比较复杂,大体的流程可以用下面的顺序图来解释:

图 6. Connector 处理一次请求顺序图

(查看清晰大图)

Tomcat5 中默认的 Connector 是 Coyote,这个 Connector 是可以选择替换的。Connector 最重要的功能就是接收连接请求然后分配线程让 Container 来处理这个请求,所以这必然是多线程的,多线程的处理是 Connector 设计的核心。Tomcat5 将这个过程更加细化,它将 Connector 划分成 Connector、Processor、Protocol, 另外 Coyote 也定义自己的 Request 和 Response 对象。

下面主要看一下 Tomcat 中如何处理多线程的连接请求,先看一下 Connector 的主要类图:

图 7. Connector 的主要类图

(查看清晰大图)

看一下 HttpConnector 的 Start 方法:

清单 6. HttpConnector.Start

123456789101112131415public void start() throws LifecycleException {`if (started)throw new LifecycleException(sm.getString(“httpConnector.alreadyStarted”));threadName = "HttpConnector[" + port + "]";lifecycle.fireLifecycleEvent(START_EVENT, null);started = true;threadStart();while (curProcessors < minProcessors) {if ((maxProcessors > 0) && (curProcessors >= maxProcessors))break;HttpProcessor processor = newProcessor();recycle(processor);}`}

threadStart() 执行就会进入等待请求的状态,直到一个新的请求到来才会激活它继续执行,这个激活是在 HttpProcessor 的 assign 方法中,这个方法是代码如下 

清单 7. HttpProcessor.assign

12345678910111213synchronized void assign(Socket socket) {`while (available) {try {wait();} catch (InterruptedException e) {}}this.socket = socket;available = true;notifyAll();if ((debug >= 1) && (socket != null))log(” An incoming request is being assigned”);`}

创建 HttpProcessor 对象是会把 available 设为 false,所以当请求到来时不会进入 while 循环,将请求的 socket 赋给当期处理的 socket,并将 available 设为 true,当 available 设为 true 是 HttpProcessor 的 run 方法将被激活,接下去将会处理这次请求。

Run 方法代码如下:

清单 8. HttpProcessor.Run

12345678910111213141516public void run() {`while (!stopped) {Socket socket = await();if (socket == null)continue;try {process(socket);} catch (Throwable t) {log("process.invoke", t);}connector.recycle(this);}synchronized (threadSync) {threadSync.notifyAll();}}`

解析 socket 的过程在 process 方法中,process 方法的代码片段如下:

清单 9. HttpProcessor.process

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657private void process(Socket socket) {`boolean ok = true;boolean finishResponse = true;SocketInputStream input = null;OutputStream output = null;try {input = new SocketInputStream(socket.getInputStream(),connector.getBufferSize());} catch (Exception e) {log("process.create", e);ok = false;}keepAlive = true;while (!stopped && ok && keepAlive) {finishResponse = true;try {request.setStream(input);request.setResponse(response);output = socket.getOutputStream();response.setStream(output);response.setRequest(request);((HttpServletResponse) response.getResponse()).setHeader(“Server”, SERVER_INFO);} catch (Exception e) {log(“process.create”, e);ok = false;}try {if (ok) {parseConnection(socket);parseRequest(input, output);if (!request.getRequest().getProtocol().startsWith("HTTP/0"))parseHeaders(input);if (http11) {ackRequest(output);if (connector.isChunkingAllowed())response.setAllowChunking(true);}}。。。。。。try {((HttpServletResponse) response).setHeader(“Date”, FastHttpDateFormat.getCurrentDate());if (ok) {connector.getContainer().invoke(request, response);}。。。。。。}try {shutdownInput(input);socket.close();} catch (IOException e) {;} catch (Throwable e) {log(“process.invoke”, e);}socket = null;`}

当 Connector 将 socket 连接封装成 request 和 response 对象后接下来的事情就交给 Container 来处理了。

Servlet 容器“Container”

Container 是容器的父接口,所有子容器都必须实现这个接口,Container 容器的设计用的是典型的责任链的设计模式,它有四个子容器组件构成,分别是:Engine、Host、Context、Wrapper,这四个组件不是平行的,而是父子关系,Engine 包含 Host,Host 包含 Context,Context 包含 Wrapper。通常一个 Servlet class 对应一个 Wrapper,如果有多个 Servlet 就可以定义多个 Wrapper,如果有多个 Wrapper 就要定义一个更高的 Container 了,如 Context,Context 通常就是对应下面这个配置:

清单 10. Server.xml

12345<`Contextpath="/library"docBase=“D:projectslibrarydeploytargetlibrary.war”reloadable="true"/>`

容器的总体设计

Context 还可以定义在父容器 Host 中,Host 不是必须的,但是要运行 war 程序,就必须要 Host,因为 war 中必有 web.xml 文件,这个文件的解析就需要 Host 了,如果要有多个 Host 就要定义一个 top 容器 Engine 了。而 Engine 没有父容器了,一个 Engine 代表一个完整的 Servlet 引擎。

那么这些容器是如何协同工作的呢?先看一下它们之间的关系图:

图 8. 四个容器的关系图

(查看清晰大图)

当 Connector 接受到一个连接请求时,将请求交给 Container,Container 是如何处理这个请求的?这四个组件是怎么分工的,怎么把请求传给特定的子容器的呢?又是如何将最终的请求交给 Servlet 处理。下面是这个过程的时序图:

图 9. Engine 和 Host 处理请求的时序图

(查看清晰大图)

这里看到了 Valve 是不是很熟悉,没错 Valve 的设计在其他框架中也有用的,同样 Pipeline 的原理也基本是相似的,它是一个管道,Engine 和 Host 都会执行这个 Pipeline,您可以在这个管道上增加任意的 Valve,Tomcat 会挨个执行这些 Valve,而且四个组件都会有自己的一套 Valve 集合。您怎么才能定义自己的 Valve 呢?在 server.xml 文件中可以添加,如给 Engine 和 Host 增加一个 Valve 如下:

清单 11. Server.xml

12345678910111213<`Engine defaultHost=“localhost” name=“Catalina”><Valve` `className="org.apache.catalina.valves.RequestDumperValve"/>………<Host` `appBase="webapps"` `autoDeploy="true"` `name="localhost"` `unpackWARs="true"xmlNamespaceAware=“false” xmlValidation=“false”><Valve` `className="org.apache.catalina.valves.FastCommonAccessLogValve"directory=“logs”  prefix=“localhost_access_log.” suffix=“.txt”pattern="common"` `resolveHosts="false"/>    …………</Host></Engine`>

StandardEngineValve 和 StandardHostValve 是 Engine 和 Host 的默认的 Valve,它们是最后一个 Valve 负责将请求传给它们的子容器,以继续往下执行。

前面是 Engine 和 Host 容器的请求过程,下面看 Context 和 Wrapper 容器时如何处理请求的。下面是处理请求的时序图:

图 10. Context 和 wrapper 的处理请求时序图

(查看清晰大图)

从 Tomcat5 开始,子容器的路由放在了 request 中,request 中保存了当前请求正在处理的 Host、Context 和 wrapper。

Engine 容器

Engine 容器比较简单,它只定义了一些基本的关联关系,接口类图如下:

图 11. Engine 接口的类结构

它的标准实现类是 StandardEngine,这个类注意一点就是 Engine 没有父容器了,如果调用 setParent 方法时将会报错。添加子容器也只能是 Host 类型的,代码如下:

清单 12. StandardEngine. addChild

1234567891011public void addChild(Container child) {`if (!(child instanceof Host))throw new IllegalArgumentException(sm.getString(“standardEngine.notHost”));super.addChild(child);}public void setParent(Container container) {throw new IllegalArgumentException(sm.getString("standardEngine.notParent"));}`

它的初始化方法也就是初始化和它相关联的组件,以及一些事件的监听。

Host 容器

Host 是 Engine 的字容器,一个 Host 在 Engine 中代表一个虚拟主机,这个虚拟主机的作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器通常是 Context,它除了关联子容器外,还有就是保存一个主机应该有的信息。

下面是和 Host 相关的类关联图:

图 12. Host 相关的类图

(查看清晰大图)

从上图中可以看出除了所有容器都继承的 ContainerBase 外,StandardHost 还实现了 Deployer 接口,上图清楚的列出了这个接口的主要方法,这些方法都是安装、展开、启动和结束每个 web application。

Deployer 接口的实现是 StandardHostDeployer,这个类实现了的最要的几个方法,Host 可以调用这些方法完成应用的部署等。

Context 容器

Context 代表 Servlet 的 Context,它具备了 Servlet 运行的基本环境,理论上只要有 Context 就能运行 Servlet 了。简单的 Tomcat 可以没有 Engine 和 Host。

Context 最重要的功能就是管理它里面的 Servlet 实例,Servlet 实例在 Context 中是以 Wrapper 出现的,还有一点就是 Context 如何才能找到正确的 Servlet 来执行它呢?Tomcat5 以前是通过一个 Mapper 类来管理的,Tomcat5 以后这个功能被移到了 request 中,在前面的时序图中就可以发现获取子容器都是通过 request 来分配的。

Context 准备 Servlet 的运行环境是在 Start 方法开始的,这个方法的代码片段如下:

清单 13. StandardContext.start

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162public synchronized void start() throws LifecycleException {`………if(!initialized) {try {init();} catch(Exception ex) {throw new LifecycleException("Error initializaing", ex);}}………lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);setAvailable(false);setConfigured(false);boolean ok = true;File configBase = getConfigBase();if (configBase != null) {if (getConfigFile() == null) {File file = new File(configBase, getDefaultConfigFile());setConfigFile(file.getPath());try {File appBaseFile = new File(getAppBase());if (!appBaseFile.isAbsolute()) {appBaseFile = new File(engineBase(), getAppBase());}String appBase = appBaseFile.getCanonicalPath();String basePath =(new File(getBasePath())).getCanonicalPath();if (!basePath.startsWith(appBase)) {Server server = ServerFactory.getServer();((StandardServer) server).storeContext(this);}} catch (Exception e) {log.warn("Error storing config file", e);}} else {try {String canConfigFile =  (new File(getConfigFile())).getCanonicalPath();if (!canConfigFile.startsWith (configBase.getCanonicalPath())) {File file = new File(configBase, getDefaultConfigFile());if (copy(new File(canConfigFile), file)) {setConfigFile(file.getPath());}}} catch (Exception e) {log.warn("Error setting config file", e);}}}………Container children[] = findChildren();for (int i = 0; i < children.length; i++) {if (children[i] instanceof Lifecycle)((Lifecycle) children[i]).start();}if (pipeline instanceof Lifecycle)((Lifecycle) pipeline).start();………}`

它主要是设置各种资源属性和管理组件,还有非常重要的就是启动子容器和 Pipeline。

我们知道 Context 的配置文件中有个 reloadable 属性,如下面配置:

清单 14. Server.xml

12345<`Contextpath="/library"docBase=“D:projectslibrarydeploytargetlibrary.war”reloadable="true"/>`

当这个 reloadable 设为 true 时,war 被修改后 Tomcat 会自动的重新加载这个应用。如何做到这点的呢 ? 这个功能是在 StandardContext 的 backgroundProcess 方法中实现的,这个方法的代码如下:

清单 15. StandardContext. backgroundProcess

12345678910111213141516171819202122232425262728public void backgroundProcess() {`if (!started) return;count = (count + 1) % managerChecksFrequency;if ((getManager() != null) && (count == 0)) {try {getManager().backgroundProcess();} catch (Exception x) {log.warn(“Unable to perform background process on manager”,x);}}if (getLoader() != null) {if (reloadable && (getLoader().modified())) {try {Thread.currentThread().setContextClassLoader(StandardContext.class.getClassLoader());reload();} finally {if (getLoader() != null) {Thread.currentThread().setContextClassLoader(getLoader().getClassLoader());}}}if (getLoader() instanceof WebappLoader) {((WebappLoader) getLoader()).closeJARs(false);}}}`

它会调用 reload 方法,而 reload 方法会先调用 stop 方法然后再调用 Start 方法,完成 Context 的一次重新加载。可以看出执行 reload 方法的条件是 reloadable 为 true 和应用被修改,那么这个 backgroundProcess 方法是怎么被调用的呢?

这个方法是在 ContainerBase 类中定义的内部类 ContainerBackgroundProcessor 被周期调用的,这个类是运行在一个后台线程中,它会周期的执行 run 方法,它的 run 方法会周期调用所有容器的 backgroundProcess 方法,因为所有容器都会继承 ContainerBase 类,所以所有容器都能够在 backgroundProcess 方法中定义周期执行的事件。

Wrapper 容器

Wrapper 代表一个 Servlet,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。

Wrapper 的实现类是 StandardWrapper,StandardWrapper 还实现了拥有一个 Servlet 初始化信息的 ServletConfig,由此看出 StandardWrapper 将直接和 Servlet 的各种信息打交道。

下面看一下非常重要的一个方法 loadServlet,代码片段如下:

清单 16. StandardWrapper.loadServlet

1234567891011121314151617181920212223242526272829303132333435363738394041public synchronized Servlet loadServlet() throws ServletException {`………Servlet servlet;try {………ClassLoader classLoader = loader.getClassLoader();………Class classClass = null;………servlet = (Servlet) classClass.newInstance();if ((servlet instanceof ContainerServlet) &&(isContainerProvidedServlet(actualClass) ((Context)getParent()).getPrivileged())) {((ContainerServlet) servlet).setWrapper(this);}classLoadTime=(int) (System.currentTimeMillis() -t1);try {instanceSupport.fireInstanceEvent(InstanceEvent.BEFORE_INIT_EVENT,servlet);if(System.getSecurityManager() != null) {Class[] classType = new Class[]{ServletConfig.class};Object[] args = new Object[]{((ServletConfig)facade)};SecurityUtil.doAsPrivilege(“init”,servlet,classType,args);} else {servlet.init(facade);}if ((loadOnStartup >= 0) && (jspFile != null)) {………if(System.getSecurityManager() != null) {Class[] classType = new Class[]{ServletRequest.class,ServletResponse.class};Object[] args = new Object[]{req, res};SecurityUtil.doAsPrivilege(“service”,servlet,classType,args);} else {servlet.service(req, res);}}instanceSupport.fireInstanceEvent(InstanceEvent.AFTER_INIT_EVENT,servlet);………return servlet;}`

它基本上描述了对 Servlet 的操作,当装载了 Servlet 后就会调用 Servlet 的 init 方法,同时会传一个 StandardWrapperFacade 对象给 Servlet,这个对象包装了 StandardWrapper,ServletConfig 与它们的关系图如下:

图 13. ServletConfig 与 StandardWrapperFacade、StandardWrapper 的关系

Servlet 可以获得的信息都在 StandardWrapperFacade 封装,这些信息又是在 StandardWrapper 对象中拿到的。所以 Servlet 可以通过 ServletConfig 拿到有限的容器的信息。

当 Servlet 被初始化完成后,就等着 StandardWrapperValve 去调用它的 service 方法了,调用 service 方法之前要调用 Servlet 所有的 filter。

Tomcat 中其它组件

Tomcat 还有其它重要的组件,如安全组件 security、logger 日志组件、session、mbeans、naming 等其它组件。这些组件共同为 Connector 和 Container 提供必要的服务。

正文完
 0