一、Filter 基础
- Filter 是 Servlet 规范的三大组件之一。顾名思义,就是过滤。可以在请求到达目标资源之前先对请求进行拦截过滤,即对请求进行一些处理;也可以在响应到达客户端之前先对响应进行拦截过滤,即对响应进行一些处理
1.1 Filter 的生命周期
- Filter 的生命周期与 Servlet 的生命周期类似,其主要生命周期阶段有四个
- Filter 对象的创建
- Filter 对象的初始化
- Filter 执行 doFilter() 方法
- Filter 对象被销毁。
- Filter 的整个生命周期过程的执行,均由 Web 服务器负责管理。即 Filter 从创建到销毁的整个过程中方法的调用,都是由 Web 服务器负责调用执行,程序员无法控制其执行流程
1.1.1 代码测试
(1) 定义 Filter
- 在 Servlet 规范中,有一个 javax.servlet.Filter 接口。实现了该接口的类称为过滤器。该接口中有三个方法需要实现:
- init():初始化方法,即 Filter 被创建后,可以在此处进行后面所需资源的初始化工作
- doFilter():Filter 的核心方法,在此处进行请求和响应的过滤
- destroy():销毁方法。Filter 被销毁前所调用执行的方法。在此处进行资源的释放
(2) 注册 Filter
- 过滤器 Filter 与 Servlet、Listener 一样,也需要在 web.xml 中进行注册。其注册的标签结构与 Servlet 的非常相似,也需要指定该 Filter 可以过滤的请求类型
1.1.2 Filter 的特征
- Filter 是单例多线程的
- Filter 是在应用被加载时创建并初始化,这是与 Servlet 不同的地方。Servlet 是在该 Servlet 被第一次访问时创建。Filter 与 Servlet 的共同点是,其无参构造器与 init()方法只会执行一次
- 用户每提交一次该 Filter 可以过滤的请求,服务器就会执行一次 doFilter() 方法,即 doFilter() 方法是可以被多次执行的
- 当应用被停止时执行 destroy() 方法,Filter 被销毁。即 destroy() 方法只会执行一次
- 由于 Filter 是单例多线程的,所以为了保证其线程安全性,一般情况下是不为 Filter 类定义可修改的成员变量的。因为每个线程均可修改这个成员变量,会出现线程安全问题
1.2 FilterConfig
1.2.1 FilterConfig 的获取
- 与 ServletConfig 类似,FilterConfig 指的是当前 Filter 在 web.xml 中的配置信息。同样是一个 Filter 对象对应一个 FilterConfig 对象,多个 Filter 对象会有多个 FilterConfig 对象。
- 在 Web 容器调用 init() 方法时,Web 容器首先会将 web.xml 中当前 Filter 类的配置信息封装为一个 FilterConfig 对象。Web 容器会将这个对象传递给 init() 方法中的 FilterConfig 参数。也就是说,我们要获取 FilterConfig 对象,就需要像 Servlet 获取 ServletConfig 一样,在 Filter 类中声明一个 FilterConfig 成员变量,并在 init() 方法中赋值
1.2.2 FilterConfig 中的方法
- FilterConfig 接口中的方法与 ServletConfig 接口中的方法,方法名与意义完全相同
- Filter 在 web.xml 中进行配置时,可以指定多个初始化参数
1.2.3 代码测试
(1) 定义 OneFilter
(2) 注册 OneFilter
1.3 Filter 的
对于 Filter 的
的匹配路径设置问题,与 Servlet 的类似。不过,有两点需要注意: 1.3.1 全路径匹配不支持 /
对于 Servlet 中的
的全路径匹配设置方式,可以设置为 /,也可以设置为 /。但对于 Filter 的 的全路径匹配设置,只支持 /,而不支持 /。也就是说,若一个 Filter 的 指定为 /,则将不过滤任何请求 为什么要设计为不支持 / 呢?因为全路径的 / 只会拦截静态资源请求,对动态资源请求是不拦截的。若支持 /,则可能会出现对某些请求的不拦截
1.3.2 Filter 可以不指定
中可以不指定 ,但需要使用 标签。该标签用来指定该 Filter 专门过滤指定 Servlet 的请求。即相当于当前 Filter 的 的值与指定 Servlet 的 的值相同
1.4 标签
1.4.1 四种取值
在
- FORWARD:若请求是由一个 Servlet 通过 RequestDispatcher 的 forward() 方法所转发的,那么这个请求将被
值为 FORWARD 的 Filter 拦截,即当前 Filter 只会拦截由 RequestDispatcher 的 forward() 方法所转发的请求,其它请求均不拦截

