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 change. 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, cache content can be encapsulated by start tags (<jc:cache>) and end 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 container (such as Tomcat) to apply the EL API Expressions in the JSP page are assigned values 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) throwsJspException { 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, that is, the JSP expression must be assigned 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 the CacheTag class inherits 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().
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 a 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.
Summary
Content caching is a very easy-to-use method for improving web application performance. This article focuses on using the JSP expression language to customize cache content for each user or request. The tag libraries briefly introduced throughout this article are suitable for small Web apps and can improve the performance of medium-sized applications. For developing large-scale enterprise-level applications, you should consider using a framework that supports better caching mechanisms, rather than just using JSP variables.