内容缓存是Web应用中最普通的优化技术之一例如可以使用一个自定义地JSP标签——我们将之命名为——由和将每一个需要被缓存的页面片段封装起来任何自定义标签可以控制它所包含部分 (也即预先封装的页面片段)在何时执行并且动态输出结果可以被捕获标签使得JSP容器(例如Tomcat)只生成内容一次作为应用程序范围内的JSP变量来存储每一个缓存片段每次JSP页面被执行时自定义标签将缓存页面片段载入而无需再次执行JSP代码来生成输出结果作为Jakarta工程的一个部分标签库的开发使用了这项技术当被缓存内容无需被每一个用户或者请求所定制的时候它工作的十分良好
这篇文章对上面描述的技术做了改进通过使用JSP 表达式语言(EL)允许JSP页面为每一个请求和用户定制缓存内容缓存页面片段可以包含未被JSP容器赋值的JSP表达式在每一次页面被执行时由自定义标签来确定这些表达式的值因此动态内容的建立被最优化但是缓存片段可以含有部分由每一个请求使用本机JSP表达式语言产生的内容通过JSP 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变量正如下例所示
缓存页面片段用下列语句输出结果
${applicationScopecachedFragment}
当缓存片段需要被每一个请求所定制的时候到底发生了什么?
例如如果希望包含一个计数器需要缓存两个片段
可以使用下面语句输出缓存内容
${cachedFragment} ${counter} ${cachedFragment}通过专门的标签库的帮助需要定制的页面片段的缓存变得异常容易了上面已经提及缓存内容可以被开始标签()和结尾标签()封装起来而每一个定制可以使用另一个标签()输出一个JSP表达式(${})来表现动态内容用JSP表达式缓存并在每一次缓存内容被输出时赋值在下面的部分可以看到这是如何实现的Counterjsp缓存了一个包含计数器的页面片段当每一次用户刷新这个页面的时候计数器会自动+
JSP 变量易于使用对于简单的Web apps这是一个不错的内容缓存方案然而如果应用程序产生大量的动态内容没有对缓存大小的控制无疑是一个问题一种专用的缓存框架结构能够提供一个更加有力的方案允许对缓存的监视限制缓存大小控制缓存策略等等……使用JSP 表达式语言APIJSP容器(例如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中获得的变量JspUtilseval()方法返回表达式的值package comdevspherearticlesjspcache;
import javaxservletjspJspContext;
import javaxservletjspJspException;
import javaxservletjspPageContext;
import javaxservletjspelELException;
import javaxservletjspelExpressionEvaluator;
import javaioIOException;public class JspUtils
{
public static Object eval(String expr Class type JspContext jspContext)
throws JspException
{
try
{
if (exprindexOf("${") == )return expr;
ExpressionEvaluator evaluator= jspContextgetExpressionEvaluator();
return evaluatorevaluate(expr type
jspContextgetVariableResolver() null);
} catch (ELException e)
{
throw new JspException(e);
}
}
}注意JspUtilseval()主要封装了标准的ExpressionEvaluator如果expr不包含${JSP EL API不被调用因为没有JSP表达式创建标签库描述符(TLD)文件JSP标签库需要一个标签库描述符(TLD)文件来自定义标签的命名它们的属性以及操作该标签的Java类jspcachetld描述了两个自定义标签拥有两个属性缓存页面片段的id和JSP scope—JSP页面总需要被储存的内容范围只有一个属性即JSP表达式必须在每一次缓存片段被输出时被赋值TLD文件将这两个自定义标签映射到CacheTag和DynamicTag类如下所示
TLD文件包含在Web应用描述符文件(webxml)中这五个文件同样包含一个初始参数指出cache是否可用
理解的工作机理JSP容器为JSP页面中的每一个标签创建一个CacheTag实例来对其处理JSP容器负责调用setJsp()setParent()和setJspBody()方法这是CacheTag类从SimpleTagSupport继承而来JSP容器同事还为所操作标签的每一个属性调用setter方法SetId()和setScope()方法存储属性值到私有域这个值已经用CacheTag()构造函数用缺省值初始化package comdevspherearticlesjspcache;
xmlns:xsi="
xsi:schemaLocation="
version="">
comdevspherearticlesjspcacheenabled
import javaxservletServletContext;
import javaxservletjspJspContext;
import javaxservletjspJspException;
import javaxservletjspPageContext;
import javaxservletjsptagextSimpleTagSupport;
import javaioIOException;import javaioStringWriter;
public class CacheTag extends SimpleTagSupport
{
public static final String CACHE_ENABLED=
"comdevspherearticlesjspcacheenabled";
private String id;private int scope;
private boolean cacheEnabled;public CacheTag()
{
id = null;scope = PageContextAPPLICATION_SCOPE;
}public void setId(String id)
{
thisid = id;
}
public void setScope(String scope)
{
thisscope = JspUtilscheckScope(scope);
}
}
setScope()方法调用JspUtilscheckScope()来校验已经
从String转换为int类型的scope的属性值 public class JspUtils { public static int checkScope(String scope) { if ("page"equalsIgnoreCase(scope)) return PageContextPAGE_SCOPE; else if ("request"equalsIgnoreCase(scope)) return PageContextREQUEST_SCOPE; else if ("session"equalsIgnoreCase(scope)) return PageContextSESSION_SCOPE; else if ("application"equalsIgnoreCase(scope)) return PageContextAPPLICATION_SCOPE; elsethrow new IllegalArgumentException ("Invalid scope: " + scope);}}一旦CacheTag实例准备对标签进行操作JSP容器调用doTag()方法用getJspContext()来获得JSP context这个对象被造型为PageContext从而可以调用getServletContext()方法servlet context用来获取初始化参数的值这个值标明缓存机制是否被启用如果缓存被启用doTag()尝试使用id和scope属性值来获得缓存页面片段如果页面片段还没有被缓存doTag()使用getJspBody()invoke()来执行由和封装的JSP代码由JSP body产生的输出结果缓沖在StringWriter并且被toStirng()方法获得这样doTag()调用JSP context的setAttribute()方法新建一个JSP变量这个变量控制可能包含JSP表达式(${…})的缓存内容这些表达式在用jspContextgetOut()print()输出内容前被JspUtilseval()赋值只有当缓存被启用的时候这些行为才发生否则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= applicationgetInitParameter(CACHE_ENABLED);
cacheEnabled = cacheEnabledParam != null
&& cacheEnabledParamequals("true");
if (cacheEnabled)
{
String cachedOutput= (String) jspContextgetAttribute(id scope);
if (cachedOutput == null)
{
StringWriter buffer = new StringWriter();
getJspBody()invoke(buffer);
cachedOutput = buffertoString();
jspContextsetAttribute(id cachedOutput scope);
}
String evaluatedOutput = (String)
JspUtilseval(cachedOutput Stringclass jspContext);
jspContextgetOut()print(evaluatedOutput);
}
elsegetJspBody()invoke(null);
}
}注意一个单独的JspUtilseval()调用给所有的${…} 表达式赋值因为一个包含了大量的${…}结构的text也是一个表达式每一个缓存片段都可以被当作一个复杂的JSP表达式来进行处理IsCacheEnabled()方法返回cacheEnabled的值这个值已经被doTag()初始化IsCacheEnabled()方法返回cacheEnabled的值这个值已经被doTag()初始化
public class CacheTag extends SimpleTagSupport
{
public boolean isCacheEnabled() {return cacheEnabled;
}
}标签允许页面开发者自主选择缓存页面片段的ID这使得缓存一个页面片段可以被多个JSP页面共享当需要重用JSP代码时这是很有用处的但是仍然需要一些命名协议来避免可能的沖突通过修改CacheTag类来在自动ID内部包含URL可以避免这种副作用理解在做什么每一个被一个DynamicTag类的实例处理setExpr()方法将expr属性值存储到一个私有域DoTag()方法创建JSP表达式在expr属性值加上${前缀和}后缀然后doTag()使用findAncestorWithClass()来查找含有标签元素的的CacheTag handler如果没有查找到或者缓存被禁用JSP表达式被JspUtilseval()赋值并且值被输出否则doTag()输出无值表达式package comdevspherearticlesjspcache;
import javaxservletjspJspException;
import javaxservletjsptagextSimpleTagSupport;
import javaioIOException;
public class DynamicTag extends SimpleTagSupport
{
private String expr;public void setExpr(String expr)
{
thisexpr = expr;
}
public void doTag() throws JspException IOException
{
String output = "${" + expr + "}";
CacheTag ancestor = (CacheTag) findAncestorWithClass
(this CacheTagclass);
if (ancestor == null || !ancestorisCacheEnabled())
output = (String) JspUtilseval
(output Stringclass getJspContext());
getJspContext()getOut()print(output);
}
}分析上面的代码大家可以注意到和合作来实现一个尽可能有效率的方案如果缓存可用页面片段和由生成并被CacheTag赋值的JSP表达式一起放入缓沖器如果缓存被禁用缓沖变得没有意义只是执行其JSP body部分而让DynamicTag给JSP表达式赋值禁用缓存有时候是必要的特别是在开发过程期间出现内容的改变和JSP页面被重新编译的时候当然在开发完毕的成品环境中缓存必须被启用总结内容缓存是一种非常易用的改善Web应用性能的方法这篇文章集中讨论了使用JSP表达式语言来为每一个用户或者请求定制缓存内容贯穿全文的简单介绍的标签库适合小型Web apps并且可以提升中等应用的性能对于开发大型企业级应用则应当考虑使用支持更好的缓存机制的框架结构而不仅仅局限于使用JSP变量