Jasper Configuration & Precompilation

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Jakarta Pages 4.0 (Jakarta EE 11), Jakarta Servlet 6.1, Jakarta Expression Language 6.0 and Jakarta Standard Tag Library 3.0, as implemented by the Jasper engine of Apache Tomcat 11.0.26 on Java 21, with side notes for Eclipse Jetty 12.1.14 (jetty-ee11-jspc-maven-plugin 12.1.14, which is also Apache Jasper) and Eclipse GlassFish 8.0.x (8.0.5 on Maven Central; its JSP engine is Eclipse WaSP). Everything was verified on 2026-10-08: the parameter names and defaults against the Tomcat 11.0.26 Jasper how-to, the Jetty 12.1 operations guide and the GlassFish deployment guide, and every Tomcat example by building it into a Maven WAR (the g7jasper* family of scratch WARs, about thirty small variants of one web application) and running it on Tomcat 11.0.26 with curl; JspC was run through Ant 1.10.15, through its command line and through the Jetty Maven plugin. Where a statement was only read in the documentation and not run (everything about Jetty at run time and GlassFish), the page says so. Jasper is the part of the container that turns a page into a servlet. The JSP Lifecycle and Translation page explains what it does; this page explains how to tell it to do it differently, how to do the work before the first request, and which settings belong in a developer’s Tomcat and which in a production one. The running scenario is the Bookshelf, a small catalogue of books, here with a servlet that forwards to a catalogue page, two small pages that make the settings visible, a tag file and a tag library.

Where Jasper Is Configured

Jasper is one servlet. In Tomcat it is the class org.apache.jasper.servlet.JspServlet, declared under the name jsp in the global $CATALINA_BASE/conf/web.xml and mapped there to *.jsp and *.jspx. Every Jasper setting is an init-param of that servlet (a handful are context parameters, covered below), so the question "where do I configure it" is the question "where do I declare or redefine the jsp servlet". There are four places and they stack.

Table 1. Where the Jasper settings can live in Tomcat 11, from widest to narrowest scope
Place Scope Notes (rows marked "observed" were reproduced)

$CATALINA_BASE/conf/web.xml, servlet jsp

Every web application of the Tomcat instance

The stock file already sets fork to false and xpoweredBy to false and loads the servlet at start-up with load-on-startup 3 (observed in the 11.0.26 distribution). Editing it changes the defaults of every application, which is rarely what a team wants

WEB-INF/web.xml, redefining servlet jsp with <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>

One web application

Observed: the redefinition may omit the servlet-mapping, the mapping from the global file still applies. The how-to warns that another container may not recognise the class

WEB-INF/tomcat-web.xml

One web application, Tomcat only

Identical format to web.xml. It overrides the container defaults but not what WEB-INF/web.xml sets. Observed: development=false declared only here took effect (the page was not recompiled after an edit). The way to keep a portable web.xml

Context parameters (context-param in WEB-INF/web.xml)

One web application

Used for the pluggable interpreters and the page-context pool (see below). They are read from the ServletContext

Parameter names are case-sensitive in Tomcat. Observed: keepGenerated is silently ignored (the generated .java file was kept) while keepgenerated works, and scratchdir and mappedfile are all lower case but genStringAsCharArray, modificationTestInterval and compilerSourceVM are camel case. The Jetty operations guide says the same in other words: if a value does not take effect, try a different capitalisation, because the names are inconsistent. A typo is never an error, the parameter is simply ignored.

A production profile for the Bookshelf, as the jsp servlet redefinition in WEB-INF/web.xml (g7jasperprod.war)
<servlet>
  <servlet-name>jsp</servlet-name>
  <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
  <init-param><param-name>development</param-name><param-value>false</param-value></init-param>
  <init-param><param-name>genStringAsCharArray</param-name><param-value>true</param-value></init-param>
  <init-param><param-name>trimSpaces</param-name><param-value>single</param-value></init-param>
  <init-param><param-name>keepgenerated</param-name><param-value>false</param-value></init-param>
  <init-param><param-name>xpoweredBy</param-name><param-value>true</param-value></init-param>
  <init-param><param-name>classdebuginfo</param-name><param-value>false</param-value></init-param>
  <init-param><param-name>mappedfile</param-name><param-value>false</param-value></init-param>
  <load-on-startup>3</load-on-startup>
</servlet>
The same redefinition in WEB-INF/tomcat-web.xml keeps web.xml portable (g7jaspertw.war; only development is set)
<servlet>
  <servlet-name>jsp</servlet-name>
  <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
  <init-param><param-name>development</param-name><param-value>false</param-value></init-param>
  <load-on-startup>3</load-on-startup>
</servlet>

Observed with curl -D - http://localhost:18080/g7jasperprod/books: the response carried X-Powered-By: JSP/4.0 (xpoweredBy is on), the work directory held only the .class file and no .java file (keepgenerated is off), and the page body came back 184 bytes long instead of 198 (trimSpaces). The default jsp servlet of the same Tomcat sends no X-Powered-By header.

What Jasper 2 Adds: Pooling, Background Compilation and the Eclipse Compiler

The how-to’s introduction lists the reasons Jasper 2 behaves as it does, and each one maps to a parameter below.

  • Custom tag pooling. Tag handler instances are pooled and reused, which makes pages with many custom tags considerably faster (enablePooling, poolTagsWithExtends).

  • Background compilation. If a page that was already compiled changes, Jasper can compile the new version in the background while the old class keeps serving requests, and swap it in once compilation succeeds (checkInterval).

  • Recompile on a changed include. A page that is included at translation time (the include directive) is tracked, and the including page is recompiled when it changes. The dependency list is the _jspx_dependants map of the generated class; the behaviour is covered on the Includes, Forwards and Page Composition page.

  • The Eclipse JDT compiler. The generated source is compiled with the Eclipse compiler (ecj-4.40.jar in the Tomcat 11.0.26 lib directory), which loads its dependencies from the container class loader. No JDK is needed at run time. Ant and javac can still be used by setting compiler.

Every Jasper Parameter and Its Default

The tables list all the init parameters documented for the Tomcat 11.0.26 Jasper servlet, with the documented default. The last column says what was done here: observed means the setting was changed in a scratch WAR and the effect was seen, docs means the behaviour is the documentation’s and was not exercised.

Table 2. Compilation and the compiler
Parameter Default Effect Checked

compiler

none

An Ant javac compiler name. If unset, Jasper uses the Eclipse JDT compiler and not Ant. When set, ant.jar, ant-launcher.jar and tools.jar must be on the class path (put them in setenv.sh)

docs

compilerSourceVM

17

The Java source level of the generated source

observed

compilerTargetVM

17

The class file level of the generated classes. Observed: with the default the class file major version is 61 (Java 17) on a Java 21 run time; 21 gives 65; 8 is raised to 61 and the page still works

observed

fork

true

Only meaningful with Ant: compile in a separate JVM. The stock conf/web.xml sets it to false

docs

classdebuginfo

true

Compile with debugging information. Observed: the class file had LocalVariableTable attributes with the default (14 in the small page) and none with false

observed

classpath

built from the web application

The compile class path. It only counts if the ServletContext attribute org.apache.jasper.Constants.SERVLET_CLASSPATH is unset, and Tomcat always sets it

