在一个Web应用中,每一个Web组件都用于响应特定的客户请求,不过,在这些Web组件响应客户请求的过程中,可能都会完成一些相同的操作。比如都要先检查客户的的IP地址是否位于预定义的拒绝IP地址范围内,如果满足这一条件,就直接向客户端返回拒绝响应客户请求的信息,而不会继续执行后续操作。
如果在多个Web组件中编写完成相同操作的程序代码,显然会导致重复编码,从而降低开发效率和软件的可维护性。
为了解决上述问题,过滤器应运而生,它是在Java Servlet 2.3规范中出现的技术。过滤器能够对一部分客户请求先进行预处理操作,然后再把请求转发给相应的Web组件,等到Web组件生成响应结果后,过滤器还能对响应结果进行检查和修改,然后再把修改后的响应结果发送给客户。各个Web组件中的相同操作可以放到同一个过滤器中来完成,来减少重复编码。
1. 过滤器简介
过滤器能够对Servlet容器传给Web组件的ServletRequest和ServletResponse对象进行检查和修改。过滤器本身并不生成这两个对象,它只为Web组件提供如下过滤功能:
- 过滤器能够在Web组件被调用之前检查ServletRequest对象,修改请求头和请求正文的内容,或者对请求进行预处理操作。
- 还能在Web组件被调用之后检查ServletResponse对象,修改响应头和响应正文。
过滤器的逻辑位置是处于Servlet容器和Servlet、JSP、HTML等文件之间的中间件,它具有以下特点:
- 可以检查ServletRequest和ServletResponse对象,并且利用ServletRequestWrapper和ServletResponseWrapper包装类来修改这两个对象。
- 可以在web.xml文件中为过滤器映射特定的URL,当客户请求访问此URL时,Servlet容器就会先触发过滤器工作。
- 过滤器可以多个串联在一起,协同为Web组件过滤请求对象和响应对象。
2. 创建过滤器
所有自定义的过滤器类都要实现javax.servlet.Filter接口,这个接口含有以下3个过滤器类必须实现的方法:
- init(FilterConfig config):这是过滤器的初始化方法。在Web应用启动时,Servlet容器先创建包含了过滤器配置信息的FilterConfig对象,然后创建Filter对象,接着调用Filter对象的init方法,在这个方法中可通过config参数来读取web.xml中为过滤器配置的初始化参数。
- doFilter(ServletRequest req,ServletResponse resp,FilterChain chain):这个方法完成实际的过滤操作。当客户请求访问的URL与为过滤器映射的URL匹配时,Servlet容器将先调用过滤器的doFilter()方法。FilterChain参数用于访问后续过滤器或者Web组件。
- destroy():Servlet容器在销毁过滤器对象之前调用该方法,该方法可以释放过滤器占用的资源。
过滤器由Servlet容器创建,其生命周期包含以下阶段:
- 初始化阶段:当Web应用启动时,Servlet容器会加载过滤器类,创建过滤器配置对象(FilterConfig)和过滤器对象,并调用过滤器对象的init方法。
- 运行时阶段:当客户请求访问的URL与为过滤器映射的URL相匹配时,Servlet容器将先调用过滤器的doFilter()方法。
- 销毁阶段:当Web应用终止时,Servlet容器将先调用过滤器对象的destroy()方法,然后销毁过滤器对象。
下面例程的NoteFilter类是一个过滤器的例子,它为NoteServlet(留言板)提供以下过滤功能:
- 判断客户IP地址是否在预定义的IP地址范围内,如果满足这一条件,就直接返回拒绝信息,不再调用后续的NoteServlet组件。
- 判断username请求参数表示的姓名是否位于预定义的黑名单中,如果满足这一条件,就直接返回拒绝信息。
- 将NoteServlet响应客户请求所花的时间写入日志。
mypack/NoteServlet.java 主要功能是一个表单。
package mypack;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class NoteServlet extends HttpServlet{
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("mypack.NoteServlet:service()");
resp.setContentType("text/html;charset=utf-8");
PrintWriter out = resp.getWriter();
out.println("<html><head><title> 留言板 </title></head><body>");
String username = req.getParameter("username");
String content = req.getParameter("content");
if (username != null) {
username = new String(username.getBytes("ISO-8859-1"), "utf-8");
}
if (content != null) {
content = new String(content.getBytes("ISO-8859-1"), "utf-8");
}
if (content != null && !content.equals("")) {
out.println("<p>"+username+"的留言为:"+content+"</p");
}
out.println("<form action="+req.getContextPath()+"/note method=POST>");
out.println("<b>姓名</b>");
out.println("<input type=text size=10 name=username><br>");
out.println("<b>留言</b>");
out.println("<textarea name=content rows=5 cols=20></textarea><br><br>");
out.println("<input type=submit value=提交>");
out.println("</form>");
out.println("</body></html>");
out.close();
}
}
该表单将会提交到localhost:8080/note地址。这时候,就是过滤器派上用场的时候了。在原来的NoteServlet页面表单重定向到到新的NoteServlet页面时,NoteFilter将会在NoteServlet接收到提交来的信息之前先捕获这些信息,进行一些预处理。
mypackl/NoteFilter.java
package mypack;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.PrintWriter;
public class NoteFilter implements Filter {
private FilterConfig config = null;
private String blackList = null;
private String ipblock = null;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("NoteFilter: init()");
this.config = filterConfig;
ipblock = config.getInitParameter("ipblock");
blackList = config.getInitParameter("blacklist");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException,ServletException{
System.out.println("NoteFilter: doFilter()");
if (!checkRemoteIP(servletRequest,servletResponse)) return ;
if (!checkUsername(servletRequest,servletResponse)) return ;
long before = System.currentTimeMillis();
config.getServletContext().log("NoteFilter:before call chain.doFilter()");
filterChain.doFilter(servletRequest,servletResponse);
config.getServletContext().log("NoteFilter:after call chain.doFilter()");
long after = System.currentTimeMillis();
String name="";
if (servletRequest instanceof HttpServletRequest) {
name = ((HttpServletRequest)servletRequest).getRequestURI();
}
config.getServletContext().log("NoteFilter:"+name+":"+(after - before)+"ms");
}
private boolean checkRemoteIP(ServletRequest servletRequest,ServletResponse servletResponse)
throws IOException,ServletException {
String addr = servletRequest.getRemoteAddr();
if (addr.indexOf(ipblock) == 0) {
servletResponse.setContentType("text/html;charset=utf-8");
PrintWriter out = servletResponse.getWriter();
out.println("<h1>对不起,服务器拒绝为您提供服务。</h1>");
out.flush();
return false;
} else {
return true;
}
}
private boolean checkUsername(ServletRequest servletRequest,ServletResponse servletResponse)
throws IOException,ServletException {
String username = ((HttpServletRequest)servletRequest).getParameter("username");
if (username != null) {
username = new String (username.getBytes("ISO-8859-1"),"utf-8");
}
if (username != null && username.contains(blackList)) {
servletResponse.setContentType("text/html;charset=utf-8");
PrintWriter out = servletResponse.getWriter();
out.println("<h1>对不起," + username + ",你没有权限留言</h1>");
out.flush();
return false;
} else {
return true;
}
}
@Override
public void destroy() {
System.out.println("NoteFilter:destroy()");
config = null;
}
}
web.xml中要设定对应的NoteServlet和NoteFilter的地址映射相同。这里需要注意,一个NoteFilter可以有多个地址映射。同时在web.xml设定一些初始化的黑名单字段和ip:
<filter>
<filter-name>NoteFilter</filter-name>
<filter-class>mypack.NoteFilter</filter-class>
<init-param>
<param-name>ipblock</param-name>
<param-value>221.45</param-value>
</init-param>
<init-param>
<param-name>blacklist</param-name>
<param-value>捣蛋鬼</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>NoteFilter</filter-name>
<url-pattern>/note</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>NoteServlet</servlet-name>
<servlet-class>mypack.NoteServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>NoteServlet</servlet-name>
<url-pattern>/note</url-pattern>
</servlet-mapping>
OK,那么现在启动服务器,访问localhost:8080/note,注意下方打印出的信息:
NoteFilter的init方法是在访问localhost:8080/note之前就已经运行的,也就是说,当服务器部署起来后,该方法就会被Servlet容器调用。而2和3两个方法都是访问的时候才会执行的。现在我们可以看到一个表单:
尝试输入姓名包含“捣蛋鬼”的文字:
提交:
而如果输入的姓名不包含“捣蛋鬼”,就可以留言成功。
3. 串联过滤器
多个过滤器可以串联起来协同工作,Servlet容器将根据它们在web.xml中定义的先后顺序来依次调用它们的doFilter()方法。假定有两个过滤器串联,它们的doFilter()方法均采用如下结构:
Code1; //表示调用doFilter()之前的代码
chain.doFilter();
Code2; //表示调用doFilter()之后的代码
假定这两个过滤器都会为同一个Servlet预处理客户请求,当客户请求访问这个Servlet时,这两个过滤器及Servlet的工作流程如下所示:
加入一个过滤文本的过滤器:TextFliter:
package mypack;
import javax.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;
public class TextFilter implements Filter {
private FilterConfig config = null;
private String searchStr = null;
//private String replaceStr = null;
@Override
public void init(FilterConfig filterConfig) throws ServletException{
System.out.println("TextFilter:init()");
this.config = filterConfig;
this.searchStr = config.getInitParameter("search");
// this.replaceStr = config.getInitParameter("replace");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException,IOException{
System.out.println("TextFilter:doFilter()");
config.getServletContext().log("TextFilter:before calling chain.doFilter()");
String content = servletRequest.getParameter("content");
if (content != null) content = new String(content.getBytes("ISO-8859-1"),"utf-8");
if (content != null && content.contains(searchStr)) {
servletRequest.setAttribute("content_error","true");
servletResponse.setContentType("text/html;charset=utf-8");
PrintWriter out = servletResponse.getWriter();
out.println("<h1>Error:Contains illegal contents!</h1>");
out.flush();
}
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("TextFilter:after calling chain.doFilter()");
config.getServletContext().log("TextFilter:after calling chain.doFilter()");
}
@Override
public void destroy() {
System.out.println("TextFilter:destroy()");
config = null;
}
}
然后需要在NoteServlet的代码中修改一处判定语句:
if (content != null && !content.equals("") && req.getAttribute("content_error")!="true") {
out.println("<p>"+username+"的留言为:"+content+"</p");
}
在web.xml中:
<filter>
<filter-name>TextFilter</filter-name>
<filter-class>mypack.TextFilter</filter-class>
<init-param>
<param-name>search</param-name>
<param-value>暴力</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>TextFilter</filter-name>
<url-pattern>/note</url-pattern>
</filter-mapping>
如此,运行应用,如果在内容框中输入带“暴力”的内容,就会如下效果:
控制台打印的语句:
可以看到执行的顺序。
具体相关内容参考:点击打开链接
至此,我们已经接触到了JavaWeb开发需要的大部分知识。但是应用中的JavaWeb都是基于框架的,需要结合框架来使用我们已经学到的Web知识。
anyway,本系列的目的已经达到了,于是光荣结束~ 撒花