Directives
|
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, running on Apache Tomcat 11.0.26 and Java 21. Everything was verified on 2026-10-08:
the facts against section 1.10 (Directives), section 3.3 (JSP Property Groups) and section 8.5 (Tag File Directives) of the
Pages 4.0 specification, and every example by building it into a Maven WAR and running it on Tomcat 11.0.26 with curl;
the outputs and error messages shown are the ones that run produced. A directive is a message to the JSP container: it
is read once, when the page is translated into a servlet, it produces no output of its own, and it changes how the page is
translated (which encoding to read the source in, which classes to import, which tag libraries to load, which files to
paste in) and how the generated servlet behaves (buffering, sessions, the error page, the content type). There are three
directives for JSP pages (page, include, taglib) and three more that only exist in tag files (tag, attribute,
variable). The running scenario is the Bookshelf, a small catalogue of books, here as a handful of pages that each
turn one directive on or off and show what changes in the HTTP response.
What a Directive Is
The syntax is <%@ directiveName attribute="value" ... %>, with optional white space after <%@ and before %>.
Directives are evaluated at translation time, before any request is served, so their attribute values are literals:
${...} and scriptlet expressions are not allowed in them, and a directive can never depend on a request parameter. The
page’s own pieces of template text, EL and actions are the request-time part (see JSP Lifecycle and Translation for
the translation and request phases).
| Directive | Where | Purpose | Position and repetition |
|---|---|---|---|
|
JSP pages (not tag files; a translation error in a tag file) |
Page-level properties: language, imports, session, buffering, error page, content type, encoding, EL and whitespace switches |
Position independent (except |
|
JSP pages and tag files |
Translation-time inclusion of another file’s text |
Wherever the file’s content should be pasted |
|
JSP pages and tag files |
Declares a tag library and binds a prefix |
Must precede the first use of its prefix |
|
Tag files only (translation error in a JSP page) |
The tag-file counterpart of |
Position independent |
|
Tag files only |
Declares one attribute of the custom action |
One per attribute name |
|
Tag files only |
Declares a variable the tag exposes to the calling page |
One per variable name |
A directive that Jasper does not know is a hard error, not a warning. Verified on Tomcat 11.0.26: an unknown attribute
(<%@ page colour="red" %>) fails the first request with [Page directive] has invalid attribute: [colour]. The
translation unit is the page plus everything pasted in by static include directives and implicit includes, and all the
page directives in that unit are merged into one set of attributes.
Most page attributes have a site-wide counterpart in web.xml (a jsp-property-group, described in
Implicit Includes and JSP Property Groups and on Configuration and Property Groups), so a team can set a convention once and let a page override it only when it must. The sections
below take the directives in turn, starting with the one that has the most attributes.
The page Directive
| Attribute | Meaning | Default | jsp-property-group element |
|---|---|---|---|
|
Scripting language of the scriptlets, expressions and declarations. |
|
none |
|
Fully qualified superclass of the generated servlet. Discouraged: it takes away the container’s freedom to supply a better superclass |
container specific |
none |
|
Comma-separated types or |
|
none (an |
|
Whether the page takes part in an HTTP session. |
|
none |
|
|
buffered, at least 8kb |
|
|
|
|
none |
|
A string returned by the generated servlet’s |
none |
none |
|
Context-relative or page-relative URL to which an uncaught |
implementation dependent |
none |
|
Marks this page as an error target. |
|
none |
|
MIME type and optionally the response charset: |
|
|
|
Character encoding of the JSP file itself (an IANA name). Not needed for JSP documents. At most once per file |
the |
|
|
|
|
|
|
|
unspecified, which behaves as |
|
|
|
|
|
|
|
|
|
The directive may appear any number of times in a translation unit, anywhere, with these rules, all enforced by Jasper:
-
A repeated attribute must repeat the same value. Verified: a second
contentTypewith a different value fails withPage directive: illegal to have multiple occurrences of 'contentType' with different values (old: [text/html], new: [text/plain]). -
importmay repeat and accumulates in order;pageEncodingmay repeat across files but occurs at most once per file. -
pageEncodingandcontentTypeshould be at the top, because the container needs them to decide how to decode the rest of the file. -
An unrecognized attribute or value is a fatal translation error, which includes
isThreadSafe(see isThreadSafe, Removed in Pages 4.0).
A page that sets most of the attributes once is the best first example. The Bookshelf catalogue below sets the encoding,
imports a class that EL can then use by its simple name, avoids creating a session, asks for a 16 KB buffer, adds an
info string, trims directive white space and names an error page.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"
import="java.time.Year, org.example.bookshelf.Book"
session="false" buffer="16kb" autoFlush="true"
info="Bookshelf catalogue page" trimDirectiveWhitespaces="true"
errorPage="/WEB-INF/views/error.jsp" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!DOCTYPE html>
<html>
<body>
<h1>Bookshelf</h1>
<p>Servlet info: ${pageContext.page.servletInfo}</p>
<p>Year: ${Year.now().value >= 2026}</p>
<ul>
<c:forEach var="b" items="${books}">
<li>${b.title} (${b.year})</li>
</c:forEach>
</ul>
</body>
</html>
The servlet that supplies books and forwards to the page is the only Java the examples on this page need.
@WebServlet("/p/*")
public class PageServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getPathInfo().substring(1);
req.setAttribute("books", List.of(
new Book(1, "Dune", "Frank Herbert", 1965),
new Book(2, "Neuromancer", "William Gibson", 1984)));
req.getRequestDispatcher("/WEB-INF/views/" + name + ".jsp").forward(req, resp);
}
}
Running curl -si http://localhost:18080/g3directives/p/catalogue on Tomcat 11.0.26 returned status 200 with no
Set-Cookie header and this body:
<!DOCTYPE html>
<html>
<body>
<h1>Bookshelf</h1>
<p>Servlet info: Bookshelf catalogue page</p>
<p>Year: true</p>
<ul>
<li>Dune (1965)</li>
<li>Neuromancer (1984)</li>
</ul>
</body>
</html>
Three things show in that output. The info string is the generated servlet’s getServletInfo(), reachable from EL through
pageContext.page.servletInfo because the implicit page object is the servlet instance. The imported
java.time.Year class is usable from EL as Year.now(), which is a Pages 3.1 clarification: the EL environment has the
same default imports as the scripting environment. Finally the trimDirectiveWhitespaces attribute removed the blank
lines that the two directive lines would otherwise leave at the top of the document (see trimDirectiveWhitespaces).
language, extends and info
language="java" is the default and the only value Pages 4.0 defines; naming another language fails on Tomcat with
Page directive: invalid language attribute, verified with language="groovy". Writing it explicitly only documents the
obvious, so most pages omit it.
extends names the superclass of the generated servlet. The superclass has to cooperate with the container’s generated
code: verified, extends="java.lang.Object" fails translation with The method getServletConfig() is undefined for the
type …. The specification itself recommends against it, because it stops the container from choosing its own base
class; adding objects to every page is better done with an implicit include (see Implicit Includes and JSP Property Groups) or, for scoped
variables, with a servlet or filter that puts them in a scope.
info costs nothing, appears in getServletInfo() and so in management tooling, and is the safest attribute to learn
what a page is. It is not secret, so it must not hold anything sensitive.
import and the Implicit Imports
The container always imports java.lang.*, jakarta.servlet.*, jakarta.servlet.jsp.* and jakarta.servlet.http.*, and
nothing else. Any other class used in a scriptlet, or referenced by simple name in EL, needs an import, which may be a
comma-separated list in one attribute or several directives:
<%@ page import="java.time.Year" %>
<%@ page import="java.time.Month" contentType="text/plain" %>
<%@ page contentType="text/plain" %>
${Year.of(1965).isLeap()}-${Month.MARCH.value}
The output on Tomcat 11.0.26 is false-3. The two contentType attributes repeat the same value, which is allowed, and
the two import directives add up. A static method or constant is reachable from EL only through a class, not an
interface: verified, ${List.of(1)} with import="java.util.List" fails with The class [java.util.List] must be
public, in an exported package, non-abstract and not an interface. When many pages need the same imports and tag
libraries, put them in one file and include it with include-prelude (see Implicit Includes and JSP Property Groups).
session
Every JSP page takes part in a session by default, and the container creates the session eagerly when the page starts,
whether or not the page uses it. Verified on Tomcat 11.0.26: a page that only prints text answers with
Set-Cookie: JSESSIONID=...; Path=/g3directives; HttpOnly, and a page with session="false" does not:
<%@ page contentType="text/html; charset=UTF-8" session="false" %>
<p>no session</p>
On a public catalogue that no visitor logs in to, this attribute saves a cookie and a server-side session object per
visitor. The price is that the implicit session variable is off limits on that page: <% out.print(session.getId()); %>
fails with session cannot be resolved, a compile error reported against the generated servlet rather than a Jasper
translation error. The attribute only controls the page’s own participation: a servlet that forwards to the page may
still have created a session earlier. The servlet-level behaviour of sessions is covered in
Servlet Fundamentals.
buffer and autoFlush
The out object is a JspWriter with an in-memory buffer, so the page can still change its mind (forward, set a header,
send a redirect, hand over to an error page) until the buffer has been sent. The attribute takes none, or a size that
must end in kb; buffer="8" fails with Page directive: invalid value for buffer. autoFlush decides what happens
when the buffer fills: true (the default) sends it and keeps going, false throws. Verified results on Tomcat 11.0.26:
| Page | Result | Consequence |
|---|---|---|
|
Translation fails; Tomcat 11.0.26 prints the unresolved message key |
Without a buffer nothing can be held back, so |
|
|
Use when truncated output is worse than an error |
|
HTTP 200; the body is the part of the output that had already been flushed, followed by the error page’s markup |
The response was already committed when the exception was thrown, so the status can no longer become 500 |
|
HTTP 500 with the error page’s body and nothing of the earlier output |
The buffer was discarded and the error page took over |
<%@ page buffer="1kb" errorPage="/WEB-INF/views/error.jsp" contentType="text/plain" %>
<% for (int i = 0; i < 400; i++) { out.print("0123456789"); } %>
<% if (true) throw new IllegalStateException("late failure"); %>
The rule that follows is the one the specification states: once the buffer has been flushed, the dispatch of an uncaught exception to the error page may fail, because the status line and headers are on the wire. On Tomcat 11.0.26 the error page’s markup is appended to the half-sent page under a 200 status, which is worse than a clean 500. Pages that build large output (a CSV export, a report) should be written so that nothing that can fail happens after the first flush, or use a larger buffer, or compute the data in a servlet before the forward.
errorPage and isErrorPage
errorPage points an uncaught Throwable at another resource, and isErrorPage="true" on that resource turns on the
implicit exception variable. The container stores the throwable in the request under both
jakarta.servlet.jsp.jspException (backwards compatibility) and jakarta.servlet.error.exception (the Servlet name).
From EL, pageContext.exception and pageContext.errorData (a jakarta.servlet.jsp.ErrorData) are the clean routes:
<%@ page isErrorPage="true" contentType="text/html; charset=UTF-8" %>
<h1>Something went wrong</h1>
<p>message: ${pageContext.exception.message}</p>
<p>status ${pageContext.errorData.statusCode} for ${pageContext.errorData.requestURI}</p>
<%@ page errorPage="/WEB-INF/views/error.jsp" %>
<p>before</p>
<%
if (request.getParameter("fail") != null) {
throw new IllegalStateException("catalogue backend is down");
}
%>
<p>after</p>
A request to /p/boom?fail=1 returned status 500 and this body (the earlier before line was in the buffer, so it was
discarded):
<h1>Something went wrong</h1>
<p>message: catalogue backend is down</p>
<p>status 500 for /g3directives/WEB-INF/views/boom.jsp
Without isErrorPage="true" the target cannot see the exception: verified, <% out.print(exception); %> in an
ordinary page fails with exception cannot be resolved to a variable. The error page should never print
exception.printStackTrace or the message of an unknown exception to the visitor, which discloses internals; log the
throwable and show a reference number instead. Note also that the attribute overrides web.xml: when a page names an
errorPage, the error-page elements of the deployment descriptor are not consulted for exceptions that page throws. The
whole mechanism, including the deployment-descriptor error pages and the ErrorData fields added in Pages 4.0 (query
string and request method), is the subject of Error Handling.
contentType and pageEncoding
Two different encodings are involved and the two attributes are easy to confuse. pageEncoding says how the JSP file is
stored: it is how the container turns the bytes of the .jsp into characters. contentType says what the response is:
the MIME type and, through its charset, how the output characters are encoded for the client. For a JSP in standard
syntax the file encoding is the pageEncoding charset if present, otherwise the contentType charset if present,
otherwise ISO-8859-1.
(standard syntax)"] --> pe{"pageEncoding
present?"} pe -->|"yes"| usePe["Decode the file
with that charset"] pe -->|"no"| ct{"contentType has
a charset?"} ct -->|"yes"| useCt["Decode the file
with that charset"] ct -->|"no"| iso["Decode the file
as ISO-8859-1"] usePe --> resp["Response encoding is separate:
charset of contentType if given,
else the container default"] useCt --> resp iso --> resp
The failing case is the third arm. A Bookshelf page saved as UTF-8 that contains an accented letter and declares neither attribute is decoded as ISO-8859-1, and the container then encodes the (wrong) characters again for the response. On Tomcat 11.0.26 the three variants gave:
<%-- enc-nodecl.jsp: nothing declared --%>
<p>Café Dune</p>
<%-- enc-utf8.jsp: the contentType charset is also used to read the file --%>
<%@ page contentType="text/html; charset=UTF-8" %>
<p>Café Dune</p>
<%-- enc-pe.jsp: pageEncoding only --%>
<%@ page pageEncoding="UTF-8" %>
<p>Café Dune</p>
The first page produced the bytes C a f 303 203 302 251 (the two bytes of the UTF-8 letter, each re-encoded: Café in
a browser). The second and third produced C a f 303 251 (correct). All three answered with a UTF-8 charset in the Content-Type header (Tomcat 11.0.26 defaults the response to UTF-8
when nothing is declared): only the file decoding is wrong. The fix is to put pageEncoding="UTF-8" (or a contentType with a charset) at the top of every JSP, or, once for
the site, page-encoding in a jsp-property-group. The encoding of request parameters, forms and fmt is a separate
topic covered on the character-encoding page of this section.
Non-HTML Content: contentType Beyond text/html
The default contentType is text/html (or text/xml for a JSP document), but the attribute accepts any MIME type, which
makes a JSP a general text template: CSV, plain text, JSON-like text, dynamic CSS or JavaScript, XML feeds. The directive
is the declarative way to set the response content type before any output is produced, and response.setContentType
sets the same property from a scriptlet or a servlet (a call made after the response is committed has no effect, see
Servlet Fundamentals). Dynamic CSS (text/css), JavaScript, plain text,
XML and CSV are all served this way. A header such as Content-Disposition is not part of the directive and has to be set
on the response by the forwarding servlet or a filter; the page below produces only the body. Detecting the client from
User-Agent to pick a format, a technique older JSP texts spend a chapter on, is better replaced today by content
negotiation (the Accept header) in the servlet.
<%@ page contentType="text/csv; charset=UTF-8" trimDirectiveWhitespaces="true" %><%@ taglib prefix="c" uri="jakarta.tags.core" %>id,title,author
<c:forEach var="b" items="${books}">
${b.id},"${b.title}","${b.author}"
</c:forEach>
id,title,author
1,"Dune","Frank Herbert"
2,"Neuromancer","William Gibson"
The response carried Content-Type: text/csv;charset=UTF-8, no <html> wrapper and no blank first line. Without
trimming, the line break that follows an ordinary taglib line becomes the first bytes of the body (compare the 8-byte
and 7-byte bodies in trimDirectiveWhitespaces), which breaks a CSV, an XML declaration that must start at byte 0 and anything else
that is strict about its first character. CSV fields built from user text still need quoting and, for spreadsheets,
protection against cells that start with an equals sign, plus, minus or at sign; c:out escapes XML, not CSV.
isELIgnored, errorOnELNotFound and deferredSyntaxAllowedAsLiteral
Three attributes decide how the page treats ${...} and #{...}. The expression language itself is covered on Expression Language Basics and the switches on EL Configuration & ELProcessor.
isELIgnored="true" passes EL through untouched, which is the right switch for a page that documents EL, generates a
template for another engine or a JavaScript template literal, and is not written for the container’s EL. The default
depends on the web.xml version: Servlet 2.4 and later descriptors evaluate EL; a Servlet 2.3 or older descriptor ignores
it. The web-app of the Bookshelf is version 6.1, so EL is on. A tag file never inherits a default and always evaluates.
<%@ page isELIgnored="true" contentType="text/plain" %>
price is ${1 + 2}
Run on Tomcat 11.0.26 the page answers with the text exactly as written, and no 3 appears:
price is ${1 + 2}
errorOnELNotFound is the new one (Pages 3.1). By default an identifier that no resolver can find, for example the
mistyped ${noSuchName}, evaluates to null and prints as nothing, which hides misspelt attribute names. With
errorOnELNotFound="true" the same expression throws jakarta.el.PropertyNotFoundException. The value can be given
site-wide in web.xml with error-on-el-not-found, and the page attribute overrides it in either direction:
error-on-el-not-found in web.xml |
errorOnELNotFound in the page |
Unknown identifier |
|---|---|---|
unspecified |
unspecified |
|
|
unspecified |
|
|
unspecified |
|
any |
|
|
any |
|
|
<%-- elmissing.jsp --%>
<%@ page errorOnELNotFound="true" contentType="text/plain" %>
Hello [${noSuchName}]
<%-- elmissing-false.jsp --%>
<%@ page contentType="text/plain" %>
Hello [${noSuchName}]
The second page answers Hello []. The first answers 500, and the root cause in the page shown is
jakarta.el.PropertyNotFoundException: Unknown identifier [noSuchName]. Only an unresolvable identifier is affected:
a missing property of an existing bean was already an error, and null in the middle of an expression still behaves as EL specifies. The setting is an easy way to catch typos in
tests, and a development-site switch worth having; but it also turns a missing optional request attribute into a 500, so
applying it blindly in production changes behaviour.
deferredSyntaxAllowedAsLiteral exists because #{ has been reserved since JSP 2.1 for deferred EL. By default the
sequence in template text is a translation error. Verified:
<%-- hash.jsp: fails --%>
<%@ page contentType="text/plain" %>
literal: #{not an expression}
<%-- hash-ok.jsp: prints the text --%>
<%@ page deferredSyntaxAllowedAsLiteral="true" contentType="text/plain" %>
literal: #{not an expression}
The first page fails with #{...} is not allowed in template text (line 2, column 11), the second prints
literal: #{not an expression}. Escaping the sequence (\#{) is the alternative and is covered on the syntax page of this
section. The attribute exists mostly for pages written before JSP 2.1, for which Pages 3.4 defines backwards
compatibility.
trimDirectiveWhitespaces
White space in template text is kept, so each directive on its own line leaves a line break behind. For a stylesheet,
a CSV file or a fragment that is inserted into another page, those line breaks are noise. With
trimDirectiveWhitespaces="true" any template text that consists only of white space is dropped; text that has any
other character keeps all of its white space.
<%-- trim-off.jsp --%>
<%@ page contentType="text/plain" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
Hello
<%-- trim-on.jsp --%>
<%@ page contentType="text/plain" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
Hello
On Tomcat 11.0.26 the first body is 8 bytes (\n\n\nHello\n) and the second is 7 (\n\nHello\n). Only the whitespace-only
text between the two directives was removed. The line break after the last directive is attached to Hello, so the text
node is not whitespace-only and stays. That is why the CSV above puts its directives on one line: trimming does not
remove the line break that leads into real content.
Tomcat also has a container-wide Jasper option, trimSpaces, with the values true, single, false (the default) and
extended; a page or tag file that sets trimDirectiveWhitespaces to true takes precedence over it. The setting is
ignored by JSP documents in XML syntax.
isThreadSafe, Removed in Pages 4.0
JSP 1.x and 2.x had a thirteenth attribute, isThreadSafe (default true). With false the page promised to be unsafe
for concurrent access and the container had to serialize requests to it, which it did through the servlet
SingleThreadModel. Servlet 6.0 removed SingleThreadModel, Pages 3.1 deprecated the attribute because of that, and
Pages 4.0 removed it. The attribute is not in the 4.0 syntax list at all, so the attribute is a translation error today:
<%@ page isThreadSafe="false" %>
x
Verified on Tomcat 11.0.26: [Page directive] has invalid attribute: [isThreadSafe]. Old pages that carry it are
upgraded by deleting it; the page is then handled by many threads at once, like every servlet. Whatever the attribute
used to protect (instance fields declared in <%! %>, non-thread-safe helpers kept as fields) has to be fixed in the code:
Scripting Elements (Legacy) shows why instance fields in a JSP are shared state and how to replace them
with request attributes and EL.
The include Directive
<%@ include file="relativeURL" %> pastes the text of another file into the page while the page is translated. The
included file becomes part of the same translation unit, so it is parsed as JSP, shares one generated servlet with the
including page, and sees everything declared before it: tag library prefixes, imports, variables created by earlier
actions. This is also why it is different from the request-time jsp:include action: the directive is a compile-time
copy, the action is a call to another resource that returns its own output. The file attribute is a path relative to the
including file (or context-relative when it starts with /), and it must be a literal: a request-time expression cannot
be used, because the include is finished before any request.
| Syntax | When | Path is relative to | The target is treated as |
|---|---|---|---|
|
Translation time |
the including file |
Static text that the JSP container parses and merges |
|
Request time |
the current page |
A resource that is called; its output is included |
The Bookshelf shares a header and footer through .jspf fragments under /WEB-INF/jspf/:
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<header>${"Bookshelf"}</header>
<footer>Bookshelf, 2 titles</footer>
<%@ page contentType="text/plain" trimDirectiveWhitespaces="true" %>
<%@ include file="/WEB-INF/jspf/header.jspf" %>
<c:out value="${books[0].title}"/>
<%@ include file="../jspf/coda.jspf" %>
The page uses c:out without declaring the c prefix: the taglib directive that does it is inside header.jspf, which is
part of the translation unit. The response is:
<header>Bookshelf</header>
Dune<footer>Bookshelf, 2 titles</footer>
Both path forms worked, the context-relative /WEB-INF/jspf/header.jspf and the file-relative ../jspf/coda.jspf. A path
that does not resolve fails at translation: JSP file [/WEB-INF/jspf/none.jspf] not found.
Three consequences are worth knowing:
-
The syntax of the included file is detected independently. A standard-syntax page can include a file in XML syntax and the reverse; the container parses each file by its own rules and requires an XML file to be well formed.
-
Because the file is pasted, the start tag and the end tag of a JSP action or custom action must be in the same file (
<c:if>in the page and</c:if>in the fragment is rejected); plain HTML may span files. -
A change to the included file only affects the including page after the page is translated again. The specification says a container may track included files for that and gives the author no way to declare the dependency. Check the Tomcat documentation for how Jasper detects it (the
developmentandmodificationTestIntervalsettings) before depending on it, and restart or touch the page after editing a fragment when in doubt.
Put included fragments under /WEB-INF/ so that a visitor cannot request them directly. The usual name .jspf is only a
convention; what makes a file a JSP segment is that something includes it. The request-time alternative, jsp:include and
c:import, with the forward-versus-redirect trade-offs, is the subject of Includes, Forwards & Page Composition.
The taglib Directive
<%@ taglib uri="..." prefix="..." %> or <%@ taglib tagdir="/WEB-INF/tags" prefix="..." %> makes a tag library
available to the page and binds a prefix to it. The container resolves the identifier to a tag library descriptor (TLD):
a uri is looked up among the TLDs in the application and its jars (a TLD declares its own <uri>), and a tagdir is a
directory of tag files under /WEB-INF/tags, for which the container builds an implicit TLD (see Tag Library Descriptors & EL Functions and Tag Files & Layouts).
| Attribute | Rules |
|---|---|
|
An absolute or relative URI that identifies the TLD. Jakarta Tags 3.0 uses |
|
A directory of tag files, starting with |
|
Required and non-empty; follows the XML namespace naming rules. The prefixes |
Run against Tomcat 11.0.26 with the JSTL 3.0.1 implementation, the five standard URIs resolved:
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="f" uri="jakarta.tags.functions" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<%@ taglib prefix="x" uri="jakarta.tags.xml" %>
<%@ taglib prefix="sql" uri="jakarta.tags.sql" %>
ok ${f:length("Dune")}
The page answered ok 4. A tagdir library is used in the same way (see the tag file example under
Tag File Directives: tag, attribute and variable). Verified failures, each with the message Tomcat 11.0.26 printed:
-
an identifier no TLD declares:
The absolute uri: [urn:example:nothing] cannot be resolved in either web.xml or the jar files deployed with this application -
both
uriandtagdir:Both 'uri' and 'tagdir' attributes specified -
a
tagdiroutside the permitted directory:Tag file directory [/tags] does not start with "/WEB-INF/tags" -
a reserved prefix,
prefix="jsp": aValidation error messages from TagLibraryValidatormessage about the already declaredxmlns:jsp -
using a prefix before its
taglibdirective:The prefix [c] specified in this tag directive has been previously used by an action in file … line [1]
The last one matters when a page uses an action in a fragment that appears before the page’s own taglib line: declare the
libraries in the prelude, in the first lines of the page, or in the same fragment that uses them. One more property is that
a prefix, once bound, applies to the whole translation unit, and redefining it to a different URI in an included file is a
translation error.
The http://java.sun.com/jsp/jstl/core style URIs still resolved on this setup (a page with
uri="http://java.sun.com/jsp/jstl/core" printed its output), because the JSTL 3.0.1 implementation jar still ships
legacy descriptors such as c-1_2.tld, but they are not the identifiers the Jakarta Tags 3.0 specification documents and a
later implementation may drop them. Write jakarta.tags.* in new code and replace the old URIs when pages are migrated.
Implicit Includes and JSP Property Groups
Almost every JSP starts with the same few lines: the taglib directives and the imports. The jsp-config section of
web.xml lets a group of pages (selected by a url-pattern) get those lines, and a number of other defaults, without
repeating them. Each jsp-property-group can contain these elements:
| Element | Same effect as | Notes |
|---|---|---|
|
— |
Required, can repeat. Same pattern rules as servlet mappings |
|
|
Group default; the page attribute overrides it |
|
|
Pages 3.1 and later |
|
|
Encoding of the JSP file |
|
— (no page attribute) |
|
|
— |
Marks the matched files as JSP documents (XML syntax) |
|
|
Context-relative path; several allowed, in order |
|
|
Same, for the end of the page |
|
|
Default |
|
|
Default |
|
|
Used when the page has no |
|
|
Same values as the page attribute |
|
— |
|
The Bookshelf’s /WEB-INF/views/pg/ pages use one group:
<jsp-config>
<jsp-property-group>
<url-pattern>/WEB-INF/views/pg/*</url-pattern>
<el-ignored>false</el-ignored>
<error-on-el-not-found>true</error-on-el-not-found>
<include-prelude>/WEB-INF/jspf/prelude.jspf</include-prelude>
<include-coda>/WEB-INF/jspf/coda.jspf</include-coda>
<trim-directive-whitespaces>true</trim-directive-whitespaces>
<default-content-type>text/html;charset=UTF-8</default-content-type>
</jsp-property-group>
</jsp-config>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ page import="org.example.bookshelf.Book" %>
<h1>Bookshelf</h1>
<c:forEach var="b" items="${books}">
<p>${b.title} (${b.year})</p>
</c:forEach>
The page has no taglib and no contentType, and works. The response (status 200, Content-Type: text/html;charset=UTF-8)
is:
<h1>Bookshelf</h1>
<p>Dune (1965)</p>
<p>Neuromancer (1984)</p>
<footer>Bookshelf, 2 titles</footer>
Three further checks, all run: a page in that group with ${noSuchName} answers 500 with
PropertyNotFoundException: Unknown identifier [noSuchName] (the group sets error-on-el-not-found), and the same page
with <%@ page errorOnELNotFound="false" %> answers <p>Hello []</p> followed by the coda (the page overrides the
group). The include-coda content comes after the page’s output. A common pitfall is the url-pattern: it follows the
servlet mapping rules, so /WEB-INF/views/pg/*.jsp (a path prefix and an extension) is rejected when the application is
deployed: Invalid <url-pattern> [/WEB-INF/views/pg/*.jsp] in servlet mapping. Use /WEB-INF/views/pg/* or *.jsp.
When more than one group matches a page, the prelude and coda elements of all matching groups apply, in the order the
groups appear in web.xml; the "most specific wins" rule of other mappings does not apply here. Preludes and codas follow
the rules of statically included files, so start and end tags must be balanced in each file.
The group also controls scripting. scripting-invalid is the lever for banning scriptlets from a code base: set it to
true for *.jsp and any <% ... %>, <%! ... %> or <%= ... %> becomes a translation error, which makes the
migration to EL, JSTL and tag files enforceable. The Bookshelf pages that were run for the error cases above use scriptlets
deliberately and so would fail under that setting.
Tag File Directives: tag, attribute and variable
A tag file (.tag, or .tagx in XML syntax) under /WEB-INF/tags is a custom action written in JSP syntax (Tag Files & Layouts builds them in full). It is not a
page, so some directives change: page is an error in a tag file (use tag), taglib and include work the same, and
three directives are only valid in tag files. Verified both ways on Tomcat 11.0.26: a <%@ tag %> in a JSP page fails with
[<%@ tag] directive can only be used in a tag file, and a <%@ page %> in a tag file fails with
[<%@ page] directive cannot be used in a tag file.
| Attribute | Meaning and default |
|---|---|
|
Short name for tools. Default: the tag file name without |
|
|
|
Accepts attributes with names not declared. The value is the name of a page-scoped |
|
Paths of icon images for tools |
|
Documentation strings |
|
As in the |
|
As in the |
| Attribute | Meaning and default |
|---|---|
|
Required and unique within the tag file |
|
Default |
|
|
|
Whether the value may be a runtime expression. Unlike the TLD element, defaults to |
|
Runtime type of the value. Default |
|
Documentation string |
|
Declare a deferred value or method expression attribute (for example for Faces); |
| Attribute | Meaning and default |
|---|---|
|
A constant variable name. Exactly one of |
|
The name comes from the value of another (translation-time, required, non- |
|
The local name that holds the value inside the tag file; the container synchronizes it with the caller’s variable |
|
Class of the variable. Default |
|
Whether the variable is declared in the caller after the tag. Default |
|
|
|
Documentation string |
The Bookshelf’s bookCard tag uses all three. It takes a required typed attribute, an optional fragment, a name given by
an attribute, and a constant-name variable that survives the tag:
<%@ tag description="Renders one book" body-content="scriptless" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" %>
<%@ attribute name="book" required="true" type="org.example.bookshelf.Book" description="The book to show" %>
<%@ attribute name="heading" fragment="true" %>
<%@ attribute name="varName" required="true" rtexprvalue="false" %>
<%@ variable name-from-attribute="varName" alias="label" variable-class="java.lang.String" scope="NESTED" %>
<%@ variable name-given="age" variable-class="java.lang.Long" scope="AT_END" declare="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:set var="label" value="${book.title} by ${book.author}"/>
<c:set var="age" value="${2026 - book.year}"/>
<article><jsp:invoke fragment="heading"/>${label}<jsp:doBody/></article>
<%@ page contentType="text/plain; charset=UTF-8" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="bs" tagdir="/WEB-INF/tags" %>
<c:forEach var="b" items="${books}" end="0">
<bs:bookCard book="${b}" varName="t"><jsp:attribute name="heading">[card]</jsp:attribute><jsp:body> /inner:${t}</jsp:body></bs:bookCard>
age=${age}
</c:forEach>
The response is the card, then age computed by the tag and visible to the page after the tag ended (AT_END):
<article>[card]Dune by Frank Herbert /inner:Dune by Frank Herbert</article>
age=61
The variable named by the caller (varName="t") was visible inside the tag body as t (NESTED), the value of the alias
label was synchronized with it, and age was visible after the tag (AT_END). Two points. First, varName must be a
literal string (rtexprvalue="false"): the container needs the name at translation time. Second, arithmetic in EL on a
Long field such as 2026 - book.year produces a Long, so the first version of this tag, with variable-class="java.lang.Integer",
failed on the first request with java.lang.ClassCastException: class java.lang.Long cannot be cast to class
java.lang.Integer: the variable-class is a promise about the runtime type and the container casts to it. The tag
mechanics themselves (fragments, jsp:invoke and jsp:doBody, variable synchronization, packaging as a JAR with a TLD)
belong to the custom-tags pages of this section.
The XML Forms of the Directives
Each directive has an XML equivalent for JSP documents (.jspx): jsp:directive.page, jsp:directive.include, and, in tag
files, jsp:directive.tag, jsp:directive.attribute and jsp:directive.variable. The taglib directive has no element
form: in XML syntax a tag library is a namespace declaration on the root element
(xmlns:c="jakarta.tags.core"). The attributes are the same as in the <%@ %> syntax.
<jsp:directive.page contentType="text/plain" session="false"/>
xml-form ok
The specification defines these elements for JSP documents. Tomcat 11.0.26 also accepted this page as a normal .jsp in
standard syntax and honoured it (the response was Content-Type: text/plain;charset=UTF-8 with no session cookie), which is
tolerated behaviour rather than a promise; keep the element for .jspx files and the <%@ %> form for .jsp. JSP
documents, including is-xml and the other XML-specific rules, are covered on the jsp-documents-xml-syntax page of this
section.
Security and Operations Notes
-
Errors are information. A page without
errorPageand without aweb.xmlerror-pageshows the container’s own report, which on Tomcat 11.0.26 includes the source lines of the failing page and a stack trace (the runs above returned them). An error page declared once inweb.xml, or per page witherrorPage, avoids that. The error page must not echo exception messages from untrusted sources. -
isELIgnored="true"is not a security feature. It stops EL from being evaluated on that page, which is useful when a template contains user-visible${...}text, but it does nothing about output escaping. Output is still escaped withc:outorfn:escapeXml. -
Do not let a path come from the request. The
includedirective cannot, because itsfileis a literal, but thejsp:includeaction andc:importaccept expressions, and apage="${param.p}"is a file disclosure risk. The includes page of this section covers it. -
scripting-invalidin a property group removes the whole class of scriptlet-based injection, and costs nothing for a code base that already uses EL and JSTL. -
session="false"on public pages reduces the surface for session fixation and cookie handling, and reduces memory. -
info,errorOnELNotFoundandtrimDirectiveWhitespacescarry no security risk in themselves, but a page that printsinfomay disclose the version of an internal component. -
JSP files and fragments belong under
/WEB-INF/so the only way to reach them is through a servlet or a filter, which also keeps anyerrorPageconfiguration from being bypassed. A JSP in the web root can be requested directly with no data set up for it.
Pitfalls and Error Names
All the messages below are the ones Tomcat 11.0.26 returned in the runs on this page; the exception class is
org.apache.jasper.JasperException for translation problems unless stated otherwise.
| Symptom or message | Cause | Fix |
|---|---|---|
Mojibake such as |
No |
|
|
The attribute was removed in Pages 4.0 |
Delete it, and make shared state thread-safe |
|
Two |
Keep one value; a prelude and the page must agree |
|
No |
|
|
|
Choose a buffer, or remove |
HTTP 200 with half a page followed by the error page’s markup |
The buffer was flushed before the exception |
A larger |
|
Output bigger than the buffer with |
|
|
|
Use |
|
The page is not marked |
Add |
|
A literal |
|
|
|
Fix the name or put it in a scope |
|
|
Call |
|
|
Use the context-relative path with a leading |
|
No TLD declares that URI; the tag library jar is missing |
Add the JSTL implementation dependency; check the |
|
|
Move the |
|
The two forms were mixed in one directive |
Two directives with two prefixes |
|
|
Move tag files under |
|
|
Use |
|
|
Declare |
|
A |
|
|
|
Remove |
What Changed Recently
| Version | Change |
|---|---|
JSP 2.0 |
EL and |
JSP 2.1 |
|
JSP 2.x, after the JSP 1.2 era of the books |
|
JSP 3.0 (Jakarta EE 9) |
Package rename |
JSP 3.1 (Jakarta EE 10) |
|
Pages 4.0 (Jakarta EE 11) |
|
Jakarta Tags 3.0 |
The tag library identifiers are |
The specification document is still titled "Jakarta Server Pages" while the project page says "Jakarta Pages"; the introduction page of this section explains the naming, and this page uses the shorter current name.
Where the Books Are Outdated
The JSP book consulted for this section (a JSP 1.2 text) and the JSTL book (a JSTL 1.0 text) cover the same three
directives, and the concepts carried over unchanged: directives are translation-time messages, the page attributes
configure the generated servlet, include merges source while an include action calls another resource, and a taglib
binds a prefix. What has moved since:
| In the JSP 1.2 and JSTL 1.0 era | Today | Where it matters |
|---|---|---|
|
Removed in Pages 4.0; unrecognized attribute |
Old pages fail to translate until it is deleted |
Scriptlets that import |
The implicit imports are |
Scriptlet code that names |
|
|
Replace them while migrating, even though 3.0.1 still resolves them |
No tag files, so no |
Tag files replace most simple custom tags |
A tag file is declared with three directives, not a Java class and a TLD |
No |
All four exist, with group-level equivalents |
Whitespace and EL behaviour can be tuned per site |
File encoding left to the server |
The specification’s fallback for the file is ISO-8859-1, while Tomcat 11.0.26 answered with a UTF-8 response charset |
The mojibake case above is the mismatch between the two |
Scripting-heavy pages with |
Scripting is legacy; |
Teach EL, JSTL and tag files first |
Non-HTML content (MIME types, CSV, dynamic CSS, code generation) through |
Same directive; |
Whitespace before the first byte of a CSV or XML document |
The books are used here only to decide which topics deserve coverage; no text, listing or figure from them appears on this page.