Caching Dynamic Content with JSP 2.0
by Andrei Cioroianu
01/05/2005
Content caching is one of the most common optimization techniques used in web applications, and it can be implemented easily. For example, you can use a custom JSP tag--let's call it
--to wrap every page fragment that must be cached between
and . Any custom tag can control when its body (i.e. the wrapped page fragment) is executed, and the dynamic output can be captured. The
tag lets the JSP container (e.g. Tomcat) generate the content only once, storing each cached fragment as a JSP variable in the application
scope. Every time the JSP page is executed, the custom tag inserts the cached page fragment without re-executing the JSP code that generated the output. A tag library developed as part of the Jakarta project uses this technique, and it works fine when the cached content doesn't need to be customized for each user or request.
This article improves the technique described above, allowing the JSP page to customize the cached content for each request or user, using the JSP 2.0 Expression Language (EL). Cached page fragments can contain JSP expressions that are not evaluated by the JSP container, the custom tag evaluating these expressions each time the page is executed. Therefore, the creation of the dynamic content is optimized, but the cached fragments can have pieces of content that are generated for each request using the native expression language of JSP. This is possible with the help of the JSP 2.0 EL API, which exposes the expression language to the Java developer.
Content Caching Versus Data Caching
Content caching is not the only option. For example, data extracted from a database can be cached, too. In fact, data caching can be more efficient, since it stores the information without the HTML markup, requiring less memory. In many situations, however, content caching is easier to implement. Let's suppose you have lots of business objects producing some complex data, using significant CPU resources. You also have JSP pages that present the data. Everything works well until one day when the server's load suddenly increases, which requires an urgent solution. Building a caching tier between those business objects and the presentation tier can be a very elegant and efficient solution, but it could be much quicker and easier to modify the JSP pages, caching the dynamic content. Changes in the application's business logic usually require more work and more testing than simple editing of the JSP pages. In addition, there are fewer changes in the web tier when one page aggregates information from multiple sources. The problem is that the cache sometimes needs to be invalidated when the information becomes stale, and the business objects better know when this happens. Therefore, when choosing to implement content caching, data caching, or another optimization technique, you have to take into account many factors, which are sometimes specific to the application you are building.
Data caching and content caching do not necessarily exclude each other. They can be used together; for example, in database-driven applications. Data extracted from the database and the HTML that presents the data can be cached separately. This is similar to using some sort of templates, which are generated on the fly using JSP. The techniques based on the EL API discussed in this article show how you could use the JSP EL to insert the data into the templates for presentation.
Using JSP Variables to Cache Dynamic Content
Every time you implement a caching mechanism, you need a way to store the cached objects, which are strings in the case presented in this article. You could use an object-caching framework, or you might implement a custom caching solution, using Java maps. JSP already provides the so-called "scoped attributes" or "JSP variables," which offer the ID-object mappings needed by the caching mechanism. It doesn't make sense to use the page
or request
scopes, but the application
scope is a good place for storing the cached content, since it's shared by all users and all pages. The session
scope can also be used when you need one cache per user, but this isn't very efficient. The JSTL tag library can be used to cache content, using JSP variables as in the following example:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
...
The cached page fragment can be outputted with:
${applicationScope.cachedFragment}
What happens if the cached 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" %>
...
...
Then, you can output the cached content with:
${cachedFragment1} ${counter} ${cachedFragment2}
It is much easier to cache the page fragments that need customization with the help of a specialized tag library. As already mentioned, the cached content can be wrapped between a start tag (
) and an end tag (), while each customization is represented by another tag (
) that outputs a JSP expression (${...}
). The dynamic content is cached with JSP expressions that are evaluated each time the cached content is outputted. You'll see how this is implemented in the following sections of the article. The counter.jsp page caches a page fragment containing a counter that is incremented each 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" %>
... ...
Related Reading Head First Servlets and JSP |
JSP variables are easy to use and are a good content-caching solution for simple web apps. The lack of control over the cache's size may be a problem, though, if your application produces large amounts of dynamic content. A dedicated caching framework would provide a more robust solution, allowing you to monitor the cache, limit the cache's size, control the caching policy, and so on.
Using the JSP 2.0 Expression Language API
JSP containers (such as Tomcat) evaluate the expressions from the JSP pages using the EL API, which you can use in your Java code, too. This allows you to work with the JSP EL outside of your web pages, for example, in XML files, text-based resources, and custom scripts. The EL API is also useful when you want to control when the expressions from a web page are evaluated or when you build expressions programmatically. For example, cached page fragments can contain JSP expressions for customization and the EL API will be used to evaluate and reevaluate those expressions each time the cached content is outputted.
The example application (see Resources below) provided with this article includes a Java class (JspUtils
) that contains a method named eval()
, which takes three parameters: a JSP expression, the expected type of the expression's value, and a JSP context object. The eval()
method gets an ExpressionEvaluator
from the JSP context and calls the evaluate()
method, passing the expression, the expected type, and a variable resolver that is obtained from the JSP context. 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 that JspUtils.eval()
is basically a wrapper around the standard ExpressionEvaluator
. If expr
doesn't contain ${
, the JSP EL API isn't used, since there are no JSP expressions.
Creating the TLD
The JSP tag library needs a Tag Library Descriptor (TLD) file that specifies the names of the custom tags, their attributes, and the Java classes that handle the custom tags. The jspcache.tld file describes the two custom tags. The
tag has two attributes: the id
for the cached page fragment and the JSP scope
where the content should be stored. The
tag has only one attribute, which should be a JSP expression that must be evaluated each time the cached fragment is outputted. The TLD file maps the two custom tags to the CacheTag
and DynamicTag
classes, which are presented in the following sections:
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">
1.0
jc
http://devsphere.com/articles/jspcache
cache
com.devsphere.articles.jspcache.CacheTag
scriptless
id
true
true
scope
false
false
dynamic
com.devsphere.articles.jspcache.DynamicTag
empty
expr
true
false
The TLD file is declared in the web application descriptor (web.xml), which also contains an initialization parameter that indicates whether the cache is enabled or not:
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">
com.devsphere.articles.jspcache.enabled
true
http://devsphere.com/articles/jspcache
/WEB-INF/jspcache.tld
|
Understanding How
Works
For each occurrence of the
tag in a JSP page, the JSP container creates a CacheTag
instance, which is prepared for handling the tag. It is the responsibility of the JSP container to call the setJspContext()
, setParent()
, and setJspBody()
methods that CacheTag
inherits from SimpleTagSupport
. The JSP container also calls a setter method for each attribute of the handled tag. The setId()
and setScope()
methods store the attribute values into the private fields, which are initialized with default values in 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 value of the scope
attribute, which is converted from String
to int
:
...
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 prepared to handle the tag, the JSP container calls the doTag()
method, which obtains a JSP context with getJspContext()
. This object is casted to PageContext
in order to call getServletContext()
. The servlet context is used to obtain the value of the initialization parameter that indicates whether the caching mechanism is enabled or not. If the cache is enabled, doTag()
tries to get the cached page fragment, using the values of the id
and scope
attributes. If the page fragment hasn't been cached yet, doTag()
uses getJspBody().invoke()
to execute the JSP code wrapped between
and . The output generated by the JSP body is buffered in a
StringWriter
and is obtained with toString()
. At this point, doTag()
calls the setAttribute()
method of the JSP context to create a JSP variable that will hold the cached content, which may contain JSP expressions (${...}
). Those expressions are evaluated with JspUtils.eval()
before the content is outputted with jspContext.getOut().print()
. All of these actions take place only if the cache is enabled. Otherwise, doTag()
just executes the JSP body with getJspBody().invoke(null)
and the output 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 call of JspUtils.eval()
evaluates all ${...}
expressions, since a text containing multiple ${...}
constructs is an expression, too. Each cached fragment can be processed as a big JSP expression.
The isCacheEnabled()
method returns the value of the cacheEnabled
flag, which is initialized in doTag()
:
...
public class CacheTag extends SimpleTagSupport {
...
public boolean isCacheEnabled() {
return cacheEnabled;
}
}
The
tag allows the page developer to choose the IDs of the cached page fragments. This opens the possibility to cache a page fragment that is shared by multiple JSP pages, which is helpful when you reuse the JSP code, but this also requires some naming convention to avoid conflicts. If you want to avoid this side effect, you can modify the CacheTag
class to include the URL within the ID automatically.
Understanding What
Does
Each
tag is handled by an instance of the DynamicTag
class, whose setExpr()
method stores the value of the expr
attribute into a private field. The doTag()
method builds a JSP expression, adding the ${
prefix and the }
suffix to the value of the expr
attribute. Then, doTag()
uses findAncestorWithClass()
to find the CacheTag
handler of the
element that contains the
tag. If the ancestor isn't found or the caching is disabled, the JSP expression is evaluated with JspUtils.eval()
and then its value is outputted. Otherwise, doTag()
outputs the unevaluated 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 fragments, you'll observe that
and
cooperate in order to implement a solution that is as efficient as possible. If the cache is enabled, page fragments are buffered together with the JSP expressions that are generated by
and evaluated by CacheTag
. If the cache is disabled, the buffering is not necessary and
just executes its JSP body, letting DynamicTag
evaluate the JSP expressions. It is useful to disable the caching, especially during development when the content is changed and the JSP pages are recompiled. Of course, the caching should be enabled in a production environment.
Summary
Content caching is a very easy way to improve the performance of your web applications. This article focused on customizing the cached content for each user or request, using the JSP Expression Language. The simple tag library presented throughout the article is suitable for small web apps and could be enhanced for medium ones. If you develop large enterprise applications, you should consider using a framework that provides a better caching mechanism than the use of the JSP variables, but you'll probably still find useful the customization technique based on the EL API.
Resources
Andrei Cioroianu
is the founder of Devsphere and an author of many Java articles published by ONJava, JavaWorld, and Java Developer's Journal.