docs

servletClasspathAttribute

org.apache.catalina.jsp_classpath

The name of the ServletContext attribute that provides the class path to the JSP

docs

javaEncoding

UTF8

The encoding of the generated .java file (see Character Encoding)

docs

errorOnUseBeanInvalidClassAttribute

true

Fail when the class attribute of jsp:useBean is not a valid bean class

docs

strictGetProperty

true

Enforce that the object named in jsp:getProperty was introduced to the page, as the specification requires

docs

strictWhitespace

true

When false, a missing space before an attribute name is tolerated

docs

strictQuoteEscaping

true

Apply the JSP.1.6 quote-escaping rules to scriptlet expressions used as attribute values

docs

quoteAttributeEL

true

Apply the JSP.1.6 attribute quoting rules to EL in attribute values

docs

Table 3. Recompilation, loading and unloading
Parameter Default Effect Checked

development

true

Development mode: pages are checked for modification, at most once per modificationTestInterval

observed

modificationTestInterval

4 (seconds)

Do not check a page and its dependencies again within this interval; 0 checks on every access. Development mode only

observed

checkInterval

0 (seconds)

With development false and a value above zero, enables background compilation: the interval between checks

observed

recompileOnFail

off

After a failed compilation, ignore modificationTestInterval so the next access retries. Development mode only, off because compilation is expensive

observed

maxLoadedJsps

-1

The most pages loaded at once; beyond it the least recently used page is unloaded. Zero or less means no limit

observed

jspIdleTimeout

-1 (seconds)

An idle page is unloaded after this time; zero or less means never

observed

Table 4. Generated output, source and debugging
Parameter Default Effect Checked

keepgenerated

true

Keep the generated .java file

observed

scratchdir

the web application’s work directory

Where generated sources and classes go. It must exist and be writable

observed

generatedJspPackageName

org.apache.jsp

The default package of compiled pages

observed

generatedTagFilePackageName

org.apache.jsp.tag

The default package of handlers generated from tag files (the tag file classes of the example below ended up in org.apache.jsp.tag.web)

docs

mappedfile

true

One out.write call per line of static template text, to ease debugging. Observed in g7jnosmap.war (set together with suppressSmap): the generated source of the small page had one out.write call fewer than with the default

observed

suppressSmap

false

Do not generate the JSR-045 line table (SourceDebugExtension). Observed: the class had no SourceDebugExtension attribute with true

observed

dumpSmap

false

Also write the line table to a .smap file next to the class; false if suppressSmap is true. Observed: shelf_jsp.class.smap appeared

observed

displaySourceFragment

true

Include a fragment of the source in exception messages

docs

tempVariableNamePrefix

_jspx_temp

Prefix of the generated temporary variables (the tag plugin example below shows _jspx_temp0)

docs

variableForExpressionFactory

_el_expressionfactory

Name of the generated expression factory variable

docs

variableForInstanceManager

_jsp_instancemanager

Name of the generated instance manager variable

docs

jspServletBase

org.apache.jasper.runtime.HttpJspBase

The base class of the generated servlets

docs

serviceMethodName

_jspService

The service method the base class calls

docs

xpoweredBy

false

Add X-Powered-By to the response

observed

Table 5. Output size and template text
Parameter Default Effect Checked

trimSpaces

false

true removes template text that is only whitespace, single replaces it with one space, extended also strips leading and trailing whitespace of template text and collapses runs of whitespace and newlines into one newline. A page or tag file with trimDirectiveWhitespaces true takes precedence over this setting

observed

genStringAsCharArray

false

Generate text strings as char arrays, which can be faster

observed

Table 6. Tag handlers and expression evaluation
Parameter Default Effect Checked

enablePooling

true

Tag handler pooling. A compilation option: it does not change pages already compiled

observed

poolTagsWithExtends

false

Pages with their own base class (the extends attribute) have pooling disabled because Jasper cannot guarantee the base class initialised the pools. Set true only if the base class calls _jspInit() from init, otherwise NullPointerException follows

docs

useInstanceManagerForTags

false

Obtain tag handler instances from the instance manager

docs

limitBodyContentBuffer

false

Destroy and recreate any tag buffer that grows beyond bodyContentTagBufferSize

docs (accepted)

bodyContentTagBufferSize

512 (characters)

The size of a tag buffer

docs (accepted)

Table 7. Extension points and names
Parameter Default Effect Checked

engineOptionsClass

EmbeddedServletOptions

A replacement Options class that configures Jasper

docs

jspPrecompilationQueryParameter

jsp_precompile

The name of the request parameter that precompiles without invoking the page (specification 11.4.2). Observed: with the value precomp the request ?precomp=true returned 200 with an empty body and ?jsp_precompile=true ran the page normally

observed

The same parameters have different names in GlassFish (genStrAsCharArray) and a differently spelled Ant attribute in JspC (smapSuppressed, poolingEnabled and so on); the JspC and GlassFish names appear in their own sections below.

Development Versus Production

The defaults are tuned for a developer who edits a page and reloads it: modification checks on, generated source kept, debug information on. The how-to names the four settings to revisit for production and says the main optimisation is precompilation, which is sometimes impossible (it gives the jsp-property-group feature as an example) or impractical, and that the configuration of the servlet matters most in exactly those cases.

Table 8. Development defaults against the production recommendations (Tomcat 11.0.26 how-to, with the Bookshelf’s choices)
Parameter Development default Production Why

development

true

false

Stops the on-access modification check. Observed: an edited page was then never recompiled (still the old text after more than 30 seconds)

genStringAsCharArray

false

true

Slightly more efficient char arrays

modificationTestInterval

4

a high value, only if development must stay true (for example dynamically generated pages)

Fewer file system checks

trimSpaces

false

single or extended

Removes unnecessary bytes. Observed on the Bookshelf catalogue page: 198 bytes with false, 179 with true, 184 with single, 156 with extended

keepgenerated

true

false (optional)

Not in the how-to’s list; keeps the work directory small. Keep it true while diagnosing

classdebuginfo, mappedfile, suppressSmap

true, true, false

false, false, true (optional)

Smaller classes, no debugger support. The GlassFish deployment guide recommends the same set. A trade-off against being able to debug: see the testing and debugging page of this section

Modification Detection in Development Mode

With the defaults, a page is checked for modification when a request arrives and at least modificationTestInterval seconds have passed since the last check. The first request, the edit and a request inside the interval show the old page, a request after the interval shows the new page and the old instance is destroyed (jspDestroy) before the new one is initialised (jspInit).

Observed on g7jasper.war: stamp.jsp declares jspInit and jspDestroy that print to the log; the file was edited between the second and third request
$ curl -s http://localhost:18080/g7jasper/stamp.jsp | tail -1
Bookshelf edition: TWO
$ sed -i '' 's/TWO/THREE/' webapps/g7jasper/stamp.jsp
$ curl -s http://localhost:18080/g7jasper/stamp.jsp | tail -1      # immediately: inside the 4 second interval
Bookshelf edition: TWO
$ sleep 5
$ curl -s http://localhost:18080/g7jasper/stamp.jsp | tail -1
Bookshelf edition: THREE
The log lines of the same run, in catalina.out
g7jasper stamp.jsp: jspDestroy
g7jasper stamp.jsp: jspInit org.apache.jsp.stamp_jsp

