Scripting Elements (Legacy)
|
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 Expression Language 6.0, Jakarta Standard Tag
Library 3.0 and Jakarta Servlet 6.1 on Apache Tomcat 11.0.26 and Java 21 (the specifications need Java 17 or later).
Everything was verified on 2026-10-08: the facts against the Pages 4.0 specification (sections 1.12 and 3.3.3 and
chapter 9), the Jakarta Tags 3.0 specification and the Tomcat 11 documentation, and every example by building it into a
Maven WAR (context path /g3scripting) and running it on Tomcat 11.0.26 with curl; the outputs shown are the outputs
that run produced. Scripting elements are the oldest way to put logic into a JSP: Java code written between <% and
%>. They still work in every container and the specification still requires them, but since JSP 2.0 (2003) the
Expression Language and the tag libraries do the same job without Java code in the page, and this page therefore treats
the three elements as legacy. It explains how they work, because a large amount of existing code uses them and because
the translation model explains most of the surprising behaviour of JSP, and then shows how to switch them off and how to
replace them. The running scenario is the Bookshelf, a small catalogue of books: the same list page is written first
with scripting elements and then with EL, JSTL and a tag file.
The Three Scripting Elements
A JSP page contains template text (sent to the browser unchanged), directives, actions and, optionally, three kinds of
scripting element. The scripting language of a page is given by the language attribute of the page directive, and in
Pages 4.0 the only value the specification defines is java. All containers must support Java scripting, and the
semantics are defined precisely by chapter 9 of the specification in terms of the generated page implementation class.
| Element | Standard syntax | XML syntax | What the container does with it | Output |
|---|---|---|---|---|
Declaration |
|
|
Copied into the class body, before |
None |
Scriptlet |
|
|
Copied unchanged into the body of |
Only what the code itself writes to |
Expression |
|
|
Wrapped as |
The value converted to a |
Four rules from the specification matter everywhere else on this page:
-
White space after
<%!,<%and<%=and before%>is optional. -
A declaration must be a complete declarative statement, or a sequence of them. A scriptlet need not be a complete statement, because several scriptlets together must form valid statements: that is what makes
<% for (...) { %>, template text and<% } %>legal. -
An expression must be a complete expression, with no trailing semicolon, and its value is coerced to a
String. A value that cannot be coerced is a translation error when detected at translation time, otherwise aClassCastExceptionat request time. -
To use the characters
%>inside a scriptlet without ending it, write%\>.
How the Container Translates Them
Pages are translated, not interpreted. In the translation phase the container turns the page into the source of a Java class (the JSP page implementation class), compiles it and loads it; in the execution phase it calls that class like any servlet. The spec describes the class as three sections: the declarations section, the implicit objects section and the main section, with the main section containing the template data, scriptlets and expressions in the order in which they appear in the page. The container need not generate this class literally, but it must behave as if it did, and Tomcat’s Jasper does generate it.
Consequences that follow from the diagram, each of which is demonstrated below:
-
Declared fields belong to the single page instance, so they are shared by every request and every session, and they must be thread-safe.
-
Variables declared in a scriptlet are local variables of
_jspService: they exist for one request, are invisible to EL, to other pages (except through a static include) and to declared methods, and cannot be declared twice in one page. -
The code of all scriptlets is one method body, so a block opened in one scriptlet can be closed in a later one with template text in between.
-
A syntax error in scriptlet code is found by the Java compiler, which reports it against the generated class; Jasper maps the error back to the JSP line.
-
Names of the form
_jsp*and_jspx*(in any combination of case) are reserved for the container.
The Bookshelf Used on This Page
All the examples are in one WAR, g3scripting.war, deployed at /g3scripting on Tomcat 11.0.26. It has the Maven
dependencies of the Getting Started page of this section (the Servlet, Pages and EL APIs with scope provided, the
Jakarta Tags API and the GlassFish JSTL implementation), the record Book(long id, String title, String author, int year)
and a servlet that puts the list in request scope and forwards to a view under WEB-INF/views.
@WebServlet("/books/*")
public class BooksServlet extends HttpServlet {
private static final List<Book> BOOKS = List.of(
new Book(1, "Dune", "Frank Herbert", 1965),
new Book(2, "Neuromancer", "William Gibson", 1984),
new Book(3, "Solaris", "Stanislaw Lem", 1961));
private final AtomicInteger hits = new AtomicInteger();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String view = req.getPathInfo() == null ? "/after" : req.getPathInfo();
req.setAttribute("books", BOOKS);
req.setAttribute("hits", hits.incrementAndGet());
req.getRequestDispatcher("/WEB-INF/views" + view + ".jsp").forward(req, resp);
}
}
The path after /books selects the view, so /g3scripting/books/before renders WEB-INF/views/before.jsp and
/g3scripting/books/modern renders WEB-INF/views/modern.jsp. The servlet and the dispatch are explained in
Servlet Fundamentals, section RequestDispatcher.
Declarations
A declaration adds members to the generated class. It can declare instance fields, static fields, methods and nested types, and it can override the page lifecycle methods. A declaration produces no output.
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.List,org.example.bookshelf.Book" %>
<%! private int hits = 0;
private String shout(String s) {
return s.toUpperCase();
}
%>
<!DOCTYPE html>
<html>
<body>
<h1>Bookshelf (scriptlet version)</h1>
<%
@SuppressWarnings("unchecked")
List<Book> books = (List<Book>) request.getAttribute("books");
hits++;
%>
<p>Page served <%= hits %> time(s); <%= books.size() %> books.</p>
<ul>
<% for (Book b : books) { %>
<li><%= shout(b.title()) %> by <%= b.author() %> (<%= b.year() %>)</li>
<% } %>
</ul>
</body>
</html>
The page uses all three elements: a declaration for a field and a helper method, a scriptlet that reads the model and
increments the field, and expressions inside a loop built from two scriptlets. Request curl http://localhost:18080/g3scripting/books/before
and the response is:
<!DOCTYPE html>
<html>
<body>
<h1>Bookshelf (scriptlet version)</h1>
<p>Page served 1 time(s); 3 books.</p>
<ul>
<li>DUNE by Frank Herbert (1965)</li>
<li>NEUROMANCER by William Gibson (1984)</li>
<li>SOLARIS by Stanislaw Lem (1961)</li>
</ul>
</body>
</html>
The blank lines are the line breaks that follow each scriptlet; they are template text. Tomcat can remove them, which is
a setting of the Directives and Configuration and Property Groups pages of this section (trimDirectiveWhitespaces and the trimSpaces option
of the Jasper servlet, in the Tomcat Jasper How-To), not of scripting.
What Jasper Generates
Tomcat keeps the generated source (the option keepgenerated defaults to true) in the work directory of the
application, here work/Catalina/localhost/g3scripting/org/apache/jsp/WEB_002dINF/views/before_jsp.java. An excerpt,
reduced to the lines that matter:
package org.apache.jsp.WEB_002dINF.views;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import jakarta.servlet.jsp.*;
import java.util.List;
import org.example.bookshelf.Book;
public final class before_jsp extends org.apache.jasper.runtime.HttpJspBase
implements org.apache.jasper.runtime.JspSourceDependent, ... {
private int hits = 0;
private String shout(String s) {
return s.toUpperCase();
}
...
public void _jspInit() { }
public void _jspDestroy() { }
public void _jspService(final jakarta.servlet.http.HttpServletRequest request,
final jakarta.servlet.http.HttpServletResponse response)
throws java.io.IOException, jakarta.servlet.ServletException {
...
final jakarta.servlet.jsp.PageContext pageContext;
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);
...
out.write("<h1>Bookshelf (scriptlet version)</h1>\n");
@SuppressWarnings("unchecked")
List<Book> books = (List<Book>) request.getAttribute("books");
hits++;
out.write("\n");
out.write("<p>Page served ");
out.print( hits );
...
for (Book b : books) {
out.write("\n");
out.write(" <li>");
out.print( shout(b.title()) );
...
}
out.write("\n");
out.write("</ul>\n");
} catch (java.lang.Throwable t) {
...
if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
...
} finally {
_jspxFactory.releasePageContext(_jspx_page_context);
}
}
}
Four observations follow from the listing. The page directive attribute import became ordinary import statements,
and the page class name, package and superclass are implementation details of Jasper (the specification says that the
name is implementation dependent and that minimal assumptions should be made about the package). The declaration text sits
in the class body, ahead of the framework members. The scriptlet and the expressions are in _jspService, with
the out.write calls for template text around them, so the order of output equals the order in the page. And the
catch block calls handlePageException, which is the entry point for the error-page mechanism covered in the Error Handling page of this
section. Debugging a scripting error normally starts here: when the Java compiler rejects the
page, the Jasper message names the JSP line, not the line of the generated class.
jspInit and jspDestroy
A declaration is the only place where a page can run code when it is initialised and when it is destroyed. The page
implementation class implements jakarta.servlet.jsp.JspPage, whose jspInit() and jspDestroy() "can be defined by a
JSP author", while _jspService() is generated and cannot. Note the names: Jasper itself generates empty _jspInit()
and _jspDestroy() methods (visible above) and calls them from its own init and destroy; the ones a page author
overrides have no leading underscore.
<%@ page contentType="text/plain; charset=UTF-8" %>
<%!
public void jspInit() {
System.out.println("g3scripting: jspInit of " + getServletConfig().getServletName());
}
public void jspDestroy() {
System.out.println("g3scripting: jspDestroy");
}
%>
ok
On the first request to /g3scripting/pit/lifecycle.jsp Tomcat’s catalina.out showed
g3scripting: jspInit of jsp (the servlet name is that of the container’s JSP servlet) and, when the application was
redeployed, g3scripting: jspDestroy. As with a servlet, the page is initialised once and destroyed once; this is the
legitimate use of a declaration. A modern application does the same work in a ServletContextListener or a managed
bean, which is easier to test.
Declared Fields Are Shared by All Threads
The container normally creates one instance of the page class and runs it on many request threads at once, as with any
servlet. A field in a declaration is therefore an instance field of that one object. The specification warns that
initialising such a variable "should be done with care in the presence of multiple requests on the page", and the
isThreadSafe page directive attribute, which used to ask the container to serialise requests, is gone (see
What Changed Recently), so there is no container-provided way to serialise access.
<%@ page contentType="text/plain; charset=UTF-8" %>
<%! private int hits = 0; %>
<% hits++; %>
hits=<%= hits %>
<%@ page contentType="text/plain; charset=UTF-8" import="java.util.concurrent.atomic.AtomicInteger" %>
<%! private final AtomicInteger hits = new AtomicInteger(); %>
hits=<%= hits.incrementAndGet() %>
The test sent one warm-up request, then 2000 requests with 32 parallel curl processes, then one more request, to each
page (seq 1 2000 | xargs -P 32 -I{} curl -s -o /dev/null ...):
counter: hits=2000
counter-atomic: hits=2002
2002 requests were made. The plain int lost two increments, because hits++ is a read, an add and a write, and two
threads interleaved. The number varies between runs and can be zero on a lightly loaded machine, which is what makes
the defect dangerous: it passes every manual test. The cure is the one for any shared state: do not keep per-request or
per-user data in a field; use a local variable, a request or session attribute, or a thread-safe object, as the Servlet Fundamentals
page of this section explains. A scriptlet variable is not affected, because it is local to _jspService. The hits
counter of the finished application lives in the controller (an AtomicInteger), not in a page.
Scriptlets
A scriptlet is a fragment of Java that runs on every request. Fragments are concatenated into a single method body, so each fragment is not independently valid: only their combination must be. This is how loops and conditionals are written around template text:
<% if (books.isEmpty()) { %>
<p>The shelf is empty.</p>
<% } else { %>
<p><%= books.size() %> books.</p>
<% } %>
Scriptlets have access to the implicit objects described below, to the page’s imports and to every declared member. The rules that bite are these.
Local Variables, Scope and Closers
<%@ page contentType="text/plain; charset=UTF-8" %>
<% String title = "Dune"; %>
scriptlet: <%= title %>
EL (local variable): [${title}]
<% pageContext.setAttribute("title", title); %>
EL (page scope): [${title}]
scriptlet: Dune
EL (local variable): []
EL (page scope): [Dune]
EL resolves names against the scoped attributes (page, request, session, application), never against the local
variables of _jspService. The bridge between the two worlds is pageContext.setAttribute, request.setAttribute and
their getAttribute counterparts, and this is the single biggest source of confusion when scripting and EL are mixed.
<%@ page contentType="text/plain; charset=UTF-8" %>
<% int n = 1; %>
<% int n = 2; %>
<%= n %>
The response is HTTP 500 with a JasperException whose message points at the JSP, not at the generated class:
An error occurred at line: [3] in the jsp file: [/pit/dup.jsp]
Duplicate local variable n
1: <%@ page contentType="text/plain; charset=UTF-8" %>
2: <% int n = 1; %>
3: <% int n = 2; %>
4: <%= n %>
The two scriptlets are one method body, so the second declaration is a duplicate; by the same logic a variable declared
inside the { of one scriptlet’s loop is not visible after the matching }. The message above also shows the source
fragment that Tomcat includes in the error (the displaySourceFragment option, default true); that fragment, and a
stack trace, are a disclosure problem in production, which the Error Handling page of this section treats.
<%@ page contentType="text/plain; charset=UTF-8" %>
<% out.println("100%>"); %>
The first %> ends the scriptlet, however the Java string looks, and the compiler then sees an unterminated literal:
String literal is not properly closed by a double-quote at line 2. With the escape from the specification
(pit/closer-ok.jsp) the page prints the expected text:
<% out.println("100%\>"); %>
100%>
The Java Language Level of Scriptlets
Scriptlet code is compiled by the container’s compiler, which is the Eclipse compiler (JDT) bundled with Tomcat, and the
language level is a setting of the JSP servlet, not the level of the application’s own classes. The Jasper How-To lists
compilerSourceVM and compilerTargetVM with the default 17 for Tomcat 11. Observed: a switch with a type pattern in a
scriptlet failed on a default Tomcat 11.0.26 although the application is built for Java 21:
<%@ page contentType="text/plain; charset=UTF-8" %>
<%
Object o = request.getParameter("q");
String kind = switch (o) {
case String s -> "string " + s;
default -> "other";
};
%>
<%= kind %>
An error occurred at line: [4] in the jsp file: [/pit/switch21.jsp]
Cannot switch on a value of type Object. Only convertible int values, strings or enum variables are permitted
Declaring the container’s JSP servlet in the application’s web.xml with the two options, and redeploying, made the same
page answer string x for ?q=x:
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
<init-param>
<param-name>compilerSourceVM</param-name>
<param-value>21</param-value>
</init-param>
<init-param>
<param-name>compilerTargetVM</param-name>
<param-value>21</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>
This is a Tomcat setting, not a Jakarta Pages feature, and the error text (a message about switch on Object) does not
mention the language level, so it is easy to misdiagnose. It is one more reason to keep logic in compiled classes, which
use the project’s own compiler settings.
Leaving the Page with return
A scriptlet is inside _jspService, so return ends the page. The specification does not define this, and the example is
a Tomcat observation.
<%@ page contentType="text/plain; charset=UTF-8" %>
first
<% if (request.getParameter("stop") != null) { return; } %>
second
curl .../pit/ret.jsp printed first and second; with ?stop=1 it printed only first and finished normally. It is
legal on Tomcat, it skips the rest of the output, and it is the kind of control flow that a designer maintaining the page
cannot see. A jsp:forward or a controller does the job explicitly.
Expressions
An expression is wrapped by the container as out.print(...), with the value converted to a String. The converted value
is written without escaping, and null becomes the text null:
<%@ page contentType="text/plain; charset=UTF-8" %>
scripting: [<%= request.getParameter("missing") %>]
EL: [${param.missing}]
scripting: [null]
EL: []
The two rows of that output are why page authors prefer EL: a missing value yields an empty string rather than the word
null. The expression must be complete and must not end with a semicolon:
<%@ page contentType="text/plain; charset=UTF-8" %>
<%= "Dune"; %>
An error occurred at line: [2] in the jsp file: [/pit/semi.jsp]
Syntax error on token ";", delete this token
Neither Scripting Nor EL Escapes Output
An expression writes what it is given. So does an EL expression written in template text, and cross-site scripting is the result when the value comes from the request:
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<p>Unsafe: <%= request.getParameter("q") %></p>
<p>Safe: <c:out value="${param.q}"/></p>
With ?q=<script>alert(1)</script> (URL-encoded) the output was:
<p>Unsafe: <script>alert(1)</script></p>
<p>Safe: <script>alert(1)</script></p>
pit/elraw.jsp, which prints ${param.q} directly in template text, reproduced the problem without any scripting
(<p>EL in template text: <b>x</b></p> for ?q=<b>x</b>). The fix is c:out (which escapes <, >, &, ' and "
by default), fn:escapeXml, or an encoder library such as the OWASP Java Encoder tag library, and the habit of never
printing request data any other way. The Security page of this section collects the rules.
Source: Jakarta Tags 3.0 — c:out
Implicit Objects Available to Scripting
The initialisation section of the generated class declares a set of local variables that scripting elements can use without declaring them (the generated class above shows them). The scoping and the EL counterparts are the subject of the Implicit Objects & Scopes page of this section; this table is what a scriptlet sees.
| Name | Type | Meaning and scope |
|---|---|---|
|
|
The request being served; request scope |
|
|
The response; page scope |
|
|
The page context, gateway to all scopes; page scope |
|
|
The session, only valid for HTTP and only when the page takes part in a session; session scope |
|
|
The application; application scope |
|
|
The buffered writer for the page’s output; page scope |
|
|
The servlet configuration of the page; page scope |
|
|
The page instance; page scope |
|
|
Only in an error page ( |
The names are variables, not attributes: ${request} is not the request variable, and the EL implicit objects have
different names (requestScope, param, pageContext and so on). out is the buffered stream that Jasper’s
out.write calls use, so a scriptlet that calls out.println is interleaved correctly with the template text around it.
Scripting Variables Defined by Actions
An action can introduce a variable that later scriptlets can use. In the standard actions the one that does so is
jsp:useBean: "the name of the variable introduced is the name of the id attribute and its type is the type of the
class attribute". Custom tags can declare variables in their TLD or through a TagExtraInfo, with the scopes
AT_BEGIN, NESTED and AT_END (the classic and SimpleTag custom tag pages of this section). The scope attribute of the action does not
change the visibility of the Java variable: it determines where, and so for how long, additional references to the
object are kept.
<%@ page contentType="text/plain; charset=UTF-8" %>
<jsp:useBean id="shelf" class="java.util.ArrayList" scope="page"/>
<% shelf.add("Dune"); shelf.add("Solaris"); %>
size from scripting: <%= shelf.size() %>
size from EL: ${shelf.size()}
size from scripting: 2
size from EL: 2
The bean is both a scripting variable and a page-scoped attribute, which is why the EL side finds it. The Standard
Actions & JavaBeans page of this section covers jsp:useBean, jsp:setProperty and jsp:getProperty in full.
Scriptlet Variables and the Include Mechanisms
The include directive merges the included file into the page before translation, so all scriptlets, local variables and
declarations are in the same class. jsp:include runs a different page at request time, in a different class.
<%@ page contentType="text/plain; charset=UTF-8" %>
<% String shared = "from the parent"; %>
<%@ include file="/pit/inc-part.jspf" %>
The included pit/inc-part.jspf contains the single line static include sees: <%= shared %>, and the response is
static include sees: from the parent. The dynamic version, pit/inc-dyn.jsp, is the same page with
<jsp:include page="/pit/inc-dyn-part.jsp"/> in place of the directive and <%= shared %> in the included page; it
failed with HTTP 500 and the compiler message shared cannot be resolved to a variable for /pit/inc-dyn-part.jsp.
Variables must be passed to a dynamically included page as request attributes or jsp:param parameters. The Includes,
Forwards & Page Composition page of this section compares the two mechanisms.
The XML Syntax
In a JSP document (an XML file) the only valid forms of the three elements are <jsp:declaration>, <jsp:scriptlet>
and <jsp:expression>. The semantics are those of the standard syntax. The rest of the XML view, and the well-formedness
rules (a < in code must be escaped or placed in CDATA), are the subject of the JSP Documents page of this section.
<?xml version="1.0" encoding="UTF-8"?>
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="2.1">
<jsp:directive.page contentType="text/plain; charset=UTF-8"/>
<jsp:declaration>private String shout(String s) { return s.toUpperCase(); }</jsp:declaration>
<jsp:scriptlet>String t = "dune";</jsp:scriptlet>
<jsp:text>Title: </jsp:text><jsp:expression>shout(t)</jsp:expression>
</jsp:root>
curl .../pit/doc.jspx returned Title: DUNE. The xmlns:jsp namespace
http://java.sun.com/JSP/Page is the identifier that Pages 4.0 still uses, although it names a company that no longer
owns the technology; it is an identifier, not an address. A standard-syntax page is converted to the same XML view for
tag library validators, with <% ... %> becoming <jsp:scriptlet>.
Request-Time Attribute Values
A scripting expression can also appear as the whole value of an attribute of an action, written "<%= expr %>" or
'<%= expr %>': a request-time attribute value. Rules from the specification: the expression must appear by itself,
not mixed with text; it works only in attributes of actions that accept request-time values (a custom tag attribute whose
TLD says rtexprvalue true, or a standard action attribute that the specification lists), and an attribute with page
translation-time semantics rejects it; and in a directive it is a translation error. In a JSP page the EL form
"${expr}" is the replacement and it can be mixed with text.
<%@ page contentType="text/plain; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:out value="<%= request.getParameter(\"q\") %>" default="(none)"/>
With ?q=a%26b the page printed a&b, because c:out escapes. Note the quoting: the inner double quotes must be
escaped as \" (the specification’s quoting conventions), or the attribute must be written with single quotes; this is
one of the reasons the form is rarely written correctly the first time. The same value in the EL form is
<c:out value="${param.q}" default="(none)"/>.
Why Scripting Elements Are Discouraged
The specification itself calls EL "an alternative to scripting elements" and describes EL pages as scriptless pages that "can’t use Java scriptlets, Java expressions, or Java declaration elements". The Jakarta Tags specification states the motivation: page authors "often see scripting languages as complex and not very well adapted to their needs". The practical reasons, each shown by an example above or in the tables below, are:
| Problem | What happens | Alternative |
|---|---|---|
Business logic in the view |
Database access, decisions and formatting in the page cannot be unit-tested without a container, and cannot be reused by a REST endpoint or a batch job |
Controller and service classes; the page only renders a model |
Thread safety |
Declared fields are shared by all threads (see Declared Fields Are Shared by All Threads) |
No state in the page; request or session attributes |
Output encoding |
|
|
Two variable worlds |
Scriptlet locals are invisible to EL and to tags (see Local Variables, Scope and Closers) |
Scoped attributes only |
Language level and tooling |
The scriptlet language level is a container setting (see The Java Language Level of Scriptlets); IDEs and refactoring tools understand Java files far better than Java inside a template |
Compiled classes with the project’s own compiler options |
Errors found late |
A page is compiled on its first request, so a typo is a runtime HTTP 500 |
Precompile with Jasper’s |
Mixed responsibilities |
A designer cannot edit a page full of braces without breaking it |
Tags and EL, which are the page author’s vocabulary |
Hidden control flow |
|
Explicit tags and controllers |
The first of those rows is also the architectural argument of the Model 2 approach covered in the Introduction & History page of this section: the view should not decide anything. Nothing here makes scripting elements incorrect, and a small internal tool that never leaves a single developer’s hands can use them; the point is that the cost grows with the number of people who touch the page and the number of years it lives.
Disabling Scripting Elements
A project that has decided to be scriptless can make the decision enforceable. There are two mechanisms: a switch in
web.xml and a tag library validator, and each is described below together with what was observed on Tomcat 11.0.26.
scripting-invalid
The element scripting-invalid is a child of jsp-property-group (the Configuration and Property Groups page of this section covers the
rest of that element) with the values true and false. Scripting is enabled by default, and when the value is true
scripting elements found in a matching page are a translation error. The table gives the effect (Table JSP.3-3):
scripting-invalid |
Scripting elements encountered |
|---|---|
unspecified |
Valid |
|
Valid |
|
Translation error |
The Java EE 5 tutorial (the archived javax era text, chapter 9, "Disabling Scripting") describes the same element and says
that with scripting invalid "scriptlets, scripting expressions, and declarations will produce a translation error". That
sentence is the behaviour that Tomcat implements, as the observations below show.
<jsp-config>
<jsp-property-group>
<url-pattern>/WEB-INF/views/strict/*</url-pattern>
<scripting-invalid>true</scripting-invalid>
</jsp-property-group>
</jsp-config>
The specification’s own example uses the extension pattern *.jsp for the whole application. A folder pattern gives a
migration path: new and converted pages go under the folder, old pages stay where they are, and the folder grows. The
property group applies to a page reached through a forward as well as one reached directly (the pattern is compared with
the path of the JSP file).
<%@ page contentType="text/html; charset=UTF-8" %>
<p><%= request.getAttribute("books") %></p>
curl .../books/strict/before answered HTTP 500 and the page text
JasperException: /WEB-INF/views/strict/before.jsp (line: [2], column: [6]) Scripting elements ( <%!, <jsp:declaration, <%=, <jsp:expression, <%, <jsp:scriptlet ) are disallowed here.
The error appears when the page is translated, that is at the first request, unless the build precompiles the pages. The
EL page in the same folder, strict/after.jsp, is fine:
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<p>${books.size()} books, first: <c:out value="${books[0].title}"/></p>
curl .../books/strict/after returned <p>3 books, first: Dune</p>.
The specification’s grammar also says that when scripting is disabled, a request-time attribute value is a translation
error (the validity constraint RTAttributeScriptingEnabled). Tomcat 11.0.26 did not enforce that part:
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:set var="v" value="<%= 1 + 1 %>"/>[${v}]
The page was translated and answered [2] with HTTP 200; strict/attr.jsp (<c:out value="<%= \"x\" %>"/>) printed x.
So scripting-invalid on Tomcat blocks declarations, scriptlets and expressions in template text, but not scripting
expressions in action attributes. A project that wants to ban those as well needs the tag library validator described next, or
a source scan (see Finding What Is Left).
ScriptFreeTLV
Before scripting-invalid existed, the way to forbid scripting was a tag library validator (TLV), and the Jakarta Tags API
still ships jakarta.servlet.jsp.jstl.tlv.ScriptFreeTLV (in the Jakarta Tags API JAR, version 3.0.2 here). It takes four
initialisation parameters, allowDeclarations, allowScriptlets, allowExpressions and allowRTExpressions. A validator
is applied to every page that imports the TLD that names it, so it is opt-in per page, but it distinguishes expressions
and request-time attribute values from scriptlets and declarations, which scripting-invalid does not. The GlassFish
implementation JAR 3.0.1 contains a ready-made TLD with all four parameters false, META-INF/scriptfree.tld, whose
URI is jakarta.tags.scriptfree; a page opts in with one directive:
<%@ taglib prefix="sf" uri="jakarta.tags.scriptfree" %>
On Tomcat 11.0.26 pit2/sf-ok.jsp (EL only) printed 2, and the other two pages failed with HTTP 500 and these validator
messages:
pit2/sf-expr.jsp (<%= 1 + 1 %> in the page)
Validation error messages from TagLibraryValidator for [scriptfree] in [/pit2/sf-expr.jsp]
null: JSP page contains 1 expression.
pit2/sf-rt.jsp (<c:out value="<%= 1 + 1 %>"/>)
Validation error messages from TagLibraryValidator for [scriptfree] in [/pit2/sf-rt.jsp]
null: JSP page contains 1 request-time attribute value.
So the validator closes the gap left by scripting-invalid on Tomcat (see above): it rejects the request-time attribute
value. A project that wants a looser rule writes its own TLD with other parameter values, for example to forbid scriptlets
and declarations but allow expressions:
<?xml version="1.0" encoding="UTF-8" ?>
<taglib xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-jsptaglibrary_3_0.xsd"
version="3.0">
<description>Rejects scriptlets and declarations, still allows expressions.</description>
<tlib-version>1.0</tlib-version>
<short-name>scriptfree</short-name>
<uri>https://example.org/bookshelf/scriptfree</uri>
<validator>
<validator-class>jakarta.servlet.jsp.jstl.tlv.ScriptFreeTLV</validator-class>
<init-param>
<param-name>allowDeclarations</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>allowScriptlets</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>allowExpressions</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>allowRTExpressions</param-name>
<param-value>true</param-value>
</init-param>
</validator>
</taglib>
With a page that imports https://example.org/bookshelf/scriptfree, pit2/free-ok.jsp (an expression, <%= 1 + 1 %>)
printed expression allowed: 2, while a scriptlet (pit2/free-bad.jsp) and a declaration (pit2/free-bad2.jsp) failed
with HTTP 500 and the messages null: JSP page contains 1 scriptlet. and null: JSP page contains 1 declaration.
The example in the Jakarta Tags specification (chapter 14, Tag Library Validators) is the model for this TLD, but it
cannot be copied as printed: its <jsp-version>2.0</jsp.version> line has mismatched end tags and is not well-formed XML,
so the TLD above leaves that element out.
Choosing a Level of Strictness
| Mechanism | Blocks | Does not block |
|---|---|---|
Nothing (the default) |
Nothing |
Anything |
|
Declarations, scriptlets and expressions in the page body (observed) |
Request-time attribute values on Tomcat 11.0.26 (observed) |
|
Declarations, scriptlets, expressions and request-time attribute values in the pages that import the TLD |
Pages that do not import the TLD |
A build check (grep, or a unit test over the view folder) |
Whatever the pattern matches, in every page |
Nothing, but only as reliable as the pattern |
Migrating to EL, JSTL and Tag Files
The replacement is not one-to-one, because the aim is to move logic out of the page. The table maps each scripting idiom to its modern form; the following sections show the Bookshelf page converted.
| Scripting | Replacement | Where it lives |
|---|---|---|
|
|
Page, EL |
|
|
Page, EL |
|
|
Page, EL |
|
|
JSTL core |
|
|
JSTL core |
|
|
JSTL core |
|
Functions in the |
JSTL functions, TLD |
A helper declared with |
A tag file for markup, an EL function for a pure computation, a service class for logic |
|
|
A counter in the controller or a service, put in a scope as an attribute |
Java |
|
A tag file |
|
Database or business calls in the page |
A controller and a service, with a model attribute |
Java |
|
A |
Java |
Source: Jakarta Tags 3.0 — c:forEach, c:choose and c:if
The EL and JSTL Version
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
<!DOCTYPE html>
<html>
<body>
<h1>Bookshelf (EL and JSTL version)</h1>
<p>${fn:length(books)} books.</p>
<ul>
<c:forEach var="b" items="${books}">
<li>${fn:toUpperCase(b.title)} by <c:out value="${b.author}"/> (${b.year})</li>
</c:forEach>
</ul>
</body>
</html>
curl .../books/after (the default view) returned the same list as the scripting version:
<!DOCTYPE html>
<html>
<body>
<h1>Bookshelf (EL and JSTL version)</h1>
<p>3 books.</p>
<ul>
<li>DUNE by Frank Herbert (1965)</li>
<li>NEUROMANCER by William Gibson (1984)</li>
<li>SOLARIS by Stanislaw Lem (1961)</li>
</ul>
</body>
</html>
There is no declaration, no scriptlet and no cast. ${b.title} works on a Java record on Tomcat 11.0.26, because EL 6.0
resolves record components; on GlassFish 8.0.5 the same expression failed with PropertyNotFoundException for the record
(finding recorded on the Getting Started page of this section), so a portable project adds JavaBean-style getters. The
<li> line uses fn:toUpperCase where the old page called a declared method.
Source: Jakarta Tags 3.0 — c:out
A Tag File for the Repeated Markup
A tag file is a JSP fragment saved as WEB-INF/tags/<name>.tag and used as a custom tag. It keeps the markup that the
declared shout method and the loop body used to share in one reusable place, with typed attributes.
<%@ tag body-content="empty" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
<%@ attribute name="book" required="true" type="org.example.bookshelf.Book" %>
<%@ attribute name="highlight" required="false" type="java.lang.Boolean" %>
<li><c:if test="${highlight}">* </c:if>${fn:toUpperCase(book.title)} by <c:out value="${book.author}"/> (${book.year})</li>
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="b" tagdir="/WEB-INF/tags" %>
<!DOCTYPE html>
<html>
<body>
<h1>Bookshelf (tag file version)</h1>
<p>Page served ${hits} time(s); ${books.size()} books.</p>
<ul>
<c:forEach var="book" items="${books}" varStatus="s">
<b:book-item book="${book}" highlight="${s.first}"/>
</c:forEach>
</ul>
<c:choose>
<c:when test="${empty books}"><p>The shelf is empty.</p></c:when>
<c:otherwise><p>Last on the shelf: ${books[books.size() - 1].title}</p></c:otherwise>
</c:choose>
</body>
</html>
curl .../books/modern produced (blank lines removed):
<h1>Bookshelf (tag file version)</h1>
<p>Page served 1 time(s); 3 books.</p>
<ul>
<li>* DUNE by Frank Herbert (1965)</li>
<li>NEUROMANCER by William Gibson (1984)</li>
<li>SOLARIS by Stanislaw Lem (1961)</li>
</ul>
<p>Last on the shelf: Solaris</p>
The counter that was a declared field is now hits in the controller, set as a request attribute, and thread-safe because
it is an AtomicInteger. A first attempt at the "last" line with a stream lambda and a method reference
(books.stream().map(x -> x.year()).min(Integer::compare)) failed at translation with ELException: Failed to parse the
expression, because the Tomcat 11.0.26 EL parser rejects the :: method-reference syntax: EL 6.0 has lambdas and the
stream API in EL, but not Java method references, which is one more place where "it is Java" misleads. Put such
calculations in the controller. The tag file page and the custom tag pages of this section go into attributes, jsp:doBody
and fragments.
Finding What Is Left
A conversion needs a to-do list, and a build needs a guard. Because <%@ (directives) and <%-- (comments) are not
scripting, a pattern that excludes them lists all scripting elements and the XML forms:
grep -rnE '<%([^@-]|-[^-]|$)|<jsp:(declaration|scriptlet|expression)' src/main/webapp \
--include='*.jsp' --include='*.jspf' --include='*.jspx' --include='*.tag'
On before.jsp, after.jsp and modern.jsp of this application the command printed exactly the six scripting lines of
before.jsp (lines 2, 12, 17, 19, 20 and 21) and nothing for the other two. The pattern also catches request-time
attribute values, since they contain <%=. Run in a test or a CI step (! grep ..., or a JUnit test that walks the
view folder) it keeps a migrated folder clean. Precompiling the application with Jasper’s JspC (the Tomcat How-To
section "Web Application Compilation") finds translation and compile errors in every page at build time instead of at the
first request.
A Migration Order That Keeps the Application Running
-
Put the data the page needs in request attributes in a controller (a servlet, or a Struts or Spring MVC action), so that the page reads the model and never fetches it.
-
Replace output scriptlets and expressions with EL and
c:out, page by page, escaping anything that came from a user. -
Replace loops and conditionals with
c:forEach,c:ifandc:choose. -
Replace declared helper methods with tag files, EL functions or service methods.
-
Move the converted pages into a folder that has
scripting-invalidset totrue, so that nobody adds code again. -
Add the grep check to the build for the folders that are not yet protected.
-
When the legacy folder is empty, set
scripting-invalidon*.jsp.
Pitfalls and Error Names
| Symptom or error | Cause | Fix |
|---|---|---|
Counter or cached value is wrong under load (observed: 2000 instead of 2002) |
A field declared with |
No state in declarations; thread-safe types or attributes |
|
A semicolon at the end of an expression |
Remove it; an expression is a value, not a statement |
|
Two scriptlets of one page declare the same local name |
Rename, or scope the variable with braces inside one scriptlet |
|
A |
Write |
EL prints nothing for a variable that the scriptlet "has" (observed) |
A scriptlet local is not a scoped attribute |
|
|
A |
Pass the value as an attribute or a |
Output text is |
A scripting expression of a |
Use EL, which prints an empty string, or |
Script injection in the page (observed) |
|
|
Switch pattern, record pattern or other recent syntax fails to compile in a page (observed) |
|
Raise the options on the JSP servlet, or move the code to a class |
|
|
Convert to EL/JSTL, or use a different folder; do not switch the check off |
Validation messages such as |
|
Convert the page; the message does not name the line |
|
The attribute was removed in Pages 4.0 |
Delete the attribute; make the state thread-safe |
|
The reserved |
Override |
|
EL has lambdas but not method references |
Use a lambda, or compute in Java |
Scripting expression accepted in an attribute although |
Tomcat 11.0.26 does not enforce |
Import |
HTTP 500 shows source lines of the page |
Jasper includes a source fragment in the error text |
An |
What Changed Recently
| Version | Change |
|---|---|
Pages 4.0 (Jakarta EE 11) |
Removes the deprecated |
Pages 3.1 (Jakarta EE 10) |
Deprecates |
Pages 3.0 (Jakarta EE 9) |
Package move from |
Pages 2.0 |
Adds EL and the |
The Pages 2.0 row is the historical background given by the specification text itself ("JSP 2.0 added EL expressions as an alternative to scripting elements" and "JSP 2.0 made this slightly easier through a JSP configuration element"). The Pages 3.0 row is a consequence of the namespace change, which the What’s Changed & Migration page of this section covers.
Where the Books Are Outdated
The section bibliography lists two books that teach scripting: Web Development with JavaServer Pages (Manning, 2001) is written for JSP 1.2, before EL and JSTL existed; JSTL in Action (Manning, 2002) teaches JSTL 1.0 on JSP 1.2. The concepts, such as the translation model, the scope rules and the thread-safety warning, remain true. The practices do not.
| The book’s era | Today (Pages 4.0) |
|---|---|
Scriptlets, declarations and expressions as the normal way to write a page, with beans and conditionals in Java |
Scriptless pages: EL, JSTL and tag files, with |
The |
|
|
Removed in Pages 4.0 and rejected by Tomcat 11; no replacement, because |
JSTL 1.0 with two sets of libraries: EL libraries ( |
JSTL attributes accept EL, and the library URIs are |
|
|
EL available only inside JSTL tag attributes (JSP 1.2) |
EL in template text and in action attributes of every page, unless |
|
Removed (Pages 4.0, Servlet 6.0) |
|
Same names, but the default of |
Source: Manning — JSTL in Action
The archived Java EE 5 tutorial has the same limits: its chapter 9, "Scripting in JSP Pages", is written for JSP 2.1 and the javax packages, so it remains useful for the concepts and not for imports, namespaces or versions.
References
-
Jakarta Pages 4.0 — 1.4.1 Translation-Time Processing Errors
-
Jakarta Pages 4.0 — 2.10 Resolution of Variables and their Properties
-
Jakarta Pages 4.0 — Table JSP.1-6, Implicit Objects Available in JSP Pages
-
Apache Tomcat 11 — Jasper 2 JSP Engine How To, Configuration
-
Apache Tomcat 11 — Jasper 2 JSP Engine How To, Web Application Compilation
-
The Java EE 5 Tutorial — 9. Scripting in JSP Pages (archived,
javaxera) -
The Java EE 5 Tutorial — Initializing and Finalizing a JSP Page
-
Manning — Web Development with JavaServer Pages, Second Edition