- INCLUDE:当前 Filter 只会拦截由 RequestDispatcher 的 include() 方法所转发的请求,其它请求均不拦截

- ERROR:在 web.xml 中可以配置错误页面
,当发生指定状态码的错误后,会跳转到指定的页面。而这个跳转同样是发出的请求。若 的值设置为 EEROR,则当前过滤器只会拦截转向错误页面的请求,其它请求不会拦截

- REQUEST:默认值。即不设置
标签,也相当于指定了其值为 REQUEST。只要请求不是由 RequestDispatcher 的 forward() 方法或 include() 方法转发的,那么该请求会被拦截,即使是向错误页面的跳转请求,同样会被拦截 1.4.2 代码测试
(1) 定义两个 Servlet
这两个 Servlet 分别为 SomeServlet 与 OtherServlet,SomeServlet 通过 RequestDispatcher 跳转到 OtherServlet
(2) 注册两个 Servlet
(3) 定义一个 Filter
(4) 注册 Filter
(5) 访问
在浏览器中直接访问 SomeServlet,其中包含了两次请求:由地址栏发出的对 SomeServlet 的请求,由 SomeServlet 将请求转发到 OtherSrevlet。以下三种情况根据
取值的不同,其运行效果是不同的: 在 web.xml 中添加错误跳转页面配置:
(7) 定义错误处理页面 error.jsp
(8) 访问
- 若
的值为 REQUEST,则在浏览器中访问当前应用中的一个不存在的资源时, 会跳转到 error.jsp 页面,同时发现也经过了 OneFilter 若
的值为 ERROR,在浏览器中访问当前应用中的一个不存在的资源,同样会跳转到 error.jsp 页面,同时发现也经过了 OneFilter。但访问一个存在的资源,例如 someServlet,则请求不会被 OneFilter 拦截。因为此时 OneFilter 只会拦截向错误页面的跳转请求 1.5 Filter 的执行过程
要分析 Filter 的执行过程,需要在项目中定义两个 Filter 与一个 Servlet。它们之间的关系如下图所示:
1.5.1 代码测试
(1) 定义两个 Filter
- 这两个 Filter 均对请求与响应进行了修改。
(2) 注册两个 Filter
(3) 定义 SomeServlet
- 在 Servlet 中获取到 Filter 向请求中添加的数据。
(4) 注册 SomeServlet
(5) 修改 index.jsp 页面
- 修改 index.jsp 页面的字符编码格式为 UTF-8,否则在进入欢迎页面时会显示 Filter 向响应中添加的数据,而其中有中文乱码。
(6) 运行结果
- 页面上显示结果:

- 控制台显示结果:
1.5.2 执行过程分析
若应用中配置了多个 Filter,那么这些 Filter 的执行是以“链”的方式执行的。会将这些与请求相匹配的 Filter 串成一个可执行的“链”,然后按照这个链中的顺序依次执行。这些 Filter 在链中的顺序与它们在 web.xml 中的注册顺序相同,web.xml 中的注册顺序,即为 Filter 的执行顺序
一个 Filter 的执行完毕,转而执行另一个 Filter,这个转向工作是由 FilterChain 的 doFilter() 方法完成的。当然,若当前 Filter 是最后一个 Filter,则 FilterChain 的 doFilter() 会自动转向最终的请求资源
当请求到达 Filter 后,Filter 可以拦截到请求对象,并对请求进行修改。修改过后,再将该修改过的请求转向下一个资源
当最终的资源执行完毕,并形成响应对象后,会按照请求访问 Filter 的倒序,再次访问 Filter。此时 Filter 可以拦截到响应对象,并对响应进行修改。最终,客户端可以收到已被修改过的响应
1.5.3 服务器中的一个 Map 与一个数组
(1) 回顾servlet执行原理
- 两个 Map:
- Web 容器中存在两个 Map,这两个 Map 的 key 均为 Servlet 注册时的
值,但其 value 是不同的 - 第一个 Map 的 value 是 Servlet 实例对象的引用,第二个 Map 的 value 为
的值,即 Servlet 类的全限定性类名
- Web 容器中存在两个 Map,这两个 Map 的 key 均为 Servlet 注册时的
执行原理:
- 当对 Servlet 的请求到达 Servlet 容器时,会先对请求进行解析,使用解析出的 URI,作为比较对象,从第一个 Map 中查找是否有匹配的 key
- 若存在匹配的 key,那么读取其 value,即 Servlet 对象的引用,执行该 Servlet 的 service() 方法。不再向后查找了
- 若不存在匹配的 key,那么再从第二个 Map 中查找是否有匹配的 key。若存在,则读取其 value,即要访问的 Servlet 的全限定性类名。然后使用反射机制创建该 Servlet 实例,并将该实例写入到第一个 Map 中,然后再执行该 Servlet 的 service() 方法。Class.forName(className)
- 若第二个 Map 中也没有找到匹配的 key,那么跳转到系统错误处理页面 404
(2) 一个 Map
像存放 Servlet 信息的两个 Map 一样,在服务器中同样存在用于存放 Filter 相关信息的 Map。只不过 Map 只有一个,而非两个。这个 Map 的 key 是 Filter 的
。当然, 若 Filter 没有设置 而是使用了 ,则会将指定 Servlet 的 值放到 Map 中作为 key。Map 的 value 为该 Filter 的引用。在应用被启动时,服务器会自动将所有的 Filter 实例创建,并将它们的引用放入到相应 Map 的 value 中
(3) 一个数组
- 在服务器中,对于每一个请求,还存在着一个数组,用于存放满足当前请求的所有 Filter 及最终的目标资源
- 当请求到达服务器后,服务器会解析出 URI,然后会先从 Filter 的 Map 中查找所有与该请求匹配的 Filter,每找到一个就将其引用存放到数组中,然后继承查找。直到将所有匹配的 Filter 全部找到并添加到数组中
- 这个数组就是对于当前请求所要进行处理的一个“链”,包含多个 Filter。服务器将按照这个“链”的顺序对请求进行依次过滤处理
注意,我们发现对于 Filter 的 Map 的查询过程与对于 Servlet 的 Map 的查询过程是不同的。对于 Servlet 的 Map 的查询过程是:只要找到一个匹配的 key,则将不再向后查找。而对于 Filter 的 Map 的查找,则是遍历所有 key,将所有匹配的元素都查找出来
1.5.4 FilterChain 源码分析
FilterChain,即过滤器链,用于完成过滤器的过滤功能,且完成向一个 Filter 或资源的跳转,其是 Filter 接口中的方法 doFilter() 的参数。
Javax.servlet.FilterChain 是一个接口,其实现类为 org.apache.catalina.core.ApplicationFilterChain
(1) FilterChain 的作用
查看 ApplicationFilterChain 源码中的注释可知,FilterChain 是用于管理“对于特定请求的过滤器集合”的执行的。当集合中的过滤器全部执行完毕,doFilter()方法下一个要调用执行的就是 Servlet 的 service() 方法。