Production Without Precompilation: Background Compilation

With development false the pages are never checked, so a changed page is only picked up by a redeploy. If pages must change in place on a production server, set checkInterval above zero: Jasper then checks in the background and compiles in the background, and the old class keeps serving until the new one is ready (the availability point of the introduction). Observed with development=false and checkInterval=2 (g7jaspercheck.war): three requests in the nine seconds after the edit still returned the old page, the new text appeared between nine and about twenty-five seconds after the edit, and in a second run after six seconds. A background check runs when the container’s background processor runs, and the Engine’s backgroundProcessorDelay defaults to 10 seconds, so checkInterval cannot make the delay shorter than that cycle. The same edit in g7jasperprod.war (no checkInterval) was still not visible after more than 30 seconds.

Recovering From a Failed Compilation

In development mode a page that fails to compile is not retried inside modificationTestInterval. Observed with modificationTestInterval=600 (g7jrofoff.war and g7jrofon.war): a page with a syntax error in a scriptlet returned 500, was fixed, and kept returning 500 for as long as it was tried (25 seconds) without recompileOnFail. With recompileOnFail=true the fixed page returned 200 about four seconds after the edit, the same delay as the default interval, so the retry is allowed but not instantaneous. The documentation says the setting makes the next access trigger a recompilation attempt; the observed behaviour was a delay of about four seconds.

Shaping the Output: trimSpaces and genStringAsCharArray

Template text is the HTML around the tags, and whitespace-only runs of it are what a page’s directives and loops leave behind. The Bookshelf catalogue page is a good subject because it has two directives, a forEach and indented markup. The Bookshelf servlet forwards to it and the response was measured with curl for each setting of trimSpaces.

The catalogue page, WEB-INF/views/books.jsp
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!DOCTYPE html>
<html>
<body>

  <h1>Bookshelf</h1>

  <ul>
    <c:forEach var="b" items="${books}">
      <li>${b.title} by <c:out value="${b.author}"/> (${b.year})</li>
    </c:forEach>
  </ul>

</body>
</html>
Table 9. The response size of /books for each trimSpaces value on Tomcat 11.0.26 (g7jtrimf, g7jtrimt, g7jtrims and g7jtrime)
trimSpaces Bytes What was removed

false (default)

198

Nothing

true

179

Template text that is only whitespace

single

184

The same text, each run replaced by one space (so two directives on separate lines leave two spaces at the top)

extended

156

Also leading and trailing whitespace of text and runs of whitespace collapsed to one newline

The complete response with trimSpaces=extended, produced by curl -s http://localhost:18080/g7jtrime/books
<!DOCTYPE html>
<html>
<body>
<h1>Bookshelf</h1>
<ul><li>Dune by Frank Herbert (1965)</li><li>Neuromancer by William Gibson (1984)</li></ul>
</body>
</html>

The page-level equivalent, trimDirectiveWhitespaces, is a page directive attribute and takes precedence over the container setting for that page or tag file; it is covered with the other directives on the JSP Directives page. Choose the container setting for a whole application and the directive for the pages where whitespace is significant (a pre block or an inline-formatted fragment); extended changes the layout of such text.

genStringAsCharArray changes how the static text is written. Observed on the two-line page shelf.jsp (the text Shelf: ` and `class=): the default writes the text as string literals in the generated servlet and the setting writes it through static char arrays.

Fragment of the generated servlet, default (g7jasper) and with genStringAsCharArray=true (g7jchars)
// default
      out.write("\n");
      out.write("class=");

// genStringAsCharArray=true
      out.write(_jspx_char_array_1);
  ...
  static char[] _jspx_char_array_0 = "Shelf: ".toCharArray();
  static char[] _jspx_char_array_1 = "\nclass=".toCharArray();

The Work Directory, Package Names and the Scratch Directory

Jasper writes generated files below the web application’s work directory, work/Catalina/localhost/<context>/ in Tomcat, under the package org.apache.jsp, with the path of the page encoded into the package: WEB-INF/views/books.jsp becomes org/apache/jsp/WEB_002dINF/views/books_jsp.java, because - is not legal in a Java name. Observed on g7jasper.war after three requests: a .java and a .class file for each of books_jsp, shelf_jsp and stamp_jsp.

scratchdir moves the output elsewhere and generatedJspPackageName renames the package. Observed (g7jpkg.war, scratchdir pointing at an existing directory, generatedJspPackageName=org.example.gen): the request returned class=org.example.gen.shelf_jsp and the directory held org/example/gen/shelf_jsp.java and .class.

A scratch directory that does not exist is an initialisation error, not something Jasper creates (observed on Tomcat 11.0.26)
java.lang.IllegalStateException: The scratchDir you specified: [.../scratch-out] is unusable.

The failure appears as a 500 on the first request (Servlet.init() for servlet [jsp] threw exception), and after the directory was created the next request succeeded without a redeploy.

The default class file level deserves a check on a new server. The how-to says compilerSourceVM and compilerTargetVM default to 17, and the generated class files are Java 17 files (major version 61) even on a Java 21 run time; to use newer language features in scriptlets, set both parameters.

Controlling Which Pages Stay Loaded, and Warming Pages Up

Every page that has been requested is a loaded servlet instance. Two parameters bound that set. Observed with jspIdleTimeout=2 (g7jidle.war): stamp.jsp was requested and, with no further requests, jspDestroy was logged within 25 seconds (the unload happens in the background processor cycle); the next request ran jspInit again. With maxLoadedJsps=1 (g7jmax.war): after stamp.jsp, shelf.jsp and books.jsp had been requested, jspDestroy of stamp.jsp, the least recently used page, was logged within the next 22 seconds; the unload is not instantaneous. Use these when a very large application would otherwise keep every page in memory; they cost a reload for an unloaded page.

The opposite need is to avoid the slow first request. A servlet declaration can name a JSP file and give it a load-on-startup value, which also gives a page a servlet name and a second URL.

Warming a page up and mapping it to a friendly URL with jsp-file (g7jfile.war)
<servlet>
  <servlet-name>edition</servlet-name>
  <jsp-file>/stamp.jsp</jsp-file>
  <load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
  <servlet-name>edition</servlet-name>
  <url-pattern>/edition</url-pattern>
</servlet-mapping>

Observed: jspInit of stamp.jsp was logged during the deployment, before any request, and /g7jfile/edition and /g7jfile/stamp.jsp both returned Bookshelf edition: ONE. The jsp-file element and servlet naming are described with the other web.xml elements on the Servlet Fundamentals page.

The Optimisation Extension Points: Pooling, Tag Plugins and Interpreters

The how-to’s Optimisation section lists three extension points. All three change generated code and were checked by reading the generated source.

Tag Handler Pooling

The default generated servlet creates one TagHandlerPool per tag-and-attributes combination in _jspInit and releases it in _jspDestroy. Observed on shelf.jsp: six references to TagHandlerPool with the default and none with enablePooling=false (g7jnopool.war), where each use is a new org.apache.taglibs.standard.tag.rt.core.IfTag(). The parameter is a compilation option, so it only affects pages compiled after it is set. The behaviour that makes pooling a correctness concern (a handler instance reused across invocations) is the subject of the Classic Custom Tags page.

