一、过滤器的基本概念

1、什么是过滤器

过滤器是一个服务器端的组件,它可以截取客户端的请求和服务端的响应信息,并对这些信息进行过滤。
2、过滤器的工作原理

过滤器的工作原理可以依据下图进行分析(图片转自慕课网)。

用户在请求Web资源时,用户的请求会先被过滤器拦截,过滤器对用户的请求进行过滤,过滤之后过滤器再将用户的请求发送到Web资源,Web资源在将响应返回给用户时,响应也会先被过滤器拦截,对响应进行过滤之后由过滤器将响应发送给用户。
二、过滤器的生命周期与常用方法

1、过滤器的生命周期

过滤器的生命周期分为四个阶段:实例化、初始化、过滤和销毁:实例化是指在Web工程的web.xml文件里声明一个过滤器,在声明了过滤器之后,Web容器会创建一个过滤器的实例;初始化是指在创建了过滤器实例之后,服务器会执行过滤器中的init()方法,这是过滤器的初始化方法;初始化之后过滤器就可以对请求和响应进行过滤了,过滤主要调用的是过滤器的doFilter()方法;最后当服务器停止时,会将过滤器销毁,销毁过滤器前主要调用过滤器的destory()方法,释放资源。
2、过滤器的常用方法

从上面对过滤器的生命周期的分析中可以看到,过滤器最常用的方法有三个:init()、doFilter()和destory()。
1)init()方法:这是过滤器的初始化方法,在Web容器创建了过滤器实例之后将调用这个方法进行一些初始化的操作,这个方法可以读取web.xml中为过滤器定义的一些初始化参数。
2)doFilter()方法:这是过滤器的核心方法,会执行实际的过滤操作,当用户访问与过滤器关联的URL时,Web容器会先调用过滤器的doFilter方法进行过滤。
3)destory()方法:这是Web容器在销毁过滤器实例前调用的方法,主要用来释放过滤器的资源等。
三、第一个过滤器实例

在了解了过滤器的相关概念之后,就可以尝试写一个过滤器了。首先定义一个过滤器名为FirstFilter,它是继承自Filter这个接口的,而要继承Filter接口,就需要实现这个接口的三个方法init()、doFilter()和destory(),可以发现这三个方法正是过滤器的常用方法,FirstFilter内容如下。
public class FirstFilter implements Filter {

public void destroy() {
System.out.println("First Filter------Destory");
}

public void doFilter(ServletRequest arg0, ServletResponse arg1,
FilterChain arg2) throws IOException, ServletException {
System.out.println("First Filter------doFilter start");

arg2.doFilter(arg0, arg1);

System.out.println("First Filter------doFilter end");
}

public void init(FilterConfig arg0) throws ServletException {
System.out.println("First Filter------Init");
}

}
在这三个方法里分别输出一句话表示这个方法已经执行了,其中doFilter()方法中FiterChain.doFilter()方法表示将请求传给下一个过滤器或目标资源,当过滤器收到响应之后再执行FilterChain.doFilter()之后的内容。
定义了过滤器之后需要在web.xml文件中进行声明,过滤器在web.xml文件中的配置可以参考下图(图片转自慕课网)。

下面是我在web.xml文件中的配置,当用户请求URL为index.jsp时会触发过滤器。
<filter>
<filter-name>firstFilter</filter-name>
<filter-class>com.imooc.filter.FirstFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>firstFilter</filter-name>
<url-pattern>/index.jsp</url-pattern>
</filter-mapping>
index.jsp的内容很简单,只是输出一句话。
<html>
<body>
<h2>Hello World!</h2>

