1. 程式人生 > >Servlet – Listener、Filter、Decorator

Servlet – Listener、Filter、Decorator

ide info sha sys nstat blog rep etl 級別

Listener-監聽器
Listener為在Java Web中進行事件驅動編程提供了一整套事件類和監聽器接口.Listener監聽的事件源分為ServletContext/HttpSession/ServletRequest三個級別:

ServletContext級別
技術分享圖片
HttpSession級別
技術分享圖片
ServletRequest級別
技術分享圖片
註冊
創建監聽器只需實現相關接口即可,但只有將其註冊到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(), 獲取過濾器的初始化參數, 以完成更精細化的過濾規則.不過他還提供了如下實用方法:

技術分享圖片
攔截方式
過濾器的攔截方式有四種: 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頁面, 可參考上篇博客的購物車案例).

Servlet – Listener、Filter、Decorator