3.3 过滤器
过滤器是Java组件,它允许动态改变进入资源的请求和从资源返回的响应的有效载荷(Payload)和头(Header)信息。
Java Servlet API类和方法提供了用于过滤动态和静态内容的轻量级框架。它描述了如何在Web应用程序中配置过滤器,以及实现过滤器的约定和语义。
3.3.1 什么是过滤器
过滤器是一组可重用的代码,能转换HTTP请求、响应和头信息的内容。过滤器通常不产生响应或者像Servlet那样对请求做出响应,而是修改或者调整对资源的请求,修改和调整来自资源的响应。
过滤器可以作用在动态或静态内容上。动态和静态内容指的是Web资源。
过滤器可以用于以下场景:
- 对资源的请求执行之前访问资源。
- 对资源的请求之前对请求进行处理。
- 通过对请求对象进行自定义版本包装来对请求头和数据进行修改。
- 提供自定义版本的响应对象来修改响应头和响应数据。
- 调用资源后对其进行侦听。
- 在一个Servlet、一组Servlet或静态内容上按照指定的顺序执行0个、1个或多个过滤器。
在现实项目中,经常使用过滤器来实现以下功能:
- 验证过滤器。
- 日志和审计过滤器。
- 图片转换过滤器。
- 数据压缩过滤器。
- 加密过滤器。
- 标记化过滤器。
- 触发资源访问事件过滤器。
- 转换XML内容的XSL/T过滤器。
- MIME-type链过滤器。
- 缓存过滤器。
应用开发者通过实现javax.servlet.Filter接口并且提供一个无参公用构造器创建过滤器。此类跟构建Web应用的静态内容和Servlet一起打包进Web归档中。过滤器在部署描述符中使用<filter>元素声明。一个过滤器或过滤器集合可以通过在部署描述符里定义<filter-mapping>元素来配置调用。通过映射Servlet的逻辑名称把过滤器映射到一个特别的Servlet,通过映射一个URL模式将过滤器映射到一组Servlet和静态内容资源来完成配置。
3.3.2 过滤器生命周期
Web应用部署之后,在请求导致容器访问Web资源之前,容器必须定位应用到Web资源的过滤器列表。容器必须保证列表中的每一个过滤器(元素)都实例化了一个适当类的过滤器(对象),然后调用它的init(FilterConfig config)方法。过滤器可能会抛出一个异常,表明它不能正常运转。如果异常是UnavailableException类型的,容器就可以检查异常的isPermanent属性,并选择将来某个时候重试该过滤器。
在部署描述符中声明的每个<filter>在容器的每个JVM中只实例化一个实例。容器提供在过滤器部署描述符中声明的过滤器配置、Web应用的ServletContext的引用和一组初始化参数。
当容器接收到一个进入的请求时,容器获取列表中的第一个过滤器的实例并且调用它的doFilter,传入ServletRequest和ServletResponse,以及一个它将使用的FilterChain的引用。过滤器的doFilter方法通常按照下面模式的子集来实现:
- 该方法检查请求的头。
- 为了修改请求头或数据,该方法可能使用ServletRequest或HttpServletRequest的自定义实现来包装请求对象。
- 该方法可能用ServletResponse或HttpServletResponse的自定义实现来包装响应对象,传入doFilter方法来修改响应头或数据。
- 该过滤器可能会调用过滤器链中的下一个实体。下一个实体可能是另一个过滤器,如果执行调用的过滤器是此过滤器链中在部署描述符中为其配置的最后一个过滤器,下一个实体就是目标Web资源。FilterChain对象调用doFilter方法将影响下一个实体的调用并传入它被调用时的request和response或者传入它可能已创建的包装版本。过滤器链的doFilter方法的实现由容器提供,必须定位过滤器链中的下一个实体并且调用它的doFilter方法,传入恰当的request和response对象。或者,过滤器链可以通过不调用下一个实体阻塞请求,由离开的过滤器负责填充响应对象。service方法必须与应用到Servlet的所有过滤器运行在相同的线程中。
- 链中的下一个过滤器调用之后,该过滤器可能检查响应的头。
- 过滤器可以抛出一个异常表明一个错误正在处理。如果过滤器在其doFilter处理中抛出了一个UnavailableException,容器不应该尝试继续处理剩下的过滤器链,如果异常没有标识为永久的,它就可能选择晚些时候重试整个过滤器链。
- 当链中的最后一个过滤器被调用时,下一个访问的实体是目标Servlet或者位于链尾的资源。
- 在一个过滤器实例可以被容器从服务中移除之前,容器必须首先调用过滤器的destroy方法使过滤器释放所有资源和执行其他清除操作。
3.3.3 包装请求和响应
过滤的核心概念是包装请求和响应以便它能覆盖行为执行过滤任务。在此模型中,开发者不仅有能力覆盖请求和响应对象已存在的方法,也能提供新的API对链中剩余的过滤器或目标Web资源执行特别过滤任务。例如,开发者可能希望使用更高级输出对象(output stream或writer)来扩展响应对象,比如允许DOM对象被写回客户端的API。
为了支持这种风格的过滤器,容器必须满足下面的要求。当过滤器调用容器的过滤器链实现的doFilter方法时,容器必须确保传递给过滤器链的下一个实体或目标Web资源(如果此过滤器是链中的最后一个)的request和response对象与传入调用过滤器的doFilter方法的(request和response)对象相同。
当调用者包装request或response对象时,对包装对象标识的要求同样适用于从Servlet或Filter到RequestDispatcher.forward或RequestDispatcher.include的调用。
3.3.4 过滤器环境
通过在部署描述符中使用<init-params>元素可以将一组初始化参数与过滤器关联。通过过滤器的FilterConfig对象的getInitParameter和getInitParameterNames方法,过滤器可以在运行时使用这些参数的名称和值。除此之外,FilterConfig提供对Web应用的ServletContext的访问来加载资源、记录日志和在ServletContext的属性列表中存储状态。
3.3.5 Web应用中过滤器的配置
过滤器既可以通过@WebFilter注解来定义,又可以通过在部署描述符中使用<filter>来定义。在此元素中,开发人员可以进行以下声明:
- filter-name:用来映射过滤器到Servlet或URL。
- filter-class:容器用来识别过滤器类型。
- init-params:过滤器的初始化参数。
容器必须为在部署描述中声明的每一个过滤器准确实例化一个定义该过滤器的Java类的实例。因此,如果开发者为同一个过滤器类做了两个过滤器声明,容器就会实例化两个相同过滤器类的实例。
这里有一个过滤器声明的例子:
一旦过滤器在部署描述中声明,组装者使用<filter-mapping>元素定义此过滤器将应用在此Web应用中的哪些Servlet和静态资源上。使用<servlet-name>元素,过滤器可以关联一个Servlet。例如,下面的代码示例将映射Image Filter过滤器到ImageServlet上:
使用<url-pattern>风格的过滤器映射,过滤器可以关联一组Servlet和静态内容:
在此,Logging过滤器被应用到Web应用中所有的Servlet和静态内容页面,因为每个请求URI都匹配“/*”URL模式。
容器为特定请求URI创建应用到它的过滤器链的顺序如下:
- 首先,按照这些元素在部署描述符中出现的顺序,<url-pattern>匹配过滤器映射。
- 接下来,按照这些元素在部署描述符中出现的顺序,<servlet-name>匹配过滤器映射。
如果一个过滤器映射同时包含<servlet-name>和<url-pattern>,容器就必须展开此过滤器映射为多个过滤器映射(每个<servlet- name>和<url-pattern>一个映射),保留<servlet-name>和<url-pattern>元素的顺序。例如下面的过滤器映射:
3.3.6 过滤器和请求分派器
从Java Servlet规范新版本2.4起可以配置请求分派器forward()和include()调用时被调用的过滤器。在部署描述符中使用新的<dispatcher>元素,开发者可以为filter-mapping指明希望此拦截器应用到的请求:
- 直接来自客户端的请求。可以由带有REQUEST值的<dispatcher>元素或者没有任何<dispatcher>元素表示。
- 该请求正在请求分配器下处理,该分配器代表使用forward()调用与<url-pattern>或<servlet-name>匹配的Web组件。这由具有值FORWARD的<dispatcher>元素指示。
- 该请求正在请求分配器下处理,该分配器代表使用include()调用与<url-pattern>或<servlet-name>匹配的Web组件。这由具有值INCLUDE的<dispatcher>元素指示。
- 对匹配<url-pattern>的错误资源,使用错误页面机制来处理请求。用一个带有值ERROR的<dispatcher>元素表示。
- 正在使用异步上下文分派机制时,使用分派调用将请求处理到Web组件。这由带有值ASYNC的<dispatcher>元素指示。
例如:
以/products/...开始的客户端请求将导致Logging Filter被调用,但在以路径开头为/products/...的请求分派器的请求分派调用时不会。Logging Filter在请求的初始分派和恢复请求时都会被调用。
下面的代码:
客户端对ProductServlet的请求和请求分派器forward()调用到ProductServlet时不会导致Logging Filter被调用,但是以ProductServlet开始的名字的请求分派器include()调用时会调用它。
下面的代码:
以/products/...开始的客户端请求和路径开头为/products/...的请求分派器在forward()调用时会导致Logging Filter被调用。
最后,下面的代码使用了特殊的Servlet名字“*”:
按照名字或路径获得的所有请求分派器forward()调用时,这些代码会导致All Dispatch Filter被调用。