Servlet - Listener、Filter、Decorator

Servlet

標籤 : Java與Webhtml


Listener-監聽器

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

  • 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的註冊方法有註解和部署描述符兩種:web

1. @WebListener

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

@WebListener
public class ListenerClass implements ServletContextListener {

    // ...
}

2. 部署描述符

<listener>
    <listener-class>com.fq.web.listener.ListenerClass</listener-class>
</listener>

注: 因爲HttpSessionBindingListener/HttpSessionActivationListener是直接綁定在JavaBean上, 而並不是綁定到Session等域對象, 所以能夠不一樣註冊.apache


示例

加載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就用於感知對象鈍化/活化事件:markdown

對於鈍化/活化,其實就是讓對象序列化/反序列化穿梭於內存與持久化存儲設備中.所以實現HttpSessionActivationListener接口的JavaBean也須要實現Serializable接口.session

  • 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()方法.app


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頁面, 可參考上篇博客的購物車案例).

相關文章
相關標籤/搜索