<%
System.out.println("index.jsp------");
%>
</body>
</html>
这样一个简单的过滤器就完成了,下面启动服务器观察输出。首先在服务器启动过程中会发现有如下输出:
……
……
First Filter------Init
八月 12, 2016 7:00:24 下午 org.apache.coyote.AbstractProtocol start
信息: Starting ProtocolHandler ["http-nio-8080"]
八月 12, 2016 7:00:24 下午 org.apache.coyote.AbstractProtocol start
信息: Starting ProtocolHandler ["ajp-nio-8009"]
八月 12, 2016 7:00:24 下午 org.apache.catalina.startup.Catalina start
信息: Server startup in 1510 ms
这就表示当服务器启动时,服务器会创建一个web.xml文件中定义的过滤器实例并执行过滤器中的init()方法。当服务器启动好后,访问index.jsp,这时控制台就会输出如下内容:
First Filter------doFilter start
index.jsp------
First Filter------doFilter end
这就表示当用户请求index.jsp时,请求首先会被发送到过滤器,过滤器执行doFilter()方法进行过滤,在doFilter()方法中,过滤器首先对请求执行过滤操作,然后调用FilterChain.doFilter()将请求传给资源,资源响应后对响应进行过滤,最后才将过滤后的响应显示给客户端。
最后,当我们停止服务器时,控制台会打印如下的输出:
……
……
First Filter------Destory
八月 12, 2016 7:09:23 下午 org.apache.coyote.AbstractProtocol stop
信息: Stopping ProtocolHandler ["http-nio-8080"]
八月 12, 2016 7:09:23 下午 org.apache.coyote.AbstractProtocol stop
信息: Stopping ProtocolHandler ["ajp-nio-8009"]
八月 12, 2016 7:09:23 下午 org.apache.coyote.AbstractProtocol destroy
信息: Destroying ProtocolHandler ["http-nio-8080"]
八月 12, 2016 7:09:23 下午 org.apache.coyote.AbstractProtocol destroy
信息: Destroying ProtocolHandler ["ajp-nio-8009"]
可以看到,过滤器的destory()方法执行了,表示Web容器已经准备销毁过滤器释放资源了。
从这个实例中可以很清晰地分析一个过滤器的生命周期,从创建实例、初始化到进行过滤最后销毁,整个过程都是通过调用过滤器的相应方法来实现的。
四、过滤器链

一个Web项目可能存在多个过滤器,而对于同一个Web资源,也可能有多个过滤器与之相关联,所以当为同一个Web资源关联多个过滤器时,如果客户端请求相应的Web资源,Web容器该按照什么样的顺序进行过滤呢?这时就用到了过滤器链的知识,服务器会按照web.xml中过滤器定义的先后顺序组装成一条链顺序执行。过滤器链的执行过程可以参考下图(图片转自慕课网)。


当用户发送请求请求Web资源时,过滤器1会首先获取用户的请求,对请求进行过滤,然后执行FilterChain.doFilter()将请求发送给过滤器2,过滤器2在过滤了用户请求之后执行FilterChain.doFilter()方法请求实际的Web资源,Web资源的响应将首先被过滤器2获取,在过滤器2对响应进行过滤之后将响应传递给过滤器1,在过滤器1过滤之后才将响应发送给用户。
下面仍然通过一个实例来分析过滤器链,首先定义两个过滤器FirstFilter和SecondFilter,FirstFilter定义仍然使用上面的实例,SecondFilter的定义如下,也只是简单的输出一些语句。
public class SecondFilter implements Filter {

public void destroy() {
System.out.println("Second Filter------Destory");
}

public void doFilter(ServletRequest arg0, ServletResponse arg1,
FilterChain arg2) throws IOException, ServletException {
System.out.println("Second Filter------doFilter start");
arg2.doFilter(arg0, arg1);
System.out.println("Second Filter------diFilter end");
}

public void init(FilterConfig arg0) throws ServletException {
System.out.println("Second Filter------Init");
}

}
然后在web.xml中声明两个过滤器,两个过滤器都对index.jsp进行过滤,其中FirstFilter在SecondFilter之前声明。
<filter>
<filter-name>firstFilter</filter-name>
<filter-class>com.imooc.filter.FirstFilter</filter-class>
</filter>
<filter>
<filter-name>secondFilter</filter-name>
<filter-class>com.imooc.filter.SecondFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>firstFilter</filter-name>
<url-pattern>/index.jsp</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>secondFilter</filter-name>
<url-pattern>/index.jsp</url-pattern>
</filter-mapping>
下面就可以启动服务器,访问index.jsp,观察控制台的输出:
First Filter------doFilter start
Second Filter------doFilter start
index.jsp------
Second Filter------diFilter end
First Filter------doFilter end
从输出不难发现,Web容器对过滤器的执行过程就是依据前面所介绍的过滤器链的执行过程执行的。
五、过滤器的分类

