Avoiding Unnecessary JSP Recompilation

by Nagesh Susarla
01/05/2005

Abstract

One of the most common questions on the JavaServer Pages (JSP) newsgroups is related to JSP recompilation. Having to recompile a JSP when it is not desired is an annoyance that many developers face. This article describes the scenarios that cause recompilation, starting with an explanation of the internal workings of the WebLogic JSP container, and then taking each apparent "undesired " scenario and applying the container's stale checking algorithm. Furthermore, this article discusses parameters that control the reloading of JSPs and servlet classes. These are highly recommended for servers that run in production mode.

The JSP Container's Stale Checking Mechanism

In WebLogic, JSPs are compiled to .class files. We use the term Stale Checking Mechanism to refer to the logic used to decide whether a particular JSP .class file is older ("stale") than the current JSP file. The WebLogic JSP container ensures that any JSP and its dependent files are only recompiled when they are modified. Looking at the generated Java code is a great way to understand the internal workings of the JSP container. As an example, we will take a JSP, compile it with the command-line JSP compiler, and look at the generated source code. The JSP compiler ( weblogic.jspc) comes with the standard WebLogic Server install kit.

Consider a simple JSP page called foo.jsp:

A simple JSP page

Now using the command-line JSP compiler, we compile the JSP, specifying an option called keepgenerated, which, as the name suggests, generates the corresponding Java code for the JSP page and leaves it on the disk.

java weblogic.jspc -keepgenerated -d .\WEB-INF\classes foo.jsp



[jspc]warning: expected file /WEB-INF/web.xml not found, 

   tag libraries cannot be resolved.

   <Jul 11, 2004 7:29:26 PM PDT> <Warning> <HTTP> 

   <BEA-101181> <Could not find web.

   xml under WEB-INF in the doc root: ..>

The compiler generates a .java file and its corresponding .class file in the output directory ( -d) specified as an option above. It puts the generated class file in a package called jsp_servlet, which happens to be the default JSP package prefix (unless overridden in weblogic.xml), and therefore the generated Java file can be found in .\WEB-INF\classes\jsp_servlet and will be called __foo.java.

Note that we can ignore the warning emitted by the compiler about not finding a web.xml file since we are not really working with tag libraries at this point.

The most relevant part of the generated code ( __foo.java) for our discussion is the staticIsStale() method, shown below.

Listing 1. staticIsStale() method

public static boolean _staticIsStale(weblogic.servlet.jsp.StaleChecker sci) {

   if (sci.isResourceStale("/foo.jsp", 1089594167518L, "8.1.2.0", 

                                            "America/Los_Angeles")) 

     return true;

   return false;

}

From the above snippet it is clear that the isResourceStale() method on the weblogic.servlet.jsp.StaleChecker interface is called to verify whether the JSP has been modified. The parameters to the isResourceStale() method are as follows and in the order indicated below:

  1. Resource to be checked, for example, /foo.jsp.
  2. Timestamp (as a long) of the JSP page.
  3. WebLogic Release Build version.
  4. Default time zone of the current machine.

The JSP container calls the _staticIsStale() method with an implementation of the StaleChecker interface. This implementation receives a callback ( isResourceStale()) with the parameters shown in Listing 1. With these parameters, the implementation simply gathers all the necessary information to deduce whether the given resource is stale or not. The JSP container considers a JSP .class file "stale" when either the timestamp (parameter 2) of the resource (parameter 1) /foo.jsp is newer (greater) than the timestamp stored inside the compiled class file, or when the release build version is different.

Let's look at some important consequences of this:

  • Since the timestamp of the JSP page is stored inside the class file and computed at compile time, modifying the timestamp of a class file has no effect on the stale checking process. (This is a very common myth and hopefully the above example clearly disproves it.)

  • The Forth parameter, that is, the time zone, is only used when the deployment is in an archived format ( .war).

  • The WebLogic Release Build version changes for each service pack, and therefore all the JSPs need to be precompiled for each service pack. This requirement was established to ensure that the JSP classes can take advantage of any compiler bug fixes or any JSP runtime changes in a later service pack or release.

What About Static Includes?

The next logical question one might have is: Will the JSP container recompile a given JSP page even when one of its static includes has been modified? The answer is yes. Even when a dependent file such as a static include is modified, the entire page (better called a "compilation unit") is recompiled. To look at how the container handles such a dependency, consider the following JSP with a static include called baz.inc.

Listing 2. foo.jsp

A simple jsp page.

<%@ include file="baz.inc"%>

Listing 3. baz.inc

--

Simple Static Include

--

Rerunning the previous command line for the JSP compiler on foo.jsp now produces a Java file that has the interesting snippet of code shown below. As you can see, each dependency is batched up in the _staticIsStale() method such that even if a dependency ( baz.inc in this case) is modified, the entire JSP or "compilation unit" is recompiled. The JSP container expects the root JSP page ( foo.jsp) to return a boolean indicating whether it is stale. Therefore, each generated JSP class file generates code that checks all of its dependency files too.