Tag Plugins

A tag plugin replaces the handler of a tag with code inlined into the page. Plugins are registered in WEB-INF/tagPlugins.xml; Jasper ships plugins for the JSTL core tags in the org.apache.jasper.tagplugins.jstl.core package and a sample registration file inside jasper.jar.

WEB-INF/tagPlugins.xml registering three of the shipped JSTL plugins (g7jplug.war)
<tag-plugins>
  <tag-plugin>
    <tag-class>org.apache.taglibs.standard.tag.rt.core.IfTag</tag-class>
    <plugin-class>org.apache.jasper.tagplugins.jstl.core.If</plugin-class>
  </tag-plugin>
  <tag-plugin>
    <tag-class>org.apache.taglibs.standard.tag.rt.core.OutTag</tag-class>
    <plugin-class>org.apache.jasper.tagplugins.jstl.core.Out</plugin-class>
  </tag-plugin>
  <tag-plugin>
    <tag-class>org.apache.taglibs.standard.tag.rt.core.ForEachTag</tag-class>
    <plugin-class>org.apache.jasper.tagplugins.jstl.core.ForEach</plugin-class>
  </tag-plugin>
</tag-plugins>

Observed with shelf.jsp (a c:set, a c:if and a c:out): the generated servlet of the default application referenced IfTag and OutTag (two matches) and was 248 lines long; with the plugins it had no reference to either, was 207 lines long, and the c:if had become an inline if. The page output was identical.

