Detailed explanation of dynamic content caching under JSP 2.0
Author:Eve Cole
Update Time:2009-07-03 16:56:34
Content caching is one of the most common optimization techniques in web applications and can be easily implemented. For example, you can use a custom JSP tag - let's name it <jc:cache> - with <jc:cache> and </jc:cache> to encapsulate each page fragment that needs to be cached. Any custom tag can control when the part it contains (that is, the pre-packaged page fragment) is executed, and the dynamic output results can be captured. The <jc:cache> tag enables a JSP container (such as Tomcat) to generate content only once, as application-wide JSP variables, to store each cache fragment. Each time the JSP page is executed, the custom tag will load the cached page fragment without having to execute the JSP code again to generate the output. As part of the Jakarta project, the tag library was developed using this technology. It works well when the cached content does not need to be customized for each user or request.
This article improves on the technique described above by using JSP 2.0 Expression Language (EL), allowing JSP pages to customize cached content for each request and user. Cache page fragments can contain JSP expressions that are not evaluated by the JSP container. The values of these expressions are determined by custom tags each time the page is executed. Therefore, the creation of dynamic content is optimized, but cached fragments can contain parts of the content generated by each request using the native JSP expression language. With the help of the JSP 2.0 EL API, Java developers can make this possible using expression languages.
Content Caching VS Data Caching Content caching is not the only option. For example, data extracted from a database can also be cached. In fact, data caching may be more efficient since the stored information does not contain HTML markup and requires less memory. In many cases, however, in-memory caching is easier to implement. Assume that in a certain case, an application consists of a large number of transaction objects, occupies important CPU resources, generates complex data, and uses JSP pages to present the data. Everything was working fine until one day the load on the server suddenly increased and an emergency solution was needed. At this time, establishing a cache layer between the transaction object and the presentation layer is a very good and effective solution. But JSP pages that cache dynamic content must be modified very quickly and smoothly. Relative to simple JSP page editing, changes to the application's business logic usually require more work and testing; in addition, if a page aggregates information from multiple composite sources, the Web layer only needs a small amount of changes. The problem is that cache space needs to be freed when cached information becomes stale, and the transaction object should know when this happens. However, when choosing to implement content caching or data caching, or other optimization techniques, there are many factors that have to be considered, and sometimes there are special requirements for the program being developed.
Data caching and content caching are not necessarily mutually exclusive, they can be used together. For example, in a database-driven application; the data extracted from the database and the HTML that presents the data are cached separately. This is somewhat similar to using JSP to generate templates in real time. The EL-based API techniques discussed in this article illustrate how to use JSP EL to load data into rendering templates.
Caching dynamic content using JSP variables Whenever you implement a caching mechanism, you need a method to store cache objects. In this article, objects of type String are involved. One option is to use an object-caching framework, or use Java maps to implement a custom caching scheme. JSP already has what are called "scoped attributes" or "JSP variables" to provide ID-object mapping, which is what the caching mechanism requires. For using page or request scope this doesn't make sense, whereas in application scope this is a good place to store cached content since it is shared by all users and pages. Session scope can also be used when separate caching is required for each user, but this is not very efficient. The JSTL tag library can be used with that to cache content, by using JSP variables as shown in the following example:
<%@ 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>
The cached page fragment outputs the results using the following statement:
${applicationScope.cachedFragment}
What happens when the cache fragment needs to be customized for each request? For example, if you want to include a counter, you need to cache two fragments:
<%@ 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>
You can use the following statement to output the cache content:
${cachedFragment1} ${counter} ${cachedFragment2}
With the help of a dedicated tag library, caching of page fragments that require customization becomes extremely easy. As mentioned above, cached content can be enclosed by opening tags (<jc:cache>) and closing tags (</jc:cache>). Each customization can be expressed using another tag (<jc:dynamic expr="..."/>) to output a JSP expression (${...}). Dynamic content is cached using JSP expressions and assigned each time the cached content is emitted. You can see how this is achieved in the section below. Counter.jsp caches a page fragment containing a counter. The counter will automatically increase by 1 every time the user refreshes the page.
<%@ 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 variables are easy to use and are a good content caching solution for simple Web apps. However, if the application generates a lot of dynamic content, having no control over cache size is definitely a problem. A dedicated cache framework can provide a more powerful solution, allowing cache monitoring, limiting cache size, controlling cache policy, etc...
Using the JSP 2.0 Expression Language API
JSP containers (such as Tomcat) evaluate expressions in JSP pages using the EL API and can be used by Java code. This allows development outside of Web pages using JSP EL, for example, on XML files, text-based resources, and custom scripts. The EL API is also useful when you need to control when expressions in a Web page are assigned or written expressions related to them. For example, cached page fragments can contain custom JSP expressions, and the EL API will be used to assign or re-evaluate these expressions each time the cached content is emitted.
The article provides an example program (see the resources section at the end of the article). This application contains a Java class (JspUtils) and a method eval() in the class. This method has three parameters: JSP expression, the expected type of the expression and A JSP context object. The Eval() method gets the ExpressionEvaluator from the JSP context and calls the evaluate() method, passing the expression, the expression's expected type, and a variable obtained from the JSP congtext. The JspUtils.eval() method returns the value of the expression.
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);
}
}
...}
Note: JspUtils.eval() mainly encapsulates the standard ExpressionEvaluator. If expr does not contain ${, the JSP EL API is not called because there is no JSP expression.
Creating a tag library descriptor (TLD) file A JSP tag library requires a tag library descriptor (TLD) file to customize the naming of tags, their attributes, and the Java classes that operate on the tag. jspcache.tld describes two custom tags. <jc:cache> has two attributes: the id of the cached page fragment and the JSP scope—the content scope of the JSP page that always needs to be stored. <jc:dynamic> has only one attribute, the JSP expression that must be evaluated every time the cache fragment is output. The TLD file maps these two custom tags to the CacheTag and DynamicTag classes as follows:
<?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>
The TLD file is included in the Web application descriptor file (web.xml). These five files also contain an initial parameter indicating whether the cache is available.
<?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>
Understand the working mechanism of <jc:cache>. The JSP container creates a CacheTag instance for each <jc:cache> tag in the JSP page to process it. The JSP container is responsible for calling the setJsp (), setParent () and setJspBody () methods, which is the CacheTag class inherited from SimpleTagSupport. The JSP container also calls the setter method for each attribute of the manipulated tag. The SetId() and setScope() methods store the attribute value into a private field. This value has been initialized with the default value using the CacheTag() constructor.
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);
}
...}
The setScope() method calls JspUtils.checkScope() to verify the property value of the scope that has been converted from String to int type.
...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);
}}
Once the CacheTag instance is ready to operate on the tag, the JSP container calls the doTag() method and obtains the JSP context using getJspContext(). This object is cast as PageContext, so that the getServletContext() method can be called. The servlet context is used to obtain the value of the initialization parameter, which indicates whether the caching mechanism is enabled. If caching is enabled, doTag() attempts to obtain the cached page fragment using the id and scope attribute values. If the page fragment has not been cached, doTag() uses getJspBody().invoke() to execute the JSP code encapsulated by <jc:cache> and </jc:cache>. The output generated by the JSP body is buffered in a StringWriter and retrieved by the toStirng() method. In this way, doTag() calls the setAttribute() method of the JSP context to create a new JSP variable. This variable controls the cache content that may contain JSP expressions (${…}). These expressions are assigned by JspUtils.eval() before outputting content using jspContext.getOut().print(). This behavior only occurs when caching is enabled. Otherwise, doTag() just executes the JSP body through getJspBody().invoke(null) and the output result is not cached.
...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);
}
...}
Note that a single JspUtils.eval() call evaluates all ${…} expressions. Because a text containing a large number of ${...} structures is also an expression. Each cache fragment can be processed as a complex JSP expression.
The IsCacheEnabled() method returns the value of cacheEnabled, which has been initialized by doTag().
...public class CacheTag extends SimpleTagSupport {
... public boolean isCacheEnabled() {
return cacheEnabled;
}}
The <jc:cache> tag allows page developers to choose the ID of cached page fragments. This allows cached page fragments to be shared by multiple JSP pages, which is useful when reusing JSP code. But some naming protocol is still needed to avoid possible conflicts. This side effect can be avoided by modifying the CacheTag class to include the URL inside the automatic ID.
Understanding what <jc:dynamic> is doing Each <jc:dynamic> is handled by an instance of the DynamicTag class, and the setExpr() method stores the expr attribute value into a private field. The DoTag() method creates a JSP expression and adds ${ prefix and } suffix to the expr attribute value. Then, doTag() uses findAncestorWithClass() to find the CacheTag handler of <jc:cache> that contains the <jc:dynamic> tag element. If not found or caching is disabled, the JSP expression is evaluated with JspUtils.eval() and the value is output. Otherwise, doTag() outputs a valueless expression.
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);
}}
Analyzing the above code, you can notice that <jc:cache> and <jc:dynamic> cooperate to achieve a solution that is as efficient as possible. If cache is available, the page fragment is put into the buffer together with the JSP expression generated by <jc:dynamic> and assigned the CacheTag value. If caching is disabled, caching becomes meaningless and <jc:cache> simply executes its JSP body part, leaving the DynamicTag to assign values to the JSP expression. Disabling caching is sometimes necessary, especially during the development process when content changes and JSP pages are recompiled. Of course, caching must be enabled in the production environment after development.
Summarize
For developing large enterprise-level applications, you should consider using a framework that supports better caching mechanisms instead of just using JSP variables. But it is undoubtedly helpful to understand the customization technology based on EL API.