JSP Lifecycle and Translation
|
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), together with Jakarta Servlet 6.1, Jakarta Expression
Language 6.0 and Jakarta Standard Tag Library 3.0, as implemented by Apache Tomcat 11.0.26 (the Jasper engine) on Java 21.
Everything was verified on 2026-10-08: the facts against the Pages 4.0 specification, the HttpJspPage and JspPage
Javadoc and the Tomcat 11 Jasper documentation, and every example by building it into a Maven WAR (g3lifecycle.war) and
running it on Tomcat 11.0.26 with curl; the outputs, generated sources and log lines shown are the ones that run
produced. A JSP page is not interpreted. The container translates it into the source of a Java servlet class, compiles that
class, loads it, creates one instance and calls it from many threads, and this page follows that path step by step: the two
phases the specification defines, the three methods of the page contract, the generated servlet read straight from the
work/ directory, the threading model that follows from a single instance, how and when the class is rebuilt, how errors
are mapped back to JSP lines, how the response is buffered, and how to translate pages ahead of time. The running scenario
is the Bookshelf, a small catalogue of books, here reduced to the pages that make the lifecycle visible.
A JSP Page Is a Servlet in Waiting: Two Phases
The specification describes the life of a page as two phases. In the translation phase the container validates the page
(and the tag files and tag libraries it uses) and determines the JSP page implementation class that corresponds to it.
In the execution phase the container manages one or more instances of that class in response to requests and other
events. The specification allows translation at any moment between the deployment of the page and the processing of the first
client request, and deliberately leaves the choice open as a quality-of-service decision of the container. Tomcat translates lazily, on the first request, unless the page is declared
with load-on-startup (shown below), and rebuilds the class when the source changes.
| Phase | What the container does | Typical failure |
|---|---|---|
Translation |
Parses the page, processes directives ( |
A syntax error in the JSP, an unknown tag, an invalid directive attribute (observed: |
Compilation |
Compiles that source to a |
A Java error inside a scriptlet or declaration (observed: |
Class loading and initialisation |
Loads the class in the web application’s class loader, creates one instance, calls |
An exception in |
Execution (request time) |
Calls |
Any run-time exception ( |
Destruction |
Calls |
Resources opened in |
The same path as a diagram. The upper half runs once per page (and again whenever the source changes); the lower half runs once for every request.
(source in the WAR)"] --> chk{"Is a compiled class
available and up to date?"} chk -->|"no: first request, or source changed"| tr["Translation
parse, validate, generate
lifecycle_jsp.java"] tr --> cp["Compilation
lifecycle_jsp.class
with an SMAP line table"] cp --> old{"An older instance
of the page exists?"} old -->|yes| dst["old instance: destroy()
runs jspDestroy"] old -->|no| ld dst --> ld["Load the class, create ONE instance
init(config) runs jspInit, then _jspInit"] chk -->|yes| svc ld --> svc["service() runs _jspService(request, response)
on the request thread, many threads at once"] svc --> buf["JspWriter buffer
8 KB by default"] buf --> resp["Response committed: headers, then body"] stop["Web application stops"] -.-> dst2["destroy() runs jspDestroy"]
The whole of this machinery is the container’s job: a JSP author writes the page and, at most, jspInit and jspDestroy.
The class that results is an ordinary servlet, which is why everything on the Servlet Fundamentals
page (one instance, many threads, filters, error pages, dispatchers, sessions) applies to a page unchanged.
The Contract: JspPage, HttpJspPage and the Three Methods
Chapter 11 of the specification defines the contract between a JSP container and the class that implements a page. It
corresponds to the Servlet interface, extended by two interfaces in jakarta.servlet.jsp: JspPage (generic) and
HttpJspPage (the HTTP form every page uses). Of the three methods of the contract, only two can be written by the page
author.
| Method | Who writes it | Contract and observed behaviour |
|---|---|---|
|
Optional, in a declaration ( |
Called once when the page is initialised; every method of the servlet, including |
|
Optional, in a declaration |
Called before the page is destroyed, at a time when no request is being serviced. |
|
Never: generated by the container |
The body of the page; invoked on every request. |
Three details of that table matter in practice:
-
init,destroyandserviceare not available for redefinition. TheJspPageJavadoc tells a page to redefineinit()anddestroy()fromServlet, but Tomcat’sHttpJspBasedeclaresinit,destroyandserviceasfinal(verified withjavap), so onlyjspInitandjspDestroycan be overridden in a declaration. This is an inconsistency in the Javadoc, recorded in the section’s errata. -
The page is its own servlet.
getServletConfig().getServletName()returnsjspfor a page reached by its path (it is served by Tomcat’s defaultjspservlet), and the name given inweb.xmlwhen the page is declared as a servlet with<jsp-file>(shown below).infoin thepagedirective becomes the return value ofgetServletInfo(). -
Names of the form
jsp_*andjspx_*, with or without a leading underscore and in any combination of upper and lower case, are reserved by the specification (section 9.1.2). A declaration must not define such a name: the generated class already uses them for its own fields and methods.
By default the generated class extends a container-specific base class (org.apache.jasper.runtime.HttpJspBase in Tomcat),
which implements HttpJspPage. The extends attribute of the page directive replaces that superclass; the author then
guarantees that the replacement supports the contract. Almost nobody needs it, and it is covered with the other attributes
on the Directives page of this section.
The Bookshelf Used on This Page
All the examples below are files of one WAR, deployed as g3lifecycle.war (context path /g3lifecycle) on Tomcat 11.0.26,
built with Maven from the same pom.xml shape as the Getting Started page of this section (Jakarta APIs with scope
provided, plus the JSTL API and implementation). The page directory is the web root; WEB-INF/web.xml declares only
one servlet, shown later.
| File | Request | Concept |
|---|---|---|
|
|
declarations, |
|
|
translation at deployment, servlet name and init parameters |
|
|
recompilation, static include versus |
|
|
translation and compilation errors versus request-time errors, line mapping |
|
same names |
buffer size, overflow, commit, late headers, redirect |
A First Page and Its Lifecycle
The first page prints a list of titles and, to make the lifecycle visible, defines jspInit and jspDestroy in a
declaration, counts its own hits in an instance field and reports the class, the superclass, the identity of the instance
and the thread that ran the request.
lifecycle.jsp)<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.List" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%!
private int hits;
private long loadedAt;
public void jspInit() {
loadedAt = System.currentTimeMillis();
getServletContext().log("lifecycle.jsp: jspInit, instance " + System.identityHashCode(this));
}
public void jspDestroy() {
getServletContext().log("lifecycle.jsp: jspDestroy after " + hits + " hits");
}
%>
<%
synchronized (this) {
hits++;
}
request.setAttribute("books", List.of("Dune", "Neuromancer"));
%>
<!DOCTYPE html>
<html>
<body>
<h1>Bookshelf</h1>
<p>class=<%= getClass().getName() %></p>
<p>super=<%= getClass().getSuperclass().getName() %></p>
<p>instance=<%= System.identityHashCode(this) %> hits=<%= hits %> thread=<%= Thread.currentThread().getName() %></p>
<p>servlet name=<%= getServletConfig().getServletName() %></p>
<ul>
<c:forEach var="t" items="${books}">
<li>${t}</li>
</c:forEach>
</ul>
</body>
</html>
Requesting it twice returns the same instance on two different request threads, with the counter advancing (blank lines
at the top of the real response come from the directives and declaration lines, which are template text; the
trimDirectiveWhitespaces attribute is covered on the Directives page):
curl -s http://localhost:18080/g3lifecycle/lifecycle.jsp
curl -s http://localhost:18080/g3lifecycle/lifecycle.jsp | grep instance=
<p>class=org.apache.jsp.lifecycle_jsp</p>
<p>super=org.apache.jasper.runtime.HttpJspBase</p>
<p>instance=1937448563 hits=1 thread=http-nio-18080-exec-95</p>
<p>servlet name=jsp</p>
<ul>
<li>Dune</li>
<li>Neuromancer</li>
</ul>
...
<p>instance=1937448563 hits=2 thread=http-nio-18080-exec-97</p>
The container’s log (ServletContext.log goes to localhost.<date>.log in Tomcat’s logs/ directory) shows the other
two events. jspInit ran once, during the first request, not at deployment; jspDestroy ran when the web application was
removed (three requests had been served by the instance that was current at that time):
13:02:40.057 INFO [http-nio-18080-exec-95] ...ApplicationContext.log lifecycle.jsp: jspInit, instance 1937448563
13:06:22.382 INFO [Catalina-utility-2] ...ApplicationContext.log lifecycle.jsp: jspDestroy after 3 hits
The Generated Servlet, Read from the work Directory
Jasper keeps the Java source it generates (keepgenerated defaults to true) in the web application’s scratch directory,
$CATALINA_BASE/work/Catalina/<host>/<context>/. The source is the best documentation of what a page really does. For
the page above the files are:
work/Catalina/localhost/g3lifecycle/org/apache/jsp/lifecycle_jsp.java
work/Catalina/localhost/g3lifecycle/org/apache/jsp/lifecycle_jsp.class
work/Catalina/localhost/g3lifecycle/org/apache/jsp/WEB_002dINF/views/warmup_jsp.java
The class lives in the package org.apache.jsp, followed by the directory of the page; its name is the file name with
_jsp appended. Characters that are illegal in a Java identifier are escaped (WEB-INF becomes WEB_002dINF, where 002d is the
hexadecimal code of -), and the tag handler pool fields carry names such as _005fjspx_005ftagPool_005fc_005fforEach
for the same reason. The file name, the package and these identifiers are implementation details of Tomcat and may differ
in Jetty or GlassFish; the specification says so itself (section 9.1.3: an implementation need not perform the transformations
literally), so a page must never depend on them.
The structure of a generated class follows specification Table JSP.9-1: optional imports, the superclass, the declaration
section, then _jspService with the implicit-object section and the main section. This is the real source for
lifecycle.jsp, with the repeated plumbing shortened where marked:
lifecycle_jsp.java)package org.apache.jsp;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import jakarta.servlet.jsp.*;
import java.util.List;
public final class lifecycle_jsp extends org.apache.jasper.runtime.HttpJspBase
implements org.apache.jasper.runtime.JspSourceDependent,
org.apache.jasper.runtime.JspSourceImports,
org.apache.jasper.runtime.JspSourceDirectives {
private int hits; // the <%! %> declaration, copied verbatim
private long loadedAt;
public void jspInit() { ... } // the author's methods, also copied verbatim
public void jspDestroy() { ... }
private static final jakarta.servlet.jsp.JspFactory _jspxFactory =
jakarta.servlet.jsp.JspFactory.getDefaultFactory();
private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants;
static {
_jspx_dependants = new java.util.LinkedHashMap<java.lang.String,java.lang.Long>(2);
_jspx_dependants.put("/WEB-INF/lib/jakarta.servlet.jsp.jstl-3.0.1.jar", Long.valueOf(1664453510000L));
...
}
public void _jspInit() { // generated: obtain the pool of tag handlers
_005fjspx_005ftagPool_005fc_005fforEach_0026_005fvar_005fitems =
org.apache.jasper.runtime.TagHandlerPool.getTagHandlerPool(getServletConfig());
}
public void _jspDestroy() {
_005fjspx_005ftagPool_005fc_005fforEach_0026_005fvar_005fitems.release();
}
_jspService public void _jspService(final jakarta.servlet.http.HttpServletRequest request,
final jakarta.servlet.http.HttpServletResponse response)
throws java.io.IOException, jakarta.servlet.ServletException {
if (!jakarta.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
final java.lang.String _jspx_method = request.getMethod();
if ("OPTIONS".equals(_jspx_method)) { ... return; }
if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method)) {
response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED,
"JSPs only permit GET, POST or HEAD. Jasper also permits OPTIONS");
return;
}
}
final jakarta.servlet.jsp.PageContext pageContext; // implicit objects: LOCAL variables
jakarta.servlet.http.HttpSession session = null;
final jakarta.servlet.ServletContext application;
final jakarta.servlet.ServletConfig config;
jakarta.servlet.jsp.JspWriter out = null;
final java.lang.Object page = this;
...
try {
response.setContentType("text/html; charset=UTF-8");
pageContext = _jspxFactory.getPageContext(this, request, response, null, true, 8192, true);
application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();
out.write('\n'); // template text
synchronized (this) { hits++; } // the scriptlet, verbatim
request.setAttribute("books", List.of("Dune", "Neuromancer"));
out.write("<p>class=");
out.print( getClass().getName() ); // <%= ... %>
out.write("</p>\n");
...
if (_jspx_meth_c_005fforEach_005f0(_jspx_page_context)) // <c:forEach> becomes a method
return;
out.write("</html>\n");
} catch (java.lang.Throwable t) {
if (!(t instanceof jakarta.servlet.jsp.SkipPageException)){
out = _jspx_out;
if (out != null && out.getBufferSize() != 0)
try {
if (response.isCommitted()) { out.flush(); } else { out.clearBuffer(); }
} catch (java.io.IOException e) {}
if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
else throw new ServletException(t);
}
} finally {
_jspxFactory.releasePageContext(_jspx_page_context);
}
}
Reading it gives the rules that the other pages of this section rely on.
| Element in the page | Generated code | Consequence |
|---|---|---|
Template text (HTML, whitespace) |
|
Blank lines after directives and scriptlets reach the browser |
|
Copied into the class body, before |
Fields are instance fields shared by all requests; methods are instance methods; the implicit objects are not visible here because they are locals of |
|
Copied verbatim into the body of |
Local variables are per request and therefore thread safe; one scriptlet may open a block that a later scriptlet closes |
|
|
The expression is converted to a string; no escaping happens |
|
|
Evaluated at request time against the page, request, session and application scopes |
|
A private method |
Tag handlers are pooled and reused, so a handler must not keep state between uses unless it resets it |
|
|
Imports apply to the whole translation unit |
|
No code of its own: the file’s content is merged into the translation unit before translation |
Changing the included file changes the including page’s class (see recompilation) |
|
|
A request-time call to another resource; the included page has its own class and lifecycle |
Standard header of |
Method guard that accepts GET, POST, HEAD and OPTIONS only (outside error dispatches) |
A |
Final |
|
Where an |
The translation of a static include and a dynamic include is visible side by side in composed.jsp, which has both:
composed.jsp)<%@ page contentType="text/html; charset=UTF-8" %>
<html>
<body>
<h1>Bookshelf</h1>
<%@ include file="/WEB-INF/fragments/footer.jspf" %>
<jsp:include page="/hello.jsp"/>
</body>
</html>
Its generated source writes the footer as plain template text (out.write("<footer>Bookshelf, footer version one</footer>\n"))
and calls JspRuntimeLibrary.include(...) for hello.jsp; the footer appears in _jspx_dependants with its
last-modified time, which is how Jasper knows the page depends on it.
One Instance, Many Threads
The generated class is a servlet and the container creates a single instance of it (specification section 2.2 of the
Servlet specification: one instance per declaration unless the application is distributable), so every request to
lifecycle.jsp runs _jspService on that same object, on a different thread. The consequences follow directly from the
translation table above:
-
Variables declared in a scriptlet are locals of
_jspService: each thread has its own copy. They are safe. -
Fields declared in a
<%! %>declaration are fields of the shared instance. They are shared mutable state, as in any servlet, and need the same care (synchronized, atomic types, or no mutation afterjspInit). -
The implicit objects
request,response,out,sessionandpageContextare locals as well;applicationandconfigare locals initialised from the shared servlet context and configuration. -
The page-scope attributes belong to a
PageContextcreated for the request and released at its end, not to the instance.
The example above increments hits inside synchronized (this). Forty parallel requests against it (twenty at a time,
with xargs -P 20) leave the counter exactly where it should be, on one instance:
seq 1 40 | xargs -P 20 -I{} curl -s -o /dev/null http://localhost:18080/g3lifecycle/lifecycle.jsp
curl -s http://localhost:18080/g3lifecycle/lifecycle.jsp | grep instance=
<p>instance=1937448563 hits=43 thread=http-nio-18080-exec-99</p>
(hits=43 is the two earlier requests plus the forty parallel ones plus this last one.)
The old escape hatch for a page that was not thread safe, <%@ page isThreadSafe="false" %>, made the container
serialise requests with the SingleThreadModel interface. That interface was deprecated for years and was removed in
Servlet 6.0, and the attribute was deprecated in Pages 3.1 and removed in Pages 4.0. On Tomcat 11 it is a translation
error:
org.apache.jasper.JasperException: /threadsafe.jsp (line: [1], column: [2]) [Page directive] has invalid attribute: [isThreadSafe]
The replacement is design, not configuration: keep state out of instance fields, or protect it.
When a Page Is Translated: First Request, Startup and Servlet Declarations
By default Tomcat translates a page on the first request that reaches it (the first hit pays for translation and
compilation; the instance created then serves all the others). A page can be moved to deployment time by declaring it as a
servlet with <jsp-file> and <load-on-startup>, which also lets web.xml give it a name, initialisation parameters
and its own URL mapping.
WEB-INF/web.xml)<servlet>
<servlet-name>bookshelf-warmup</servlet-name>
<jsp-file>/WEB-INF/views/warmup.jsp</jsp-file>
<init-param>
<param-name>greeting</param-name>
<param-value>Hello from web.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>bookshelf-warmup</servlet-name>
<url-pattern>/warmup</url-pattern>
</servlet-mapping>
<%@ page contentType="text/plain; charset=UTF-8" %>
name=<%= getServletConfig().getServletName() %> greeting=<%= getServletConfig().getInitParameter("greeting") %>
curl -s http://localhost:18080/g3lifecycle/warmup
name=bookshelf-warmup greeting=Hello from web.xml
The generated file for warmup.jsp carries the header Generated at: 2026-10-08 11:02:36 UTC, the moment the application
was deployed, while lifecycle_jsp.java, which was only requested later, carries 11:02:40: translation happened at
deployment for the first and at request time for the second. Pages under WEB-INF (as here) cannot be requested directly
by a client; only the mapping /warmup reaches this one, which is the right place for views. Two other things to know
about the container’s page management:
-
A page that is not used does not have to stay loaded: Tomcat’s
jspIdleTimeout(seconds, default -1, never) unloads idle pages andmaxLoadedJsps(default -1, unlimited) keeps only the most recently used ones; unloading destroys the instance, sojspDestroyruns, and a later request creates a new instance and callsjspInitagain. -
Which files are treated as pages (the
*.jspand*.jspxmappings of Tomcat’s defaultjspservlet, andjsp-property-grouppatterns) belongs to the Configuration and Property Groups page of this section.
Recompilation: What Changes a Class and When Tomcat Notices
In development mode (the Jasper parameter development, default true) Tomcat checks a page for changes when it is
requested, but not more often than modificationTestInterval seconds (default 4); a value of 0 checks on every access.
When the source, or a file the page depends on, is newer than the compiled class, the page is translated and compiled
again, the old instance is destroyed (jspDestroy) and a new instance of the new class is created (jspInit). The
experiment below changes hello.jsp on disk, five seconds after the previous request:
<%@ page contentType="text/plain; charset=UTF-8" %>
Hello version one
curl -s http://localhost:18080/g3lifecycle/hello.jsp
# edit hello.jsp in the exploded web application: "version one" becomes "version two"
curl -s http://localhost:18080/g3lifecycle/hello.jsp
Hello version one
Hello version two
The generated hello_jsp.java was rewritten (out.write("Hello version two\n")) and its Generated at header moved. Editing
lifecycle.jsp the same way produced the log pair that shows what recompilation does to a page’s state: the old instance
reported jspDestroy after 43 hits, the new instance reported jspInit, instance 157172844, and the counter on the page
started again at hits=1. A page that keeps data in an instance field therefore loses it whenever the page is edited, a
second reason (after concurrency) to keep state out of declarations.
Dependencies are tracked too. composed.jsp includes footer.jspf with the static include directive, and the footer
is listed in _jspx_dependants. Editing only the footer (after the interval) made the next request to composed.jsp
show footer version two: the including page was translated again, because a static include is part of its
translation unit. Editing only hello.jsp, which composed.jsp includes with jsp:include, did not recompile
composed.jsp at all; hello.jsp was rebuilt on its own the next time it was served.
| What changes | What is rebuilt |
|---|---|
The page itself |
The page’s class (and a new instance) |
A file brought in with |
The including page, never the fragment on its own |
A page reached with |
Only that page, when it is next requested |
A tag library descriptor or a tag file the page uses |
Jasper lists such dependencies in |
A Java class under |
Not tracked by the page check; classes are reloaded only if the container reloads the whole web application |
Production changes the defaults. The Jasper HowTo recommends development=false (no checks on access, so a page is never
retranslated unless the application is redeployed), genStringAsCharArray=true, a high modificationTestInterval if
development mode cannot be switched off, and trimSpaces=single or extended; the Jasper checkInterval (seconds,
default 0) enables background recompilation, when development is false, so that the previous class keeps serving
requests until the new one has compiled. The full parameter list, JspC and the Maven and Ant integrations are the topic
of the Jasper Configuration & Precompilation page of this section.
| Parameter | Default | Effect on translation, compilation and recompilation |
|---|---|---|
|
|
If true, pages are checked for modification on access, at most every |
|
|
Seconds between modification checks in development mode; 0 checks on every access |
|
|
If |
|
|
After a failed compilation, retry on the next access without waiting for the interval (development mode only) |
|
|
Keep the |
|
|
Do not write the SMAP line table into the class |
|
|
Also write the SMAP of each page to a file |
|
|
Generate one |
|
|
Unload least-recently-used or idle pages |
|
|
Java level of the generated source and class (the HowTo lists the current value) |
Translation Errors Versus Request-Time Errors, and Line Mapping with SMAP
Because translation and compilation happen on the first request, a mistake in a page is not reported at deployment but to the first client. The two kinds of failure look different, and both are mapped back to lines of the page.
A translation or compilation error is a JasperException with the heading Unable to compile class for JSP:
broken.jsp)<%@ page contentType="text/html; charset=UTF-8" %>
<html>
<body>
<% int total = ; %>
</body>
</html>
HTTP Status 500 – Internal Server Error
org.apache.jasper.JasperException: Unable to compile class for JSP:
An error occurred at line: [4] in the jsp file: [/broken.jsp]
Syntax error on token ";", delete this token
1: <%@ page contentType="text/html; charset=UTF-8" %>
2: <html>
3: <body>
4: <% int total = ; %>
5: </body>
6: </html>
The message shows the JSP line and a few lines of context (displaySourceFragment, default true), not the generated
Java. A request-time error is an exception thrown while _jspService runs. Here the page dereferences a request
parameter that is absent:
runtimefail.jsp)<%@ page contentType="text/html; charset=UTF-8" %>
<html>
<body>
<h1>Bookshelf</h1>
<%
String title = request.getParameter("title");
%>
<p>Title length: <%= title.length() %></p>
</body>
</html>
org.apache.jasper.JasperException: An exception occurred processing [/runtimefail.jsp] at line [8]
5: <%
6: String title = request.getParameter("title");
7: %>
8: <p>Title length: <%= title.length() %></p>
9: </body>
10: </html>
Root Cause: java.lang.NullPointerException: Cannot invoke "String.length()" because "title" is null
org.apache.jsp.runtimefail_jsp._jspService(runtimefail_jsp.java:130)
Jasper says line 8 of the JSP, but the stack frame in the cause says line 130 of the generated Java. The two numbers are
related by the SMAP, the line table that the Jakarta Debugging Support for Other Languages specification defines and
that the Pages specification requires of every container (section 11.5): the class file gets a SourceDebugExtension
attribute, with a stratum named JSP, mapping lines of the page to lines of the generated class. Tomcat uses it to turn
the Java line into the page line in the message, and IDE debuggers use it to step through the page rather than the
generated servlet. javap -v shows the table of runtimefail_jsp.class:
SourceDebugExtension:
SMAP
runtimefail_jsp.java
JSP
*S JSP
*F
+ 0 runtimefail.jsp
runtimefail.jsp
*L
1,7:121
7:128
8:129,3
9,3:132
*E
Each *L line reads "input lines a,b map to output starting at line n`": JSP line 8 is output lines 129 to 131, and
the failing statement is at 130. The same table is why a debugger breakpoint on line 8 of a JSP works, and why a stack
trace of the page in the server log needs the cause section, not the first frames. With `suppressSmap=true Tomcat leaves
the table out, and the message falls back to Java line numbers. In development, keep mappedfile on and read the
.java in work/ when a page misbehaves in a way the message cannot explain.
Output Buffering: JspWriter, buffer, autoFlush and Commit
The implicit object out is a jakarta.servlet.jsp.JspWriter, a buffered writer that sits between the generated code
and the servlet response. Headers are not sent until the response is committed, which happens when the buffer is flushed
or, with autoFlush="true", fills up. Until then the page can still change the status, set headers, redirect or call
sendError; after that those calls no longer work, exactly as for any servlet. Two attributes of the page directive
control it, and a third affects whether the page touches the session at all.
| Attribute | Default | Meaning |
|---|---|---|
|
buffered, at least 8kb (Tomcat generates |
|
|
|
|
|
|
If true the page gets the |
Four short pages show the behaviour on Tomcat 11.0.26.
buffer-default.jsp)<%@ page contentType="text/plain; charset=UTF-8" %>
size=<%= out.getBufferSize() %> autoFlush=<%= out.isAutoFlush() %> remaining=<%= out.getRemaining() %>
<% response.setHeader("X-Late", "set-after-output"); %>
committed=<%= response.isCommitted() %>
curl -s -D- http://localhost:18080/g3lifecycle/buffer-default.jsp
HTTP/1.1 200
Set-Cookie: JSESSIONID=3577B8A9BABA8ECB56C3AF074E57B8DA; Path=/g3lifecycle; HttpOnly
X-Late: set-after-output
Content-Type: text/plain;charset=UTF-8
Content-Length: 58
size=8192 autoFlush=true remaining=8156
committed=false
The header was added after the first line of body had been written, yet it reaches the client, because the line was still
in the buffer, and the response has a Content-Length (58), which a buffered page can compute. The Set-Cookie line is
the default session="true" at work, on a page that never mentions a session.
buffer.jsp)<%@ page contentType="text/plain; charset=UTF-8" buffer="1kb" autoFlush="false" %>
size=<%= out.getBufferSize() %> autoFlush=<%= out.isAutoFlush() %>
<% if (request.getParameter("big") != null) {
for (int i = 0; i < 200; i++) { out.print("0123456789"); }
} %>
done
$ curl -s http://localhost:18080/g3lifecycle/buffer.jsp
size=1024 autoFlush=false
done
$ curl -s "http://localhost:18080/g3lifecycle/buffer.jsp?big=1" (HTTP 500)
java.io.IOException: An exception occurred processing [/buffer.jsp] at line [4]
Root Cause: java.io.IOException: Error: JSP Buffer overflow
org.apache.jasper.runtime.JspWriterImpl.bufferOverflow(JspWriterImpl.java:149)
Two thousand characters do not fit in 1024 bytes and, with autoFlush="false", the page fails instead of flushing.
With the default autoFlush="true" the same page would flush at 1 KB, commit the response and send it chunked.
flush.jsp)<%@ page contentType="text/plain; charset=UTF-8" %>
before flush
<% out.flush(); %>
<% response.setHeader("X-Too-Late", "never-sent"); %>
committed=<%= response.isCommitted() %>
HTTP/1.1 200
Set-Cookie: JSESSIONID=BE31A4793F860C1DAE50E940F3B80D8C; Path=/g3lifecycle; HttpOnly
Content-Type: text/plain;charset=UTF-8
Transfer-Encoding: chunked
before flush
committed=true
X-Too-Late is absent: after out.flush() the headers had gone, so setHeader did nothing and the response is chunked
because its length was not known when it was committed. Finally, a redirect issued from a scriptlet after some output,
while the response is still uncommitted, works and the buffered text is discarded:
redirect.jsp)<%@ page contentType="text/plain; charset=UTF-8" %>
this text is in the buffer and is discarded
<% response.sendRedirect(request.getContextPath() + "/hello.jsp"); %>
HTTP/1.1 302
Location: /g3lifecycle/hello.jsp
Content-Length: 0
The specification’s wording of this case (section 11.3) names the method response.redirect; the method is
sendRedirect, as above. The thing that matters in design is that anything which must be decided before the first byte
(status codes, redirects, cookies, content type, a forward) has to happen before the buffer is flushed, either
explicitly or by overflowing; a larger buffer, or doing the work in a servlet before forwarding to the view, are the
usual remedies. The Servlet response buffer, which is the layer below JspWriter, is discussed in the
Servlet Fundamentals page.
Precompilation: The jsp_precompile Request and Build-Time Translation
The specification gives containers a precompilation protocol (section 11.4). Request parameter names beginning with jsp
are reserved; a request to a page with the parameter jsp_precompile is a precompilation request that must not be
delivered to the page itself. The parameter may have no value, true or false; true or no value is a suggestion to
translate and compile the page, which the container may ignore. Any other value is an error (HTTP 500). Tomcat honours
the suggestion and returns an empty 200:
curl -s -w '%{http_code} len=%{size_download}\n' "http://localhost:18080/g3lifecycle/hello.jsp?jsp_precompile=true"
curl -s -o /dev/null -w '%{http_code}\n' "http://localhost:18080/g3lifecycle/hello.jsp?jsp_precompile=foo"
200 len=0
500
After the first call, hello_jsp.java and hello_jsp.class exist in work/ although the page body never ran; the second
call produced jakarta.servlet.ServletException: Cannot have precompilation request parameter [jsp_precompile] set to
[foo], as the specification requires. This is a way to warm up a deployed application page by page, with a script that
requests each page with the parameter. A better one for production is to translate and compile every page during the
build (JspC, the Ant <jasper> task, the Jetty JspC Maven plugin) and package the result, which removes the first-request
delay and the need for a compiler on the server; the specification describes this in section 1.1.9 and in Appendix A, and
the tooling is the subject of the Jasper configuration and precompilation page of this section. Precompilation does not
work with every feature (the HowTo points out that jsp-property-group can make it impractical), which is why the
production settings above still matter.
Which Engine Does This: Jasper, WaSP and Portability
The lifecycle above is the specification’s; the generated source is Tomcat’s. Tomcat and Jetty implement Pages with Apache
Jasper (Jetty names its JSP module apache-jsp), and GlassFish with Eclipse WaSP (eclipse-ee4j/wasp), which uses the
Expressly EL implementation. The three produce differently shaped classes, so a page, a tag or a framework must not
depend on the class name, the package, the superclass or the helper methods seen above; the supported seams are the
HttpJspPage contract, the tag APIs, PageContext and the SMAP. File locations differ too: the work/ directory path
shown here is Tomcat’s, so check the container’s own documentation for where it keeps generated sources.
Source: Eclipse Jetty documentation
Pitfalls and Error Names
| Symptom or error | Cause | Fix |
|---|---|---|
|
Translation and compilation are lazy; the error belongs to the first client |
Precompile ( |
|
The attribute was removed in Pages 4.0 along with |
Delete it; make state thread safe |
Counters or caches in a |
One instance serves every request, and a recompiled page gets a new instance |
Use locals, |
Stack trace points at |
The first frames are generated lines; the SMAP maps them to the page |
Read Jasper’s "at line [8]" and the code fragment; keep SMAP on; open the |
A header, cookie, status or redirect is ignored, or |
|
Decide before writing; raise |
|
|
Remove |
A |
|
|
|
The specification makes any value other than none/true/false an error |
Use |
Edits to an included fragment have no effect, or they only apply after a while |
A static include is part of the includer; the check interval (4 s) applies to the includer |
Wait one interval, or set |
Edits to a deployed page never show up in production |
|
Redeploy the application, or set |
First request after a deployment is slow |
Translation, compilation and class loading of the page and its tag libraries |
Precompile at build time, or declare warm-up pages with |
|
A servlet whose |
Fix the cause and make |
Resources opened in |
|
Release them in |
Generated source edited by hand has no effect |
Jasper overwrites it on the next recompile |
Change the JSP |
A page uses Tomcat’s generated names ( |
They are implementation details of Jasper |
Use only |
|
The generated |
Handle those methods in a servlet |
|
A page, tag handler or library still uses the |
Migrate to |
What Changed Recently
| Release | Change relevant to this page |
|---|---|
Pages 4.0 (Jakarta EE 11) |
Minimum Java 17; |
Pages 3.1 (Jakarta EE 10) |
|
Pages 3.0 (Jakarta EE 9) |
The namespace moves from |
Pages 4.1 (Jakarta EE 12, in progress) |
Milestone 4.1.0-M2 is on Maven Central; nothing on this page depends on it |
Tomcat 11.0.x |
Implements Pages 4.0, Servlet 6.1 and EL 6.0; the default |
Where the Books Are Outdated
Web Development with JavaServer Pages (Manning, 2001, listed in the section bibliography) describes JSP 1.2 on Tomcat 4,
and its chapter on how JSP works and its appendices (the JSP 1.1 to 1.2 changes, running Tomcat, the syntax and API
references) are the origin of most of what developers still remember about the lifecycle. The model is intact: translation
phase and request phase, a servlet underneath, jspInit, _jspService and jspDestroy, output buffering and
recompilation on change. What differs is the platform around it.
| The book’s era | Today |
|---|---|
|
|
|
Removed (Pages 4.0, Servlet 6.0); the page is a translation error on Tomcat 11 |
A JDK with its compiler on the server for translated pages |
Tomcat 11 bundles the Eclipse compiler ( |
Tomcat 4 era scratch directories and generated class names |
|
Scriptlets and declarations as the normal way to write pages |
Legacy: the specification keeps them, but EL, JSTL and tag files replace them; the Scripting Elements page of this section covers them and their migration |
|
Unchanged; application-level hooks are listeners and filters, and Jasper adds its own generated |
|
Removed from Pages 4.0; no major browser runs applets |
Precompilation through the container’s own tools, or the |
The |
JSP errors read from the generated Java stack trace |
SMAP (JSR 45, now Jakarta Debugging Support for Other Languages) maps errors to page lines in messages and debuggers |
Tag library validation ( |
Concept unchanged and part of the translation phase today (specification chapter 10); see the JSP Documents page of this section |
References
-
Jakarta Pages 4.0 — Appendix C.1, Changes between JSP 4.0 and JSP 3.1
-
Jakarta Pages 4.0 — Appendix C.2, Changes between JSP 3.1 and JSP 3.0
-
Jakarta Pages 4.0 — Appendix C.3, Changes between JSP 3.0 and JSR 245
-
Jakarta Servlet 6.1 — 10.7 Web Application Deployment Descriptor
-
Apache Tomcat 11 — Jasper 2 JSP Engine How-To, Configuration
-
Apache Tomcat 11 — Jasper 2 JSP Engine How-To, Production Configuration
-
Apache Tomcat 11 — Jasper 2 JSP Engine How-To, Web Application Compilation
-
Manning — Web Development with JavaServer Pages, Second Edition