Java后端开发 - Servlet

Servlet生命周期与过滤器

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

  • Servlet 通过调用 init () 方法进行初始化;

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

  • Servlet 调用 service() 方法来处理客户端的请求;

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

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

  • Servlet 通过调用 destroy() 方法终止(结束);

  • 最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。

Servlet中的过滤器Filter是实现了javax.servlet.Filter接口的服务器端程序,只要你在web.xml 文件配置好要拦截的客户端请求,它都会帮你拦截到请求。Filter可认为是Servlet的一种“变种”,它主要用于对用户请求进行预处理,也可以对HttpServletResponse进行后处理,是个典型的处理链。它与Servlet的区别在于:它不能直接向用户生成响应。完整的流程是:Filter对用户请求进行预处理,接着将请求交给Servlet进行处理并生成响应,最后Filter再对服务器响应进行后处理。

Session和Cookie的区别

Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;

Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。

Servlet的异步请求

  • Servlet3.0的异步请求

    Servlet3.0规范2.3.3.3节的“Asynchronous processing”讲述了异步处理,当启用异步处理时,响应数据可以由其他线程创建,而当前线程可以立即交还给Servlet容器处理其他请求。当要启用异步处理时,首先需要声明HttpServlet支持异步处理,然后才能调用request.startAsync方法获得AsyncContext,它包含requestresponse。调用startAsync方法后,程序能保证在退出service方法时,不会完成本次请求,即不响应任何数据给客户端。需要在之后调用AsyncContext.complete方法来完成本次请求,向客户端写入数据。

  • Servlet3.1的NIO

    Servlet3.1规范第3.7节“Non Blocking IO”讲述了非阻塞IO。Servlet 3.0对请求的处理虽然是异步的,但是对InputStream和OutputStream的IO操作却依然是阻塞的,对于数据量大的请求体或者返回体,阻塞IO也将导致不必要的等待,因此在Servlet 3.1中引入了非阻塞IO。

Tomcat架构

Tomcat的Servlet容器是用来处理请求Servlet资源,并为web客户端填充response对象的模块。在Tomcat中,共有四种类型的容器,分别是:Engine、Host、Context和Wrapper。Servlet容器是org.apache.catalina.Container接口的实例。Container接口及其相关类的UML类图如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
                                     +----------------+
| <<interface>> |
| Container |
+----------------+
^ ^
| |
| +---------------------------+
| |
+------------------+------------------+------------------+ |
| | | | |
+-------------+ +-------------+ +-------------+ +-------------+ +-------------+
|<<interface>>| |<<interface>>| |<<interface>>| |<<interface>>| |ContainerBase|
| Engine | | Host | | Context | | Wrapper | | |
+-------------+ +-------------+ +-------------+ +-------------+ +-------------+
^ ^ ^ ^ ^
| | | | |
| +---------------^---+---------------^-----+---------------^---+---------+
| | | | | | | |
+--------------+ INC +-------------+ INC +---------------+ INC +---------------+
|StandardEngine| <>-- | StandartHost| <>-- |StandardContext| <>-- |StandardWrapper|
+--------------+ 1 n +-------------+ 1 n +---------------+ 1 n +---------------+

Tomcat的Pipeline管道和Valve阀的工作机制类似于Servlet的过滤器,在Servlet容器的管道中,有一个基础阀,也可以添加任意数量的阀,基础阀总是最后一个执行。

当Tomcat启动时,相关组件也会一起启动,Tomcat关闭时,这些组件也会随之关闭,这是通过实现Lifecycle接口达到统一启动/关闭组件的效果。实现了Lifecycle接口的组件可以触发一个或多个事件:BEFORE_START_EVENTSTART_EVENTAFTER_START_EVENTBEFORE_STOP_EVENTSTOP_EVENTAFTER_STOP_EVENT,事件是LifecycleEvent类的实例,事件监听器是LifecycleListener类的实例,LifecycleSupport提供了一种简单的方法来触发某个组件的生命周期事件,并对事件监听器进行处理。

