Listener-监听器
Listener为在Java Web中进行事件驱动编程提供了一整套事件类和监听器接口.Listener监听的事件源分为ServletContext/HttpSession/ServletRequest三个级别:

ServletContext级别
Servlet – Listener、Filter、Decorator
HttpSession级别
Servlet – Listener、Filter、Decorator
ServletRequest级别
Servlet – Listener、Filter、Decorator
注册
创建监听器只需实现相关接口即可,但只有将其注册到Servlet容器中,才会被容器发现,这样才能在发生事件时,驱动监听器执行.Listener的注册方法有注解和部署描述符两种:

  1. @WebListener
    在Servlet 3.0中, 提供了@WebListener注解:
@WebListener
public class ListenerClass implements ServletContextListener {

    // ...
}
  1. 部署描述符
    <listener>
    <listener-class>com.fq.web.listener.ListenerClass</listener-class>
    </listener>

    注: 由于HttpSessionBindingListener/HttpSessionActivationListener是直接绑定在JavaBean上, 而并非绑定到Session等域对象, 因此可以不同注册.
    示例
    加载Spring容器
    ContextLoaderListener
    public class ContextLoaderListener extends ContextLoader implements ServletContextListener {

public ContextLoaderListener(WebApplicationContext context) {
        super(context);
}

/**
 * Initialize the root web application context.
 */
@Override
public void contextInitialized(ServletContextEvent event) {
        initWebApplicationContext(event.getServletContext());
}

/**
 * Close the root web application context.
 */
@Override
public void contextDestroyed(ServletContextEvent event) {
        closeWebApplicationContext(event.getServletContext());
        ContextCleanupListener.cleanupAttributes(event.getServletContext());
}

}

web.xml

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

统计HTTP请求耗时
监控ServletRequest的创建/销毁事件, 以计算HTTP处理耗时

/**
 * @author jifang.
 * @since 2016/5/4 15:17.
 */
@WebListener
public class PerforationStatListener implements ServletRequestListener {

    private static final Logger LOGGER = Logger.getLogger("PerforationStatListener");

    private static final String START = "Start";

    public void requestInitialized(ServletRequestEvent sre) {
        ServletRequest request = sre.getServletRequest();
        request.setAttribute(START, System.nanoTime());
    }

    public void requestDestroyed(ServletRequestEvent sre) {
        HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
        long start = (Long)request.getAttribute(START);
        long ms = (System.nanoTime() - start)/1000;
        String uri = request.getRequestURI();
        LOGGER.info(String.format("time token to execute %s : %s ms", uri, ms));
    }
}

HttpSessionBindingListener
当JavaBean实现HttpSessionBindingListener接口后,就可以感知到本类对象被添加/移除Session事件:

