SRV.2.3 Servlet生命周期
servlet有着定义良好且明确的生命周期,它定义了servlet以怎样的方式加载和实例化、初始化、怎样处理客户端请求、以及怎样停止服务。生命周期表现在API中就是javax.servlet.Servlet接口的init、service和destroy三个方法。所有servlet必须直接或者通过GenericServlet/HttpServlet间接实现这些方法。
SRV.2.3.1 加载和实例化
servlet容器负责加载和实例化servlet。加载和实例化可以发生在容器启动时,或者发生在容器判断出servlet需要处理请求时。
servlet引擎启动时,一些基础的必需型servlet必须被定位、找到,然后servlet容器通过正常的Java类加载机制来加载这些servlet。加载可能源于本地文件系统,也可能是远程文件系统,或者是网络服务。
servlet类加载完成后,容器对其进行实例化以备使用。
SRV.2.3.2 初始化
servlet对象实例化之后,容器必须初始化servlet才能用于处理客户端请求。初始化的过程主要用于servlet读取持久化的配置信息,初始化比较耗时的资源如jdbc等,以及执行其他的一次性操作。容器通过调用Servlet接口的init方法来完成servlet实例的初始化,当然,调用init方法时需要传入一个全局唯一配置参数,该参数必须实现了ServletConfig接口。这个配置对象允许servlet从Web 应用程序的配置信息中访问一些名-值对形式的初始化参数。这个配置对象也允许servlet访问servlet运行时环境的一个实现了ServletContext接口的特定对象。关于ServletConfig接口的更多信息请看第三章SRV.3中“Servlet Context”一节。
SRV.2.3.2.1 初始化出错的情况
初始化期间,servlet实例可能会抛出UnavailableException或ServletException。此时,该servlet就不允许提供服务并且servlet容易必须释放它。这时destroy方法是不会被调用的,因为这个servlet根本就没有初始化成功。
初始化失败后,容器可以尝试再次实例化并初始化一个新的实例。当然,如果UnavailableException指明了unavailability的最小不可用时间后,容器必须等过了这段时间后才能创建和初始化新的servlet实例。
SRV.2.3.2.2 关于工具的思考
当一个工具类加载和自检web应用系统时,它的静态初始化方法是完全不同于servlet的init方法的。直到Servlet接口的init方法被调用后,开发人员才应该把当前servlet列为已激活的容器运行时环境。例如,当静态初始化方法被调用时,servlet并不应该尝试建立与数据库或EJB容器的连接,而应该等到Servlet接口的init方法被成功执行后才行。
SRV.2.3.3 请求的处理
servlet正常初始化完成后,容器就可以用它来处理客户端请求了。客户端的请求用ServletRequest类型的实例对象来表示。servlet通过调用ServletResponse类型实例对象的对应方法来生成响应信息,并把响应信息作为Servlet接口中service方法的参数传递下去。
在http请求中,容器提供出来的代表客户端请求和响应信息的对象分别是HttpServletRequest和HttpServletResponse。
值得注意的是,被容器放在服务队列中的Servlet实例可能在它的整个生命周期中都不处理客户端请求。这个是很有可能的,例如整个声明周期中都没有请求过来,那它就不会被执行的。
SRV.2.3.3.1 多线程的问题
servlet容器可能发送多个并发请求给servlet的service方法。为了处理这些请求,serlvet开发人员必须仔细设计service方法,让它可以同时妥善处理这些请求。
开发人员可以通过实现SingleThreadModel接口来强制要求servlet容器去保证每次只有一个请求传给service方法,当然了,这种方式并不建议使用。servlet容器可以通过添加请求排队机制或者维护一个servlet实例池来满足对并发请求的处理要求。如果当前servlet是分布式应用系统的一部分,容器可能会针对分布式环境下的每一个JVM维护一个serlvet实例池,以处理并发请求。
对于没有实现SingleThreadModel接口的servlet,如果service已被定义为同步方法,那对应的servlet容器就不能再使用实例池机制,而只能添加请求排队机制了。强烈建议开发人员不要为service方法实现同步锁,因为这个同步锁会造成很大的性能影响。
SRV.2.3.3.2 请求处理期间的异常
在处理请求期间,servlet是很可能抛出ServletException或UnavailableException的。ServletException标志着处理请求期间发生了一些错误,此时servlet容器应该采取合适的措施清理掉这些请求。
UnavailableException标志着当前servlet临时或永久性的不能处理请求了。
如果UnavailableException明确指示出servlet永久性的出现故障,那么容器必须从服务列表中通过调用servlet的destroy方法去移除并释放servlet实例。然后凡是被容器拒绝的请求都必须返回给客户端一个404响应信息。
如果只是临时性的出现故障,容器可以不再把客户端请求传递给当前servlet,直到该servlet服务恢复正常为止。此时凡是被容器拒绝的请求都必须返回给客户端一个服务不可用(503)响应信息。
当然了,servlet容器完全可以不区分永久性和临时性故障,一视同仁的把所有UnavailableExceptions当作永久性故障,并随后从服务列表中移除出现故障的servlet。
SRV.2.3.3.3 线程安全性
请求和响应对象的实现方式并不是确保线程安全的。这意味着这些对象应该仅被用在请求处理线程的局部范围内。
请求和响应对象的引用绝不应该传递给当前正在执行的其他线程,以免对其他线程的执行结果造成干扰和不确定性。
如果应用创建的线程使用了由容器管理的对象,例如请求或响应对象,这些对象必须确保仅在servlet的服务周期内访问并且这样的线程在servlet的service方法内必须自己独立的生命周期,不然的话访问service方法执行结束后的这些对象很可能产生莫名其妙的各种问题。一定要做到请求和响应对象的线程安全性。如果这些对象不可避免的要在多线程下访问,那必须确保访问的方法已被同步或者对的封装添加线程安全控制,例如,同步一下访问这些请求属性的方法调用,或者在一个线程内使用本地局部变量去响应客户端请求。
SRV.2.3.4 服务结束
某些时间内容器并不需要一直保持着servlet处于服务列表内。一个servlet实例的服务时间并不是固定的,可能只保持一小段时间,或者一直到容器的生命周期终结,或者居于两者之间。
当容器判定一个servlet应该被移除时,它会调用Servlet接口的destroy方法告知servlet释放它使用的任何资源并保存数据。例如,容器很可能在清理内存资源时销毁不必要的servlet,或者是在容器停止服务时。
在servlet容器调用destroy方法前,它必须确保所有正在执行的线程顺利完成所有操作,或者直接等到他们超过时间限制为止。总之,只有所有servlet都执行完了或者已经超过时间限制了,servlet容器才能调用destroy方法销毁servlet。
一旦servlet实例的destroy方法被调用了,容器就不会再传递任何请求给当前servlet实例。如果容器又要启用这个servlet,它也只能等到新的servlet实例启动完成后才传递请求过来。
destroy方法执行完成后,servlet容器必须释放servlet实例,然后垃圾回收机制才会适时回收servlet占用的资源和空间。