从源码注释可知,在 FilterChain 中存在一个过滤器集合,这个集合中存放着对于当前请求进行过滤的所有过滤器。这个集合就是我们前面说的“一个数组”
(2) 一个数组
从 ApplicationFilterChain 源码中可以看到一个数组 filters 。该数组元素为 ApplicationFilterConfig,而 ApplicationFilterConfig 是 javax.servlet.FilterConfig 的实现类。
- 为什么这个数组元素类型不是 Filter,而是 FilterConfig 呢?通过前面的学习我们知道,一个 Filter 对应一个 FilterConfig,即 FilterConfig 中包含着 Filter 的所有配置信息,可以代表 Filter

- FilterChain 的 doFilter() 方法中调用执行了 internalDoFilter() 方法。

- 而 internalDoFilter() 方法首先就是从这个数组中取出一个 FilterConfig,并从中获取到 Filter 对象,然后再调用执行该 Filter 对象的 doFilter() 方法,继承向后执行
- 其中 n 为数组中真正的 Filter 的个数,但不是数组长度。而 pos,则表示当前 Filter 在数组中的位置索引
(3) 向数组中添加 Filter
- 数组中存放的是 FilterConfig,那么这些 FilterConfig 是什么时候,怎样放入数组中的呢? 在 ApplicationFilterChain 源码中有一个方法 addFilter(),服务器调用了这个方法向数组中添加 了 FilterConfig。
- 当请求到达服务器后,服务器解析出了 URI,并从 Filter 的 Map 中逐个查询出所有与请求匹配的 Filter,每查询出一个,便调用 addFilter() 方法向数组中添加一个 FilterConfig。

