Servlet过滤器与封装器

来源:互联网 发布:学校网络管理制度大全 编辑:程序博客网 时间:2024/06/03 22:14

  在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压缩格式的客户端也可以收到原有的响应内容。