Servlet过滤器与封装器

2020-08-29  本文已影响0人  霸体

在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()方法“后”中间进行实现。如下图所示:


image.png

性能评测、用户验证、字符替换、编码设置等需求,基本上与应用程序的业务逻辑没有直接的关系,只是应用程序额外的元件服务之一。因此,这些需求应该设计为独立的元件,使之随时可以加入到应用程序中,也随时可以移除,或随时可以修改设置而不用修改原有的业务代码。这类元件就像是一个过滤器,安插在浏览器与Servlet中间,可以过滤请求与响应而作进一步的处理,如下图所示。


image.png

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

转载

https://blog.csdn.net/fuzhongmin05/article/details/72723969

上一篇下一篇

猜你喜欢

热点阅读