所谓过滤器的分类就是指定义的过滤器对哪一种类型的请求进行过滤,具体体现在在web.xml文件中声明一个过滤器时,声明的dispatcher标签的值,具体的取值及用法可以参考下图(图片转自慕课网)。

下面来分别介绍一下这几个值的用法。
1、REQUEST

这个类型是最常用的,也是dispatcher标签的默认值,表示用户直接请求一个Web资源时触发过滤器。之前介绍的几个过滤器实例都是REQUEST类型的,当用户直接请求index.jsp页面时触发了相应的过滤器,下面来看另一个例子,取消SecondFilter,修改上面的FirstFilter如下,在doFIlter()方法中使用HttpServletResponse的重定向方法sendRedirect()方法,重定向到index.jsp。
public class FirstFilter implements Filter {

public void destroy() {
System.out.println("First Filter------Destory");
}

public void doFilter(ServletRequest arg0, ServletResponse arg1,
FilterChain arg2) throws IOException, ServletException {
System.out.println("First Filter------doFilter start");
HttpServletResponse response = (HttpServletResponse) arg1;
response.sendRedirect("index.jsp");
arg2.doFilter(arg0, arg1);
System.out.println("First Filter------doFilter end");
}

public void init(FilterConfig arg0) throws ServletException {
System.out.println("First Filter------Init");
}

}
这时再访问index.jsp会发现浏览器处于一个卡死的状态,观察控制台会发现控制台不停地打印内容,陷入了一个死循环的状态,这是因为当用户请求index.jsp时,过滤器首先接收到用户请求,在对请求过滤的过程中,重定向到了index.jsp,而重定向相当于一个新的请求,也就是用户再次请求index.jsp,这样再次触发过滤器,如此循环往复,陷入了死循环的状态。现在思考一个问题,如果将重定向改为服务器内部转发,会有什么结果呢?再次修改FirstFilter的doFilter()方法,使用HttpServletRequest..getRequestDispatcher("index.jsp").forward(arg0, arg1)方法将请求转发给index.jsp。
public void doFilter(ServletRequest arg0, ServletResponse arg1,
FilterChain arg2) throws IOException, ServletException {
System.out.println("First Filter------doFilter start");
HttpServletRequest request = (HttpServletRequest) arg0;
request.getRequestDispatcher("index.jsp").forward(arg0, arg1);
arg2.doFilter(arg0, arg1);

System.out.println("First Filter------doFilter end");
}
运行程序,访问index.jsp,这时会发现页面可以正常访问,而后台也没有陷入死循环的状态。这是为什么呢?因为过滤器FirstFilter的类型为REQUEST,而服务器内部转发的请求是FORWARD类型的,所以过滤器不会对这种类型的请求进行过滤,所以可以正常访问页面,而要想过滤FORWARD请求,就需要将过滤器的类型改为FORWARD类型。
2、FORWARD

仍然使用上面服务器内部转发的案例,修改web.xml中对FirstFilter的配置,将过滤器类型改为FORWARD。
<filter-mapping>
<filter-name>firstFilter</filter-name>
<url-pattern>/index.jsp</url-pattern>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>
定义一个middle.jsp,这个jsp只是包含了一个forward的动作,将请求转发给index.jsp。
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<jsp:forward page="index.jsp"/>
</body>
</html>
再修改FirstFilter的doFilter()方法,将请求转发到index.jsp。
public void doFilter(ServletRequest arg0, ServletResponse arg1,
FilterChain arg2) throws IOException, ServletException {
System.out.println("First Filter------doFilter start");
HttpServletRequest request = (HttpServletRequest) arg0;
request.getRequestDispatcher("index.jsp").forward(arg0, arg1);
arg2.doFilter(arg0, arg1);

System.out.println("First Filter------doFilter end");
}
这时访问middle.jsp,会发现页面再次变成卡死状态,后台也变成了死循环的状态。这是因为在middle.jsp页面中将请求转发到index.jsp,这时被FORWARD类型的过滤器拦截,然后再次转发到index.jsp,再次触发过滤器,程序也因此陷入了死循环状态。
3、INCLUDE

