一、Filter概述
Filter是过滤器的意思,过滤器是一种代码重用技术,它可以改变HTTP请求的内容、响应及header信息。过滤器通常不产生响应或像servlet那样对请求作出响应,而是修改或调整到资源的请求及修改或调整来自资源的响应。
供开发人员使用的过滤器功能有如下几种类型:
- 在执行请求之前访问资源
- 在执行请求之前处理资源的请求
- 用请求对象的自定义版本包装请求对请求的header和数据进行修改
- 用响应对象的自定义版本包装响应对响应的header和数据进行修改
- 拦截资源调用之后的调用
- 作用在Web资源上多个拦截器按指定的顺序执行
二、Filter的基本工作原理
Filter程序是一个实现了特殊接口(javax.servlet.Filter)的类,与Servlet程序类似,它也是由Servlet容器进行调度和执行的。Filter程序需要在web.xml文件中进行注册和设置它所能拦截的web资源。
当在web.xml文件中注册了一个Filter来对某个某个Servlet程序进行拦截处理时,这个Filter就成了Servlet容器与该Servlet程序的通信线路上的一道关卡,它可以对Servlet容器发送给Servlet程序的请求和Servlet程序回送给Servlet容器的响应进行拦截,可以决定是否将请求继续传递给Servlet程序,以及对请求和响应信息是否进行修改。当Servlet容器开始调用某个Servlet程序时,如果发现已经注册了一个Filter程序来对该Servlet进行拦截,那么,Servlet容器将不再直接调用Servlet的service方法,而是调用Filter的doFilter方法,再由doFilter方法决定是否去激活Servlet的service方法。基本工作原理如下图:
在一个web应用中,可以开发编写多个Filter,这些Filter组合起来称之为一个Filter链。
web服务器根据Filter在web.xml文件中的注册顺序,决定先调用哪个Filter,当第一个Filter的doFilter方法被调用时,web服务器会创建一个代表Filter链的FilterChain对象传递给该方法。在doFilter方法中,开发人员如果调用了FilterChain对象的doFilter方法,则web服务器会检查FilterChain对象中是否还有filter,如果有,则调用第2个filter,如果没有,则调用目标资源。
三、Filter生命周期
一个Filter程序就是一个Java类,他必须实现javax.servlet.Filter接口,此接口中定义了三个方法:init、doFilter、destroy。
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyFilter implements Filter {
private int parameter;
/**
* web程序启动时调用此方法,用于初始化Filter
* filterConfig可以从此参数中获取初始化参数信息及Servletontext信息等
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
this.parameter = Integer.parseInt(filterConfig.getInitParameter("init"));
}
/**
* 客户请求服务器时会经过
* chain.doFilter(httpServletRequest, httpServletResponse);将请求传给下个Filter或Servlet
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest)request;
HttpServletResponse httpServletResponse = (HttpServletResponse)response;
System.out.println(parameter++);
//执行下一个过滤器
chain.doFilter(httpServletRequest, httpServletResponse);
System.out.println(parameter);
}
/**
* web程序关闭时调用此方法,用于销毁资源
*/
@Override
public void destroy() {
}
}
四、编写Filter
1,编写Filter类(设置编码格式为例)
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class FirstFilter implements Filter {
private String paramValue = null;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
this.paramValue = filterConfig.getInitParameter("encoding");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest)request;
HttpServletResponse httpServletResponse = (HttpServletResponse)response;
httpServletRequest.setCharacterEncoding(paramValue);
httpServletResponse.setCharacterEncoding(paramValue);
System.out.println("begin Filter-----");
//执行下一个过滤器
chain.doFilter(httpServletRequest, httpServletResponse);
System.out.println("end Filter-----");
}
@Override
public void destroy() {
}
}
2,将该Filter在web.xml中注册
在web.xml中,一个<filter>元素用于注册一个Filter。<filter>元素中包含三个主要的子元素:<filter-name>、<filter-class>和<init-param>,它们分别用于设置Filter的注册名称、Filter的完整类名和Filter的初始化参数。
<filter>
<filter-name>firstFilter</filter-name>
<filter-class>com.inspur.tax.common.autorun.FirstFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
3,映射Filter
在web.xml文件中,一个<filter-mapping>元素用于设置一个Filter所负责拦截的资源。一个Filter拦截的资源可以通过两种方式来指定:Servlet名称和资源的访问请求路径。另外,Servlet容器调用一个资源的方式有以下4种:
- 通过正常的访问请求调用
- 通过RequestDispatcher.include方法调用
- 通过RequestDispatcher.forward方法调用
- 作为错误响应资源调用
<filter-mapping>元素定义了四个子元素:<filter-name>、<servlet-name>、<url-pattern>和<dispatcher>,它们分别用于设置Filter的注册名称、Filter所拦截的Servlet名称、Filter所拦截的访问请求路径、Filter所拦截的资源被Servlet容器所调用的方式。
<filter-mapping>
<filter-name>firstFilter</filter-name>
<url-pattern>/*</url-pattern>
<servlet-name>springServlet</servlet-name>
</filter-mapping>
<dispatcher> 子元素可以设置的值及其意义:
- EQUEST:当用户直接访问页面时,Web容器将会调用过滤器。如果目标资源是通过RequestDispatcher的include()或forward()方法访问时,那么该过滤器就不会被调用。
- INCLUDE:如果目标资源是通过RequestDispatcher的include()方法访问时,那么该过滤器将被调用。除此之外,该过滤器不会被调用。
- FORWARD:如果目标资源是通过RequestDispatcher的forward()方法访问时,那么该过滤器将被调用,除此之外,该过滤器不会被调用。
- ERROR:如果目标资源是通过声明式异常处理机制调用时,那么该过滤器将被调用。除此之外,过滤器不会被调用。
五,Filter高级应用
Filter程序能修改request请求信息,意思就是目标servlet调用request的getXxx等方法时不要返回原来的信息,而是返回修改过的信息,但是有什么办法可以修改request对象的信息呢?查看HttpServletRequest的接口文档,看到的尽是getXxx方法,并没有定义对应的setXxx方法,显然无法直接修改request请求信息。
Filter程序能修改response响应结果,首先要做的事得到Servlet写入到response对象中的数据,这样才能在这些数据的基础上修改。但是问题是Servlet写入到response对象中的实体内容是以IO流的形式写入的,一旦写入,无法修改,那么怎么才能修改呢?
回想一下Filter的拦截处理过程,Filter程序是调用doFilter方法向目标Servlet传递了一个request和response对象,目标Servlet并不知道Servlet容器对它的调用过程是否被Filter过滤过,它不关心其调用过程是否被Filter过滤过,它只知道从request中读取请求和向response写入响应消息,因此,只要在Filter程序中创建对原始request和response对象包装过的另外两个request和response对象,然后将包装对象传递下去那就可以实现修改效果。细节如下:
- 在Filter程序中新建一个request对象,用这个新的request对象包装原始的request对象,然后将新的request对象传递给目标Servlet。当目标Servlet调用新的request对象方法读取信息时,该方法内部可以地调用原始的request相应方法,也可以在此基础上修改后返回servlet,这样的话Sservlet得到的就是修改后的信息,这就完成了对request请求信息的修改。
- 当目标Servlet调用这个新的response对象写入相应信息时,可以将写入的信息缓存起来,然后修改缓存信息,这就完成了对response响应信息修改。但是需要注意的是,Servlet容器只认它最初创建的那个response对象,因此新的response对象将写入到其中的数据修改后再写入到原始的response对象中,Servlet容器得到的就是修改后的数据,这就完成了对响应消息的修改。
下面以文本替换为例:将页面上的某些敏感文字替换成**。(只修改response响应信息)
1,过滤器程序
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Properties;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.inspur.filter.WaterMarkResponseWrapper;
public class ReplaceTextFilter implements Filter {
private Properties p = new Properties();
@Override
public void init(FilterConfig filterConfig) throws ServletException {
//获得文件路径
String file = filterConfig.getInitParameter("filePath");
String realPath = filterConfig.getServletContext().getRealPath(file);
try {
//加载properties文件
p.load( new InputStreamReader(new FileInputStream(realPath),"utf-8"));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest)request;
HttpServletResponse httpServletResponse = (HttpServletResponse)response;
//自定义response
ReplaceTextResponseWrapper replaceTextResponseWrapperResponse = new ReplaceTextResponseWrapper(httpServletResponse);
System.out.println("begin replaceText----------");
//业务逻辑
chain.doFilter(httpServletRequest, replaceTextResponseWrapperResponse);
//修改返回的Response
replaceTextResponseWrapperResponse.finishResponse(p);
System.out.println("end replaceText----------");
}
@Override
public void destroy() {
}
}
2,自定义response(包装的response)
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Properties;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
public class ReplaceTextResponseWrapper extends HttpServletResponseWrapper {
private HttpServletResponse response;
//缓存类
private ReplaceTextOutputStream replaceTextOutputStream = new ReplaceTextOutputStream();
public ReplaceTextResponseWrapper(HttpServletResponse response) {
super(response);
this.response = response;
}
/**
* 缓存到这个输出流replaceTextOutputStream中的byteArrayOutputStream
*/
@Override
public ServletOutputStream getOutputStream() throws IOException {
return replaceTextOutputStream;
}
/**
* 缓存
*/
@Override
public PrintWriter getWriter() throws IOException {
return new PrintWriter(new OutputStreamWriter(getOutputStream(),"utf-8"));
}
/**
* 强制刷新
*/
@Override
public void flushBuffer() throws IOException {
replaceTextOutputStream.flush();
}
/**
* 替换文本,并输出到浏览器
* @param p
* @throws IOException
*/
public void finishResponse(Properties p) throws IOException{
byte[] byteSource = replaceTextOutputStream.getByteArrayOutputStream().toByteArray();
String source = new String(byteSource,"utf-8");
//文本替换
for(Object object :p.keySet()){
String key = (String) object;
source = source.replaceAll(key, p.getProperty(key));
}
//输出到浏览器
PrintWriter out = response.getWriter();
out.write(source);
out.flush();
out.close();
}
}
3,缓存response相应信息
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import javax.servlet.ServletOutputStream;
public class ReplaceTextOutputStream extends ServletOutputStream {
//临时存储response信息
private ByteArrayOutputStream byteArrayOutputStream;
public ReplaceTextOutputStream(){
byteArrayOutputStream = new ByteArrayOutputStream();
}
@Override
public void write(int b) throws IOException {
byteArrayOutputStream.write(b);
}
@Override
public void write(byte[] b) throws IOException {
byteArrayOutputStream.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
byteArrayOutputStream.write(b, off, len);
}
@Override
public void flush() throws IOException {
byteArrayOutputStream.flush();
}
@Override
public void close() throws IOException {
byteArrayOutputStream.flush();
byteArrayOutputStream.close();
}
public ByteArrayOutputStream getByteArrayOutputStream() {
return byteArrayOutputStream;
}
}
4,web.xml中注册和映射
<!-- 文字替换 -->
<filter>
<filter-name>replaceTextFilter</filter-name>
<filter-class>com.inspur.filter.replaceText.ReplaceTextFilter</filter-class>
<init-param>
<param-name>filePath</param-name>
<param-value>/WEB-INF/p.properties</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>replaceTextFilter</filter-name>
<url-pattern>*.html</url-pattern>
</filter-mapping>
5,p.properties文件内容
色情 = **
6,test.html文件内容
<!DOCTYPE HTML><html lang="en"><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Apache Tomcat Examples</title>
</head>
<body>
杜绝浏览色情信息
</body>
</html>
最终访问test.html页面效果:
六、介绍一下response的getOutputStream和getWriter
1.选择getOutputStream 和getWriter方法的要点
- PrintWriter对象输出字符文本内容时,它内部还是将字符串转换成了某种字符集编码的字节数组后再进行输出,使用PrintWriter对象的好处就是不用编程人员自己来完成字符串到字节数组的转换。
- 使用ServletOutputStream对象也能输出内容全为文本字符的网页文档,但是,如果网页文档内容是在Servlet程序内部使用文本字符串动态拼凑和创建出来的,则需要先将字符文本转换成字节数组后输出。
- 如果一个网页文档内容全部为字符文本,但是这些内容可以直接从一个字节输入流中读取出来,然后再原封不动地输出到客户端,那么就应该使用ServletOutputStream对象直接进行输出,而不要使用PrintWriter对象进行输出。
2.两种方法区别
- getOutputStream方法用于返回Servlet引擎创建的字节输出流对象,Servlet程序可以按字节形式输出响应正文。
- getWriter方法用于返回Servlet引擎创建的字符输出流对象,Servlet程序可以按字符形式输出响应正文。
- getOutputStream和getWriter这两个方法互相排斥,调用了其中的任何一个方法后,就不能再调用另一方法。
- getOutputStream方法返回的字节输出流对象的类型为ServletOutputStream,它可以直接输出字节数组中的二进制数据。
- getWriter方法将Servlet引擎的数据缓冲区包装成PrintWriter类型的字符输出流对象后返回,PrintWriter对象可以直接输出字符文本内容。
- Servlet程序向ServletOutputStream或PrintWriter对象中写入的数据将被Servlet引擎获取,Servlet引擎将这些数据当作响应消息的正文,然后再与响应状态行和各响应头组合后输出到客户端。
- Serlvet的service方法结束后,Servlet引擎将检查getWriter或getOutputStream方法返回的输出流对象是否已经调用过close方法,如果没有,Servlet引擎将调用close方法关闭该输出流对象。
参考书籍: 《javaWeb整合之王者归来》
《Servlet3.1规范》
《深入体验java Web开发内幕-高级特性》