public static boolean _staticIsStale(weblogic.servlet.jsp.StaleChecker sci) {

  if (sci.isResourceStale("/foo.jsp", 1089616972487L, "8.1.2.0", "America/Los_Angeles")) 

    return true;

  if (sci.isResourceStale("/baz.inc", 1089616984268L, "8.1.2.0", "America/Los_Angeles")) 

    return true;

  return false;

}

To summarize, the WebLogic JSP container lets each JSP .class maintain its own list of dependencies and also relies on it to store the state (timestamps) of the original JSP (and its dependents). The container calls the _staticIsStale() method on the JSP .class, which in turn calls back to the JSP container with weblogic.servlet.jsp.StaleChecker.isResourceStale() passing along all information necessary to determine whether a single resource is stale. This greatly simplifies the task of stale checking and eliminates the need for maintaining the timestamps of each JSP in a separate location.

Scenarios That Cause Recompilation of JSPs

We've examined the factors taken into account by the JSP container when it performs its stale checking. Now let's look at a few common scenarios where the JSP will get recompiled:

  1. The copying of files using build scripts can modify the timestamps of the JSPs. This can lead to recompilation of all the JSPs.

    Consider a scenario in which all our JSPs are located in a directory called src. Say that a build script copies all JSPs and compiles the servlet Java files into the build directory. Then the script runs weblogic.jspc over the src directory and places all the compiled JSPs in the build directory. Here copying the JSPs to the build directory could very well have changed the timestamps of the JSPs (unless our build scripts used cp –p/-m to preserve file timestamps). And when this web application is deployed to the server from the build directory, all the JSPs get recompiled since the JSP classes were compiled with JSPs older than the ones that were deployed (that is, the JSPs that were copied to the build directory in this case). This is one of the most common cases of recompilation, which is avoidable by making sure that copying preserves file timestamps.

  2. Modifying the packagePrefix parameter of weblogic.xml will cause recompilation. The stale check mechanism looks for the packagePrefix in the weblogic.xml file of a particular web app and searches for a class called <packagePrefix>.__foo.class for /foo.jsp. Suppose we prebuilt all the JSPs using weblogic.jspc, placing them in the WEB-INF/classes directory of a web app, and then we deployed this web application archive (WAR) to the server. Say we had a JSP called foo.jsp in this web app. In the absence of a "packagePrefix" in our weblogic.xml, the stale check mechanism would look for the class jsp_servlet.__foo.class. Now suppose we modify the weblogic.xml and add a package prefix, say com.bar, and then we redeploy the same WAR to the server. Accessing foo.jsp at this point would cause the JSP to be recompiled since the stale check mechanism would be looking for a class called " com.foo.__foo.class". You can get around this by ensuring that you invoke the weblogic.jspc command with the -package parameter, and use the same package name.

  3. Modifying the workingDir parameter of weblogic.xml will also cause recompilation. In this case, the JSP container would look for the JSP classes in the new "workingDir" in addition to the usual web application classpath. Since the old working directory had the JSP classes before the new version was deployed, the JSP container wouldn't be able to find them and therefore would recompile the requested JSP.

    Note: Scenario 2 and 3 clearly explain the need for rebuilding or precompiling the web app even when a modification is made to weblogic.xml. Deploying the precompiled WAR after any modifications ensures that JSPs do not get recompiled.

  4. Deploying a prebuilt WAR to a newer version of WebLogic Server causes a recompile of all JSPs. As explained in the section describing the stale checking mechanism, the JSP container recompiles all JSPs when they are deployed to a server with a different version. This is done to ensure that all JSP compiler/runtime enhancements or bugs fixed in a given release or a service pack are available in the generated code. (In the absence of such a restriction, it is possible to end up with classes referring to missing methods in the JSP runtime.) Ideally, the build scripts that precompile JSPs must use the same version of weblogic.jar used by the server that is being deployed to. It is recommended that any patches or rolling fixes being used with the server be added to the classpath used by the build scripts. To summarize, the build and deployment environments must be exactly the same. This prevents any unnecessary recompilation issues after a deployment.

Further Control Over Staleness Checking

Controlling when the container performs a stale check allows us to tune the container to perform better and therefore give us better response times for JSPs as well as servlets. Each stale check requires the JSP container to go to disk and reread the last-modified time for that particular JSP. When called too often, this process can lead to bad performance because it affects the response time of a JSP. Ideally, we need stale checking to be very active at development time, typically when the application undergoes a lot of changes. It's great to test out changes made to a particular JSP by clicking refresh/reload on the browser and having the JSP container recompile as well as reload the new page. But in production mode, the same thing can cause bad performance.

The default values for the following parameters are best suited for development mode. We recommend that you change them accordingly when deploying in a production scenario.

PageCheckSeconds

