在Servlet容器调用某个Servlet的service()方法前,Servlet并不会知道有请求的到来,而在Servlet的service()方法运行之后,容器真正对浏览器进行HTTP响应之前,浏览器也不会知道Servlet真正的响应是什么。过滤器正如其名称所示,它介于Servlet之前,可拦截过滤浏览器对Servlet的请求,也可以改变Servlet对浏览器的响应。本文将介绍过滤器的运用,了解如何实现Filter接口来编写过滤器,以及如何使用请求封装器及响应封装器,将容器产生的请求与响应对象加以包装,针对某些请求信息或响应进行加工处理。
1、过滤器的概念
想象已经开发好应用程序的主要商务功能了,但现在有几个需求出现:
(1)针对所有的servlet,产品经理想要了解从请求到响应之间的时间差。
(2)针对某些特定的页面,客户希望只有特定的几个用户有权浏览。
(3)基于安全的考量,用户输入的特定字符必须过滤并替换为无害的字符。
(4)请求与响应的编码从Big5改用UTF-8。
在修改源代码之前,先分析一下这些需求:
(1)在运行Servlet的service()方法“前”,记录起始时间,Servlet的service()方法运行“后”,记录结束时间并计算时间差。
(2)在运行Servlet的service()方法“前”,验证是否为允许的用户。
(3)在运行Servlet的service()方法“前”,对请求参数进行字符过滤与替换。
(4)在运行Servlet的service()方法“前”,对请求与响应对象设置编码。
经过以上分析,可以发现这些需求,可以在真正运行Servlet的service方法“前”与Servlet的service()方法“后”中间进行实现。如下图所示:
性能评测、用户验证、字符替换、编码设置等需求,基本上与应用程序的业务逻辑没有直接的关系,只是应用程序额外的元件服务之一。因此,这些需求应该设计为独立的元件,使之随时可以加入到应用程序中,也随时可以移除,或随时可以修改设置而不用修改原有的业务代码。这类元件就像是一个过滤器,安插在浏览器与Servlet中间,可以过滤请求与响应而作进一步的处理,如下图所示。
Servlet/JSP提供了过滤器机制让你实现这些元件服务,可以视 需求抽换过滤器或调整过滤器的顺序,也可以针对不同的URL应用不同的过滤器。甚至在不同的Servlet间请求转发或包含时应用过滤器。
2、实现并设置过滤器
在Servlet中要实现过滤器,必须实现Filter接口,并使用@WebFilter标注或在web.xml中定义过滤器,让容器知道该加载哪些过滤器类。Filter接口有三个要实现的方法:init()、doFilter()与destroy()。
package javax.servlet;
import java.io.IOException;
public interface Filter {
public void init(FilterConfig filterConfig) throws ServletException;
public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain) throws IOException, ServletException;
public void destroy();
}
FilterConfig类似于Servlet接口init()方法参数上的ServletConfig,FilterConfig是实现Filter接口的类上使用标注或web.xml中过滤器设置信息的代表对象。如果在定义过滤器时设置了初始参数,则可以通过FilterConfig的getInitParameter()方法来取得初始参数。
Filter接口的doFilter()方法则类似于Servlet接口的service()方法。当请求来到容器,而容器发现调用Servlet的service()方法前,可以应用某过滤器时,就会调用该过滤器的doFilter()方法。可以在doFilter()方法中进行service()方法的前置处理,而后决定是否调用FilterChain的doFilter()方法。如果调用了FilterChain的doFilter()方法,就会运行下一个过滤器,如果没有下一个过滤器,就调用请求目标Servlet的service()方法(这里实际上用到了责任链模式)。如果没有调用FilterChain的doFilter()方法,则请求就不会继续交给接下来的过滤器或目标Servlet,这就是所谓的拦截请求(从Servlet的角度来看,根本不知道浏览器有发出请求)。
以下是一个简单的性能评测过滤器,用来记录请求与响应的时间差。
@WebFilter(
filterName="PerformanceFilter",
urlPatterns={"/*"},
dispatcherTypes={
DispatcherType.FORWARD,
DispatcherType.INCLUDE,
DispatcherType.REQUEST,
DispatcherType.ERROR,DispatcherType.ASYNC
},
initParams={@WebInitParam(name="Site", value="菜鸟教程")}
)
public class PerformanceFilter implements Filter {
private FilterConfig config;
public PerformanceFilter() {
}
public void destroy() {
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
long begin = System.currentTimeMillis();
chain.doFilter(request, response);
config.getServletContext().log("Performance process in " +
(System.currentTimeMillis() - begin) + " milliseconds");
// 输出站点名称
System.out.println("站点网址:http://www.runoob.com");
}
public void init(FilterConfig fConfig) throws ServletException {
// 获取初始化参数
this.config = fConfig;
String site = config.getInitParameter("Site");
// 输出初始化参数
System.out.println("PerformanceFilter init done! 网站名称: " + site);
}
}
当过滤器类被载入容器并实例化后,容器会运行其init()方法并传入FilterConfig对象作为参数。过滤器的设置与Servlet的设置很类似,@WebFilter中的filterName设置过滤器名称,urlPatterns设置哪些URL请求必须应用哪个过滤器,可应用的URL模式与Servlet基本上相同,而”/*“表示应用在所有的URL请求上。除了指定URL模式外,也可以指定Servlet名称,这可以通过@WebFilter的servletNames来设置:
@WebFilter(filterName="PerformanceFilter", servletNames={"Servlet1","Servlet2"})
如果想一次符合所有的Servlet名称,可以使用星号(*)。如果在过滤器初始化时,想要读取一些参数,可以在@WebFilter中使用@WebInitParam来设置initParams,例如:
@WebFilter(
filterName="EncodingFilter",
urlPatterns={"/encoding"},
initParams={
@WebInitParam(name="ENCODING", value="UTF-8")
})
public class EncodingFilter implements Filter {
private String ENCODING;
private FilterConfig config;
public EncodingFilter() {
}
public void init(FilterConfig fConfig) throws ServletException {
// TODO Auto-generated method stub
config = fConfig;
ENCODING = config.getInitParameter("ENCODING");
// 输出初始化参数
System.out.println("EncodingFilter init done! ENCODING = " + ENCODING);
}
...
}
触发过滤器的时机,默认是浏览器直接发出请求时。如果是那些通过RequestDispatcher的forward()或include()发出的请求,需要设置@WebFilter的dispatcherTypes,例如:
@WebFilter(
filterName="some",
urlPatterns={"/some"},
dispatcherTypes={
DispatcherType.FORWARD,
DispatcherType.INCLUDE,
DispatcherType.REQUEST,
DispatcherType.ERROR,DispatcherType.ASYNC
})
如果不设置任何dispatcherTypes,则默认为REQUEST。FORWARD就是指通过RequestDispatcher的forward()方法而来的请求可以套用过滤器,INCLUDE是指通过RequestDispatcher的include方法而来的请求可以套用过滤器,ERROR是指由容器处理例外而转发过来的请求可以套用过滤器,ASYNC是指异步处理器的请求可以触发过滤器。
3、实现请求封装器
以下通过两个例子,来说明请求封装器的实现与应用,分别是特殊字符替换过滤器与编码设置过滤器。
1、实现字符替换过滤器
假设有个留言板程序已经上线并正常运行中,但是发现,有些用户会在留言中输入一些HTML标签。基于安全性的考虑,不希望用户输入的HTML标签直接出现在留言中而被一些浏览器当作HTML的一部分来解释。例如,并不希望用户在留言中输入<a href=”http://openhome.cc”>OpenHome.cc</a>这样的信息。不希望在留言显示中有超链接,希望将一些HTML字符过滤掉,如将<、>这样的角括号置换为HTML实体字符,可以使用过滤器的方式。但问题在于,虽然可以使用HttpServletRequest的getParameter()取得请求参数值,但是没有一个像setParameter()的方法,可以将处理过后的参数值重新设置给HttpServletRequest。
所幸,有个HttpServletRequestWrapper帮我们实现了HttpServletRequest接口,只要继承这个类,并编写想要重新定义的方法即可。相对应于ServletRequest接口,也有个ServletRequestWrapper类可以使用。
以下范例通过继承HttpServletRequestWrapper实现一个请求封装器,可以将请求参数中的HTML字符替换为HTML实体字符。
public class EscapeWrapper extends HttpServletRequestWrapper {
public EscapeWrapper(HttpServletRequest request) {
super(request);//必须调用父类构造器,将HttpServletRequest实例传入
}
@Override
public String getParameter(String name) {
String value = getRequest().getParameter(name);
return StringEscapeUtils.escapeHtml(value);
//将请求参数值进行字符替换
}
}
之后若有Servlet想取得请求参数值,都会调用getParameter()方法,所以这里重新定义这个方法,在此方法中,进行字符替换动作。可以使用这个请求封装器搭配过滤器,以进行字符过滤的服务。例如:
@WebFilter(
filterName="EscapeFilter",
urlPatterns={"/guestbook"},
dispatcherTypes={
DispatcherType.FORWARD,
DispatcherType.INCLUDE,
DispatcherType.REQUEST,
DispatcherType.ERROR,DispatcherType.ASYNC
})
public class EscapeFilter implements Filter {
private FilterConfig config;
public EscapeFilter() {
}
public void destroy() {
System.out.println("EscapeFilter calling done!");
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
long begin = System.currentTimeMillis();
HttpServletRequest requestWrapper = new EscapeWrapper((HttpServletRequest)request);
chain.doFilter(requestWrapper, response);
config.getServletContext().log("Request escaping HTML tags in " +
(System.currentTimeMillis() - begin) + " milliseconds");
}
public void init(FilterConfig fConfig) throws ServletException {
this.config = fConfig;
System.out.println("EscapeFilter init done!");
}
}
2、实现编码设置过滤器
在之前的范例中,如果要设置请求字符编码,都是在个别Servlet中处理。可以在过滤器中进行字符编码的统一设置,如果日后想要改变编码,就不用每个Servlet逐一修改了。
由于HttpServletRequest的setCharacterEncoding()方法针对的是请求的Body内容,对于GET请求,必须在取得请求参数的字节阵列后,重新指定编码来解析。这个需求与上一个范例类似,可搭配请求封装器来实现。
public class EncodingWrapper extends HttpServletRequestWrapper {
private String ENCODING;
public EncodingWrapper(HttpServletRequest request, String ENCODING) {
super(request);
this.ENCODING = ENCODING;
}
@Override
public String getParameter(String name){
String value = getRequest().getParameter(name);
if(value != null) {
try {
//Web容器默认使用ISO-8859-1编码格式
byte[] b = value.getBytes("ISO-8859-1");
value = new String(b, ENCODING);
} catch(UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
return value;
}
}
编码过滤器的实现如下:
@WebFilter(
filterName="EncodingFilter",
urlPatterns={"/encoding"},
dispatcherTypes={
DispatcherType.FORWARD,
DispatcherType.INCLUDE,
DispatcherType.REQUEST,
DispatcherType.ERROR,DispatcherType.ASYNC
},
initParams={
@WebInitParam(name="ENCODING", value="UTF-8")
})
public class EncodingFilter implements Filter {
private String ENCODING;
private FilterConfig config;
public EncodingFilter() {
}
public void destroy() {
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)request;
if("GET".equals(req.getMethod())) {
long begin = System.currentTimeMillis();
req = new EncodingWrapper(req, ENCODING);
chain.doFilter(req, response);
config.getServletContext().log("GET Method Request Encoding process in " + (System.currentTimeMillis() - begin) + " milliseconds");
} else {
req.setCharacterEncoding(ENCODING);
chain.doFilter(req, response);
}
}
public void init(FilterConfig fConfig) throws ServletException {
config = fConfig;
ENCODING = config.getInitParameter("ENCODING");
// 输出初始化参数
System.out.println("EncodingFilter init done! ENCODING = " + ENCODING);
}
}
请求参数的编码设置是通过过滤器初始参数来设置的,并在过滤器初始化方法init()中读取,过滤器仅在GET请求以创建EncodingWrapper实例,其他方法则通过HttpServletRequest的setCharacterEncoding()来设置编码,最后都调用FilterChain的doFilter()方法传入EncodingWrapper实例或原请求对象。
3、实现响应封装器
在Servlet中,是通过HttpServletResponse对象来对浏览器进行响应的,如果想要对响应的内容进行压缩处理,就要想办法让HttpServletResponse对象具有压缩处理的功能。前面介绍过请求封装器的实现,而在响应封装器的部分,可以继承HttpServletResponseWrapper类来对HttpServletResponse对象进行封装。
若要对浏览器进行输出响应,必须通过getWriter()取得PrintWriter,或是通过getOutputStream()取得ServletOutputStream。 所以针对压缩输出的需求,主要就是继承HttpServletResponseWrapper类之后,通过重新定义这两个方法来达成。
在下面例子中,压缩的功能采用GZIP格式,这是浏览器可以授受的压缩格式,可以使用GZIPOutputStream类来实现。由于getWriter()的PrintWriter在创建时,也是必须使用到ServletOutputStream,所以在这里先扩展ServletOutputStream类,让它具有压缩的功能。
public class GZipServletOutputStream extends ServletOutputStream {
private GZIPOutputStream gzipOutputStream;
public GZipServletOutputStream(ServletOutputStream servletOutputStream) throws IOException {
this.gzipOutputStream = new GZIPOutputStream(servletOutputStream);
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener listener) {
}
public GZIPOutputStream getGzipOutputStream(){
return gzipOutputStream;
}
@Override
public void write(int b) throws IOException {
gzipOutputStream.write(b); //输出时通过gzipOutputStream来压缩输出
}
}
在HttpServletResponse对象传入Servlet的service()方法前,必须先封装它,使得调用getOutputStream()时,可以取得这里所实现的GZipServletOutputStream对象,而调用getWriter()时,也可以利用GZipServletOutputStream对象来构造PrintWriter对象。
public class CompressionWrapper extends HttpServletResponseWrapper {
private GZipServletOutputStream gzServletOutputStream;
private PrintWriter printWriter;
public CompressionWrapper(HttpServletResponse response) {
super(response);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
//响应中已经调用过getWriter,再调用getOutputStream就抛出异常
if(printWriter != null) {
throw new IllegalStateException();
}
if(null == gzServletOutputStream) {
gzServletOutputStream =
new GZipServletOutputStream(getResponse().getOutputStream());
}
return gzServletOutputStream;
}
@Override
public PrintWriter getWriter() throws IOException {
//响应中已经调用过getOutputStream,再调用getWriter就抛出异常
if(gzServletOutputStream != null) {
throw new IllegalStateException();
}
if(null == printWriter) {
gzServletOutputStream = new GZipServletOutputStream(getResponse().getOutputStream());
OutputStreamWriter osw = new OutputStreamWriter(
gzServletOutputStream, getResponse().getCharacterEncoding());
printWriter = new PrintWriter(osw);
}
return printWriter;
}
//不实现此方法,因为真正的输出会被压缩,忽略原来的内容长度设置
@Override
public void setContentLength(int len){
}
public GZIPOutputStream getGZIPOutputStream() {
if(this.gzServletOutputStream == null)
return null;
return this.gzServletOutputStream.getGzipOutputStream();
}
}
在上例中要注意,由于Servlet规范中规定,在同一个请求期间,getWriter()与getOutputStream()只能择一调用,否则必抛出IllegalStateException,因此建议在实现响应封装器时,也遵循这个规范。因此在重新定义getOutputStream()与getWriter()方法时,分别要检查是否已经存在PrintWriter与ServletOutputStream实例。
接下来就实现一个压缩过滤器,使用上面开发的CompressionWrapper来封装原HttpServletResponse。
@WebFilter(
filterName="CompressionFilter",
urlPatterns = { "/*" })
public class CompressionFilter implements Filter {
private FilterConfig config;
public CompressionFilter() {
}
public void destroy() {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)request;
HttpServletResponse res = (HttpServletResponse)response;
String encodings = req.getHeader("accept-encoding");
//检查是否接受压缩
if((encodings != null) && (encodings.indexOf("gzip") > -1)) {
long begin = System.currentTimeMillis();
CompressionWrapper responseWrapper = new CompressionWrapper(res);
responseWrapper.setHeader("content-encoding", "gzip");
//设置响应内容编码为gzip
chain.doFilter(request, responseWrapper);
GZIPOutputStream gzipOutputStream = responseWrapper.getGZIPOutputStream();
if(gzipOutputStream != null) {
gzipOutputStream.finish();
//调用GZIPOutputStream的finish方法完成压缩输出
}
config.getServletContext().log("gzip compression process in " +
(System.currentTimeMillis() - begin) + " milliseconds");
}
else {
chain.doFilter(request, response);
//不接受压缩直接进行下一个过滤器
}
}
public void init(FilterConfig fConfig) throws ServletException {
this.config = fConfig;
System.out.println("CompressionFilter init done!");
}
}
浏览器是否接受GZIP压缩格式,可以通过检查accept-encoding请求标头中是否包括gzip字符串来判断。如果可以接受GZIP压缩,创建CompressionWrapper封装原响应对象,并设置content-encoding响应标头为gzip,这样浏览器就会知道响应内容是GZIP压缩格式。接着调用FilterChain的doFilter()时,传入响应对象为CompressionWrapper对象。当FilterChain的doFilter()结束时,必须调用GZIPOutputStream的finish()方法,这才会将GZIP后的资料从缓冲区全部移出并进行响应。
如果浏览器不接受GZIP压缩格式,则直接调用FilterChain的doFilter(),这样就可以让不接受GZIP压缩格式的客户端也可以收到原有的响应内容。