Generated code for the c:if and c:out with the plugins (g7jplug); c:set, which was not registered, kept its handler
boolean _jspx_temp0=
((java.lang.Boolean) org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate("${not empty shelf}", boolean.class, (jakarta.servlet.jsp.PageContext)_jspx_page_context, null)).booleanValue();
if (_jspx_temp0){
      out.write("Shelf: ");
...
boolean _jspx_temp5 = org.apache.jasper.tagplugins.jstl.core.Out.output(out, _jspx_temp1, _jspx_temp2, _jspx_temp3, _jspx_temp4);

The plugin classes target the Tomcat JSTL implementation class names, org.apache.taglibs.standard.tag.rt.core.*, which is also the package of the Jakarta Tags 3.0 implementation used here (org.glassfish.web:jakarta.servlet.jsp.jstl 3.0.1). Check the registration file in the jasper.jar of the version in use rather than copying one from an older release.

The EL and String Interpreters

Two further extension points are chosen with context parameters named after the interface they replace. org.apache.jasper.compiler.ELInterpreter names a class implementing the EL interpreter and org.apache.jasper.compiler.StringInterpreter one that implements the String interpreter, which coerces attribute strings. The how-to points at two implementations in org.apache.jasper.optimizations, and the Javadoc is blunt about their cost: ELInterpreterTagSetters is described as not specification compliant, an interpreter that optimises a subset of tag attribute setters (expressions that are solely a literal boolean, or a constant string coerced to the setter’s type) and effectively hard-codes the fourth EL resolver; StringInterpreterEnum coerces strings to enums.

Context parameters that switch both implementations on, together with the page-context pool and the tag buffer limit (g7jopt.war)
<context-param>
  <param-name>org.apache.jasper.compiler.ELInterpreter</param-name>
  <param-value>org.apache.jasper.optimizations.ELInterpreterTagSetters</param-value>
</context-param>
<context-param>
  <param-name>org.apache.jasper.compiler.StringInterpreter</param-name>
  <param-value>org.apache.jasper.optimizations.StringInterpreterEnum</param-value>
</context-param>
<context-param>
  <param-name>org.apache.jasper.runtime.JspFactoryImpl.POOL_SIZE</param-name>
  <param-value>-1</param-value>
</context-param>
<servlet>
  <servlet-name>jsp</servlet-name>
  <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
  <init-param><param-name>limitBodyContentBuffer</param-name><param-value>true</param-value></init-param>
  <init-param><param-name>bodyContentTagBufferSize</param-name><param-value>256</param-value></init-param>
  <load-on-startup>3</load-on-startup>
</servlet>

The last two entries are the how-to’s hint for limiting memory: POOL_SIZE=-1 disables PageContext pooling and limitBodyContentBuffer caps tag buffers. They were set and the application ran; their performance effect was not measured.

The effect of the two interpreters was observed with a small tag library for the Bookshelf (bk:label, whose handler has a String text, an int copies and an enum level attribute). With literal expressions such as text="${'Dune'}" the optimised interpreters generate direct setter calls, while the default one calls PageContextImpl.proprietaryEvaluate and the property editor machinery.

The bk:label attributes in lit.jsp, and the setter calls of the default application (g7jplain) against the optimised one (g7jopt)
<bk:label text="${'Dune'}" level="${'WARN'}" copies="${'3'}"/>
// default
_jspx_th_bk_005flabel_005f0.setText((java.lang.String) org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate("${'Dune'}", java.lang.String.class, ...));
_jspx_th_bk_005flabel_005f0.setLevel((org.example.bookshelf.Level) org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate("${'WARN'}", org.example.bookshelf.Level.class, ...));
_jspx_th_bk_005flabel_005f0.setCopies(((java.lang.Integer) org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate("${'3'}", int.class, ...)).intValue());

// ELInterpreterTagSetters and StringInterpreterEnum
_jspx_th_bk_005flabel_005f0.setText("Dune");
_jspx_th_bk_005flabel_005f0.setLevel(org.example.bookshelf.Level.WARN);
_jspx_th_bk_005flabel_005f0.setCopies(3);

Two observations from the same experiment bound the claim. An expression that is not a literal (text="${shelf}") was generated identically by both, so the optimisation is for constants only. And StringInterpreterEnum generated LabelTag$Level.WARN for an enum nested in the tag handler class, which does not compile.

The failure when the enum is a nested class of the handler and StringInterpreterEnum is active (Tomcat 11.0.26)
An error occurred at line: [5] in the jsp file: [/label.jsp]
The nested type org.example.bookshelf.LabelTag$Level cannot be referenced using its binary name

Moving the enum to a top-level class (as in the working example) avoided the failure. Treat the interpreters as opt-in optimisations to be verified against the application’s pages, never as defaults.

Precompilation: Doing the Translation Before the First Request

A page is translated and compiled the first time it is requested, which is the slow first request and, more importantly, the moment a typo in a scriptlet turns into a 500 in front of a customer. Precompilation moves that work to build time. The specification names two different things under the word, and a third is a servlet container feature.

Table 10. The three meanings of "precompile"
Mechanism What happens Where it is defined

The jsp_precompile request parameter

A request with the parameter asks the container to translate and compile that page now and not to run it

Pages specification 11.4.2, implemented by Jasper (see below, and the lifecycle page of this section)

Build-time translation and compilation (JspC)

A tool turns the page sources into servlet classes plus the web.xml declarations that make them look like the original pages. The container then never translates

Pages specification 1.1.9 and Appendix A (non-normative); the tool is container-specific

Load-on-startup of a page

The container translates the page when the application starts, with no tool and no request

Servlet load-on-startup with jsp-file (shown above)

The benefits the specification lists for build-time compilation are the removal of the start-up lag, a smaller footprint (the Java compiler is not needed), and compilation in the context of the web application, so that relative URLs in include directives, tag library references and translation-time actions are resolved. Its Appendix A shows the whole idea in miniature: compile HelloWorld.jsp into a servlet class, declare it in web.xml with servlet-class and a servlet-mapping to /HelloWorld.jsp, and package WEB-INF/web.xml and WEB-INF/classes/... with no JSP source in the WAR. Every tool in this section produces that shape.

The jsp_precompile Request and Reserved Parameter Names

Specification 11.4.1 reserves every request parameter whose name starts with jsp; a page should ignore any parameter that starts with jsp_ and containers and users should not invent names with the prefix. Section 11.4.2 defines jsp_precompile: with no value or the value true or false the request is a precompilation request and must not be delivered to the page (the value true or none is the suggestion to compile, and the container may ignore it), and any other value is illegal and yields a 500. The lifecycle page of this section verified all of that against Tomcat 11.0.26 (see the Precompilation section). The Jasper-specific part is that the parameter name is configurable (jspPrecompilationQueryParameter, observed above), and that the request does not deliver anything to the page: on g7jpq.war ?precomp=true returned 200 with an empty body.

A build can use the request as a smoke test (request every page once with the parameter after deployment, to translate and compile all of them and see compile errors before users do), but it needs a list of the pages and a running server; JspC does the same job without a server.

JspC: the Pipeline

JspC is Jasper without the servlet container. It translates every JSP (and tag file) under a web application directory into Java source, optionally compiles it, and writes the servlet declarations and mappings that bind each generated class to the URL of its page.

flowchart LR src["JSP pages and tag files
in the web application"] --> jspc["JspC
Ant task, command line,
Maven plugin"] jspc --> java["generated Java source
org.apache.jsp.*"] jspc --> map["servlet and servlet-mapping
declarations"] java --> comp["compile
javac, ECJ,
or JspC compile=true"] comp --> cls["WEB-INF/classes
org/apache/jsp/*.class"] map --> wx["WEB-INF/web.xml
merged or included"] cls --> war["WAR"] wx --> war war --> run["container: each page URL
is a plain servlet mapping,
no translation at run time"]
Figure 1. The JspC precompilation pipeline, common to the Ant task, the command line and the Jetty Maven plugin

The ordering matters: JspC reads the tag libraries and TLDs the pages use, so the application’s libraries must already be in WEB-INF/lib and its own classes in WEB-INF/classes when it runs. A page’s relative references are resolved against the web application root (uriroot).

JspC With the Ant jasper Task

The how-to calls Ant the preferred way to compile a web application with JspC, and ships the script, which is also included in the Tomcat deployer download. It imports bin/catalina-tasks.xml, which defines the jasper task, runs the task to write Java source and a generated_web.xml, then compiles the source with the javac task and a class path of the application and Tomcat libraries. The output is then added to web.xml by hand, or by the task.

The build file used on g7jasperpre (the how-to script with addWebXmlMappings and release="21" added; the cleanup target is unchanged)
<project name="Webapp Precompilation" default="all" basedir=".">
  <import file="${tomcat.home}/bin/catalina-tasks.xml"/>
  <target name="jspc">
    <jasper validateXml="false"
            uriroot="${webapp.path}"
            addWebXmlMappings="true"
            webXmlInclude="${webapp.path}/WEB-INF/generated_web.xml"
            outputDir="${webapp.path}/WEB-INF/src" />
  </target>
  <target name="compile">
    <mkdir dir="${webapp.path}/WEB-INF/classes"/>
    <mkdir dir="${webapp.path}/WEB-INF/lib"/>
    <javac destdir="${webapp.path}/WEB-INF/classes" debug="on" failonerror="false"
           srcdir="${webapp.path}/WEB-INF/src" excludes="**/*.smap" release="21">
      <classpath>
        <pathelement location="${webapp.path}/WEB-INF/classes"/>
        <fileset dir="${webapp.path}/WEB-INF/lib"><include name="*.jar"/></fileset>
        <pathelement location="${tomcat.home}/lib"/>
        <fileset dir="${tomcat.home}/lib"><include name="*.jar"/></fileset>
        <fileset dir="${tomcat.home}/bin"><include name="*.jar"/></fileset>
      </classpath>
      <include name="**"/>
      <exclude name="tags/**"/>
    </javac>
  </target>
  <target name="all" depends="jspc,compile"/>
  <target name="cleanup">
    <delete>
      <fileset dir="${webapp.path}/WEB-INF/src"/>
      <fileset dir="${webapp.path}/WEB-INF/classes/org/apache/jsp"/>
    </delete>
  </target>
</project>

The run, on the exploded Bookshelf WAR, with Ant 1.10.15 launched directly from its two jars (the command the how-to gives, ant -Dtomcat.home=... -Dwebapp.path=..., is the same):

Output of the Ant run on the exploded g7jasper.war (abridged)
jspc:
   [jasper] INFO: Generation completed with [0] errors in [132] milliseconds
compile:
    [javac] Compiling 4 source files to .../webapp/WEB-INF/classes
all:
BUILD SUCCESSFUL

Result: WEB-INF/classes/org/apache/jsp/{label,shelf,stamp}_jsp.class and WEB_002dINF/views/books_jsp.class, the Java source under WEB-INF/src, and a web.xml into which the task merged, between two marker comments, a servlet and a servlet-mapping for each page.

The block merged into WEB-INF/web.xml by addWebXmlMappings="true" (abridged to two pages)
<!-- JSPC servlet mappings start -->
    <servlet>
        <servlet-name>org.apache.jsp.stamp_jsp</servlet-name>
        <servlet-class>org.apache.jsp.stamp_jsp</servlet-class>
    </servlet>
    ...
    <servlet-mapping>
        <servlet-name>org.apache.jsp.stamp_jsp</servlet-name>
        <url-pattern>/stamp.jsp</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>org.apache.jsp.WEB_002dINF.views.books_jsp</servlet-name>
        <url-pattern>/WEB-INF/views/books.jsp</url-pattern>
    </servlet-mapping>
<!-- JSPC servlet mappings end -->

Note the second mapping: a page under WEB-INF is mapped to its own path so that the servlet’s getRequestDispatcher("/WEB-INF/views/books.jsp").forward(...) finds it, which is exactly how the Bookshelf servlet reaches its view.

The WAR was then assembled from the output with WEB-INF/src, every .jsp and the JSP of WEB-INF/views removed, so the only thing that could serve the pages was the compiled classes. Observed on g7jasperpre.war: /books, /shelf.jsp, /stamp.jsp and /label.jsp all returned 200 with the same output as before, shelf.jsp?x=1 also returned 200, and the work directory of the application stayed empty (no .java, no .class): the container did not translate or compile anything.

The how-to’s hints about the task are worth reading with the experiment in mind.

  • addWebXmlMappings and webXmlInclude go together. The how-to presents addWebXmlMappings as an option that merges generated_web.xml into web.xml automatically. Observed on Tomcat 11.0.26: addWebXmlMappings="true" without webXmlInclude ends the build with BUILD FAILED ... java.lang.NullPointerException at org.apache.jasper.JspC.mergeIntoWebXml(JspC.java:1270). With both, the merged block was written and no separate generated_web.xml remained in the directory.

  • Locale and verbosity. With a non-English default locale the JspC messages are localised (the first run printed Spanish INFO lines); add -Duser.language=en when the output is parsed.

  • Do not stop at the first error. failOnError="false" with showSuccess="true" lists every page that converted, which helps when a legacy application has many broken pages.

  • Java level. The how-to says to add source and target attributes to the javac task to pick the Java version of the classes, and to set debug="off" for production (the build file above used release="21").

  • Regenerate on every Tomcat upgrade. The hint is explicit: when switching to another Tomcat release, regenerate and recompile the JSPs with the new Tomcat. The generated classes extend the Jasper runtime base class and call its internals; a class generated by one release is not guaranteed to run on another.

The task also takes more attributes than the how-to uses. They are the setters of the org.apache.jasper.JspC class, so the Javadoc lists them. The ones a Bookshelf build needs most often: compile (compile inside JspC, below), outputDir, uriroot, jspFiles (a list of pages instead of the whole tree), package, webXml, webFragmentXml (a complete web-fragment.xml for packaging the pages in a library JAR), failOnError, threadCount, trimSpaces, genStringAsCharArray, poolingEnabled, mappedFile, classDebugInfo, smapSuppressed, smapDumped, compilerSourceVM, compilerTargetVM, useNonstandardTagOptimizations and blockExternal.

JspC From the Command Line, and the compile Option

Tomcat 11.0.26 no longer ships a jspc script in bin, but the class has a main method and prints its options with -help. Observed: running it needs Ant on the class path, because the class extends the Ant Task class (NoClassDefFoundError: org/apache/tools/ant/Task without it).

A JspC run that compiles the pages itself and writes a complete web.xml (the class path is the Tomcat lib directory, tomcat-juli.jar and ant-1.10.15.jar; the output directory must exist)
$ mkdir out
$ java -cp "$CATALINA_HOME/lib/*:$CATALINA_HOME/bin/tomcat-juli.jar:ant-1.10.15.jar" \
    org.apache.jasper.JspC -uriroot webapp -d out -compile -webxml out/web.xml -s -l

Observed: out/ held the .java and .class files of the four pages and out/web.xml; no separate Java compiler step was needed (-compile uses the Eclipse compiler). The options are those printed by -help: -webapp/-uriroot, -d, -p (package), -c (class name of the first page), -mapped, -die, -uribase, -compile, -failFast, -webinc (partial mappings), -webfrg (a web-fragment), -webxml, -addwebxmlmappings, -classpath, -xpoweredBy, -trimSpaces, -javaEncoding, -source, -target and -threadCount. Three findings from the run:

  • The directory of -webxml is not created: pointing it at a directory that did not exist ended with FileNotFoundException: cli/out/web.xml (No such file or directory).

  • -help still prints "default 1.8" for -source and -target; the classes generated were Java 17 (major version 61).

  • JspC does not emit the line-number table for debuggers unless asked. Observed: with compile on and the default for smapSuppressed the class had no SourceDebugExtension attribute, and with smapSuppressed="false" (the Ant attribute; the servlet parameter is suppressSmap) it had two. This is the how-to’s remark that SMAP information is only in the classes "if suppressSmap is false and compile is true".

The Ant form of the same run, for a build that wants the line table (build2.xml, run on the exploded WAR)
<jasper validateXml="false" uriroot="${webapp.path}" outputDir="${out.dir}" compile="true"
        smapSuppressed="false" webXml="${out.dir}/web.xml" failOnError="true"/>

Tag Files and Libraries Under JspC

Tag files under WEB-INF/tags are translated and compiled with the pages. Observed with a badge.tag and a page that used it: the output contained org/apache/jsp/tag/web/badge_tag.class (in the org.apache.jsp.tag.web package, a sub-package of generatedTagFilePackageName), JspC also wrote a stray badge_tag.class next to the generated source (a reason to delete WEB-INF/src before packaging, as the how-to’s cleanup target does), and the WAR with the tag file source and the page source both removed still rendered the tag. The how-to’s javac task excludes tags/**, a pattern that matches only a top-level tags directory of the source tree and not org/apache/jsp/tag/web, so it excluded nothing in this experiment. The tag file features themselves are on the Tag Files and Layouts page, and the specification’s remark that a tag file can also be packaged as a precompiled handler is in section 8.4.4.

Jetty: The JSP Module and the JspC Maven Plugin

Jetty uses Apache Jasper for JSP. This section is from the Jetty 12.1 operations and programming guides, from the contents of the 12.1.14 artifacts, and from one experiment: the JspC plugin was run, and the WAR it produced was deployed on Tomcat. Jetty itself was not started, so the run-time statements are documentation and artifact inspection.

JSP support is a module per EE level, ee8-jsp to ee11-jsp (the ee11-jsp module depends on ee11-servlet, ee11-annotations and ee11-apache-jsp). Jetty identifies the JSP servlet by the attribute id="jsp" in the default descriptor. That descriptor, webdefault-ee11.xml in jetty-ee11-webapp-12.1.14.jar, declares the servlet jsp with the class org.eclipse.jetty.ee11.jsp.JettyJspServlet, sets xpoweredBy to false and, notably, compilerTargetVM and compilerSourceVM to 1.8, loads it with load-on-startup 0, and maps it to *.jsp, *.jspf, *.jspx and *.xsp in lower and upper case. An application changes it by re-declaring a servlet with the same servlet-name and id="jsp" in its web.xml and adding init-param entries, for example keepgenerated or the async-supported element (the JSP servlet does not support asynchronous processing by default). Logging of the org.apache.jasper packages is bridged to Jetty logging. The parameters are Jasper’s, so the table above applies; the Jetty guide lists them with Jetty’s defaults and an important caveat: parameter spellings differ in some places from Tomcat’s (the guide spells scratchDir, mappedFile and genStrAsCharArray), and the instruction is to try all lower case if a value does not take effect.

Augmenting Jetty’s JSP servlet from the application’s web.xml (from the Jetty 12.1 guide; not run here)
<servlet id="jsp">
  <servlet-name>jsp</servlet-name>
  <init-param>
    <param-name>keepgenerated</param-name>
    <param-value>true</param-value>
  </init-param>
</servlet>

The Jetty JspC Maven Plugin

For Jakarta EE 11 the plugin is org.eclipse.jetty.ee11:jetty-ee11-jspc-maven-plugin 12.1.14. It precompiles the pages and works with the maven-war-plugin so that the classes and the merged descriptor go into the WAR. The plugin’s own documentation recommends putting it in a profile that is switched on only for a production build.

The prod profile that precompiles the Bookshelf (pom.xml of g7jasperjetty; run with mvn -Dprod clean package)
<profiles>
  <profile>
    <id>prod</id>
    <activation>
      <property><name>prod</name></property>
    </activation>
    <build>
      <plugins>
        <plugin>
          <groupId>org.eclipse.jetty.ee11</groupId>
          <artifactId>jetty-ee11-jspc-maven-plugin</artifactId>
          <version>12.1.14</version>
          <executions>
            <execution>
              <id>jspc</id>
              <goals>
                <goal>jspc</goal>
              </goals>
            </execution>
          </executions>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-war-plugin</artifactId>
          <configuration>
            <webXml>${project.basedir}/target/web.xml</webXml>
          </configuration>
        </plugin>
      </plugins>
    </build>
  </profile>
</profiles>

Observed: the plugin ran after the Java compilation and before the test resources step, compiled stamp.jsp,shelf.jsp,WEB-INF/views/books.jsp,label.jsp from the default includes (**/*.jsp, **/*.jspx) and reported Generation completed with [0] errors; the build printed three The XML schema [...] could not be found warnings that did not matter. The WAR it produced contained WEB-INF/classes/org/apache/jsp/*.class for all four pages, a WEB-INF/web.xml (the merged file target/web.xml) with servlet and servlet-mapping entries for each page, and also one that Tomcat does not need: <context-param>org.eclipse.jetty.jsp.precompiled = true</context-param>, which tells Jetty’s JSP servlet to expect precompiled classes. The JSP sources stayed in the WAR, because the war plugin copies the webapp directory. The WAR was then deployed on Tomcat 11.0.26 (g7jasperjetty): /books, /shelf.jsp, /stamp.jsp and /label.jsp all returned 200 and the work directory stayed empty, so the classes the Jetty plugin generates run on Tomcat’s Jasper too. That portability is a convenience of this experiment; the classes extend the Jasper base class of the plugin’s Jasper version, and the how-to’s rule of regenerating on a release change still applies.

Table 11. The configurable parameters of the plugin and their defaults (Jetty 12.1 programming guide)
Parameter Default Meaning

webAppSourceDirectory

${project.basedir}/src/main/webapp

Root of the JSPs and tags

webXml

${project.basedir}/src/main/webapp/WEB-INF/web.xml

The web.xml the generated declarations are merged into

webXmlFragment

${project.basedir}/target/webfrag.xml

Where the generated declarations are written; merged output goes to the same directory

mergeFragment

true

Whether to merge the fragment into the source web.xml

insertionMarker

none

A marker in the source web.xml where the fragment is inserted (not preserved); if blank it is inserted before </web-app>

includes, excludes

**/*.jsp, **/*.jspx, **/.svn/**

Which files are processed

classesDirectory, generatedClasses

${project.build.outputDirectory}

Where the application classes are read and the generated classes written

useProvidedScope

false

Put provided-scope dependencies on the compile class path

keepSources

false

Keep the generated .java files

scanAllDirectories, scanManifest

true, true

Also scan class path directories and JAR manifests for TLDs

sourceVersion, targetVersion

depends on the plugin version

Java level of the pages and classes

tldJarNamePatterns

JARs named like taglibs or jstl (two patterns)

Patterns, separated by a vertical bar, of container-path JARs that contain TLDs

jspc

the JspC instance

All setters of org.apache.jasper.JspC are available through it

For a WAR that overlays another WAR, the plugin’s guide separates the work into an exploded execution of the war plugin in generate-resources (to unpack the overlay) and a war execution in package (to pack the result), and points the plugin’s webXml at the unpacked descriptor, so that the plugin can see the overlaid resources.

GlassFish: jsp-config, --precompilejsp and jspc

Eclipse GlassFish runs JSP on Eclipse WaSP (eclipse-ee4j/wasp, an implementation of Jakarta Pages; no separate Maven coordinate is published for it, so only the repository is cited). This section is from the GlassFish deployment and development guides published as latest on glassfish.org and was not run here: GlassFish was not installed in the scratch environment.

An application configures the engine in WEB-INF/glassfish-web.xml with a jsp-config element that holds property subelements, so the settings stay out of the portable web.xml.

Setting JSP engine properties for one application in glassfish-web.xml (from the GlassFish deployment guide; not run here)
<glassfish-web-app>
  <jsp-config>
    <property name="development" value="false"/>
    <property name="genStrAsCharArray" value="true"/>
    <property name="trimSpaces" value="true"/>
    <property name="keepgenerated" value="false"/>
  </jsp-config>
</glassfish-web-app>

The property names are close to Jasper’s, with GlassFish-specific additions and different defaults. The ones that matter here, from the guide’s table: development (default true), checkInterval (0), modificationTestInterval (default 0 there, so a page is checked on every access in development mode), reload-interval (0; -1 disables checks and recompilation), mappedfile (true), trimSpaces (false, and only true or false), suppressSmap, dumpSmap, classdebuginfo, keepgenerated (true with JDK 5 and before and for jspc, otherwise false), genStrAsCharArray (false) and genStrAsByteArray (true: text as bytes encoded with the page encoding, if the page is not buffered), enablePooling (true), enableTldValidation, scratchdir, usePrecompiled (false; when true an accessed page is not compiled and its precompiled servlet class, assumed to be in WEB-INF/lib or WEB-INF/classes, is used), initialCapacity, httpMethods, ignoreJspFragmentErrors, defaultBufferNone, xpoweredBy (default true in GlassFish, against false in Tomcat), saveBytecode, javaEncoding, compilerSourceVM, compilerTargetVM, fork, compiler and classpath. The guide’s performance recommendation is the same list as above, expressed as GlassFish property values: development false (or true with a large modificationTestInterval), mappedfile false, trimSpaces true, suppressSmap true, classdebuginfo false, and fork false on Solaris.

GlassFish has three ways to compile JSP files: automatically at run time, with the --precompilejsp option of asadmin deploy, and with the jspc command-line tool in glassfish/bin (glassfish/bin/jspc -help lists its options). A web application that was precompiled with jspc is used with usePrecompiled set to true.

A Known Issue: Very Large Pages

The how-to lists one known issue: a JVM defect (bug 6294277, reported against Tomcat as bug 39089) can cause java.lang.InternalError: name is too long to represent when compiling very large JSPs. The workarounds are to reduce the size of the page, which is also the right fix for readability (split it into fragments with includes or tag files), or to disable SMAP generation with suppressSmap set to true. The line table is exactly what the debugger uses, so the second workaround trades debuggability for compilability.

The Bookshelf in Production: Putting It Together

A reasonable production arrangement for the Bookshelf, as a checklist built from the sections above.

  1. Precompile in the build. Use JspC (the Ant task, -compile from the command line, or the Jetty plugin in a prod profile) and package the classes with the merged web.xml. Do not ship the JSP sources unless an operator needs them, and delete WEB-INF/src before packaging. Then the first request cost, and a class of translation errors, are gone.

  2. Check that nothing is translated at run time. After deployment, request the pages and confirm the work directory has no .java or .class files for them (observed on g7jasperpre).

  3. Keep production settings for what is still translated at run time. Pages that cannot be precompiled, such as pages covered by a jsp-property-group or generated at run time, use the how-to’s four settings (development false, and trimSpaces, genStringAsCharArray, modificationTestInterval as needed), a checkInterval if they must change in place, and jspIdleTimeout or maxLoadedJsps if there are very many.

  4. Put the settings in the right file. A portable web.xml for the application and, when it must be Tomcat specific, tomcat-web.xml; a global change in conf/web.xml only for a deliberate policy across every application.

  5. Rebuild on every container upgrade. Precompiled classes are bound to the Jasper runtime they were generated against; move from Tomcat 10.1 to 11 (the Jakarta namespace has been jakarta.* since Tomcat 10) by regenerating, never by copying WEB-INF/classes/org/apache/jsp.

  6. Keep a development profile with the defaults. Developers want modification checks, the generated source and the line table; a Maven or Gradle profile that precompiles only with -Dprod (as the Jetty guide recommends) keeps both.

Pitfalls and Error Names

Table 12. Common problems with Jasper configuration and precompilation, with the cause and the fix (rows marked "observed" were reproduced on Tomcat 11.0.26)
Symptom Cause Fix

An edited page never changes (observed)

development is false, or the check interval has not elapsed (modificationTestInterval, default 4 seconds)

Wait for the interval, set development true in development, or set checkInterval for background compilation

A setting has no effect and there is no error (observed)

The name is wrong or in the wrong case (keepGenerated against keepgenerated)

Copy the name from the table; names are case-sensitive and a typo is ignored

IllegalStateException: The scratchDir you specified: [...] is unusable on the first request (observed)

scratchdir points at a directory that does not exist or cannot be written

Create the directory with the right permissions

BUILD FAILED with a NullPointerException in JspC.mergeIntoWebXml (observed)

The Ant task has addWebXmlMappings="true" without webXmlInclude

Set both attributes

FileNotFoundException for -webxml on the JspC command line (observed)

The output directory was not created

Create it first

NoClassDefFoundError: org/apache/tools/ant/Task running JspC from Java (observed)

JspC extends the Ant Task and Ant is not on the class path

Add ant.jar, or run it from Ant or Maven

Precompiled pages still compiled at run time (or missing mappings)

The generated servlet and servlet-mapping entries were not merged into the deployed web.xml

Merge them (addWebXmlMappings and webXmlInclude, or the Maven plugin with <webXml> pointing at the merged file)

No line numbers in an exception or debugger for a precompiled page (observed)

JspC leaves smapSuppressed on by default

Set smapSuppressed="false" with compile="true"

The nested type ...$Level cannot be referenced using its binary name (observed)

StringInterpreterEnum active and a tag attribute whose type is an enum nested in the handler class

Move the enum to a top-level class, or do not use the interpreter

Tag handler state leaks between requests, or NullPointerException with a custom base class

Pooling and the extends attribute (poolTagsWithExtends)

See the classic tags page; set enablePooling false to confirm, never poolTagsWithExtends true unless the base class calls _jspInit()

jsp_precompile=foo returns 500 (observed on the lifecycle page)

Only none, true and false are legal values

Use true

Generated classes from the old container fail after an upgrade

Precompiled classes depend on the Jasper runtime and on the javax or jakarta namespace of the release that generated them

Regenerate with the new release’s JspC and recompile

java.lang.InternalError: name is too long to represent

A very large page and JVM bug 6294277

Split the page, or set suppressSmap true

Jetty ignores a Tomcat-style parameter spelling

Jetty’s guide uses different spellings in places

Try all lower case, as the Jetty guide suggests, and test the effect

What Changed Recently

Table 13. Changes that matter to Jasper configuration and precompilation, each with the release or evidence
Change Effect Evidence

Jakarta namespace

The generated servlets import jakarta.servlet.* and jakarta.servlet.jsp.*, so precompiled classes from Tomcat 9 (javax) must be regenerated for Tomcat 10 and later

Generated source of shelf_jsp.java; the how-to’s regenerate hint

Tomcat 11 Jasper implements Jakarta Pages 4.0

The engine and the expression factory follow Pages 4.0 and EL 6.0; compilerSourceVM and compilerTargetVM default to 17 and a lower setting is raised to 17

How-to 11.0.26 introduction; observed class version 61 with a value of 8

No jspc launcher script in Tomcat 11

JspC is run through Ant (the how-to’s script, with catalina-tasks.xml), the class’s main, or a build plugin

bin of the 11.0.26 distribution has catalina-tasks.xml and no jspc script

The Eclipse compiler is bundled

No JDK is needed on a production server; compile of JspC and the run-time compiler both use it

ecj-4.40.jar in lib of 11.0.26

JspC per Jakarta EE level in Jetty

A plugin per release (jetty-ee11-jspc-maven-plugin for EE 11; groupId org.eclipse.jetty.ee11), and JSP modules ee8-jsp to ee11-jsp

Jetty 12.1 guides; Maven Central

Jetty plugin writes a Jetty-only context parameter

org.eclipse.jetty.jsp.precompiled=true is added to the merged web.xml

Observed in the merged descriptor

Tomcat 9 and 10.1 still exist

Tomcat 11 is the version of this page; 10.1.60 and 9.0.x ("end of support no earlier than 31 March 2027") are in service, and each has its own Jasper how-to

Tomcat which-version page, verified 2026-10-08

Where the Books Are Outdated

  • Scripts and task names. Older material commonly describes a jspc launcher script and earlier Ant task names. Tomcat 11.0.26 ships no jspc script; the task is named jasper and imported through catalina-tasks.xml, and the how-to calls Ant the preferred route.

  • A JDK on the server. Older descriptions often assume the container compiles with the JDK’s javac and therefore needs a JDK at run time; Jasper 2 compiles with the bundled Eclipse compiler, and precompilation removes the compiler from the run-time path altogether (the specification’s own list of the benefits).

  • Hand-merging web.xml. The older workflow is to paste the generated mappings into web.xml. The Ant task’s addWebXmlMappings and the Maven plugin’s merge do it; the one trap is the webXmlInclude pairing noted above.

  • Package names. Examples from the javax.servlet era refer to the attribute javax.servlet.context.tempdir. The attribute is jakarta.servlet.context.tempdir now (the Jetty guide’s scratchDir description still says javax).

  • jsp_precompile and reserved names. This part of the specification is unchanged in Pages 4.0 (11.4), so old material on the parameter is still right; only the class names and defaults around it have changed.

  • Defaults. Check any default quoted in older material against the tables above: in Tomcat 11.0.26 the source and target levels are 17, fork is false in the stock conf/web.xml and xpoweredBy is false. GlassFish defaults xpoweredBy to true.

Two inconsistencies were found in the official documentation while preparing the page: the how-to’s addWebXmlMappings paragraph leaves out that webXmlInclude is required (a NullPointerException otherwise), and the Jetty guide names the servlet class org.eclipse.jetty.jsp.JettyJspServlet while the 12.1.14 default descriptor uses org.eclipse.jetty.ee11.jsp.JettyJspServlet; it also says javax.servlet.context.tempdir and names a genStrAsCharArray parameter that is GlassFish’s spelling.

Related Pages

The lifecycle that Jasper implements is on the JSP Lifecycle and Translation page. Container-wide page behaviour that does not need Jasper parameters (property groups, encodings, scripting) is on JSP Configuration and Property Groups. Reading the generated sources, the line table and the debugger use of suppressSmap, and testing pages, are the subject of the Testing and Debugging page of this section, and the deployment and WAR layout questions that surround precompilation are on Servlet Fundamentals and Getting Started. The error pages that show a compile failure to a user are on the JSP Error Handling page.

References