For every new request to an already compiled JSP, the container checks the value for pageCheckSeconds from its configuration file ( weblogic.xml in this case) and performs the stale checking operation if the interval between the last stale check and the current time is greater than pageCheckSeconds. For example, let's say the pageCheckSeconds value is set to 10 seconds. For a request to foo.jsp, the container checks to see if the interval between the current time and the last stale check is greater than pageCheckSeconds. In this case, suppose the page was recompiled and accessed more than 10 seconds ago; the container would check to see if the class is stale. Any request to foo.jsp within this interval is not stale checked.

If you are not in development mode and do not need the JSP page to be checked every second (default value), then changing it to either -1 (never perform the stale check) or a value such as 60 seconds is highly recommended. This avoids the need to call File.lastModified(), reloading of the JSP class and checking timestamps each time a JSP is accessed.

Listing 4. A snippet from weblogic.xml, illustrating how this parameter can be set

<!DOCTYPE weblogic-web-app PUBLIC "-//BEA Systems, Inc.//DTD Web Application 8.1//EN"

 "http://www.bea.com/servers/wls810/dtd/weblogic810-web-jar.dtd">

<weblogic-web-app>

  <jsp-descriptor>

   <jsp-param>

     <param-name>pageCheckSeconds</param-name>

     <param-value>10</param-value>

   </jsp-param>

  </jsp-descriptor>

</weblogic-web-app>

Note that in a production environment where the JSPs in a web application never change individually, it is best to configure the container to never perform the stale checking for both servlets and JSPs.

servlet-reload-check-secs

In development mode, when a servlet is modified and recompiled into, say, the WEB-INF/classes directory of an exploded WAR, we expect the container to invoke the latest version of the servlet when requested from the browser. To handle this, the WebLogic web container checks if any file in WEB-INF/classes has been modified every servlet-reload-check-secs interval. The default value for this parameter is 1 second. This is a good default value for development mode where we expect to see our latest changes to the servlet classes without having to redeploy the application. But before going into production, this value must be changed to -1 (never reload servlet files). In production mode, where the individual classes do not change, it is always best to set the value of servlet-reload-check-secs to -1.

Listing 5. Sample weblogic.xml with servlet-reload-check-secs set to a value of -1

<!DOCTYPE weblogic-web-app PUBLIC "-//BEA Systems, Inc.//DTD Web Application 8.1//EN"

 "http://www.bea.com/servers/wls810/dtd/weblogic810-web-jar.dtd">



<weblogic-web-app>

  <container-descriptor>

    <servlet-reload-check-secs>-1</servlet-reload-check-secs>

  </container-descriptor>

</weblogic-web-app>

JSP Class Loaders

We'll finish our discussion by looking at how WebLogic Server loads JSP classes. Each JSP is loaded in its own class loader (normally called as a one-off class loader). This class loader is a child of the Web Application class loader and is responsible for loading the concerned JSP class and its inner classes if any. A curious reader might be wondering why WebLogic loads every JSP in its own class loader. Is this complexity really needed? Can't WebLogic just use the web app class loader and make life easier? All these questions are valid and should be asked by every seeker of WebLogic class loader nirvana. To tackle these questions let's imagine that our web application has several JSPs, a few servlets, a filter, and a couple hundred utility classes that also include tag handler classes. Now, say all these classes were loaded in a single class loader. If you modify a single JSP and then click reload on the browser, the following things would have to occur:

  • The page would have to be recompiled by the JSP container.
  • The entire web app class loader that was used to load an older version of the class would have to be discarded.
  • A new web app class loader would have to be created, and all the servlets and JSPs (including the one just changed) reloaded and reinitialized.

Java does not allow a class loader to be reused to reload a newer version of a class. Instead, you have to discard a class loader and create a new one. For this reason the above scenario is quite undesirable; the application server would have to reload a significant number of classes even though only a single class changed.

Now let's look at how WebLogic implements its class loader scheme. Consider the same scenario mentioned before. If we modify the JSP, and hit reload on the browser, then the server performs the following tasks:

  • The JSP container recompiles the page.
  • It throws away the single JSP class loader that was used to load the old version of this JSP class.
  • It creates a new JSP class loader with the web app class loader as parent and serves the page.

As you can see, only a single JSP class had to be reloaded, and the entire web app class loader remains untouched and unaffected by our little modification to the JSP. Therefore, when a single JSP is modified, the container throws away the old class loader, recompiles, and reloads only the generated class for this JSP. This prevents the need to reload or bounce the entire web app class loader. This is a huge win when only some JSPs in a particular web application change very often.

Conclusion

With this knowledge about the internals of the JSP container, one can not only avoid the undesirable situation of having a JSP unnecessarily recompiled but can also improve page response time itself by using the pageCheckSeconds and the servlet-reload-checks-secs parameters.

Nagesh Susarla is the Technical lead for the WebLogic Server J2EE Container team at BEA Systems.