前段时间看了Spring MVC的源码,感觉有些吃力,其中有些东西如果熟悉Servlet的话就很容易明白了,所以抽出两天时间把Servlet比较系统的学习了一遍。总结一下学习笔记。
一、Servlet的定义及作用:
首先来回顾一下Servlet的定义:Servlet就是一个实现了特殊接口的Java类,它由支持Servlet的WEB服务器(具有Servlet引擎)调用和执行。一个Servlet负责处理他所对应的一个或者一组URL地址的访问请求,并用于接收客户端发出的访问请求信息和产生响应的内容。(Servlet的命名源于Applet的命名)。
接下来回顾一下Servlet的基本功能,大致总结一下有以下几点:
1. 获取客户端通过Html的FORM表单递交的数据和URL后面的参数信息。
2. 创建对客户端的响应 内容。
3. 访问服务器端的文件系统。
4. 链接并开发基于数据库的应用。
5. 调用其他的Java类。
二、Servlet的实现:
通过上面的回顾大体上明白了什么是Servlet和它的基本作用,接下来具体看一下Servlet的使用和实现原理:
通过Servlet的定义我们知道,一个Servlet就是一个实现了特殊接口的Java类,而这个特殊的接口即是javax.servlet.Servlet接口。
我们就来看一下Servlet这个借口的源码:
可以看到这个接口中声明了5个方法:init(),getServletConfig(),service(),getServletInfo()以及destroy().在后续的内容中会逐一提到并介绍上面的这5个接口。先看一下Servlet这个Interface的实现类:GenericServlet。而这个GenericServlet不仅仅实现了Servlet这一个接口,他同时还实现了ServletConfig, Serializable两个接口,继承Serializable接口是为了序列化只用,故不深究;但是GenericServlet也不是我们最终想要得到的编程所需的类,因为我们要向做web开发,那么就得加入一些支持Http协议的特性,所以进一步对GenericServlet进行具体化得到我们在web编程中常常用到的HttpServlet类;以上所述类的继承关系如下类图所示:
从上图中我们可以清晰的看到,如果要使一个类成为一个Servlet,那么它只需要继承GerericServlet或者HttpServlet即可,完全没有必要去继承Servlet接口。而我们在web编程中一般为了充分利用http协议的功能,所以常常继承HttpServlet类。
回到web编程中,我们知道,在Spring MVC框架编程中,一次Http请求进来之后会被DispatcherServlet处理并作相应的转发,通过查看Spring MVC的源代码会发现其实DispatcherServlet也是通过继承FrameworkServlet—>HttpServletBean—> HttpServlet这样的继承关系从而实现了servlet.jar中最原始的Servlet接口;这就说明在Spring MVC中一个请求的处理最终还是通过HttpServlet去处理的,没有另辟蹊径。而在最原始的Servlet编程模型中,访问一个Servlet时会调用HttpServlet的Service方法,所以我们常常在基于Servlet的web编程中覆盖service方法;以使我们编写的Servlet能够执行我们设计的逻辑。
对于Servlet的注册和运行这里这里简单提一下,在web.xml中一个<servlet>元素用于注册是一个Servlet,<servlet>包含两个主要的子元素:<servlet-name>和<servlet-class>他们分别用于指定Servlet的名称和Servlet的完整类名。另外一个<servlet-mapping>元素用于映射一个已注册的servlet的对外的访问路径。访问的路径支持使用*通配符,而“*.扩展名”的匹配优先级最低。
有一点这里需要提醒的是,一个web应用程序的描述符不仅仅包括该应用程序内部的web.xml的信息外,还包括web容器配置目录下的web.xml中的全部信息;例如Tomcat目录下conf目录下的web.xml文件中的全部配置信息。
明白了这点,我们就来看看Tomcat下自带的一些Servlet实例的访问路径:
http://localhost:8080/examples/servlet/<Servlet名称>;再看看这个例子的配置:
通过上面的配置我们可以发现访问这个路径的时候我们是去调用一个注册名称为invoker的Servlet来处理的,但是我们发现在我们应用程序内部的web.xml中压根儿就没用注册invoker,那么在何处注册的invoker?是在web容器配置目录下的web.xml中注册的!我们打开tomcat目录下的conf目录下的web.xml可以发现如下的配置:
通过查看Tomcat源码中的InvokerServlet类,我们发现这个注册名为invoker的Servlet的作用是去激活和调用其他的Servlet,所以被称为Servlet激活器。由于Servlet激活器吧请求URL中格的额外的路径信息当做某个Servlet的类名去进行调用,所以,在每个Web应用程序中为Servlet激活器所映射的路径可以各不相同。
另:如果某个servlet的路径只有一个正斜杠(/),那么这个Servlet就是当前web应用程序的默认servlet;这个默认的servlet也是在web容器中配置目录下的web.xml中进行配置的:
这个默认的servlet的作用是处理访问web服务器的某个静态的HTML文件或者图片,它会把静态资源的内容按字节读出来并传递给客户端;并且生成一些响应消息的头字段。
三、Servlet的生命周期
下面就来看看一个完整的Servlet的运行过程:
1.接收到访问某个Servlet的Http请求时,Servlet引擎首先检查是否已经创建并装载了该Servlet实例对象,如果已经装载了则直接跳过第2、3步进入第四步执行,否则执行第2步。
2.装载并创建一个Servlet的实例对象。
3.调用Servlet的init()方法,执行该实例对象的一些初始化操作。
4.创建一个用于HTTP请求消息的HttpServletRequest对象,一个用于http响应消息的HttpServletResponse对象,并调用service()方法,将创建好的请求和响应的对象作为参数传入进去。
5.当一个web应用程序被停止或者重新启动时,Servlet引擎将会调用Servlet的destroy()方法,以便在这个方法中执行扫尾工作,如释放被Servlet占用的资源。
以上五步描述了一个完整的Servlet的运行过程,也即Servlet的生命周期。
四、Servlet的线程安全问题
上次被同事问到Servlet和Struts Action之间的异同这个问题,当时只是知道Servlet不是线程安全的(其实Struts Action也是非线程安全的),但是具体到细节为什么不是线程安全的以及如何去避免就说不太清楚,所以这次回头学习Servlet时候就特意留心了一下。
Servlet引擎采用了多线程机制设计,即它会为每一个Servlet创建一个独立的线程去做响应,但是当出现多个请求同时并发访问一个Servlet时会出现并发调用统一Servlet的service()方法的情况,即会出现线程的安全问题。
避免该问题一般有以下几种解决办法:
1. 实现SingleThreadModel接口
通过查看Servlet源码我们发现SingleThreadModel已经被标记为过时,这就说明这种方法去解决线程安全问题不可取,我们先来看看这种方法:
当一个Servlet继承了SingleThreadModel方法后,当调用该Servlet的service()时采用单线程方式。这样就保证了单个Servlet实例对象的service()不可能同时被多个线程调用。但是servlet引擎依然支持该Servlet多线程并发访问,采用的方式是为每个调用线程分配一个独立的servlet实例对象;这多个实例对象组成了一个servlet对象池。这种方式并不能真正意义上解决Servlet的线程安全问题,因为这样的避免方案其实是创建了多个Servlet的实例对象;而真正的多线程安全问题是指一个servlet实例对象被多个线程同时调用时的问题;况且这样也引起了一些不必要的系统开销。
2. 同步对共享数据的操作
使用synchronized 关键字保证一次只有一个线程访问被保护的代码段,这样就避免了并发访问带来的线程安全问题。但是为保证主存内容和线程的工作内存中的数据的一致性,要频繁地刷新缓存,这也会大大地影响系统的性能。所以在实际的开发中也应避免或最小化 Servlet 中的同步代码段。
3. 避免使用实例变量
线程安全问题有的时候是由实例变量造成的,所以只要在重写Servlet的时候在其任何方法里面都不使用实例变量,那么该Servlet就可以避免由实例变量造成的线程安全问题。在Servlet中避免使用实例变量是保证Servlet线程安全的最佳选择。
另:Struts中Action的线程安全问题也可参考以上三点去避免。
五、关于ServletConfig接口
Servlet程序是发布到web应用程序中运行的,此时对于这个Servlet来说这个web应用程序就是Servlet容器;Servlet是在Servlet容器中运行的程序,他在有些情况下可能需要访问Servlet容器或借助Servlet容器访问外部的资源,所以,Servlet引擎需要将表示Servlet容器的对象传递给Servlet。另外,在web.xml文件中为某个Servlet设置的名称和初始化参数等信息也需要传递给该Servlet。
Servlet引擎将代表Servlet容器的对象以及其配置参数等相关信息封装成一个ServletConfig对象并在初始化的时候传入该Servlet;我们来看看ServletConfig类:
还记得在Servlet中有一个方法是getServletConfig (),这个方法就用于返回在Servlet的init(ServletConfig config)方法中传入的ServletConfig对象的引用。而这个方法就是将咱们自己写的Servlet组装到Servlet容器管辖下的关键步骤;即每个程序员设计的Servlet只是一个散装的处理模块,而Servlet引擎提供了通过一些配置信息把这些散装的Servlet组装起来的机制。
下面来看看ServletConfig接口中的几个方法:
1. 首先来看getInitParameterNames()方法,它用于返回一个Enumeration集合对象,该集合对象包含了在web.xml中为当前Servlet设置的所有初始化参数的名称。
2. 再来看getInitParameter()方法;该方法用于返回在web.xml中为Servlet所设置的某个名称的初始化参数的值;如果指定名称的初始化参数不存在,则返回NULL.
3. getServletName():用于返回Servlet在web.xml文件中的注册名称。
4. getServletContext():在Servlet的web项目中,每个web应用程序都用一个自身持有的ServletContext对象来表示,ServletConfig对象中包含了ServletContext对象的引用,getServletContext方法用于返回ServletConfig对象中所包含的ServletContext对象的引用。
六、关于ServletContext接口
Servlet引擎为每个Servlet程序都创建一个对应的ServletContext对象,ServletContext对象被包含在ServletConfig对象中,调用ServletConfig.getServletContext()方法可以返回ServletContext对象的引用。
首先来看一下ServletContext对象都提供了哪些可用的方法:
通过对如上方法大致进行分类,ServletContext对象提供的方法有如下几类:
1. 获取web应用程序初始化参数
2. 记录日志
3. Application域范围的属性
4. 访问资源文件
5. 获取虚拟路径所映射的本地路径
6. Web应用程序之间的访问
7. 获取所支持Servlet API版本类型信息
七、小结
以上简单总结了Servlet的一些知识,个别常见的内容没有提到,例如doGet()和doPost()方法的使用以及实现原理等;另外围绕HttpServletRequest和HttpServletResponse相关内容虽然也有深入的去过原代码并进行梳理记录,但由于这两块内容要和Http协议结合起来总结才能说清楚,内容实在太多,现在没有时间去整理了,有时间再续一篇。