Tomcat载入器是Loader接口的实例,WebappLoader是负责从WEB-INF/classesWEB-INF/lib载入Servlet所需类的载入器,它内部会使用WebappClassLoader作为类载入器(或类加载器)。Loader接口使用modified()方法来支持类的自动重载,在载入器的具体实现中,如果仓库(Loader.addRepository())中的一个或多个类被修改,那么modified()方法必须返回true,载入器内部会调用Context.reload完成类重新载入的过程。载入类时,WebappClassLoader遵循如下规则:

  • 因为所有已经载入的类都会缓存起来,所以载入类时要先检查本地缓存;
  • 若本地缓存中没有,则检查上一层缓存,即调用java.lang.ClassLoader类的findLoadedClass()方法;
  • 如果两个缓存中都没有,则使用系统的类载入器ApplicationClassLoader进行加载,防止Web应用程序中的类覆盖Java EE的类;
  • 若启用了SecurityManager,则检查是否运行载入该类。若该类是禁止载入的类,抛出ClassNotFoundException异常;
  • 若打开标志位delegate,或者待载入的类是属于包触发器WebappClassLoader.packageTriggers中的包名,则调用父类载入器来载入相关类。如果父类载入器为null,则使用系统的类载入器;
  • 从当前仓库中载入相关类;
  • 若当前仓库中没有需要的类,且标志位delegate关闭,则使用父类载入器。若父类载入器为null,则使用系统的类载入器进行加载;
  • 若仍未找到需要的类,则抛出ClassNotFoundException异常。

Tomcat使用Session管理器的组件来管理建立的Session对象,该组件由Manager接口表示。Session管理器需要与一个Context容器相关联,且必须与一个Context容器关联。Session管理器负责创建、更新、销毁Session对象,当有请求到来时,要返回一个有效的Session对象。Servlet实例通过调用javax.servlet.http.HttpServletRequest.getSession()方法获取一个Session对象。在Tomcat的默认连接器中,Request类实现HttpServletRequest接口。Session管理器会将其所管理的Session对象放在内存中,或存储在文件中,或通过JDBC写入数据库中。Session对象是Session接口的实例,实现类是StandardSessionStandardManagerManager接口的实现类,session管理器需要负责销毁已经失效的Session对象,在ManagerBase类的backgroundProcess方法负责过期Session对象的检查与销毁。Store接口是为Session管理器的Session对象提供持久化存储器的一个组件,Store接口两个重要的方法是saveloadStoreBase是一个抽象类,提供了基本功能,该类的两个子类是FileStoreJDBCStore

一般情况下一个Context容器包含一个或多个Wrapper,而一个Wrapper对应一个HttpServletStandardWrapperWrapper接口的实现类,它的基础阀StandardWrapperValve会调用Servlet的过滤器和Servlet的service方法。连接器接收到HTTP请求后,方法调用的协作图如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1:[new request] create() ->
2:[new response] create() ->
+----+
Message1 -> | | 3:invoke() -> 4:invoke() ->
+-----------+ +-----------------+ +-----------------------+
---------| Connector | ----------| StandardContext |-------------|StandardContextPipeline|
+-----------+ +-----------------+ +-----------------------+
5:invoke() |
| |
<-7:invoke() <-6:invoke() v |
+----------------------+ +-----------------+ +----------------------+
| StandardWrapperValve |----------| StandardWrapper |---------| StandardContextValve |
+----------------------+ +-----------------+ +----------------------+
|
8:allocate() | 11:service()
| | |
v | v
+----------------------+ 9:load()->
| Servlet |---------------
+----------------------+ 10:init()->

