每一个Java程序员都知道Servlet的重要性,在Java Web 开发中,程序员一定避不开 Servlet,很多 Java Web 框架都是基于 Servlet 来构建的,最终开发完成的应用也一定要在Servlet 容器里运行。Java 程序员应该都知道,Servlet API 提供了一套底层的 API 来处理 HTTP 请求和响应。所以,了解Servlet的API以及规范对于Java开发人员来说至关重要。
本文内容主要是作者在学习Servlet3.1规范的过程中记录的学习笔记,并且有些在规范中解释的并不是很清楚的地方作者参考了其他资料进行了总结。
什么是Servlet
Servlet(Server Applet) 是基于 Java 技术的 web 组件,该组件由容器托管,用于生成动态内容。他是用Java编写的服务器端程序。其主要功能在于交互式地浏览和修改数据,生成动态Web内容。
什么是 Servlet 容器
Servlet容器是web server或application server的一部分,供基于请求/响应发送模型的网络服务,解码基
于 MIME 的请求,并且格式化基于 MIME 的响应。Servlet 容器也包含并管理 Servlet 生命周期。
所有 Servlet 容器必须支持基于 HTTP 协议的请求/响应模型,比如像基于 HTTPS(HTTP over SSL)协议的 请求/应答模型可以选择性的支持。容器必须实现的 HTTP 协议版本包含 HTTP/1.0 和 HTTP/1.1。
Servlet 生命周期
Servlet 通过一个定义良好的生命周期来进行管理,该生命周期规定了 Servlet 如何被加载、实例化、初始化、 处理客户端请求,以及何时结束服务。该生命周期可以通过
javax.servlet.Servlet
接口中的init
、service
和destroy
这些 API 来表示,所有 Servlet 必须直接或间接的实现GenericServlet
或HttpServlet
抽象类。
Servlet的生命周期有四个阶段:加载并实例化、初始化、请求处理、销毁。主要涉及到的方法有init
、service
、doGet
、doPost
、destory
等
加载并实例化
Servlet容器负责加载和实例化Servelt。当Servlet容器启动时,或者在容器检测到需要这个Servlet来响应第一个请求时,创建Servlet实例。当Servlet容器启动后,Servlet通过类加载器来加载Servlet类,加载完成后再new一个Servlet对象来完成实例化。
初始化
在Servlet实例化之后,容器将调用init()
方法,并传递实现ServletConfig接口的对象。在init()
方法中,Servlet可以部署描述符中读取配置参数,或者执行任何其他一次性活动。在Servlet的整个生命周期类,init()
方法只被调用一次。
请求处理
当Servlet初始化后,容器就可以准备处理客户机请求了。当容器收到对这一Servlet的请求,就调用Servlet的service()
方法,并把请求和响应对象作为参数传递。当并行的请求到来时,多个service()方法能够同时运行在独立的线程中。service()
方法检查 HTTP 请求类型(GET、POST、PUT、DELETE 等),并在适当的时候调用 doGet()
、doPost()
、doPut()
,doDelete()
等方法。
一般情况下,当开发基于 HTTP 协议的 Servlet 时,Servlet 开发人员只关注自己的 doGet 和 doPost 请求处理 方法即可。其他方法被认为是非常熟悉 HTTP 编程的程序员使用的方法。
销毁
一旦Servlet容器检测到一个Servlet要被卸载,这可能是因为要回收资源或者因为它正在被关闭,容器会在所有Servlet的service()
线程之后,调用Servlet的destroy()
方法。然后,Servlet就可以进行无用存储单元收集清理。这样Servlet对象就被销毁了。这四个阶段共同决定了Servlet的生命周期。
Servlet的线程安全问题
为了有效利用JVM允许多个线程访问同一个实例的特性,来提高服务器性能。在非分布式系统中,Servlet容器只会维护一个Servlet的实例。
如果 Web 应用中的 Servlet 被标注为分布式的,容器应该为每一个分布式应用程序的 JVM 维护一个 Servlet 实例池。
Servlet容器通过维护一个线程池来处理多个请求,线程池中维护的是一组工作者线程(Worker Thread)。Servlet容器通过一个调度线程(Dispatcher Thread)来调度线程池中的线程。
当客户端的servlet请求到来时,调度线程会从线程池中选出一个工作者线程并将请求传递给该线程,该线程就会执行对应servlet实例的service方法。同样,当客户端发起另一个servlet请求时,调度线程会从线程池中选出另一个线程去执行servlet实例的service方法。Servlet容器并不关心这些线程访问的是同一个servlet还是不同的servlet,当多个线程访问同一个servlet时,该servlet实例的service方法将在多个线性中并发执行。
所以,Servlet对象是单实例多线程,Servlet不是线程安全的
为什么不安全?
先看两个定义:
实例变量:实例变量在类中定义。类的每一个实例都拥有自己的实例变量,如果多个线程同时访问该实例的方法,而该方法又使用到实例变量,那么这些线程同时访问的是同一个实例变量,会共享该实例变量。
局部变量:局部变量在方法中定义。每当一个线程访问局部变量所在的方法时,在线程的堆栈中就会创建这个局部变量,线程执行完这个方法时,该局部变量就被销毁。所有多个线程同时访问该方法时,每个线程都有自己的局部变量,不会共享。
看如下代码:
public class MyServlet extends HttpServlet{
private static final long serialVersionUID = 1L;
private String userName1 = null;//实例变量
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException{
userName1 = req.getParameter("userName1");
String userName2 = req.getParameter("userName2");//局部变量
//TODO 其他处理
}
}
userName1则是共享变量,多个线程会同时访问该变量,是线程不安全的。
userName2是局部变量,不管多少个线程同时访问,都是线程安全的。
解决Servlet的线程安全问题
如果不涉及到全局共享变量,就直接使用局部变量
如果使用到全局共享的场景,可以使用加锁的方式.对全局变量的读写操作置于synchronized同步块中,这样不同线程排队依次执行该代码块,从而避免线程不安全情况发生。还可以使用线程安全的数据类型。比如hashtable,blockQueue等