Listener```

public class Product implements Serializable, HttpSessionBindingListener {

private int id;
private String name;
private String description;
private double price;

public Product(int id, String name, String description, double price) {
    this.id = id;
    this.name = name;
    this.description = description;
    this.price = price;
}

// ...

public void valueBound(HttpSessionBindingEvent event) {
    System.out.println("bound...");
}

public void valueUnbound(HttpSessionBindingEvent event) {
    System.out.println("un_bound...");
}

}


Servlet

> private static final String FLAG = "flag";
>  
> @Override
> protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
>     Boolean flag = (Boolean) getServletContext().getAttribute(FLAG);
>     if (flag == null || !flag) {
>         request.getSession().setAttribute("product", new Product(8, "水晶手链", "VunSun微色天然水晶手链女款", 278.00));
>         getServletContext().setAttribute(FLAG, true);
>     } else {
>         request.getSession().removeAttribute("product");
>         getServletContext().setAttribute(FLAG, !flag);
>     }
> }

**HttpSessionActivationListener**
为节省内存, Servlet容器可以对Session属性进行迁移或序列化.一般当内存较低时,相对较少访问的对象可以序列化到备用存储设备中(钝化);当需要再使用该Session时,容器又会把对象从持久化存储设备中再反序列化到内存中(活化).HttpSessionActivationListener就用于感知对象钝化/活化事件:

对于钝化/活化,其实就是让对象序列化/反序列化穿梭于内存与持久化存储设备中.因此实现HttpSessionActivationListener接口的JavaBean也需要实现Serializable接口.

在conf/context.xml配置钝化时间
> <Context>
>     <WatchedResource>WEB-INF/web.xml</WatchedResource>
>  
>     <Manager className="org.apache.catalina.session.PersistentManager" maxIdleSwap="1">
>         <Store className="org.apache.catalina.session.FileStore" directory="sessions"/>
>     </Manager>
> </Context>

JavaBean

> public class Product implements Serializable, HttpSessionActivationListener {
>  
>     private int id;
>     private String name;
>     private String description;
>     private double price;
>  
>     // ...
>  
>     public void sessionWillPassivate(HttpSessionEvent se) {
>         System.out.println("passivate...");
>     }
>  
>     public void sessionDidActivate(HttpSessionEvent se) {
>         System.out.println("Activate...");
>     }
> }

将Product加入Session一分钟不访问后, 该对象即会序列化到磁盘, 并调用sessionWillPassivate()方法, 当再次使用该对象时, Servlet容器会自动活化该Session, 并调用sessionDidActivate()方法.
**Filter-过滤器**
Filter是指拦截请求,并可以对ServletRequest/ServletResponse进行处理的一个对象.由于其可配置为拦截一个或多个资源,因此可用于处理登录/加(解)密/会话检查/图片适配等问题.

Filter中常用的有Filter/FilterChain/FilterConfig三个接口:
![](http://i2.51cto.com/images/blog/201810/21/0a3a846aef609e074525eea0bbf4e57d.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

过滤器必须实现Filter接口, 当应用程序启动时,Servlet容器自动调用过滤器init()方法;当服务终止时,自动调用destroy()方法.当每次请求与过滤器资源相关资源时,都会调用doFilter()方法;由于doFilter()可以访问ServletRequest/ServletResponse,因此可以在Request中添加属性,或在Response中添加一个响应头,甚至可以对Request/Response进行修饰/替换,改变他们的行为(详见下).
![](http://i2.51cto.com/images/blog/201810/21/18a6b0b34055be3d6eaeadf4bccc62c9.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
FilterChain中只有一个doFilter()方法, 该方法可以引发调用链中下一过滤器或资源本身被调用.如果没有在Filter的doFilter()中调用FilterChain的doFilter()方法,那么程序的处理将会在此处停止,不会再继续请求.

示例: Filter解决GET/POST编码问题

/**

  • @author jifang.
  • @since 2016/5/2 11:55.
    */
    public class CharsetEncodingFilter implements Filter {

    private static final String IGNORE_URI = "ignore_uri";

    private static final String URI_SEPARATOR = ",";

    private Set<String> ignoreUris = new HashSet<String>();

    public void init(FilterConfig config) throws ServletException {
    String originalUris = config.getInitParameter(IGNORE_URI);
    if (originalUris != null) {
    String[] uris = originalUris.split(URI_SEPARATOR);
    for (String uri : uris) {
    this.ignoreUris.add(uri);
    }
    }
    }

    public void destroy() {
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
    HttpServletRequest request = (HttpServletRequest) req;
    String uri = request.getRequestURI();
    if (!ignoreUris.contains(uri)) {
    if (request.getMethod().equals("GET")) {
    request = new EncodingRequest(request);
    } else {
    request.setCharacterEncoding("UTF-8");
    }
    }
    chain.doFilter(request, resp);
    }

    private static final class EncodingRequest extends HttpServletRequestWrapper {

    public EncodingRequest(HttpServletRequest request) {
        super(request);
    }
    
    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name);
        if (value != null) {
            try {
                value = new String(value.getBytes("ISO-8859-1"), "UTF-8");
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }
        }
        return value;
    }

    }
    }
    注: HttpServletRequestWrapper介绍见Decorator-装饰者部分.

注册/配置
编写好过滤器后, 还需对其进行注册配置,配置过滤器的目标如下:

确定过滤器要拦截的目标资源;
传递给init()方法的启动初始值;
为过滤器命名.
web.xml

<filter>
    <filter-name>CharsetEncodingFilter</filter-name>
    <filter-class>com.fq.web.filter.CharsetEncodingFilter</filter-class>
    <init-param>
        <param-name>ignore_uri</param-name>
        <param-value>/new_servlet.do,/hello_http_servlet.do</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CharsetEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

也可用@WebFilter注解,其配置方式简单且与部署描述符类似,因此在此就不再赘述
.
FilterConfig
前面介绍了Filter/FilterChain两个接口,下面介绍FilterConfig接口, 其最常用的方法是getInitParameter(), 获取过滤器的初始化参数, 以完成更精细化的过滤规则.不过他还提供了如下实用方法:

Servlet – Listener、Filter、Decorator
拦截方式
过滤器的拦截方式有四种: REQUEST / FORWARD / INCLUDE / ERROR

REQUEST : (默认)直接访问目标资源时执行(地址栏直接访问/表单提交/超链接/重定向等只要在地址栏中可看到目标资源路径,就是REQUEST)
FORWARD : 转发访问执行(RequestDispatcher中forward()方法)
INCLUDE : 包含访问执行(RequestDispatcher中include()方法)
ERROR : 当目标资源在web.xml中配置为中时,并且出现异常,转发到目标资源时, 执行该过滤器.

<filter>
    <filter-name>CharsetEncodingFilter</filter-name>
    <filter-class>com.fq.web.filter.CharsetEncodingFilter</filter-class>
    <init-param>
        <param-name>ignore_path</param-name>
        <param-value>/new_servlet.do</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CharsetEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>INCLUDE</dispatcher>
</filter-mapping>

Decorator-装饰者
Servlet中有4个包装类ServletRequestWrapper/ServletResponseWrapper/HttpServletRequestWrapper/HttpServletResponseWrapper,可用来改变Servlet请求/响应的行为, 这些包装类遵循装饰者模式(Decorator).

由于他们为所包装的Request/Response中的每一个对等方法都提供了默认实现,因此通过继承他们, 只需覆盖想要修改的方法即可.没必要实现原始ServletRequest/ServletResponse/…接口的每一个方法.

实例-页面静态化
HttpServletRequestWrapper在解决GET编码时已经用到, 下面我们用HttpServletResponseWrapper实现页面静态化.

页面静态化是在第一次访问时将动态生成的页面(JSP/Servlet/Velocity等)保存成HTML静态页面文件存放到服务器,再有相同请求时,不再执行动态页面,而是直接给用户响应已经生成的静态页面.

Filter & Decorator

/**
 * @author jifang.
 * @since 2016/5/7 9:40.
 */
public class PageStaticizeFilter implements Filter {

    private static final String HTML_PATH_MAP = "html_path_map";

    private static final String STATIC_PAGES = "/static_pages/";

    private ServletContext context;

    public void init(FilterConfig filterConfig) throws ServletException {
        this.context = filterConfig.getServletContext();
        this.context.setAttribute(HTML_PATH_MAP, new HashMap<String, String>());
    }

    public void destroy() {
    }

    @SuppressWarnings("All")
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;
        Map<String, String> htmlPathMap = (Map<String, String>) context.getAttribute(HTML_PATH_MAP);

        String htmlName = request.getServletPath().replace("/", "_") + ".html";
        String htmlPath = htmlPathMap.get(htmlName);

        // 尚未生成静态页面
        if (htmlPath == null) {
            htmlPath = context.getRealPath(STATIC_PAGES) + "/" + htmlName;
            htmlPathMap.put(htmlName, htmlPath);
            PageStaticizeResponse sResponse = new PageStaticizeResponse(response, htmlPath);
            chain.doFilter(request, sResponse);
            sResponse.close();
        }
        String redirectPath = context.getContextPath() + STATIC_PAGES + htmlName;
        response.sendRedirect(redirectPath);
    }

    private static final class PageStaticizeResponse extends HttpServletResponseWrapper {

        private PrintWriter writer;

        public PageStaticizeResponse(HttpServletResponse response, String path) throws FileNotFoundException, UnsupportedEncodingException {
            super(response);
            writer = new PrintWriter(path, "UTF-8");
        }

        @Override
        public PrintWriter getWriter() throws IOException {
            return this.writer;
        }

        public void close() {
            this.writer.close();
        }
    }
}

注册

<filter>
    <filter-name>PageStaticzeFilter</filter-name>
    <filter-class>com.fq.web.filter.PageStaticizeFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>PageStaticzeFilter</filter-name>
    <url-pattern>*.jsp</url-pattern>
</filter-mapping>

注: 在此只是提供一个页面静态化思路, 由于代码中是以Servlet-Path粒度来生成静态页面, 粒度较粗, 细节方面肯定会有所疏漏(但粒度过细又会导致生成HTML页面过多), 因此这份代码仅供参考, 不可用于实际项目(关于该Filter所拦截的jsp页面, 可参考上篇博客的购物车案例).