协作图过程描述如下:

  • 1.连接器创建request和response对象;
  • 2.连接器调用StandardContext实例的invoke()方法;
  • 3.StandardContext实例的invoke()方法调用其管道对象的invoke()方法。StandardContext中管道对象的基础阀是StandardContextValve类的实例,因此,StandardContext的管道对象会调用StandardContextValve实例的invoke()方法;
  • 4.StandardContextValve实例的invoke()方法获取相应的Wrapper实例处理HTTP请求。调用Wrapper实例的invoke()方法;
  • 5.StandardWrapper类是Wrapper接口的标准实现,StandardWrapper实例的invoke()方法会表用其管道对象的invoke方法;
  • 6.StandardWrapper的管道对象中的基础阀是StandardWrapperValve类的实例,因此,会调用StandardWrapperValveinvoke()方法,StandardWrapperValveinvoke()方法调用Wrapper实例的allocate()方法获取Servlet实例;
  • 7.allocate()方法调用load()方法载入相应的Servlet类,若已经载入,则无需重复载入;
  • 8.load()方法调用Servlet实例的init()方法;
  • 9.StandardWrapperValve调用Servlet实例的service()方法。

FilterDef表示一个过滤器的定义,用来说明web.xml部署描述符文件中定义的过滤器。ApplicationFilterConfig类实现javax.servlet.FilterConfig接口,用于管理Web应用程序第一次启动时创建的所有的过滤器实例,其getFilter()方法会返回一个javax.servlet.Filter对象。ApplicationFilterChain类实现javax.servlet.FilterChain接口,StandardWrapperValve类的invoke()方法会创建ApplicationFilterChain类的一个实例,并调用其doFilter()方法。ApplicationFilterChain类的doFilter()方法会调用过滤器链中第一个过滤器的doFilter方法。如果某个过滤器时过滤器链中的最后一个过滤器,则会调用被请求的Servlet类的service()方法。

Host接口表示一个虚拟主机,其实现类是StandardHostEngine表示Servlet引擎,其实现类是StandardEngineHost的父容器只能是Engine,而Engine是顶级容器,不再有父容器,且子容器只能是HostServer表示整个Servlet引擎,其实现类是StandardServerServer几个重要方法是initialize()start()stop()await()addService()initialize()方法用来初始化通过addService()方法添加到Server中的服务组件。await()方法则是创建一个ServerSocket,等待客户端连接并发送关闭指令,如果收到关闭指令,则调用stop()方法关闭Server,即整个Servlet引擎。Service接口表示一个服务,其实现类是StandardService类。Service通过setContainer()方法关联一个容器,通过addConnector()方法,关联多个连接器。StandardService内部会将其容器通过Connector.setContainer方法设置Service的容器给Connector

Tomcat的总体架构图如下:

Tomcat架构

当一个处理请求时,连接器Connector创建RequestResponse对象,通过Mapper类来选择相应的ContextWrapper来处理请求。

Tomcat使用Digester库来解析%CATALINA_HOME%/conf/server.xml文件和web应用的web.xml文件。ContextConfig类的实例负责并解析读取web应用的web.xml文件,为每个Servlet元素创建一个StandardWrapper实例。ContextConfig实现了LifecycleListener接口,被添加到StandardContext作为其事件监听器。每次StandardContext实例触发事件时,会调用ContextConfig实例的lifecycleEvent()方法,该方法会调用ContextConfig.start()方法来解析web.xml文件。如果文件解析成功,调用Context.setConfigured(true)方法,使得Context可以正常启动,否则启动失败。

Tomcat的启动类是Bootstrap,其start方法首先判断是否存在Catalina实例,如果存在则初始化一个Catalina实例,然后调用Catalina.start()解析%CATALINA_HOME%/conf/server.xml文件和初始化ServerCatalina会调用Runtine.addShutdownHook()方法来注册一个CatalinaShutdownHook类型的关闭钩子,用于安全的释放资源。

StandardHost实例使用HostConfig对象作为一个生命周期监听器,当StandardHost对象启动时,它的start()方法会触发一个START事件。为了响应START事件,HostConfig中的lifecycleEvent方法和HostConfig中的事件处理程序调用start()方法。HostConfig类的deployApps()会完成Host下各个Context的部署工作。

延伸阅读