詳解JSP 2.0下的動態內容緩存
作者:Eve Cole
更新時間:2009-07-03 16:56:34
在Web應用中,內容快取是最普通的最佳化技術之一,並且能夠輕鬆實現。例如,可以使用一個自訂地JSP標籤-我們將之命名為<jc: cache>-由<jc:cache>和</jc:cache>將每一個需要被快取的頁面片段封裝起來。任何自訂標籤可以控制它所包含部分(也即預先封裝的頁面片段)在何時執行,並且動態輸出結果可以被捕獲。 <jc:cache>標籤使得JSP容器(例如Tomcat)只生成內容一次,作為應用程式範圍內的JSP變量,來儲存每一個快取片段。每次JSP頁面被執行時,自訂標籤將快取頁面片段載入而無需再次執行JSP程式碼來產生輸出結果。作為Jakarta工程的一個部分,標籤庫的開發使用了這項技術。當被快取內容無需被每個使用者或請求所客製化的時候,它工作的十分良好。
這篇文章對上面描述的技術做了改進,透過使用JSP 2.0表達式語言(EL),允許JSP頁面為每個請求和使用者自訂快取內容。快取頁面片段可以包含未被JSP容器賦值的JSP表達式,在每個頁面被執行時,由自訂標籤來決定這些表達式的值。因此,動態內容的建立被最優化,但是快取片段可以含有部分由每個請求使用本機JSP表達式語言產生的內容。透過JSP 2.0 EL API的幫助,Java開發者可以用表達式語言來使其成為可能。
內容快取VS資料快取內容快取不是唯一的選擇。例如, 從資料庫中提取的資料同樣可以被快取。事實上,由於儲存的資訊中不包含HTML markup,以及要求較少的內存,資料快取可能更加高效率。然而在很多情況下,記憶體快取更容易實現。假設在某個案例總,一個應用由大量事務對象,佔用重要的CPU資源,產生複雜的數據,並且用JSP頁面來呈現這些數據。工作一切良好,直到某天突然地伺服器的負載增加,需要緊急解決方案。這時在事務物件和呈現表達層之間建立一個快取層,時一個非常不錯且有效的方案。但是必須非常快速且流暢地修改快取動態內容的JSP頁面。相對於簡單的JSP頁面編輯, 應用程式的業務邏輯變更通常要求更多的工作量和測試;另外,如果一個頁面從多個複合來源聚合資訊時,Web層僅有少量的變更。問題在於,當快取資訊變得失去時效時,快取空間需要被釋放,而交易物件應該知道何時發生這種情況。然而,選擇實現內容緩存還是資料緩存,或其他的最佳化技術,有許多不得不考慮的因素, 有時是所開發的程式所特殊要求的。
資料快取和內容快取沒有必要互相排斥,它們可以一起使用。例如,在資料庫驅動的應用中;從資料庫中提取出來的數據,和呈現該資料的HTML分別被快取起來。這與使用JSP即時產生的模板有些相似。這篇文章中討論的基於EL API技術說明如何使用JSP EL來將資料載入到呈現範本中。
使用JSP變數快取動態內容每當實作一個快取機制是,都需要一個儲存快取物件的方法,在這篇文章中涉及的是String類型的物件。 一種選擇是使用物件-快取框架結構,或使用Java maps來實作自訂的快取方案。 JSP已經擁有了稱為「scoped attributes」或「JSP variables」來提供ID——object映射,這正是快取機制所需要的。對於使用page或request scope,這是沒有意義的,而在應用範圍內,這是一個很好的存儲緩存內容的位置, 因為它被所有的用戶和頁面共享。當每個使用者需要單獨快取時,Session scope也可以被使用,但這不是很有效率。 JSTL標籤庫可以被是與那個來快取內容,透過使用JSP變數如下例所示:
<%@ taglib prefix="c" uri=" http://java.sun.com/jsp/jstl/core " %><c:if test="${empty cachedFragment}">
<c:set var="cachedFragment" scope="application">
…
</c:set></c:if>
快取頁面片段以下列語句輸出結果:
${applicationScope.cachedFragment}
當快取片段需要被每個請求所客製化的時候,到底發生了什麼事?例如,如果希望包含一個計數器,則需要快取兩個片段:
<%@ taglib prefix="c" uri=" http://java.sun.com/jsp/jstl/core " %><c:if test="${sessionScope.counter == null}"> <c :set var="counter" scope="session" value="0"/></c:if><c:set var="counter" value="${counter+1}" scope="session"/ ><c:if test="${empty cachedFragment1}">
<c:set var="cachedFragment1" scope="application">
…
</c:set></c:if><c:if test="${empty cachedFragment2}">
<c:set var="cachedFragment2" scope="application">
…
</c:set></c:if>
可以使用下面語句輸出快取內容:
${cachedFragment1} ${counter} ${cachedFragment2}
透過專門的標籤庫的幫助,需要自訂的頁面片段的快取變得異常容易了。上面已經提及,快取內容可以被開始標籤(<jc:cache>)和結尾標籤(</jc:cache>)封裝起來。而每一個自訂可以使用另一個標籤(<jc:dynamic expr="..."/>)輸出一個JSP表達式(${...})來表現。動態內容用JSP表達式快取並在每一次快取內容被輸出時賦值。在下面的部分可以看到這是如何實現的。 Counter.jsp快取了一個包含計數器的頁面片段,當每個使用者刷新這個頁面的時候計數器會自動+1。
<%@ taglib prefix="c" uri=" http://java.sun.com/jsp/jstl/core " %><%@ taglib prefix="jc" uri=" http://devsphere.com/ articles/jspcache " %><c:if test="${sessionScope.counter == null}">
<c:set var="counter" scope="session" value="0"/></c:if><c:set var="counter" value="${counter+1}" scope="session "/><jc:cache id="cachedFragmentWithCounter">
... <jc:dynamic expr="sessionScope.counter"/>
...</jc:cache>
JSP 變數易於使用,對於簡單的Web apps,這是一個不錯的內容快取方案。然而,如果應用程式產生大量的動態內容,沒有對快取大小的控制無疑是一個問題。一種專用的快取框架結構能夠提供一個更有力的方案,允許對快取的監視,限制快取大小,控制快取策略,等等…
使用JSP 2.0表達式語言API
JSP容器(例如Tomcat)對應用EL API的JSP頁面中的表達式予以賦值,並且可以被Java程式碼所使用。這允許在Web頁面外應用JSP EL作開發,例如,對XML檔案、基於文字的資源以及自訂腳本。當需要控制何時對Web頁面中的表達式進行賦值或書寫與之相關的表達式時,EL API同樣是有用的。例如,快取頁面片段可以包含自訂JSP表達式,並且當每個快取內容被輸出時,EL API將用來給這些表達式賦值或重新賦值。
文章提供了一個範例程式(請參閱文末資源部分),這個應用程式包含了一個Java類別(JspUtils)和類別中包含一個方法eval(),這個方法有三個參數:JSP表達式、表達式的期望型別和一個JSP context物件。 Eval()方法從JSP context取得ExpressionEvaluator並且呼叫evaluate()方法,透過表達式、表達式的期望類型、和一個從JSP congtext得到的變數。 JspUtils.eval()方法傳回表達式的值。
package com.devsphere.articles.jspcache;
import javax.servlet.jsp.JspContext;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.el.ELException;
import javax.servlet.jsp.el.ExpressionEvaluator;
import java.io.IOException;public class JspUtils {
public static Object eval(
String expr, Class type, JspContext jspContext)
throws JspException {
try {
if (expr.indexOf("${") == -1)
return expr;
ExpressionEvaluator evaluator
= jspContext.getExpressionEvaluator();
return evaluator.evaluate(expr, type,
jspContext.getVariableResolver(), null);
} catch (ELException e) {
throw new JspException(e);
}
}
....}
注意:JspUtils.eval()主要封裝了標準的ExpressionEvaluator。如果expr不包含${,JSP EL API不被調用,因為沒有JSP表達式。
建立標籤庫描述符(TLD)檔案JSP標籤庫需要一個標籤庫描述符(TLD)檔案來自訂標籤的命名,它們的屬性,以及操作該標籤的Java類別。 jspcache.tld描述了兩個自訂標籤,<jc:cache>擁有兩個屬性:快取頁面片段的id和JSP scope—JSP頁面總是需要被儲存的內容範圍。 <jc:dynamic>只有一個屬性,就是JSP表達式必須在每一次快取片段被輸出時被賦值。 TLD檔案將這兩個自訂標籤對應到CacheTag和DynamicTag類,如下所示:
<?xml version="1.0" encoding="UTF-8" ?><taglib xmlns=" http://java.sun.com/xml/ns/j2ee "
xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance "
xsi:schemaLocation=" http://java.sun.com/xml/ns/j2ee web-jsptaglibrary_2_0.xsd"
version="2.0">
<tlib-version>1.0</tlib-version>
<short-name>jc</short-name>
<uri>http://devsphere.com/articles/jspcache</uri>
<tag>
<name>cache</name>
<tag-class>com.devsphere.articles.jspcache.CacheTag</tag-class>
<body-content>scriptless</body-content>
<attribute>
<name>id</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<attribute>
<name>scope</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
</tag>
<tag>
<name>dynamic</name>
<tag-class>com.devsphere.articles.jspcache.DynamicTag</tag-class>
<body-content>empty</body-content>
<attribute>
<name>expr</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
</tag></taglib>
TLD檔案包含在Web應用描述符檔案(web.xml)中,這五個檔案同樣包含一個初始參數指出cache是否可用。
<?xml version="1.0" encoding="ISO-8859-1"?><web-app xmlns=" http://java.sun.com/xml/ns/j2ee "
xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance "
xsi:schemaLocation=" http://java.sun.com/xml/ns/j2ee web-app_2_4.xsd"
version="2.4">
<context-param>
<param-name>com.devsphere.articles.jspcache.enabled</param-name>
<param-value>true</param-value>
</context-param>
<jsp-config>
<taglib>
<taglib-uri>http://devsphere.com/articles/jspcache</taglib-uri>
<taglib-location>/WEB-INF/jspcache.tld</taglib-location>
</taglib>
</jsp-config></web-app>
理解<jc:cache>的工作機制JSP容器為JSP頁面中的每一個<jc:cache>標籤建立一個CacheTag實例,來對其處理。 JSP容器負責呼叫setJsp ()、setParent()和setJspBody()方法,這是CacheTag類別從SimpleTagSupport繼承而來。 JSP容器同事也會為所操作標籤的每一個屬性呼叫setter方法。 SetId()和setScope()方法儲存屬性值到私有域,這個值已經用CacheTag()建構子用預設值初始化。
package com.devsphere.articles.jspcache;
import javax.servlet.ServletContext;
import javax.servlet.jsp.JspContext;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.SimpleTagSupport;
import java.io.IOException;import java.io.StringWriter;
public class CacheTag extends SimpleTagSupport {
public static final String CACHE_ENABLED
= "com.devsphere.articles.jspcache.enabled";
private String id;
private int scope;
private boolean cacheEnabled; public CacheTag() {
id = null; scope = PageContext.APPLICATION_SCOPE;
} public void setId(String id) {
this.id = id;
} public void setScope(String scope) {
this.scope = JspUtils.checkScope(scope);
}
....}
setScope()方法呼叫JspUtils.checkScope()來校驗已經從String轉換為int型別的scope的屬性值。
...public class JspUtils {
…
public static int checkScope(String scope) {
if ("page".equalsIgnoreCase(scope))
return PageContext.PAGE_SCOPE;
else if ("request".equalsIgnoreCase(scope))
return PageContext.REQUEST_SCOPE;
else if ("session".equalsIgnoreCase(scope))
return PageContext.SESSION_SCOPE;
else if ("application".equalsIgnoreCase(scope))
return PageContext.APPLICATION_SCOPE;
else
throw new IllegalArgumentException(
"Invalid scope: " + scope);
}}
一旦CacheTag實例準備對標籤進行操作,JSP容器呼叫doTag()方法,就用getJspContext()來取得JSP context。這個物件被造型為PageContext,從而可以呼叫getServletContext()方法。 servlet context用來取得初始化參數的值,這個值標示快取機制是否被啟用。如果快取被啟用,doTag()嘗試使用id和scope屬性值來獲得快取頁面片段。如果頁面片段還沒有被緩存,doTag()使用getJspBody().invoke()來執行由<jc:cache>和< /jc:cache>封裝的JSP程式碼。由JSP body產生的輸出結果緩衝在StringWriter並且被toStirng()方法得到。這樣,doTag()呼叫JSP context的setAttribute()方法新建一個JSP變量,這個變數控制可能包含JSP表達式(${…})的快取內容。這些表達式在用jspContext.getOut().print()輸出內容前,被JspUtils.eval()賦值。只有當快取被啟用的時候,這些行為才會發生。 否則,doTag()只是透過getJspBody().invoke(null)執行JSP body並且輸出結果不會被緩存。
...public class CacheTag extends SimpleTagSupport {
…
public void doTag() throws JspException, IOException {
JspContext jspContext = getJspContext();
ServletContext application
= ((PageContext) jspContext).getServletContext();
String cacheEnabledParam
= application.getInitParameter(CACHE_ENABLED);
cacheEnabled = cacheEnabledParam != null
&& cacheEnabledParam.equals("true");
if (cacheEnabled) {
String cachedOutput
= (String) jspContext.getAttribute(id, scope);
if (cachedOutput == null) {
StringWriter buffer = new StringWriter();
getJspBody().invoke(buffer);
cachedOutput = buffer.toString();
jspContext.setAttribute(id, cachedOutput, scope);
} String evaluatedOutput = (String) JspUtils.eval(
cachedOutput, String.class, jspContext);
jspContext.getOut().print(evaluatedOutput);
} else
getJspBody().invoke(null);
}
....}
注意一個單獨的JspUtils.eval()呼叫給所有的${…} 表達式賦值。因為一個包含了大量的${…}結構的text也是一個表達式。每一個快取片段都可以當作一個複雜的JSP表達式來處理。
IsCacheEnabled()方法回傳cacheEnabled的值,這個值已經被doTag()初始化。
...public class CacheTag extends SimpleTagSupport {
... public boolean isCacheEnabled() {
return cacheEnabled;
}}
<jc:cache>標籤允許頁面開發者自主選擇快取頁面片段的ID。這使得快取一個頁面片段可以被多個JSP頁面共享,當需要重複使用JSP程式碼時,這是很有用處的。但是仍然需要一些命名協議來避免可能的衝突。透過修改CacheTag類別來在自動ID內部包含URL可以避免這種副作用。
理解<jc:dynamic>在做什麼每一個<jc:dynamic>被一個DynamicTag類別的實例處理,setExpr()方法將expr屬性值儲存到一個私有域。 DoTag()方法建立JSP表達式,在expr屬性值加上${前綴和}後綴。然後,doTag()使用findAncestorWithClass() 來尋找含有<jc:dynamic>標籤元素的<jc:cache>的CacheTag handler。如果沒有查找到或快取被停用,JSP表達式被JspUtils.eval()賦值且值被輸出。否則,doTag()輸出無值表達式。
package com.devsphere.articles.jspcache;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.SimpleTagSupport;
import java.io.IOException;
public class DynamicTag extends SimpleTagSupport {
private String expr;
public void setExpr(String expr) {
this.expr = expr;
} public void doTag() throws JspException, IOException {
String output = "${" + expr + "}";
CacheTag ancestor = (CacheTag) findAncestorWithClass(
this, CacheTag.class);
if (ancestor == null || !ancestor.isCacheEnabled())
output = (String) JspUtils.eval(
output, String.class, getJspContext());
getJspContext().getOut().print(output);
}}
分析以上程式碼,可以注意到<jc:cache>和<jc:dynamic>合作來實現一個盡可能有效率的方案。如果快取可用,頁面片段和由<jc:dynamic>產生並被CacheTag賦值的JSP表達式一起放入緩衝器。如果快取被停用,緩衝變得沒有意義, <jc:cache>只是執行其JSP body部分,而讓DynamicTag給JSP表達式賦值。禁用快取有時是必要的,特別是在開發過程期間出現內容的改變和JSP頁面被重新編譯的時候。當然,在開發完畢的成品環境中快取必須啟用。
總結
對於開發大型企業級應用,則該考慮使用支援更好的快取機制的框架結構,而不僅是使用JSP變數。但是了解基於EL API的客製化技術無疑是不無裨益的。