第 529-531 行代码,是为了避免同名的 Filter 被多次添加。第 533 行的判断是指,若当前数组中 Filter 的个数,即变量 n 的值,与 Filter 数组的长度相等了,则需要将数组进行扩容。第 535 行的常量 INCREMENT,即为要扩容的值。第 539 行用于将参数 filterConfig 添加到数组中,并使 Filter 的数量 n 值加 1。
二、Filter 应用举例
2.1 装饰者设计模式
由于后面的 Filter 应用举例中“统一应用字符编码(GET 与 POST 请求)”例子要使用 HttpServletRequest 的装饰者类 HttpServletRequestWrapper,所以这里先讲解一下装饰者设计模式
2.1.1 什么是装饰者设计模式
Decorator Pattern,能够在不修改目标类也不使用继承的情况下,动态地扩展一个类的功能。它是通过创建一个包装对象,也就是装饰者来达到增强目标类的目的
- 装饰者设计模式的实现有两个要求:
- 装饰者类与目标类要实现相同的接口,或继承自相同的抽象类。
- 装饰者类中要有目标类的引用作为成员变量,而具体的赋值一般通过带参构造器完成。
这两个要求的目的是,在装饰者类中的方法可以调用目标类的方法,以增强这个方法。 而增强的这个方法是通过重写的方式进行的增强,所以要求实现相同的接口或继承相同的抽象类。
在装饰者设计模式中,装饰者类一般是不对目标类进行增强的。装饰者类作为一个基类, 具体的装饰者继承自这个基类,对目标类进行具体的、单功能的增强。这样做的好处是,在很方便的情况下可以实现多重地、组合式地增强。
装饰者基类就像是一个装修公司的老板,其不做任何具体的装修工作。而具体的装饰者 则相当于装修公司中的木工、刷漆工、水电工等具体的装修师傅。装修公司的老板可以根据具体的装修工程,任意组合式地调用不同工种的装修工人。
2.1.2 装饰者设计模式的实现
下面的例子实现的功能是,对目标类中的方法 doSome()进行功能增强。为该目标类定义一个装饰者类后,再定义两个具体的装饰者类:一个用于将 doSome()的返回值去掉前后空格,一个用于将 doSome()的返回值小写变大写。当然,装饰者模式允许构造一个装饰者增强链对目标类进行连接增强。
(1) 定义业务接口 ISomeService
(2) 定义目标类 SomeServiceImpl
(3) 定义装饰者基类 SomeServiceWrapper
(4) 定义去空格装饰者类 TrimDecorator
(5) 定义小写变大写装饰者类 ToUpperCaseDecorator
(6) 定义测试类 MyTest
对于第二次增强,需要注意的是,其带参构造器中所带参数,即要增强的目标对象是 trimDecorator,是第一次增强后的对象,也是一个具体装饰者。这样的话,形成了trimDecorator 增强 service,而 toUpperCaseDecorator 增强 trimDecorator 的装饰者增强链。
2.1.3 装饰者设计模式与静态代理设计模式的对比
(1) 相同点
- 装饰者类与目标类要求实现同一接口;静态代理类与目标类要求也实现同一接口。
- 装饰者类与静态代理类都可以实现增强目标类的功能。
装饰者类与静态代理类中都具有目标类的引用,目的都是为了在其中调用目标类的方法。
(2) 不同点
装饰者设计模式就是为了增强目标类;静态代理设计模式是为了保护和隐藏目标对象, 让客户类只能访问代理对象,而不能直接访问目标对象。
- 装饰者类中的目标类的引用是通过带参构造器传入的;静态代理类中的目标类的引用, 一般都是在代理类中直接创建的,目的就是为了隐藏目标对象。
- 装饰者基类一般不对目标对象进行增强,而是由不同的具体装饰者进行增强的,且这些具体的装饰者可以形成增强链,对目标对象进行连续增强。静态代理类会直接对目标对象进行增强,需要哪些增强的功能,一次性在静态代理类中完成,没有增强链的概念。
2.1.4 静态代理设计模式
以上功能,若使用静态代理设计模式,则代码可这样编写。(1) 定义业务接口与实现类
业务接口与实现类与装饰者设计模式例子中的相同。
(2) 定义静态代理类
在代理模式中,目标对象一般都是在代理类中创建,而不是通过带参构造器传入。目的就是为了向客户类隐藏目标对象,以达到保护目标对象的效果。
(3) 定义测试类
测试类中看不到目标对象,而只能操作代理对象。
2.2 应用举例
2.2.1 统一应用字符编码(POST 请求)
该统一应用字符编码的方式,只对 POST 请求起作用,对于 GET 请求中所携带的中文, 无法解决乱码问题。当然,对于响应的字符编码问题,是不分 POST 与GET 的。该方案可以解决响应的乱码问题。(1) 定义 CharacterEncodingFilter
(2) 注册 CharacterEncodingFilter
(3) 定义登录表单页面
(4) 定义 LoginServlet
(5) 注册 LoginServlet
2.2.2 统一应用字符编码(GET 与 POST 请求)
前面的“统一应用字符编码”的解决方案,只能解决 POST 请求的乱码问题,对于 GET 请求的无法解决。现在我们要编写的代码,将解决 POST 与 GET 请求中的乱码问题。(1) 解决方案分析
下面我们再来分析一下乱码产生的原因:当浏览器将包含有中文(例如 UTF-8 编码)参 数的请求(无论是 GET 还是 POST 请求),以字节序列的形式发送到服务器后,服务器会按 照其默认的字符编码 ISO8859-1 进行解码,并将解码后的字符存放到 ParameterMap 中。此时的ParameterMap 中存放的字符其实已经是乱码了,因为将 UTF-8 的字符序列解码为 ISO8859-1 的字符,当然会出现乱码。
要从根本上解决这个乱码问题,我们可以在参数存放到 ParameterMap 之前,将字符序列按照其原有的中文编码(UTF-8)进行解码,正确解码后的字符再存放到 ParameterMap 中。这样 Servlet 再从 ParameterMap 中读取参数,就不会出现乱码了。
不过,有个问题:向ParameterMap中存放数据是由服务器自动完成的,“向ParameterMap 中存放数据”这个时间点程序员无法捕获。怎么办?我们的解决方案思路是(狸猫换太子),自定义一个 HttpServletRequest 类型,该类型是 HttpServletRequest 的一个装饰者。让这个装饰者重写 HttpServletRequest 中请求参数相关方法。例如,重写 getParameterMap()方法,在该方法中 定义一个Map,并将原始 ParameterMap 中存放的乱码问题解决后,将数据存放到这个新的 Map 中。然后,再重写其它参数相关方法,让这些方法获取请求参数,直接从这个新的 Map 中获取。这样乱码问题就得以解决。
也就是说,将来整个应用中所有的请求对象将使用我们自定义的这个请求的装饰者。这个替换工作可以通过过滤器完成,即所有请求到达应用后,首先经过这个过滤器,将HttpServletRequest 请求替换为装饰者。(2) 定义HttpServletRequest 的装饰者
值得庆幸的是,这个装饰者不用我们自己定义了。Servlet 规范中已经定义好了一个 HttpServletRequest 的装饰者 HttpServletRequestWrapper。
我们只需要定义一个类,使其继承自这个装饰者 HttpServletRequestWrapper,就完成了 HttpServletRequest 装饰者类的定义,然后重写请求参数相关方法,其它方法保持不变即可。
A、继承 HttpServletRequestWrapper
一个类继承自 HttpServletRequestWrapper 类,要求必须重写带参构造器。目的是让装饰者可以获取到原始的 HttpServletRequest 对象,然后再在装饰者中将 Request 进行增强。只不过,本例中没有使用到这个原始的 request。
B、重写 getParameterMap()
C、重写 getParameterValues()
D、重写 getParameter()
E、重写 getParameterNames()
(3) 修改 CharacterEncodingFilter
当任意请求到达时,都需要先经过该过滤器,将请求对象替换为自定义的请求对象,然 后再通过 chain.doFilter()将替换过的请求向后传递。
2.2.3 访问权限过滤器
当用户访问某网站时,有些页面或 Servlet 在不登录的情况下是可以访问的,例如首页、 登录页面等。但有些资源是必须要登录后才能访问的。此时,可以定义一个权限过滤器,对每一个访问该应用的请求进行过滤:若具有访问权限,则直接跳转到相应资源即可;若不具 有访问权限,则跳转到登录页面。
本例中,以/user 开头的请求,是需要登录后才可提交的。其中有/user 目录下的列表页面 list.jsp、退出页面 logout.jsp 等必须登录后才可看到。另外,还有 SomeServlet,也需要登录后才可访问。而/index.jsp 与/login.jsp 是无需登录的。(1) 定义首页 index.jsp
该页面无需登录即可访问。
(2) 定义登录页面 login.jsp
该页面无需登录即可访问。
(3) 定义列表页面 userList.jsp
在项目的 WebRoot 下新建一个子目录 user,在该目录中定义当前页面。对该资源的访问路径中,必定包含/user,所以该页面必须登录后才可访问。
(4) 定义退出页面 logout.jsp
在项目的 WebRoot 下新建一个子目录 user,在该目录中定义当前页面。对该资源的访 问路径中,必定包含/user,所以该页面必须登录后才可访问。
(5) 定义 LoginServlet
(6) 注册 LoginServlet
(7) 定义 SomeServlet
(8) 注册 SomeServlet
该 Servlet 的值要以/user 开头,也就是说,用户提交的 ServletPath 必须以 /user 开头。该资源必须登录后才可访问。
(9) 定义权限过滤器 PermissionFilter
(10) 注册 PermissionFilter
(11) 访问
在浏览器地址栏直接访问/index.jsp 与/login.jsp 都没有问题,可以直接看到相应页面。
在浏览器地址栏访问/user/userList.jsp 或/user/logout.jsp 或/user/someServlet,都会跳转到 /login.jsp 页面。
一旦登录,再访问/user/userList.jsp 或/user/someServlet 资源,均可以看到相应资源。
若访问/user/logout.jsp,则表示安全退出。此后再访问/user/list.jsp 或/user/someServlet,包括/user/logout.jsp,均将会跳转到/login.jsp,因为已经退出了,要求登录后才可访问。




