本章要点
- 理解过滤器的执行过程和作用
- 掌握过滤器的开发与部署
- 编写对用户进行统一验证的过滤器
- 编写对请求和相应数据进行替换的过滤器
- 编写对响应内容进行压缩的过滤器
过滤器(Filter)是从Servlet 2.3 规范开始新增的功能,并在Servlet 2.4 规范中得到增强。本章主要介绍过滤器在Web开发中得应用。
16.1 过滤器概述
过滤器,顾名思义,就是在源数据和目的数据之间起过滤作用得中间组件。例如,污水净化设备可用看做现实中得一个过滤器,它负责将污水中得杂志过滤,从而使进入的污水变成净水。而对于Web应用程序来说,过滤器是一个驻留在服务器端的Web组件,它可以截取客户端和资源之间的请求与响应信息,并对这些信息进行过滤,如图16 -1 所示。
当Web 容器接收到一个对资源的请求时,它将判断是否有过滤器与这个资源相关联。
如果有,那么容器将把请求交给过滤器进行处理。在过滤器中,你可以改变请求的内容,或者重新设置请求的报头信息,然后再将请求发送给目标资源。当目标资源对请求做出响应时,容器同样会将响应先转发给过滤器,在过滤器中,你可以对响应的内容进行转换,然后再将响应发送到客户端。从上述过程可以看出,客户端和目标资源并不需要知道过滤器的存在,也就是说,在Web应用程序中部署过滤器,对客户端和目标资源来说是透明的。
在一个Web应用程序中,可以部署多个过滤器,这些过滤器组成了一个过滤器链。过滤器链中的每个过滤器负责特定的操作和任务,客户端的请求在这些过滤器之间传递,直到目标资源,如图16-2 所示。
在请求资源时,过滤器链中的过滤器将依次对请求进行处理,并将请求传递给下一个过滤器,直到目标资源;
在发送响应时,则按照相反的顺序对响应进行处理,直到客户端。
过滤器并不是必须要将请求传递到下一个过滤器(或目标资源),它也可以自行对请求进行处理,然后发送响应给客户端,或者将请求转发给另一个目标资源。
下面是过滤器再Web开发中得一些主要应用:
- 对用户请求进行统一认证
- 对用户的访问请求进行记录和审核
- 对用户发送的数据进行过滤或替换
- 转换图像格式
- 对相应内容进行压缩,减少传输量
- 对请求和响应进行加解密处理
- 触发资源访问事件
- 对XML 的输出应用XSLT
16.2 Filter API
与过滤器开发相关的接口与类都包含在javax.servlet 和 javax.servlet.http 包中,主要有下面的接口和类。
- javax.servlet.Filter 接口
- javax.servlet.FilterConfig 接口
- javax.servlet.FilterChain 接口
- javax.servlet.ServletRequestWrapper 类
- javax.servlet.ServletResponseWrapper 类
- javax.servlet.http.HttpServletRequestWrapper 类
- javax.servlet.http.HttpServletResponseWrapper类
16.2.1 Filter 接口
与开发 Servlet 要实现 javax.servlet.Servlet 接口类似, 开发过滤器要实现 javax.servlet.Filter 接口,并提供一个公开的不带参数的构造方法。在Filter 接口中,定义了下面的3个方法。
-> public void init (FilterConfig filterConfig) throws ServletException
Web容器调用该方法来初始化过滤器。容器在调用该方法时,向过滤器传递FilterConfig 对象,FilterConfig的用法和ServletConfig类似。利用FilterConfig 对象可以得到ServletContext对象,以及在部署描述符中配置的过滤器的初始化参数。在这个方法中,可以抛出ServletException 异常,通知容器该过滤器不能正常工作。
-> public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
doFilter() 方法类似于 Servlet 接口的 Servlet() 方法。当客户端请求目标资源的时候,容器就会调用与这个目标资源相关联的过滤器的doFilter()方法, 在这个方法中,可以对请求和响应进行处理,实现过滤器的特定功能。在特定的操作完成后,可以调用chain.doFilter(request, response) 将请求传给下一个过滤器(或目标资源),也可以直接向客户端返回响应信息,或者利用RequestDispatch 的forward()方法 和 include()方法,以及HttpServletResponse的sendRedirect()方法将请求转向到其他资源。需要注意的是,这个方法的请求和响应参数的类型是ServletRequest 和 ServletResponse, 也就是说,过滤器的使用并不依赖于具体的协议。
-> public void destroy()
Web 容器调用该方法指示过滤器的生命周期结束。在这个方法中,可以释放过滤器使用的资源。
与开发Servlet 不同的是,Filter接口并没有相应的实现类可供继承,要开发过滤器,只能直接实现Filter接口。
16.2.2 FilterConfig 接口
javax.servlet.FilterConfig 接口类似于 javax.servlet.ServletConfig 接口,用于在过滤器初始化时向其传递信息。FilterConfig 接口由容器实现,容器将其实例作为参数传入过滤器对象的init() 方法中。在FilterConfig 接口中,定义了以下4个方法。
-> public java.lang.String.getFilterName()
得到在部署描述符中指定的过滤器的名字。
-> public java.lang.String getInitParameter(java.lang.String name)
返回在部署描述符中指定的名字为 name 的初始化参数的值。如果这个参数不存在,该方法将返回 null。
-> public java.util.Enumeration getInitParameterNames()
返回过滤器的所有初始化参数的名字的枚举集合。如果过滤器没有初始化参数,这个方法将返回一个空的枚举集合。
-> public ServletContext getServletContext()
返回Servlet 上下文对象的引用。
16.2.3 FilterChain 接口
javax.servlet.FilterChain 接口由容器实现, 容器将其实例作为参数传入过滤器对象的doFilter()方法中。过滤器对象使用FilterChain 对象调用过滤器中的下一个过滤器,如果该过滤器是链中最后一个过滤器,那么将调用目标资源。FilterChain 接口只有一个方法,如下:
-> public void doFilter(ServletRequest request, ServletResponse response) throws java.io.IOException, ServletException
调用该方法将使过滤器链中的下一个过滤器被调用。如果调用该方法的过滤器是链中最后一个过滤器,那么目标资源被调用。
16.3 过滤器的部署
其中
下面是使用
<filter>
<filter-name>myFilter</filter-name>
<filter-class>cn.qccr.filter.MyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>myFilter</filter-name>
<url-pattern>/filterServlet</url-pattern>
</filter-mapping>
Servlet 容器对部署描述符中声明的每一个过滤器,只创建一个实例。与Servlet类似,容器将在同一个过滤器实例上运行多个线程来同时为多个请求服务,因此开发过滤器时也要考虑注意线程安全的问题。如果在部署时,对同一个过滤器类声明了两次,那么容器将会创建两个相同的过滤器类的实例。
其中
-> REQUEST
当用户直接访问页面时, Web容器将会调用过滤器。如果目标资源是通过RequestDispatcher 的include() 或forward()方法访问时,那么该过滤器将不会被调用。
-> INCLUDE
如果目标资源是通过RequestDispatch 的include()方法访问时,那么该过滤器将被调用。除此之外,过滤器不会被调用。
-> FORWARD
如果目标资源是通过RequestDispatcher 的forward() 方法访问时,那么该过滤器将被调用。除此之外,过滤器不会被调用。
-> ERROR
如果目标资源是通过声明式异常处理机制调用时,那么该过滤器将被调用。除此之外,过滤器不会被调用。
下面是使用
16.4 过滤器的开发
这一节 我们将编写一个简单的过滤器,来体验一下过滤器的开发、部署和运行情况。
实例的开发有下列步骤。
16.5 对用户进行统一验证的过滤器
对用户请求进行认证,这是Web应用程序中常见的功能。通常的做法是,当用户访问受保护的资源时,要求用户输入用户名和密码进行验证,验证通过后,将某个标记(flag)保存到Session对象中;
当用户再次访问受保护的资源时,则取出Session中的标记进行判断,如果用户已验证,则允许用户访问受保护的资源,如果用户还没有验证,则向客户端发送登录表单或错误信息。在这种实现方式中,所有的被保护资源(JSP 页面 或Servlet) 都需要添加 从Session 中取出标记进行判断 的代码。
如果一个系统的所有资源原先是开放的,而后来又要求一部分资源只有授权用户才能访问,那么为每一个受保护页面添加这样的代码将是非常烦琐的,而且还容易出错。由于过滤器在请求到达目标资源之前,会先被容器调用,而且过滤器的配置对于客户端和目标资源来说是透明的,因此采用过滤器来实现对用户的统一验证是一个非常好的办法。
在对用户进行验证后的通常处理方式为:当用户验证成功后,向用户发送成功登录信息,并给出一个首页连接,让用户可以进入首页;当用户验证失败后,向用户发送错误信息,并给出一个返回到登录页面的链接,让用户可以重新登录。在实际操作中,有这样一种情况,当用户访问一个受保护的页面时,服务器端发送登录页面,用户在输入了正确的用户名和密码后,希望能自动进入先前访问的页面,而不是进入首页。在论坛程序中经常会遇到这种情况,现在大多数论坛都允许未登录用户浏览帖子,而发布和回复帖子则需要登录。我们在未登录的情况下浏览帖子,看到一个帖子想回复,而论坛程序要求我们登录,当成功登录后,论坛程序只给出了进入首页的链接,这对我们来说是不方便的。
为了让用户登录后直接进入先前的页面(用户访问登录页面除外),我们需要在将登录页面发送客户端之前
保存用户先前访问页面的URL.下面的代码获取用户的请求URI 和 查询字符串,并保存到请求对象中,然后将请求转发(forward)给登录页面。
String request_uri = request.getRequestURI();
String strQuery = request.getQueryString();
if(null != strQuery) {
request_uri = request_uri + "?" + strQuery;
}
request.setAttribute("origin_uri",request_uri);
RequestDispatcher rd = request.getRequestDispatcher("logon.jsp");
rd.forward(request,response);
在登录页面中,只需要包含一个隐藏输入域,它的值为用户先前的请求URL,代码如下:
<input type="hidden" name="origin_uri" value="${request.Scope.origin_uri}">
当用户提交登录表单时,我们就得到了用户先前的请求URL,在验证通过后,可以将客户端重定向到先前访问的页面。
对用户进行统一验证的过滤器实例的开放有下列步骤。
step 1: 编写登录页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form method="post" action="logon.jsp?action=logon">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="name"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password"></td>
</tr>
<tr><input type="hidden" name="origin_uri" value="${requestScope.origin_uri}"></tr>
<tr>
<td><input type="reset" value="重填"></td>
<td><input type="submit" value="提交"></td>
</tr>
</table>
</form>
</body>
</html>
step 2 : 编写过滤器类
public class LogonFilter implements Filter {
private static final String LOGON_URI = "logon_uri";
private static final String HOME_URI = "home_uri";
private String logon_page;
private String home_page;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 从部署描述符中获取登录页面 和首页的URI。
logon_page = filterConfig.getInitParameter(LOGON_URI);
home_page = filterConfig.getInitParameter(HOME_URI);
if (null == logon_page || null == home_page) {
throw new ServletException("没有指定登录页面或主页!");
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 将请求对象和相应对象的类型转换为HttpServletRequest 和 HttpServletResponse
HttpServletRequest httpReq = (HttpServletRequest) request;
HttpServletResponse httpResp = (HttpServletResponse) response;
final HttpSession session = httpReq.getSession();
// 得到用户的请求URI
String request_uri = httpReq.getRequestURI();
// 得到Web应用程序的上下文路径
String ctxPath = httpReq.getContextPath();
// 去除上下文路径,得到剩余部分的路径
String uri = request_uri.substring(ctxPath.length());
// 判断用户访问的是否是登录页面
if (logon_page.equals(uri)) {
// 如果是登录页面,则通过查看是否有附加的查询参数,来判断用户是访问登录页面,还是提交登录页面
String strLogon = httpReq.getParameter("action");
if ("logon".equals(strLogon)) {
// 如果是提交登录信息,则对用户进行验证。
String name = httpReq.getParameter("name");
String password = httpReq.getParameter("password");
if ("zhangsan".equals(name) && "1234".equals(password)) {
// 验证通过后,在session对象中设置isLogon 属性为 true
session.setAttribute("isLogon", true);
// 在Session 对象中保存用户名
session.setAttribute("user",name);
// 从请求对象中取出用户先前访问页面的URI
String origin_uri = httpReq.getParameter("origin_uri");
// 如果origin_uri不为空,则将客户端重定向到用户先前访问的页面,否则将客户端重定向到首页
if (null != origin_uri && ! "".equals(origin_uri)) {
httpResp.sendRedirect(origin_uri);
} else {
httpResp.sendRedirect(ctxPath + home_page);
}
return;
} else {
// 如果验证失败,则从请求对象中获取用户先前访问页面的URI。如果该URI存在,则再次将它作为 origin_uri 属性的值保存到请求对象中。
String origin_uri = httpReq.getParameter("origin_uri");
if (null != origin_uri && !"".equals(origin_uri)) {
httpReq.setAttribute("origin_uri", origin_uri);
}
httpResp.setContentType("text/html;charset=GB2312");
PrintWriter out = httpResp.getWriter();
out.println("<h2> 用户名或密码错误,请重新输入。</h2>");
RequestDispatcher rd = httpReq.getRequestDispatcher(logon_page);
rd.include(httpReq, httpResp);
return;
}
} else {
// 如果用户不是提交登录信息,则调用 chain.doFilter()方法 ,调用登录页面。
chain.doFilter(request, response);
return;
}
} else {
// 如果访问的不是登录页面, 则判断用户是否已经登录。
String isLogon = (String) session.getAttribute("isLogon");
if ("true".equals(isLogon)) {
chain.doFilter(request, response);
} else {
// 如果用户没有登录,则将用户的请求 URI 作为origin_uri属性的值 ,保存到请求对象中。
String strQuery = httpReq.getQueryString();
if (null != strQuery) {
request_uri = request_uri + "?" + strQuery;
}
httpReq.setAttribute("origin_uri", request_uri);
// 将用户请求 转发给登录页面
RequestDispatcher rd = httpReq.getRequestDispatcher(logon_page);
rd.forward(httpReq, httpResp);
return;
}
}
}
@Override
public void destroy() {}
}
step 3: 编写首页页面
我们将取名为 home.jsp ,内容如下
<% page contextType="text/html;charset=gb2312" %>
${sessionScope.user}, 欢迎您。
step 4 : 编译和部署过滤器
<filter>
<filter-name>logonFilter</filter-name>
<filter-class>org.sunxin.filter.LogonFilter</filter-class>
<init-param>
<param-name>logon_uri</param-name>
<param-value>/logon.jsp</param-value>
</init-param>
<init-param>
<param-name>home_uri</param-name>
<param-value>/home.jsp</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>logonFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
step 5 : 运行 Web应用程序,测试logonFiltert
16.6 对请求和响应数据进行替换的过滤器
考虑这样的一个情景,一个项目组开发了一个留言板程序,再运行过程中发现了两个问题,
问题一:是用户在留言时经常输入HTML代码,破坏了留言板的正常显示,并带来了一些安全隐患;
问题二:是一些用户在留言时输入了不雅的字句,这些字句的显示给网站带来了不好的影响。现在请你来解决两个问题,你可能立即就会想到解决方法。
对于问题一,只要把用户输入的特殊字符转换为实体引用或字符引用就可以解决;
对于问题二,可以在不雅的字句前进行过滤,用字符串“xxx”对不雅的字句进行替换。
到了真正解决问题的时候了,你不想对现有代码进行修改,毕竟读懂别人的代码也要花费一段时间。于是你想到了过滤器,因为它可以在请求到达目标资源之前和响应到达客户端之前截取请求和响应数据,而且过滤器的部署对客户端和目标资源都是透明的,不需要修改现有的代码。
不过在查看API文档后我们发现,HttpServletRequest 类并没有提供对请求信息进行修改的setXXX()方法,而HttpServletResponse类也没有提供得到响应数据的方法。也就是说,虽然过滤器可以截取到请求和响应对象,但是却无法直接使用这两个对象对它们的数据进行替换。
虽然不能够直接改变请求和响应对象的状态,但是我们可以利用请求和响应的包装(wrapper)类来间接改
变请求和响应的信息。在Servlet 规范中,共定义了4 个包装类:ServletRequestWrapper,ServletResponseWrapper, HttpServletRequestWrapper, HttpServletResponseWrapper 这4个包装类分别实现了请求或响应的接口,如下所示:
- public class ServletResponseWrapper implement ServletRequest
- public class ServletResponseWrapper implement ServletReponse
- public class HttpServletRequestWrapper implement HttpServletRequest
- public class HttpServletResponseWrapper implement HttpServletResponse
从表面上看,这个4个类就好像是真正的请求和响应类,不过实质上是:它们在构造方法中接受真正的请求或响应对象,然后利用该对象的方法来完成自己需要实现的方法。包装类是装饰(Decorator)设计模式的运用,装饰设计模式给我们提供了一种不使用继承而修改或增加现有对象功能的方法。
有了包装类,要改变请求和响应信息就变得非常简单了,我们只需要编写一个包装类的子类,然后覆盖想要修改的方法就可以了,例如,我们想要为所有的请求添加一个查询字符串,可以编写一个类,从HttpServletRequestWrapper类继承,并重写getQueryString()方法。代码如下:
public class MyRequestWrapper extends HttpServletRequestWrapper {
/**
* Constructs a request object wrapping the given request.
*
* @param request the {@link HttpServletRequest} to be wrapped.
* @throws IllegalArgumentException if the request is null
*/
public MyRequestWrapper(HttpServletRequest request) {
// 利用super 变量调用父类的构造方法,传递请求对象。
super(request);
}
@Override
public String getQueryString() {
String str = "abc=123";
// 利用super 变量调用父类的同名方法,得到原有的查询字符串
String strQuery = super.getQueryString();
if (null != strQuery) {
strQuery = strQuery+"&"+str;
return strQuery;
} else {
return str;
}
}
}
然后 在过滤器类的 doFilter方法中,构造MyRequestWrapper类的对象,将其所为参数传给chain.doFilter()方法,代码如下:
...
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException,ServletException{
HttpServletRequest httpReq = (HttpServletRequest) request;
chain.doFilter(new MyRequestWrapper(httpReq),response);cun
}
...
在目标资源的代码中,如果调用request.getQueryString() 就会得到附加了字符串“abc=123”的查询字符串。
利用包装类和过滤器就可以解决留言板程序中存在的两个问题。对于第一个问题,可以编写一个HttpServletRequestWrapper类的子类,然后重写getParameter()方法,在这个方法中,对请求参数的值进行过滤,将特殊字符转换为对应的实体引用或字符引用。对于第二问题,当然也是编写一个HttpServletResponseWrapper类的子类,但如何才能得到响应的内容呢?需要注意的是,响应的内容是通过字符(PrintWriter) 或字节(ServletOutputStream)输出流对象向客户端输出的,而字符和字节输出流对象则是通过HttpServletResponse.getWriter()和HttpServletResponse.getOutputStream()方法得到的。正常情况下,响应的内容将被容器直接发送到客户端,要想得到响应的内容,就要替换默认的输出流对象,并且新的输出流对象应该是内存输出流对象,也就是当我们调用该输出流对象的writer()方法时,数据被写到内存的缓冲区中。
我们可以使用java.io 包中的ByteArrayOutputStream类,让数据写到字节数组中,同时重写HttpServletResponse类的getWriter()和getOutputStream()方法,返回构造在ByteArrayOutputStream之上的PrintWriter对象和SerlvetOutputStream对象。
下面我们看看具体的实现。步骤
step 1: 编写 MyRequestWrapper.java
MyRequestWrapper类从HttpServletRequestWrapper类继承,并重写了getParameter()方法,对请求参数的值进行过滤,将特殊字符转换为对应的实体引用或字符引用。完整的代码如下16-9 所示。
例 16-9 MyRequestWrapper.java
public class MyRequestWrapper2 extends HttpServletRequestWrapper {
/**
* Constructs a request object wrapping the given request.
*
* @param request the {@link HttpServletRequest} to be wrapped.
* @throws IllegalArgumentException if the request is null
*/
public MyRequestWrapper2(HttpServletRequest request) {
super(request);
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
if (null !=null) {
return toHtml(value.trim());
} else {
return null;
}
}
/**
* @Author lyg
* @Description
**/
private String toHtml(String str) {
if (str == null) {
return null;
}
StringBuffer sb = new StringBuffer();
int len = str.length();
for (int i = 0; i < len; i++) {
char c = str.charAt(i);
switch (c) {
case ' ':
sb.append(" ");
break;
case '\n':
sb.append("&<br>");
break;
case '\r':
break;
case '\'':
sb.append("'");
break;
case '<':
sb.append("<");
break;
case '>':
sb.append(">");
break;
case '&':
sb.append("&");
break;
case '"':
sb.append(""");
break;
case '\\':
sb.append("\");
break;
default:
sb.append(c);
}
}
return sb.toString();
}
}
step 2 : 编写 ByteArrayServletOutputStream.java
ByteArrayServletOutputStream 类从ServletOutputStream 类继承,该类的对象用于替换HttpServletResponse.getOutputStream()方法返回的ServletOutputStream 对象,其内部使用java.io.ByteArrayOutputStream 的write(int b)方法实现ServletOutputStream 类的write(int b)方法。完整代码如下:
public class ByteArrayServletOutputStream extends ServletOutputStream {
ByteArrayOutputStream baos;
ByteArrayServletOutputStream(ByteArrayOutputStream baos) {
this.baos = baos;
}
@Override
public void write(int b) throws IOException {
baos.write(b);
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
}
step 3 : 编写MyResponseWrapper.java
MyResponseWrapper 类从HttpServletResponseWrapper类继承,并重写了getWriter() 和getOutputStream()方法,用构建在ByteArrayOutputStream 之上的PrintWriter对象和ServletOutputStream对象替换Web容器创建的PrintWriter和ServletOutputStream 对象。完整的代码如例16-11 所示。
public class MyResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream baos;
private ByteArrayServletOutputStream basos;
private PrintWriter pw;
/**
* Constructs a response adaptor wrapping the given response.
*
* @param response the {@link HttpServletResponse} to be wrapped.
* @throws IllegalArgumentException if the response is null
*/
public MyResponseWrapper(HttpServletResponse response) {
super(response);
// 创建 ByteArrayOutputStream 对象
baos = new ByteArrayOutputStream();
// 用 ByteArrayOutputSteam 对象作为参数,
// 构造 ByteArrayServletOutputStream 对象
basos = new ByteArrayServletOutputStream(baos);
// 用 ByteArrayOutputSteam 对象作为参数,
// 构造 PrintWriter 对象
pw = new PrintWriter(baos);
}
@Override
public PrintWriter getWriter() {
return pw;
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return basos;
}
/**
* @Author lyg
* @Description 以字节数组的形式返回输出流缓冲区中的内容
**/
public byte[] toByteArray(){
return baos.toByteArray();
}
}
step 4 : 编写 GuestbookFilter.java
GuestbookFilter 是过滤器类,它利用MyRequestWrapper 和 MyResponseWrapper 类来替换请求中的特殊字符和响应中的不雅字句。不雅字句与替换的内容是以Java属性文件的格式保存到一个文件中,文件的路径名作为过滤器类的初始化参数在Web.xml文件中进行配置。 完整的代码如例:16-12 所示。
public class GuestbookFilter implements Filter {
private static final String WORD_FILE="word_file";
HashMap<String, String> hm = new HashMap<>();
/**
* 在init() 方法中,读取保存了不雅字句和替换内容的文件,并以不雅字句作为key, 替换内容作为value ,保存到Hashmap对象中。
**/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
String configPath = filterConfig.getInitParameter(WORD_FILE);
ServletContext sc = filterConfig.getServletContext();
String filePath = sc.getRealPath(configPath);
try {
FileReader fr = new FileReader(filePath);
BufferedReader br = new BufferedReader(fr);
String line;
while (null != (line = br.readLine())){
String[] strTemp = line.split("=");
hm.put(strTemp[0], strTemp[1]);
}
}catch (Exception e) {
throw new ServletException("读取过滤文件信息出错!");
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpReq = (HttpServletRequest) request;
HttpServletResponse httpResp = (HttpServletResponse) response;
// 得到请求和响应对象的封装类对象
MyRequestWrapper reqWrapper = new MyRequestWrapper(httpReq);
MyResponseWrapper respWrapper = new MyResponseWrapper(httpResp);
chain.doFilter(reqWrapper, respWrapper);
String content = new String(respWrapper.toByteArray());
String result = replaceText(content);
httpResp.setContentType("text/html;charset=gb2312");
PrintWriter out = httpResp.getWriter();
out.println(result);
out.close();
}
private String replaceText(String content) {
StringBuffer sb = new StringBuffer(content);
Set keys = hm.keySet();
final Iterator it = keys.iterator();
while (it.hasNext()) {
String key = (String) it.next();
int index = sb.indexOf(key);
while (-1 != index) {
sb.replace(index, index+key.length(), hm.get(key));
index = sb.indexOf(key);
}
}
return sb.toString();
}
@Override
public void destroy() {
}
}
step 5 : 准备留言板程序
16.7 对响应内容进行压缩的过滤器
一个网站的访问速度由多种因素共同决定,这些因素包括服务器性能、网络带宽、Web应用程序的响应速度、服务器与客户端之间的网络传输速度等。从软件的角度来说,要提升网站的访问速度,首先要尽可能的提高Web应用程序的执行速度,这可以通过优化代码的执行效率和使用缓存来实现。如果在此基础上,你还想进一步提升网页的浏览速度,那么可以对响应内容进行压缩,以节省网络带宽,提高用户的访问速度。
目前主流的浏览器和Web服务器都支持网页的压缩,浏览器和Web服务器对于压缩网页的通信过程如下所示:
① 如果浏览器能够接受压缩后的网页内容,那么它会在请求中发送Accept-Encoding 请求报头,值为“gzip,deflate”表明浏览器支持gzip 和deflate这两种压缩方式。
② Web服务器通过读取Accept-Encoding 请求报头的值来判断浏览器是否支持接受压缩内容,如果接受,Web服务器就将目标页面的相应内容采用gzip压缩方式压缩后再发送到客户端,同时设置Content-Encoding实体报头,值为gzip,已告知浏览器实体正文采用了gzip压缩编码。
③ 浏览器接收到响应内容后,根据Content-Encoding实体报头的值对响应内容解压缩,然后显示响应页面的内容。
我们可以通过过滤器对目标页面的响应内容进行压缩,其实现过程类似于上一节的“对请求和响应数据进行替换的过滤器”,实现原理就是使用包装类对象替换原始的响应对象,并使用java.util.zip.GZIPOutputStream 作为响应内容的输出流对象。GZIPOutputStream是过滤器类,它使用GZIP压缩格式写入压缩数据。
下面我们看具体的实现。实例的开发有下列步骤。
step 1: 编写GZIPServletOutputStream.java
GZIPServletOutputStream 继承自ServletOutputStream,该类的对象用于替换HttpServletResponse.getOutputStream()方法返回的ServletOutputStream对象,其内部使用GZIPOutputStream 的 write(int b ) 方法实现ServletOutputStream 类的write(int b)方法,以达到压缩数据的目的。完整代码如例16-16
public class GZIPServletOutputStream extends ServletOutputStream {
private GZIPOutputStream gzipos;
private GZIPServletOutputStream (ServletOutputStream sos) throws IOException {
// 使用响应输出流对象构造 GZIPOutputStream 过滤器流对象
this.gzipos = new GZIPOutputStream(sos);
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
@Override
public void write(int b) throws IOException {
// 将写入操作委托给 GZIPOutputStream对象的write()方法,从而实现响应输出流的压缩
gzipos.write(b);
}
public GZIPOutputStream getGZIPOutputStream() {
return gzipos;
}
}
step 2 : 编写CompressionResponseWrapper.java
CompressionResponseWrapper 类从HttpServletResponseWrapper 类继承,并重写了getWriter()方法和getOutputStream()方法,用GZIPServletOutputStream 替换了ServletOutputStream 对象。完整代码如例16-17所示。
public class CompressionResponseWrapper extends HttpServletResponseWrapper {
private GZIPServletOutputStream gzipsos;
private PrintWriter pw;
/**
* Constructs a response adaptor wrapping the given response.
*
* @param response the {@link HttpServletResponse} to be wrapped.
* @throws IllegalArgumentException if the response is null
*/
public CompressionResponseWrapper(HttpServletResponse response) throws IOException {
super(response);
// 用响应输出流创建 GZIPServletOutputStream 对象
gzipsos = new GZIPServletOutputStream(response.getOutputStream());
// 用GZIPServletOutputStream对象作为参数,构造 PrintWriter对象
pw = new PrintWriter(gzipsos);
}
/**
* @Author lyg
* @Description 重写该方法,以避免Content-Length 实体报头所指出的长度和压缩后的实体正文长度不匹配
**/
@Override
public void setContentLengthLong(long len) {
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return gzipsos;
}
@Override
public PrintWriter getWriter() throws IOException {
return pw;
}
/**
* @Author lyg
* @Description 过滤器 调用这个方法来得到GZIPOutputStream对象,以便完成将压缩数据写入输出流的操作
**/
public GZIPOutputStream getGZIPOutputStream(){
return gzipsos.getGZIPOutputStream();
}
}
step 3: 编写CompressionFilter.java
CompressionFilter 是过滤器类,它使用CompressionResponseWrapper 对象来实现对响应内容的压缩。完整的代码如例 16 -18 所示。
public class CompressionFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpReq = (HttpServletRequest) request;
HttpServletResponse httpResp = (HttpServletResponse) response;
String acceptEncodings = httpReq.getHeader("Accept-Encoding");
if (acceptEncodings != null && acceptEncodings.indexOf("gzip") > -1) {
// 得到响应对象的封装类对象
CompressionResponseWrapper respWrapper = new CompressionResponseWrapper(httpResp);
// 设置Content-Encoding 实体报头,告诉浏览器实体正文采用了 gzip压缩编码
respWrapper.setHeader("content-Encoding", "gzip");
chain.doFilter(httpReq,respWrapper);
// 得到GZIPOutputStream 输出流对象
GZIPOutputStream gzipos = respWrapper.getGZIPOutputStream();
// 调用 GZIPOutputStream 输出流对象的finish() 方法完成将压缩数据写入响应输出流的操作,无须关闭输出流
gzipos.finish();
} else {
chain.doFilter(httpReq, httpResp);
}
}
@Override
public void destroy() {
}
}
step 4: 编译 部署
<!--对响应内容进行压缩处理-->
<filter>
<filter-name>CompressionFilter</filter-name>
<filter-class>org.sunxin.filter.response.CompressionFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CompressionFilter</filter-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.html</url-pattern>
</filter-mapping>
step 5 : 运行 Web应用程序, 测试 CompressFilter
启动Tomcat 服务器,打开IE 浏览器,访问留言板程序,你将看到正常的页面输出,如图16-8 所示