INCLUDE类型的过滤器与FORWARD类型的类似,在使用RequestDispatcher.include()方法调用Web资源时被触发,在这里就不再赘述了。
4、ERROR

ERROR类型的过滤器是指声明式异常处理机制被调用时触发,可以具体举个例子来分析。在Web工程里,为了用户友好常常需要配置404的错误页面,就是指当用户请求一个不存在的页面时服务器会自动跳转到一个设定好的错误页面,而错误页面需要在web.xml文件里进行如下配置:
<error-page>
<error-code>404</error-code>
<location>/error.jsp</location>
</error-page>
当用户访问发生404错误时会自动跳转到error.jsp,在这个例子里error.jsp只是简单地打印一句话。
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
Error page.
</body>
</html>
而当用户访问不存在的页面跳转到并error.jsp页面时,这时就会触发ERROR类型的过滤器,接下来修改FirstFilter过滤器的类型为ERROR,并将过滤器映射的URL改为error.jsp。
<filter-mapping>
<filter-name>firstFilter</filter-name>
<url-pattern>/error.jsp</url-pattern>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
再修改FIrstFilter,在doFilter()方法中只是简单地输出内容。
public void doFilter(ServletRequest arg0, ServletResponse arg1,
FilterChain arg2) throws IOException, ServletException {
System.out.println("First Filter------doFilter start");
arg2.doFilter(arg0, arg1);
System.out.println("First Filter------doFilter end");
}
这时启动程序,访问一个不存在的页面,比如test.jsp,这时页面会自动跳转到error.jsp,同时控制台也打印出过滤器中输出的内容。这里需要注意,如果只是直接地访问error.jsp,是不会触发过滤器的,因为直接访问页面的请求类型是REQUEST,过滤器类型是ERROR的,所以不会进行过滤。
5、ASYNC

ASYNC类型是在Servlet3.0之后新增加的过滤器类型,前面介绍的几种过滤器类型都是Servlet2.5中定义的过滤器类型,而ASYNC类型支持异步的请求,因为异步操作前台有很多种方式可以实现,所以在这里我就不介绍这种类型的过滤器了。
六、过滤器的一个综合案例

前面对过滤器的基本知识做了一个比较全面的讲解,现在可以结合前面的介绍开发一个小案例,考虑一个业务场景,用户登录,如果用户登录成功,页面跳转到成功页面,并显示用户名,如果登录失败,则跳转到登录失败页面,这个功能很简单,之前也介绍了好几次,先简单理一下。首先是登录页面login.jsp,包含用户名、密码和提交按钮。
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>登录</title>
</head>
<body>
<form action="dologin.jsp" method="post">
用户名:<input type="text" name="userName" /><br/>
密码:<input type="password" name="password"/><br/>
<input type="submit" name="登录"/>
</form>
</body>
</html>
用户点击登录后,登录的表单交给dologin.jsp进行处理。
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%
String userName = request.getParameter("userName");
String password = request.getParameter("password");
System.out.println(request.getParameter("userName"));

if("admin".equals(userName) && "admin".equals(password)) {
request.getSession().setAttribute("userName", userName);
response.sendRedirect("success.jsp");
} else {
response.sendRedirect("fail.jsp");
}
%>
这里就是简单做一个判断,如果用户名密码都为admin,表示登录成功,在session里添加一个用户名属性,并跳转到success页面将用户名显示出来,如果登录失败,则跳转到失败页面。success.jsp和fail.jsp定义如下。
success.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>登录成功</title>
</head>
<body>
登录成功!欢迎您:<%=session.getAttribute("userName") %>
</body>
</html>
fail.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>登录失败</title>
</head>
<body>
登录失败!请检查您的用户名和密码!
</body>
</html>
这时一个简单的登录功能就完成了,启动程序,进入login.jsp,输入用户名密码,就可以进行简单的登录了,这时需要思考一个问题,现在实现的功能里,当用户名和密码都为admin时,登录成功并在成功页面显示用户名,那么如果还没有登录就直接访问success.jsp,会是什么结果呢?很明显,如果直接访问success.jsp,也是可以访问的,只是会获取不到用户名,因而页面用户名就会显示为空,这显然不是我们希望的结果,我们不希望用户在没有登录的情况下就直接访问成功页面,在这里就需要使用一个过滤器进行控制了。
我们定义一个过滤器LoginFilter,在这个过滤器的doFilter()方法里进行一个判断,判断session里面是否有用户名这个属性同时这个属性的值是否为空,如果为空则重定向到login.jsp,否则继续访问Web资源。
public class LoginFilter implements Filter {

@Override
public void destroy() {


}

@Override
public void doFilter(ServletRequest arg0, ServletResponse arg1,
FilterChain arg2) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) arg0;
HttpServletResponse response = (HttpServletResponse) arg1;

