在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=" %><c:if test="${empty cachedFragment}">
<c:set var="cachedFragment" scope="application">
...
</c:set></c:if>
缓存页面片段用下列语句输出结果:
$
当缓存片段需要被每一个请求所定制的时候,到底发生了什么?例如,如果希望包含一个计数器,需要缓存两个片段:
<%@ taglib prefix="c" uri=" %><c:if test="${sessionScope.counter == null}"> <c:set var="counter" scope="session" value="0"/></c:if><c:set var="counter" value="$" 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>
可以使用下面语句输出缓存
内容:
$ $ $
通过专门的标签库的帮助,需要定制的页面片段的缓存变得异常容易了。上面已经提及,缓存
内容可以被开始标签(<jc:cache>)和结尾标签(</jc:cache>)封装起来。而每一个定制可以使用另一个标签(<jc:dynamic expr="..."/>)输出一个
JSP表达式($)来表现。动态
内容用
JSP表达式缓存并在每一次缓存
内容被输出时赋值。在下面的部分可以看到这是如何实现的。Counter.
JSP缓存了一个包含计数器的页面片段,当每一次用户刷新这个页面的时候计数器会自动+1。
<%@ taglib prefix="c" uri=" %><%@ taglib prefix="jc" uri=" %><c:if test="${sessionScope.counter == null}">
<c:set var="counter" scope="session" value="0"/></c:if><c:set var="counter" value="$" 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=" xmlns:xsi
xsi:schemaLocation="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=" xmlns:xsi="
xsi:schemaLocation=" 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容器负责调用set
JSP()、setParent()和set
JSPBody()方法,这是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()方法,用get
JSPContext()来获得
JSP context。这个对象被造型为PageContext,从而可以调用getServletContext()方法。servlet context用来获取初始化参数的值,这个值标明缓存机制是否被启用。如果缓存被启用,doTag()尝试使用id和scope属性值来获得缓存页面片段。如果页面片段还没有被缓存,doTag()使用get
JSPBody().invoke()来执行由<jc:cache>和</jc:cache>封装的
JSP代码。由
JSP body产生的输出结果缓冲在StringWriter并且被toStirng()方法获得。这样,doTag()调用
JSP context的setAttribute()方法新建一个
JSP变量,这个变量控制可能包含
JSP表达式($)的缓存
内容。这些表达式在用
JSPContext.getOut().print()输出
内容前,被
JSPUtils.eval()赋值。只有当缓存被启用的时候,这些行为才发生。否则,doTag()只是通过get
JSPBody().invoke(null)执行
JSP body并且输出结果不被缓存。
...public class CacheTag extends SimpleTagSupport {
...
public void doTag() throws
JSPException, IOException {
JSPContext
JSPContext = get
JSPContext();
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();
get
JSPBody().invoke(buffer);
cachedOutput = buffer.toString();
JSPContext.setAttribute(id, cachedOutput, scope);
} String evaluatedOutput = (String)
JSPUtils.eval(
cachedOutput, String.class,
JSPContext);
JSPContext.getOut().print(evaluatedOutput);
} else
get
JSPBody().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, get
JSPContext());
get
JSPContext().getOut().print(output);
}}
分析以上代码,可以注意到<jc:cache>和<jc:dynamic>合作来实现一个尽可能有效率的方案。如果缓存可用,页面片段和由<jc:dynamic>生成并被CacheTag赋值的
JSP表达式一起放入缓冲器。如果缓存被禁用,缓冲变得没有意义,<jc:cache>只是执行其
JSP body部分,而让DynamicTag给
JSP表达式赋值。禁用缓存有时候是必要的,特别是在开发过程期间出现
内容的改变和
JSP页面被重新编译的时候。当然,在开发完毕的成品环境中缓存必须被启用。
总结
内容缓存是一种非常易用的改善Web应用性能的方法。这篇文章集中讨论了使用
JSP表达式语言来为每一个用户或者请求定制缓存
内容。贯穿全文的简单介绍的标签库适合小型Web apps并且可以提升中等应用的性能。对于开发大型 企业级应用,则该考虑使用支持更好的缓存机制的框架结构,而不仅是使用
JSP变量。但是了解基于EL API的定制技术无疑是不无裨益的。