Servlet – Listener、Filter、Decorator
Listener為在Java Web中進行事件驅動編程提供了一整套事件類和監聽器接口.Listener監聽的事件源分為ServletContext/HttpSession/ServletRequest三個級別:
ServletContext級別
HttpSession級別
ServletRequest級別
註冊
創建監聽器只需實現相關接口即可,但只有將其註冊到Servlet容器中,才會被容器發現,這樣才能在發生事件時,驅動監聽器執行.Listener的註冊方法有註解和部署描述符兩種:
- @WebListener
在Servlet 3.0中, 提供了@WebListener註解:
@WebListener public class ListenerClass implements ServletContextListener { // ... }
- 部署描述符
<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