String userName = (String) request.getSession().getAttribute("userName");

if(null != userName) {
arg2.doFilter(arg0, arg1);
} else {
response.sendRedirect(request.getContextPath() + "/login/login.jsp");
}

}

@Override
public void init(FilterConfig arg0) throws ServletException {

}

}
定义了上面的过滤器之后就能够完成登录功能了吗?很显然还是有欠缺的,思考一下,当用户首次访问login.jsp页面时,session里也是没有用户名信息的,根据上面定义的过滤器,很显然会自动重定向到login.jsp,这时又会触发过滤器,这样程序又陷入了死循环的状态,这就意味着需要给过滤器过滤的页面添加一些例外,当用户访问这些例外的页面时过滤器将不会重定向,在这个功能里我们需要过滤的页面有登录页面login.jsp、登录处理页面dologin.jsp,因为登录页面里填写表单是提交给dologin.jsp进行处理的,而这时还没有用户信息,同时还要将登录失败页面添加未例外。添加例外的页面可以直接在doFilter()里进行添加,判断访问的资源的路径是否是这三个,但是如果在一个大型的项目里,需要添加例外的页面很多,并且经常更新,我们不能每添加一个就在代码里新增一个判断,这样的代码复用性会很差,这时就可以使用过滤器的初始化参数了,可以在web.xml中定义param-name和param-value来表示参数信息,param-name表示参数名,param-value表示参数值。在这里我定义了两个参数,exceptPage和encoding,exceptPage是指需要开例外的页面,页面名以分好间隔,encoding是指编码,因为在实际开发过程中经常会出现乱码问题,解决乱码的一种方式就是添加一个过滤器来转换编码,这里使用参数的方式将编码名传给过滤器也提高了代码的复用性。
<filter>
<filter-name>loginFilter</filter-name>
<filter-class>com.imooc.filter.LoginFilter</filter-class>
<init-param>
<param-name>exceptPage</param-name>
<param-value>login.jsp;dologin.jsp;fail.jsp</param-value>
</init-param>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>

<filter-mapping>
<filter-name>loginFilter</filter-name>
<url-pattern>*</url-pattern>
</filter-mapping>
再修改LoginFilter。
FilterConfig config;

@Override
public void destroy() {


}

@Override
public void doFilter(ServletRequest arg0, ServletResponse arg1,
FilterChain arg2) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) arg0;
HttpServletResponse response = (HttpServletResponse) arg1;

String encoding = config.getInitParameter("encoding");
if(null == encoding) {
request.setCharacterEncoding("UTF-8");
} else {
request.setCharacterEncoding(encoding);
}
String userName = (String) request.getSession().getAttribute("userName");

String exceptPage = config.getInitParameter("exceptPage");
if(null != exceptPage && !"".equals(exceptPage.trim())) {
String[] exceptPages = exceptPage.split(";");
for (String except : exceptPages) {
if(request.getRequestURI().indexOf(except) != -1) {
arg2.doFilter(arg0, arg1);
return;
}

}
}

if(null != userName) {
arg2.doFilter(arg0, arg1);
} else {
response.sendRedirect(request.getContextPath() + "/login/login.jsp");
}

}

@Override
public void init(FilterConfig arg0) throws ServletException {
this.config = arg0;
}

}
我们可以通过FilterConfig来获取过滤器的初始化参数,当获取到例外页面时,依据分号将字符串分隔开,然后通过循环来判断用户请求的页面是否是例外,如果在例外页面当中,就可以直接访问,否则仍然需要判断用户是否登录。同时我们也取出编码格式,设置request的编码。这样我们再启动应用访问程序时,过滤器就可以正确地运行并判断用户是否登录了。