Servlet - Listener、Filter、Decorator

Servlet

標籤 : Java與Web


Listener-監聽器

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

  • ServletContext級別
Listener 場景
ServletContextListener 響應ServletContext生命週期事件(創建/銷燬),在ServletContext創建/銷燬時分別調用其相應的方法.
ServletContextAttributeListener 響應ServletContext屬性的添加/刪除/替換事件.
  • HttpSession級別
Listener 場景
HttpSessionListener 響應Session生命週期事件(創建/銷燬).
HttpSessionAttributeListener 響應Session**屬性**的添加/刪除/替換事件.
HttpSessionBindingListener 實現了該接口的JavaBean會在 Session添加/刪除時做出響應.
HttpSessionActivationListener 實現了該接口的JavaBean會在被Session 鈍化/活化時做出響應.
  • ServletRequest級別
Listener 場景
ServletRequestListener 響應ServletRequest的創建/刪除事件.
ServletRequestAttributeListener 響應ServletRequest屬性的添加/刪除/替換事件.

註冊

創建監聽器只需實現相關接口即可,但只有將其註冊到Servlet容器中,纔會被容器發現,這樣才能在發生事件時,驅動監聽器執行.Listener的註冊方法有註解和部署描述符兩種:

1. @WebListener

在Servlet 3.0中, 提供了@WebListener註解:

@WebListener
public class ListenerClass implements ServletContextListener {

    // ...
}

2. 部署描述符

<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三個接口:

Filter 描述
void init(FilterConfig filterConfig) Called by the web container to indicate to a filter that it is being placed into service.
void destroy() Called by the web container to indicate to a filter that it is being taken out of service.
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) The doFilter method of the Filter is called by the container each time a request/response pair is passed through the chain due to a client request for a resource at the end of the chain.

過濾器必須實現Filter接口, 當應用程序啓動時,Servlet容器自動調用過濾器init()方法;當服務終止時,自動調用destroy()方法.當每次請求與過濾器資源相關資源時,都會調用doFilter()方法;由於doFilter()可以訪問ServletRequest/ServletResponse,因此可以在Request中添加屬性,或在Response中添加一個響應頭,甚至可以對Request/Response進行修飾/替換,改變他們的行爲(詳見下).

FilterChain 描述
void doFilter(ServletRequest request, ServletResponse response) Causes the next filter in the chain to be invoked, or if the calling filter is the last filter in the chain, causes the resource at the end of the chain to be invoked.

FilterChain中只有一個doFilter()方法, 該方法可以引發調用鏈中下一過濾器資源本身被調用.如果沒有在FilterdoFilter()中調用FilterChaindoFilter()方法,那麼程序的處理將會在此處停止,不會再繼續請求.

  • 示例: 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-裝飾者部分.


註冊/配置

編寫好過濾器後, 還需對其進行註冊配置,配置過濾器的目標如下:

  1. 確定過濾器要攔截的目標資源;
  2. 傳遞給init()方法的啓動初始值;
  3. 爲過濾器命名.
    • 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(), 獲取過濾器的初始化參數, 以完成更精細化的過濾規則.不過他還提供瞭如下實用方法:

FilterConfig 描述
String getFilterName() Returns the filter-name of this filter as defined in the deployment descriptor.
String getInitParameter(String name) Returns a String containing the value of the named initialization parameter, or null if the initialization parameter does not exist.
Enumeration<String> getInitParameterNames() Returns the names of the filter’s initialization parameters as an Enumeration of String objects, or an empty Enumeration if the filter has no initialization parameters.
ServletContext getServletContext() Returns a reference to the ServletContext in which the caller is executing.

攔截方式

過濾器的攔截方式有四種: REQUEST / FORWARD / INCLUDE / ERROR

  • REQUEST : (默認)直接訪問目標資源時執行(地址欄直接訪問/表單提交/超鏈接/重定向等只要在地址欄中可看到目標資源路徑,就是REQUEST)
  • FORWARD : 轉發訪問執行(RequestDispatcherforward()方法)
  • INCLUDE : 包含訪問執行(RequestDispatcherinclude()方法)
  • 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頁面, 可參考上篇博客的購物車案例).


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章