JSP Filters(过滤器)

  Filter是拦截Request请求的对象:在用户的请求访 问资源前处理ServletRequest以及ServletResponse,它可 用于日志记录、加解密、Session检查、图像文件保护 等。通过Filter可以拦截处理某个资源或者某些资源。 Filter的配置可以通过Annotation或者部署描述来完成。 当一个资源或者某些资源需要被多个Filter所使用到, 且它的触发顺序很重要时,只能通过部署描述来配置。

一.Filter API

1. Filter 相关接口,包含Filter, FilterConfig, FilterChain

  Filter的实现必须继承javax.servlet.Filter接口。这个 接口包含了Filter的3个生命周期:init、doFilter、 destroy。

  Servlet容器初始化Filter时,会触发Filter的init方 法,一般来说是在应用开始时。也就是说,init方法并 不是在该Filter相关的资源使用到时才初始化的,而且 这个方法只调用一次,用于初始化Filter。init方法的定 义如下:

oid init(FilterConfig filterConfig)

注意: FilterConfig实例是由Servlet容器传入init方法中的.

  当Servlet容器每次处理Filter相关的资源时,都会调 用该Filter实例的doFilter方法。Filter的doFilter方法包含 ServletRequest、ServletResponse、FilterChain这3个参 数。

  doFilter的定义如下:

void doFilter(ServletRequest request, ServletResponse response,FilterChain filterChain)

  接下来,说明一下doFilter的实现中访问 ServletRequet、ServletResponse。这也就意味着允许给 ServletRequest增加属性或者增加Header。当然也可以修 饰ServletRequest或者ServletRespone来改变它们的行 为。

  在Filter的doFilter的实现中,最后一行需要调用 FilterChain中的doFilter方法。注意Filter的doFilter方法 里的第3个参数,就是filterChain的实例:

filterChain.doFilter(request, response)

  一个资源可能需要被多个Filter关联到(更专业一 点来说,这应该叫作Filter链条),这时Filter.doFilter() 的方法将触发Filter链条中下一个Filter。只有在Filter链 条中最后一个Filter里调用的FilterChain.doFilter(),才会 触发处理资源的方法。

  如果在Filter.doFilter()的实现中,没有在结尾处调 用FilterChain.doFilter()的方法,那么该Request请求中 止,后面的处理就会中断。

注意: FilterChain接口中,唯一的方法就是doFilter。该方法与Filter中的 doFilter的定义是不一致的:在FilterChaing中,doFilter方法只有两个参 数,但在Filter中,doFilter方法有三个参数。

  Filter接口中,最后一个方法是destroy,它的定义 如下:

Void destroy()

  该方法在Servlet容器要销毁Filter时触发,一般在应 用停止的时候进行调用。

  除非Filter在部署描述中被多次定义到,否则Servlet 窗口只会为每个Filter创建单一实例。由于Serlvet/JSP的 应用通常要处理用户并发请求,此时Filter实例需要同 时被多个线程所关联到,因此需要非常小心地处理多线 程问题。

三. Filter配置

  当完成Filter的实现后,就可以开始配置Filter了。 Filter的配置需要如下步骤:

  • 确认哪些资源需要使用这个Filter拦截处理。
  • 配置Filter的初始化参数值,这些参数可以在Filter的 init方法中读取到;
  • 给Filter取一个名称。一般来说,这个名称没有什么 特别的含义,但在一些特殊的情况下,这个名字十 分有用。例如,要记录Filter的初始化时间,但这个 应用中有许多的Filter,这时它就可以用来识别Filter 了。

  FilterConfig接口允许通过它的getServletContext的 方法来访问ServletContext:

ServletContext getServletContext(

  如果配置了Filter的名字,在FilterConfig的 getFilterName中就可以获取Filter的名字。getFilterName 的定义如下:

java.lang.String getFilterName()

  当然,最重要的还是要获取到开发者或者运维给 Filter配置的初始化参数。为了获取这些初始化参数, 需要用到FilterConfig中的两个方法,第一个方法是 getParameterNames:

java.util.Enumeration<java.lang.String> getInitParameterNames()

  这个方法返回Filter参数名字的Enumeration对象。 如果没有给这个Filter配置任何参数,该方法返回的是 空的Enumeration对象。

  第二个方法是getParameter:

java.lang.String getInitParameter(java.lang.String parameterName)

  有两种方法可以配置Filter:一种是通过WebFilter 的Annotation来配置Filter,另一种是通过部署描述来注 册。使用@WebFilter的方法,只需要在Filter的实现类 中增加一个注解即可,不需要重复地配置部署描述。当 然,此时要修改配置参数,就需要重新构建Filter实现 类了。换句话说,使用部署描述意味着修改Filter配置 只要修改一下文本文件就可以了。

  使用@WebFilter,你需要熟悉下表中所列出来的 参数,这些参数是在WebFilter的Annotation里定义的。 所有参数都是可选的。

WebFilter的属性
属性 描述
asyncSupported Filter是否支持异步操作
description Filter的描述
dispatcerTypes Filter所生效范围
displayName Filter的显示名
filterName Filter的名称
initParams Filter的初始化参数
largeIcon Filter的大图名称
servletName Filter所生效的Servlet名称
smallIcon Filter的小图名称
urlPatterns Filter所生效的URL路径
value Filter所生效的URL路径

 

三. 示例1: 日志 Filter

  作为第1个例子,将做一个简单的Filter:在app09a 的应用中把Request请求的URL记录到日志文本文件 中。日志文本文件名通过Filter的初始化参数来配置。 此外,日志的每条记录都会有一个前缀,该前缀也由 Filter初始化参数来定义。通过日志文件,可以获得许 多有用的信息,例如在应用中哪些资源访问最频繁; Web站点在一天中的哪个时间段访问量最多。

   这个Filter的类名叫LoggingFilter。 一般情况下,Filter的类名都以*Filter结尾。

 

package filter;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
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.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.http.HttpServletRequest;

@WebFilter(filterName = "LoggingFilter", urlPatterns = { "/ *" }, initParams = {
        @WebInitParam(name = "logFileName", value = "log.txt"), @WebInitParam(name = "prefix", value = "URI: ") })
public class LoggingFilter implements Filter {
    private PrintWriter logger;
    private String prefix;
    
    
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        prefix = filterConfig.getInitParameter("prefix"); //得到URI
        String logFileName = filterConfig.getInitParameter("logFileName");
        String appPath = filterConfig.getServletContext().getRealPath("/"); //得到项目路径
// without path info in logFileName, the log file will be
// created in $TOMCAT_HOME/bin
        System.out.println("logFileName:" + logFileName);
        try {
            logger = new PrintWriter(new File(appPath, logFileName)); //打开文件
            logger.println("I have the output");
            logger.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            throw new ServletException(e.getMessage());
        }
    }

    @Override
    public void destroy() {
        System.out.println("destroying filter");
        if (logger != null) {
            logger.close();
        }
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {
        System.out.println("LoggingFilter.doFilter");
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        logger.println(new Date() + " " + prefix + httpServletRequest.getRequestURI()); //写入数据
        logger.flush(); //刷新缓冲
        filterChain.doFilter(request, response); //如果没有doFilter方法,后面发Filter处理就会中断
    }
    
}

  下面来仔细分析一下Filter类。 首先,该Filter的类实现了Filter的接口并声明两个 变量:PrintWriter类型的logger和String类型的prefix。

  其中PrintWriter用于记录日志到文本文件,prefix的 字符串用于每条日志的前缀。 Filter的类使用了@WebFilter的Annotation,将两个 参数(logFilteName、prefix)传入到该Filter中.

  在Filter的init方法中,通过FilterConfig里传入的 getInitParameter方法来获取prefix和getFileName的初始 化参数。其中把prefix参数中赋给了类变量prefix, logFileName则用于创建一个PrintWriter

  如果Servlet/JSP应用是通过Servlet/JSP容器启动 的,那么当前应用的工作目录是当前JDK所在的目录。 如果是在Tomcat中,该目录是Tomcat的安装目录。在 应用中创建日志文件,可以通过 ServletContext.getRealPath来获取工作目录,结合应用 工作目录以及初始化参数中的logFilterNmae,就可以得 到日志文件的绝对路径.

  当Filter的init方法被执行时,日志文件就会创建出 来。如果在应用的工作目录中该文件已经存在,那么该 日志文件的内容将会被覆盖。 当应用关闭时,PrintWriter需要被关闭。因此在 Filter的destroy方法中,需要:关闭日志

  Filter的doFilter实现中记录着所有从ServletRequest 到HttpServletRequest的Request,并调用了它的 getRequestURI方法,该方法的返回值将记录通过 PrintWriter的pringln记录下来

  每条记录都有一个时间戳以及前缀,这样可以很方 便地标识每条记录。接下来 Filter的doFilter实现调用 PrintWriter的flush方法以及FilterChain.doFilter,以唤起 资源的调用:

   如果使用Tomcat,Filter的初始化并不会等到第一 个Request请求时才触发进行。这点可以在控制台中打 印出来的logFileName参数值中可以看到。在app09a应 用中通过URL调用test.jsp页面,就可以测试该Filter了

  通过检查日志文件的内容,就可以验证这个Filter 是否运行正常。

四.  示例2:图像文件保护Filter

  本例中的图像文件保护Filter用于在浏览器中输入 图像文件的URL路径时,防止下载图像文件。应用中的 图像文件只有当图像链接在页面中被点击的时候才会显 示。该Filter的实现原理是检查HTTP Header的referer 值。如果该值为null,就意味着当前的请求中没有 referer值,即当前的请求是直接通过输入URL来访问该 资源的。如果资源的Header值为非空,将返回Request 语法的原始页面作为referer值。注意Header的referer的 属性名中,在第2个e以及第3个e中仅有一个r。

   ImageProtectorFilter的Filter实现类,如清单所 示。从WebFilter的Annotation中,可以看到该Filter应用 于所有的.png、.jpg、.gif文件后缀。

ImageProtectorFilter实现类

package 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.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;


//过滤所有以.png , .jpg  .gif 为后缀的访问
@WebFilter(filterName = "ImageProtetorFilter", urlPatterns = { "*.png", "*.jpg", "*.gif" })
public class ImageProtectorFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {
        //直接输入图片地址访问http://localhost:8080/app09/图片.jpg会被拦截
        System.out.println("ImageProtectorFilter");
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String referrer = httpServletRequest.getHeader("referer");
        System.out.println("referrer:" + referrer);
        if (referrer != null) {
         filterChain.doFilter(request, response); }
else { throw new ServletException("Image not available"); } } }

 

   这里并没有init和destroy方法。其中doFilter方法读 取到Header中的referer值,要确认是要继续处理这个资 源还是给个异常.

  测试该Filter,可以在浏览器中输入如下ULR路 径,尝试访问logo.png图像:

http://localhost:8080/app09/图片.jpg

  接下来,通过image.jsp的页面来访问该图像:

http://localhost:8080/app09/test.jsp

 

 

五.示例3:下载计数Filter

  本例子中,下载计数Filter将会示范如何在Filter中 计算资源下载的次数。这个示例特别有用,它将会得到 文档、音频文件的受欢迎程度。作为简单的示例,这里 将数值保存在属性文件中,而不保存在数据库中。其中 资源的ULR路径将作为属性名保存在属性文件中。

  因为我们把值保存在属性文件中,并且Filter可以 被多线程访问,因此涉及线程安全问题。用户访问一个 资源时,Filter需要读取相应的属性值加1,然后保存该 值。如果第二个用户在第一个线程完成前同时访问该资 源,将会发生什么呢?计算值出错。在本例中,读写的 同步锁并不是一个好的解决这个问题的方法,因为它会 导致扩展性问题。

   本示例中,解决这个线程安全问题是通过Queue以 及Executor。

  简而言之,进来的Request请求将会保存在单线程 Executor的队列中。替换这个任务十分方便,因为这是 一个异步的方法,因此你不需要等待该任务结束。 Executor一次从队列中获取一个对象,然后做相应属性 值的增加。由于Executor只在一个线程中使用,因此可 以消除多个线程同时访问一个属性文件的影响。

DownloadCounterFilter实现类

package filter;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;

// 拦截所有URL
//书上的是 urlPatterns={"/"},亲测无效
@WebFilter(filterName = "DownloadCounterFilter", urlPatterns = { "*.JPG" })
public class DownloadCounterFilter implements Filter {
    // 获取单个线程池的Executor 由于Executor只在一个线程中使用,因此可 以消除多个线程同时访问一个属性文件的影响。
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    // propeties 属性集
    Properties downloadLog;
    File logFile;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("DownloadCounterFilter");
        //获取路径
        String appPath = filterConfig.getServletContext().getRealPath("/");
        // 获取downLoadLog.txt文件
        logFile = new File(appPath, "downloadLog.txt");
        if (!logFile.exists()) {
            try {
                logFile.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        
        downloadLog = new Properties();
        try {
            downloadLog.load(new FileReader(logFile));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void destroy() {
        executorService.shutdown();
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        final String uri = httpServletRequest.getRequestURI();
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                String property = downloadLog.getProperty(uri);
                if (property == null) {
                    downloadLog.setProperty(uri, "1");
                } else {
                    int count = 0;
                    try {
                        count = Integer.parseInt(property);
                    } catch (NumberFormatException e) {
// silent
                    }
                    count++;
                    downloadLog.setProperty(uri, Integer.toString(count));
                }
                try {
                    downloadLog.store(new FileWriter(logFile), "");
                } catch (IOException e) {
                }
            }
        });
        filterChain.doFilter(request, response);
    }
}

  如果在当前应用的工作目录中不存在 downloadLog.txt文件,这个Filter的init方法就会创建 它

  接着创建Properties对象,并读取该文件.

  注意,Filter的实现类中引用到了 ExecutorService(Executor的子类).

  且当Filter销毁时,会调用ExecutorService的 shutdown方法.

  Filter的doFilter实现中大量地使用到这个Job。每次 URL请求都会调用到ExecutorService的execute方法,然 后才调用FilterChaing.doFilter()。该任务的execute实现 非常好理解:它将URL作为一个属性名,从Properties 实例中获取该属性的值,然后加1,并调用flush方法写 回到指定的日志文件中

  这个Filter可在许多资源上生效,但也可以非常简 单地配置,限定为PDF或者AVI文件资源。

properties文件

 

六. Filter顺序

  如果多个Filter应用于同一个资源,Filter的触发顺 序将变得非常重要,这时就需要使用部署描述来管理 Filter:指定哪个Filter先被触发。例如:Filter 1需要在 Filter 2前被触发,那么在部署描述中,Filter 1需要配置 在Filter 2之前:

<filter>
<filter-name>Filter1</filter-name>
<filter-class>
the fully-qualified name of the filter class
</filter-class>
</filter>
<filter>
<filter-name>Filter2</filter-name>
<filter-class>
the fully-qualified name of the filter class
</filter-class>
</filter>

通过部署描述之外的配置来指定Filter触发的顺序 是不可能的

 

posted @ 2019-04-15 13:56  江期玉  阅读(1704)  评论(0编辑  收藏  举报