Tag Files & Layouts

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 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 with the JSTL implementation org.glassfish.web:jakarta.servlet.jsp.jstl 3.0.1. Everything was verified on 2026-10-08: the rules against chapters 1, 5 and 8 of the Pages 4.0 specification and the Javadoc of JspFragment, SimpleTagSupport and DynamicAttributes, and every example by building it into a Maven WAR (context path /g6tagfiles) and requesting it with curl on Tomcat 11.0.26; the outputs and error messages shown are the ones that run produced (blank lines produced by the directives are removed from the listings). A tag file is a custom tag written in JSP syntax: a page author who knows no Java packages a piece of markup, gives it typed attributes and a body, and calls it with an XML-style element, <layout:page title="...">. Since JSP 2.0 the container must recognise tag files and compile each one into a tag handler, so no tag library descriptor and no Java class is needed for the usual case. This page covers where tag files live and how the container finds them, the three directives that describe a tag (tag, attribute and variable), the two actions that exist only inside tag files (jsp:doBody and jsp:invoke), how a tag file isolates its page scope and exchanges variables with its caller, dynamic attributes, tag files in XML syntax, packaging in a JAR, and then the two things Bookshelf pages use them for: a layout template (the JSP answer to Thymeleaf layouts and to Struts Tiles) and reusable components. It builds on the includes and page composition page, whose Pattern 4 points here, and on the directives page. The classic and simple tag handlers written in Java, and their tag library descriptors and EL functions, have pages of their own in this part.

What a Tag File Is

A tag file is a source file with the extension .tag (JSP syntax) or .tagx (XML syntax) whose content is JSP: template text, EL, standard and custom actions, and, if you insist, scriptlets. It differs from a JSP page in four ways. It is called as a custom action, with attributes and an optional body. It has its own directives (tag, attribute, variable) instead of page. It has two actions a page does not (jsp:doBody and jsp:invoke). And it has its own page scope, so nothing leaks in or out unless the tag file declares it.

The specification leaves the implementation to the container: it may compile the file to a Java tag handler or interpret it, as long as the observable behaviour is the same. Jasper in Tomcat 11 compiles each tag file to a class that extends jakarta.servlet.jsp.tagext.SimpleTagSupport, which means a tag file is a simple tag handler whose doTag method contains the translated body of the file. The generated source of WEB-INF/tags/ui/button.tag below was found in the work directory as org/apache/jsp/tag/web/ui/button_tag.java (public final class button_tag extends SimpleTagSupport implements DynamicAttributes), which is where to look when a tag file misbehaves; the build and the generated sources are described in Compilation, Reloading and Where the Generated Code Is.

Table 1. A tag file next to the other ways of sharing markup
Mechanism What it shares Parameters and body Page scope

include directive

Source text, merged at translation time

None, the including page’s variables are visible

Same as the page

jsp:include

The output of another resource, at request time

String parameters through jsp:param

Separate, a new request-level dispatch

Tag file

Markup and logic, compiled into a tag handler

Typed attributes, fragments and a body, a declared set of variables out

Separate, with explicit synchronization

Simple or classic tag handler in Java

Logic written in Java

Attributes and body, described by a TLD

Whatever the handler does

Thymeleaf fragment or Tiles definition

Markup of another engine

See the other pages of this guide

Not JSP

Tag files are current and stable: they were added in JSP 2.0, gained deferred-expression attributes and implicit.tld in JSP 2.1, and the list of changes of the Pages 4.0 specification (Java 17 minimum) names nothing in their chapter. They are the right default for reusable markup in a JSP application. They are not a good place for business logic or data access, and the Bookshelf follows that rule: data comes from the servlet or a service, and a tag file only renders it.

The Bookshelf Used on This Page

The scratch application of this page is the Bookshelf of the getting started page with the three books loaded by a ServletContextListener into the application attribute books. A Book is a record, and EL 6 reads record components as properties (${book.title}).

The Book record
package org.example.bookshelf;

public record Book(long id, String title, String author, int year, int rating) {
}

The layout of the web application, with the tag files this page builds, is:

The files of the scratch application
src/main/webapp
├── index.jsp, about.jsp, nested.jsp, tabs.jsp, sync.jsp, jar.jsp, implicit.jsp
└── WEB-INF
    ├── web.xml
    ├── lib/bookshelf-ui.jar           tag files packaged in a JAR (META-INF/tags + a TLD)
    └── tags
        ├── layout/page.tag            the page skeleton
        ├── layout/catalogue.tag       a layout built on layout/page.tag
        ├── ui/bookCard.tag, navLink.tag, rating.tag, each.tag, summary.tag
        ├── ui/button.tag, shout.tag, raw.tag, tabs.tag, note.tagx
        ├── sync/atbegin.tag, nested.tag, atend.tag, remove.tag
        └── versioned/implicit.tld, hello-world.tag, parts.tag, footer.tagf

The diagram shows the idea of the whole page: the catalogue page calls a layout tag file, which calls another, and each call is a new page scope.

Tag files on disk and the three nested page scopes of a layout call

Where Tag Files Live and How They Are Found

The container recognises tag files in exactly two places: a directory tree under /WEB-INF/tags/ of the web application, or a tree under /META-INF/tags/ of a JAR in /WEB-INF/lib/. Only files with the extension .tag or .tagx count; the recommended extension for a segment included into a tag file is .tagf, the counterpart of .jspf. A tag file anywhere else is not a tag extension, and the container treats it as content to be served.

Table 2. Where tag files are recognised
Location How the page imports it Notes

/WEB-INF/tags/ and each subdirectory

<%@ taglib prefix="ui" tagdir="/WEB-INF/tags/ui" %>

An implicit tag library per directory, no descriptor needed. There is no relationship between a directory and its subdirectories.

/META-INF/tags/... in a JAR under /WEB-INF/lib/

<%@ taglib prefix="bsui" uri="https://example.org/bookshelf/ui" %>

Needs a tag library descriptor with a tag-file element; a tag file in a JAR that no descriptor names is ignored.

/WEB-INF/tags/... named from a descriptor

As any library, through its uri

A tag-file element in a web application’s TLD may name a file under /WEB-INF/tags.

Anywhere else (/tags/leak.tag)

Not importable

Served as plain content: see Security Notes.

The tagdir attribute of the taglib directive must start with /WEB-INF/tags. A directory elsewhere is rejected when the page is translated:

org.apache.jasper.JasperException: Tag file directory [/tags] does not start with "/WEB-INF/tags"
org.apache.jasper.JasperException: Tag file directory [/WEB-INF/tagz] does not start with "/WEB-INF/tags"

A directory that exists but holds no tag of the requested name gives the usual message of any library:

org.apache.jasper.JasperException: /bad/no-tag-in-dir.jsp (line: [2], column: [0]) No tag [nothere] defined in tag library imported with prefix [b]

The implicit tag library of a directory

For every directory under /WEB-INF/tags/ the container behaves as if a descriptor existed with a tlib-version of 1.0, a short-name derived from the directory (the path below /WEB-INF/tags/ with every / replaced by -, or tags for the root), and one tag-file element per file whose name is the file name without .tag or .tagx and whose path is the file’s path from the web application root. Two consequences are worth remembering. The name of the file is the name of the tag, so bookCard.tag is called <ui:bookCard> and hello-world.tag is called <v:hello-world> (a hyphen is allowed; a space or a dot is not a good idea). And short names are not unique (/WEB-INF/tags/a-b/ and /WEB-INF/tags/a/b/ both give a-b), which does not matter because the page chooses the prefix itself.

A .tagf segment is pulled into a tag file with the include directive. The example below also uses the file name with a hyphen.

WEB-INF/tags/versioned/hello-world.tag and parts.tag
<%@ tag body-content="empty" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
hello with a hyphen; ${pageContext.request.method}
<%@ tag body-content="empty" %>
<%@ include file="/WEB-INF/tags/versioned/footer.tagf" %>
implicit.jsp
<%@ page contentType="text/plain; charset=UTF-8" %>
<%@ taglib prefix="v" tagdir="/WEB-INF/tags/versioned" %>
<v:hello-world/>
<v:parts/>
Requesting implicit.jsp
curl http://localhost:8080/g6tagfiles/implicit.jsp
hello with a hyphen; GET
included from a .tagf segment

implicit.tld: the version of the library

The JSP version of an implicit library defaults to 2.0, which matters: the attributes introduced in JSP 2.1 (deferredValue, deferredValueType, deferredMethod, deferredMethodSignature) are a translation error in a tag file whose library is still at version 2.0. A file named implicit.tld in the directory sets the library’s jsp-version and tlib-version; the container ignores its short-name, rejects any other element, and rejects a jsp-version below 2.0. Both directories below hold the same def.tag; only the second has an implicit.tld with jsp-version 4.0.

WEB-INF/tags/new/implicit.tld and def.tag (the directory old/ has def.tag only)
<?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_4_0.xsd"
        version="4.0">
  <tlib-version>2.3</tlib-version>
  <jsp-version>4.0</jsp-version>
</taglib>
<%@ attribute name="v" deferredValue="true" %>
deferred ok

Calling <t:def v="#{x}"/> from each directory gives:

old/ : org.apache.jasper.JasperException: Invalid JSP version defined for tag file at [/WEB-INF/tags/old/def.tag]
new/ : deferred ok

If a directory holds both a.tag and a.tagx, the specification says the library is invalid. Tomcat 11.0.26 does not reject it: with a sameName.tag containing FROM-TAG and a sameName.tagx containing FROM-TAGX, the call printed FROM-TAG. Do not rely on that.

A First Tag File: the Rating

The smallest useful tag file in the Bookshelf prints a rating. It has no body, one required attribute and one line of markup.

WEB-INF/tags/ui/rating.tag
<%@ tag body-content="empty" pageEncoding="UTF-8" %>
<%@ attribute name="value" required="true" type="java.lang.Integer" %>
<span class="rating" title="${value} out of 5">${value} / 5</span>
Calling it from a page
<%@ taglib prefix="ui" tagdir="/WEB-INF/tags/ui" %>
<ui:rating value="${book.rating}"/>
<span class="rating" title="5 out of 5">5 / 5</span>

Three things happened. The taglib directive with tagdir made every tag file of the directory available under the prefix ui. The attribute directive declared value, so the call is checked when the page is translated, not when it runs: a missing value or an unknown attribute stops the translation. And inside the tag file the attribute is a page-scoped variable named value, so ${value} finds it with the normal EL search.

org.apache.jasper.JasperException: /bad/missing-attr.jsp (line: [2], column: [0]) According to the TLD or the tag file, attribute [label] is mandatory for tag [emptyTag]
org.apache.jasper.JasperException: /bad/unknown-attr.jsp (line: [2], column: [0]) Attribute [other] invalid for tag [emptyTag] according to TLD

The Directives of a Tag File

Table JSP.8-1 of the specification lists which directives a tag file may use: taglib and include behave as in a page, tag, attribute and variable exist only in tag files (using them in a JSP is a translation error), and page is not allowed in a tag file. The errors are:

/bad/tag-directive-in-jsp.jsp (line: [1], column: [8]) [<%@ tag] directive can only be used in a tag file
/bad/attr-directive-in-jsp.jsp (line: [1], column: [14]) [<%@ attribute] directive can only be used in a tag file
/WEB-INF/tags/bad/usesPage.tag (line: [1], column: [9]) [<%@ page] directive cannot be used in a tag file

The tag directive

The tag directive describes the tag as a whole and is the tag file’s counterpart of the page directive: it may appear more than once, its attributes apply to the whole file, a duplicate attribute with a different value is a translation error, and an unrecognised attribute is a translation error.

Table 3. The attributes of the tag directive (Table JSP.8-2)
Attribute Default Meaning

display-name

File name without extension

Short name for tools.

body-content

scriptless

empty, tagdependent or scriptless. Any other value, including JSP, is a translation error.

dynamic-attributes

none

Names a page-scoped variable that receives a Map of the dynamic attributes; the tag handler then implements DynamicAttributes. See Dynamic Attributes.

small-icon, large-icon

none

Paths of images for tools.

description, example

none

Free text for tools.

language

java

As in the page directive.

import

none

As in the page directive; repeated uses accumulate.

pageEncoding

as for pages

As in the page directive, with no web.xml counterpart; not allowed in a .tagx.

isELIgnored

as for pages

As in the page directive; no property-group default applies to tag files.

errorOnELNotFound

as for pages

As in the page directive; no web.xml counterpart.

deferredSyntaxAllowedAsLiteral

as for pages

As in the page directive; an error in a tag file of a library below JSP 2.1 by the specification (Tomcat 11.0.26 did not enforce that).

trimDirectiveWhitespaces

as for pages

As in the page directive; see the whitespace pitfall in Dynamic Attributes.

A tag file is not governed by jsp-property-group elements of web.xml: the table says that isELIgnored, errorOnELNotFound, pageEncoding, deferredSyntaxAllowedAsLiteral and trimDirectiveWhitespaces have no global counterpart for tag files, as the configuration page explains. The tag file has to say it itself.

A tag directive with a description, an empty body and an explicit trim
<%@ tag description="A small star rating" body-content="empty" trimDirectiveWhitespaces="true" %>

An invalid body-content fails when a page that uses the tag is translated:

org.apache.jasper.JasperException: /WEB-INF/tags/bad/bodyContent.tag (line: [1], column: [2]) Invalid body-content [JSP] in tag directive

The attribute directive

The attribute directive is the tag file’s counterpart of the attribute element of a descriptor. Each attribute of the tag is declared once; the declaration controls whether it is required, whether it accepts expressions, its Java type, and whether it is a fragment.

Table 4. The attributes of the attribute directive (Table JSP.8-3)
Attribute Default Meaning

name

required

The attribute’s name. Declaring the same name twice, or using the name of a variable directive’s name-given or of dynamic-attributes, is a translation error.

required

false

Whether the caller must pass it.

fragment

false

true makes the attribute a JspFragment; the type is then fixed and a type is an error, and rtexprvalue is fixed at true.

rtexprvalue

true

Whether the value may be an expression. Unlike the descriptor element, this defaults to true. false makes it a translation-time constant.

type

java.lang.String

The Java type of the value. A primitive type is an error by the specification.

description

none

Free text.

deferredValue

false, true if deferredValueType is set

The attribute is a deferred value expression (#{...}); needs library version 2.1 or later.

deferredValueType

java.lang.Object when deferredValue

The expected type of the expression result.

deferredMethod

false, true if deferredMethodSignature is set

The attribute is a deferred method expression.

deferredMethodSignature

void methodname()

The signature of the method.

The Bookshelf’s book card shows the usual declarations: a required attribute with a record type, an optional boolean, and a body.

WEB-INF/tags/ui/bookCard.tag
<%@ tag description="One book as a card" body-content="scriptless" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="ui" tagdir="/WEB-INF/tags/ui" %>
<%@ attribute name="book" required="true" type="org.example.bookshelf.Book" description="The book to show" %>
<%@ attribute name="highlight" type="java.lang.Boolean" description="Adds the featured class" %>
<article class="card${highlight ? ' featured' : ''}" id="book-${book.id}">
  <h2><c:out value="${book.title}"/></h2>
  <p><c:out value="${book.author}"/> (${book.year}) <ui:rating value="${book.rating}"/></p>
  <div class="actions"><jsp:doBody/></div>
</article>

Note that the tag file declares its own taglib directives: the c and ui prefixes of the calling page are not inherited. Without a taglib for c, a line such as <c:out value="hello"/> in the tag file is not an error: it is copied to the output as template text.

<c:out value="hello"/>

How attribute values reach the tag file

An attribute that is not declared as a fragment or as a deferred expression becomes a page-scoped variable of the tag file with the same name, set to the value passed in. An optional attribute that was not passed creates no variable, so EL sees null. The value is converted to the declared type by the same rules as any attribute of a custom action, so passing a string to an Integer attribute converts it and a bad string fails at run time:

WEB-INF/tags/bad/typed.tag and its calls
<%@ attribute name="n" type="java.lang.Integer" %>
n=${n + 1}
<b:typed n="41"/>
<b:typed n="abc"/>
n=42
java.lang.NumberFormatException: For input string: "abc"

Four rules about values deserve a table of their own.

Table 5. Attribute rules that surprise people
Situation Result on Tomcat 11.0.26 Cause

rtexprvalue="false" attribute called with var="${param.v}"

According to TLD or attribute directive in tag file, attribute [var] does not accept any expressions

Static attributes are constants of the page; this is also what protects c:set var from request data.

type="int" (a primitive)

A Java compiler error in the generated class (The operator != is undefined for the argument type(s) int, null), not a clean translation error

The specification requires a translation error; use the wrapper type.

A fragment attribute called with plain text (first="plain text")

Unable to convert string [plain text] to class [jakarta.servlet.jsp.tagext.JspFragment] for attribute [first]

A fragment is given with jsp:attribute, see Fragments: jsp:attribute, jsp:body and jsp:invoke.

fragment="true" together with type

Cannot specify both 'fragment' and 'type' attributes. If 'fragment' is present, 'type' is fixed as 'jakarta.servlet.jsp.tagext.JspFragment'

The type is fixed.

Two attribute directives with the same name

The value of [the name attribute of the attribute directive] and the value of [the name attribute of the attribute directive] in line [1] are the same.

Names are unique within a translation unit.

The Body: scriptless, tagdependent, empty and jsp:doBody

The body-content of the tag directive says what the caller may put between the start and end tags.

Table 6. The three body contents of a tag file
Value What the caller may write What the tag file sees

scriptless (default)

Template text, EL, standard and custom actions; no scriptlets, declarations or scripting expressions

A JspFragment that jsp:doBody evaluates, in the caller’s page scope.

tagdependent

Anything; the container does not interpret it

The body as text; jsp:doBody copies it unchanged, so EL in it is not evaluated.

empty

Nothing

No body; a body in the call is a translation error.

jsp:doBody

jsp:doBody is available only in tag files (in a page it is a translation error). It runs the body of the call and sends its output to the current JspWriter, or captures it. The body is a JspFragment evaluated in the page scope of the caller, which is why a body written in a page sees the page’s variables but not the tag file’s.

Table 7. Attributes of jsp:doBody (Table JSP.5-8)
Attribute Default Meaning

var

none

Name of a scoped attribute that receives the output as a String. Cannot be combined with varReader.

varReader

none

Name of a scoped attribute that receives the output as a resettable java.io.Reader.

scope

page

page, request, session or application; an error without var or varReader. A session scope throws IllegalStateException at run time when the page has no session.

If the call had no body, jsp:doBody behaves as if it had an empty one. An element with a non-empty body is a translation error.

Capturing the body is how a tag transforms it. The shout tag upper-cases whatever its caller wrote, using a JSTL function.

WEB-INF/tags/ui/shout.tag, raw.tag and the page that uses them
<%@ tag body-content="scriptless" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
<jsp:doBody var="text"/>${fn:toUpperCase(text)}
<%@ tag body-content="tagdependent" trimDirectiveWhitespaces="true" %>
<pre><jsp:doBody/></pre>
<layout:page title="About" active="about">
  <p>A tiny catalogue. <ui:shout>loud</ui:shout></p>
  <ui:note level="warn">Beta &amp; <b>bold</b></ui:note>
  <ui:raw>${notEvaluated} <b>x</b></ui:raw>
</layout:page>

The relevant lines of the response of about.jsp:

<p>A tiny catalogue. LOUD</p>
<div class="note warn">Beta &amp; <b>bold</b></div>
<pre>${notEvaluated} <b>x</b></pre>

A scriptless body that contains a scriptlet is rejected, and a body in a call to an empty tag is too:

org.apache.jasper.JasperException: /bad/empty-body.jsp (line: [2], column: 0) According to TLD, tag [b:emptyTag] must be empty, but is not

Fragments: jsp:attribute, jsp:body and jsp:invoke

A fragment is a piece of JSP code, written by the caller, that the tag file can run zero or more times. The body of a tag is one; a tag can also receive named fragments through attributes declared with fragment="true". The caller writes a fragment attribute as a jsp:attribute child element, and when it does, the body has to be written inside jsp:body: the text between the tags may not contain anything else.

A call with named fragments and an explicit body
<layout:page title="Catalogue" active="catalogue">
  <jsp:attribute name="head"><link rel="stylesheet" href="${pageContext.request.contextPath}/bookshelf.css"></jsp:attribute>
  <jsp:attribute name="sidebar"><p>Sidebar for ${books.size()} books</p></jsp:attribute>
  <jsp:body>
    <p>The body of the page.</p>
  </jsp:body>
</layout:page>

The mistake is to mix the two styles. With jsp:attribute present, text outside jsp:body is rejected:

<layout:page title="x">
  <jsp:attribute name="head"><meta name="x" content="y"></jsp:attribute>
  some text outside jsp:body
</layout:page>
org.apache.jasper.JasperException: /bad/mixed-body.jsp (line: [4], column: [2]) Must use jsp:body to specify tag body for [<layout:page] if jsp:attribute is used.

jsp:invoke

jsp:invoke runs a named fragment attribute, in tag files only. It takes the name of the attribute in fragment and the same var, varReader and scope attributes as jsp:doBody. A fragment that the caller did not pass is null, and the specification says jsp:invoke then behaves as if the fragment produced no output, which is why an optional head or sidebar needs no test. The action must have no body.

Table 8. Attributes of jsp:invoke (Table JSP.5-7)
Attribute Default Meaning

fragment

required

The name of the fragment attribute.

var, varReader, scope

as for jsp:doBody

Capture the output as a String or a Reader in a scoped attribute instead of writing it.

The tabs tag below invokes a required fragment directly, captures an optional one with var, and declares a NESTED variable so that each fragment knows which tab it is in. Naming an attribute that is not a fragment in jsp:invoke is not detected by the translator: the generated class fails to compile with The method invoke(Writer) is undefined for the type String.

WEB-INF/tags/ui/tabs.tag and tabs.jsp
<%@ tag body-content="empty" trimDirectiveWhitespaces="true" %>
<%@ attribute name="first" fragment="true" required="true" %>
<%@ attribute name="second" fragment="true" %>
<%@ variable name-given="tabNo" variable-class="java.lang.Integer" scope="NESTED" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:set var="tabNo" value="1"/>
<section class="tab"><jsp:invoke fragment="first"/></section>
<c:set var="tabNo" value="2"/>
<jsp:invoke fragment="second" var="secondHtml"/>
<c:if test="${not empty secondHtml}"><section class="tab">${secondHtml}</section></c:if>
<%@ taglib prefix="ui" tagdir="/WEB-INF/tags/ui" %>
<ui:tabs>
  <jsp:attribute name="first">first sees tab ${tabNo}</jsp:attribute>
  <jsp:attribute name="second">second sees tab ${tabNo}</jsp:attribute>
</ui:tabs>
<section class="tab">first sees tab 1</section>
<section class="tab">second sees tab 2</section>

Leaving out the required fragment is caught at translation time, as for any attribute:

org.apache.jasper.JasperException: /bad/missing-fragment.jsp (line: [2], column: [0]) According to the TLD or the tag file, attribute [first] is mandatory for tag [tabs]

The fragment is an object (jakarta.servlet.jsp.tagext.JspFragment) whose single method invoke(Writer) writes the output to the given writer, or to the JspWriter of the fragment’s context when the writer is null.

Variables: the variable Directive and Synchronization

A tag file exchanges values with its caller in two ways. IN parameters are attributes: a copy of the value is placed in the tag’s page scope and nothing is written back. OUT parameters are variables declared with the variable directive: a page-scoped variable of the tag file that the container copies to (or from) the calling page’s page scope at defined moments.

Table 9. The attributes of the variable directive (Table JSP.8-4)
Attribute Default Meaning

name-given

one of the two

The fixed name of the variable in the caller and in the tag file.

name-from-attribute

one of the two

The name of an attribute whose value (a translation-time constant) is the variable’s name in the caller. That attribute must be a String, required and not an rtexprvalue. Needs alias.

alias

required with name-from-attribute

The local name in the tag file, for a variable whose name the caller chooses.

variable-class

java.lang.String

The class of the variable.

declare

true

Whether a scripting variable is declared in the calling page after the tag.

scope

NESTED

AT_BEGIN, AT_END or NESTED.

description

none

Free text.

The scope decides when the value crosses the boundary. In the table, "tag to page" means: if the variable exists in the tag file, create or update it in the caller; if it does not, remove it from the caller.

Table 10. Variable synchronization behaviour (Table JSP.8-6)
Moment AT_BEGIN NESTED AT_END

Beginning of the tag file

nothing

save the caller’s value

nothing

Before any fragment (a body or a fragment attribute)

tag to page

tag to page

nothing

After any fragment

nothing

nothing

nothing

End of the tag file

tag to page

restore the caller’s value

tag to page

In practice: NESTED is a loop variable, visible only in the body and gone afterwards; AT_BEGIN is visible in the body and after the tag; AT_END is a result visible only after the tag. The three tag files below set x to 2, run the body, set x to 4, and each prints the value of x before and after the body. The page sets x to 1 and the body sets it to 3. The page and the output are the ones of the specification’s examples, run on Tomcat 11.0.26.

WEB-INF/tags/sync/atbegin.tag (nested.tag and atend.tag differ only in the scope)
<%@ variable name-given="x" scope="AT_BEGIN" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
inside before: [${x}]
<c:set var="x" value="2"/>
<jsp:doBody/>
inside after: [${x}]
<c:set var="x" value="4"/>
sync.jsp (the three blocks repeat the same body with s:atbegin, s:nested and s:atend)
<%@ page contentType="text/plain; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="s" tagdir="/WEB-INF/tags/sync" %>
<c:set var="x" value="1"/>
before: [${x}]
<s:atbegin>
  in body: [${x}]
  <c:set var="x" value="3"/>
</s:atbegin>
after: [${x}]
Output of the three scopes
== AT_BEGIN
before: [1]
inside before: []
  in body: [2]
inside after: [2]
after: [4]
== NESTED
before: [1]
inside before: []
  in body: [2]
inside after: [2]
after: [1]
== AT_END
before: [1]
inside before: []
  in body: [1]
inside after: [2]
after: [4]

Read the output once, slowly. In AT_BEGIN the body sees the tag’s x (2) because the value is copied before the body, and the page sees the tag’s final value (4) at the end. In NESTED the body sees 2 too, but after the tag the page’s own x (1) is back. In AT_END the body still sees the page’s x (1); only after the tag does the page see 4. The body’s own <c:set var="x" value="3"/> (the "3") is never copied back in any scope: the value flows from the tag file to the page, not the other way.

A fourth example from the specification shows that the synchronization can remove a variable: a NESTED variable that the tag file sets, calls the body, removes with c:remove, and calls the body again.

WEB-INF/tags/sync/remove.tag
<%@ variable name-given="x" scope="NESTED" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:set var="x" value="1"/>
<jsp:doBody/>
<c:remove var="x"/>
<jsp:doBody/>

Called as <s:remove>'${x}'</s:remove> after x was set to 2, the output is 2, '1', '' and 2 again: the body saw 1, then nothing (the variable was removed from the caller), and the page’s own 2 returned at the end.

A variable whose name the caller chooses: each

JSTL’s c:forEach takes var and varStatus as attribute names; a tag file does the same with name-from-attribute and alias. The each tag repeats its body for every element of a collection and exposes the element and its index as NESTED variables with the names the caller picked.

WEB-INF/tags/ui/each.tag
<%@ tag description="Repeats the body for every element" body-content="scriptless" %>
<%@ attribute name="items" required="true" type="java.util.Collection" %>
<%@ attribute name="var" required="true" rtexprvalue="false" description="Name of the current element" %>
<%@ attribute name="varIndex" required="true" rtexprvalue="false" description="Name of the zero-based index" %>
<%@ variable name-from-attribute="var" alias="current" variable-class="java.lang.Object" scope="NESTED" %>
<%@ variable name-from-attribute="varIndex" alias="position" variable-class="java.lang.Integer" scope="NESTED" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:forEach var="item" items="${items}" varStatus="st">
  <c:set var="current" value="${item}"/>
  <c:set var="position" value="${st.index}"/>
  <jsp:doBody/>
</c:forEach>
Using it
<ui:each items="${books}" var="b" varIndex="i">
  <ui:bookCard book="${b}" highlight="${i == 0}">
    <ui:button kind="submit" class="borrow" data-id="${b.id}">Borrow</ui:button>
  </ui:bookCard>
</ui:each>

Inside the tag file the code uses the fixed names current and position; in the page the same values are b and i. The attribute behind name-from-attribute must be required and not an rtexprvalue: leaving required="true" out of varIndex makes Tomcat refuse the library.

org.apache.jasper.JasperException: /WEB-INF/tags/ui/each.tag (line: [6], column: [1]) The attribute directive declared at line [4] with name [varIndex] that matches the name-from-attribute value of this variable directive must be of type java.lang.String, must be "required" and must not be a "rtexprvalue".

An AT_END variable gives a tag a result, the way c:set does for var. The summary tag computes a text from a collection and hands it back under the name the page asks for:

WEB-INF/tags/ui/summary.tag and its use
<%@ tag body-content="empty" %>
<%@ attribute name="items" required="true" type="java.util.Collection" %>
<%@ attribute name="var" required="true" rtexprvalue="false" %>
<%@ variable name-from-attribute="var" alias="result" variable-class="java.lang.String" scope="AT_END" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:set var="result" value="${items.size()} book(s)"/>
<ui:summary items="${books}" var="total"/>
<p>${total}</p>
<p>3 book(s)</p>

Page Scope, Implicit Objects and EL Inside a Tag File

Every call of a tag file gets a clean page scope: it holds the declared attributes and nothing else. The request, session and application scopes are the caller’s own, and a change to them is visible to the caller. A change to the page scope is not visible, except through a variable. The tag’s context object (the "JSP context wrapper") is what getJspContext() returns, and it is a PageContext when the caller’s context is one.

Table 11. The implicit objects of a tag file (Table JSP.8-5)
Variable Type Notes

request

ServletRequest subtype, e.g. HttpServletRequest

The request, request scope.

response

ServletResponse subtype

The response.

jspContext

jakarta.servlet.jsp.JspContext

The context of the tag file; replaces pageContext.

session

HttpSession

HTTP only; session scope.

application

ServletContext

Application scope.

out

JspWriter

Writes to the output.

config

ServletConfig

These are scripting variables, available in scriptlets and scripting expressions of the tag file. They are not EL identifiers: the EL implicit objects (pageContext, param, requestScope…​) are those of the implicit objects page. The tag file below prints values from both worlds.

WEB-INF/tags/bad/pageScope.tag
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
caller page var: [${pageScope.callerVar}] request var: [${requestScope.reqVar}]
jspContext: ${jspContext.getClass().getSimpleName()}
request: ${request.method} contextPath=${pageContext.request.contextPath}
<c:set var="leak" value="from the tag"/>
The page that calls it
<%@ page session="false" %>
<%@ taglib prefix="b" tagdir="/WEB-INF/tags/bad" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:set var="callerVar" value="in the page" scope="page"/>
<c:set var="reqVar" value="in the request" scope="request"/>
<b:pageScope/>
after the tag, leak=[${leak}] callerVar=[${callerVar}]
caller page var: [] request var: [in the request]
jspContext:
request:  contextPath=/g6tagfiles
after the tag, leak=[] callerVar=[in the page]

The result confirms four things. The tag file does not see the caller’s page-scoped variable; it sees the request-scoped one. ${jspContext...} and ${request.method} are empty, because they are scripting variables and not EL names (a missing EL variable is not an error unless errorOnELNotFound is on). ${pageContext.request.contextPath} works: EL’s pageContext is available in a tag file. And what the tag stores in its own page scope (leak) is gone once the tag ends.

A tag file can contain scriptlets, even though its callers' bodies cannot (body-content="scriptless" governs the caller’s body). A <% out.print("scriptlet"); %> in bad/scriptlet.tag printed scriptlet. Property groups, including scripting-invalid, never apply to tag files (see the configuration page), so a project that bans scriptlets should also search WEB-INF/tags for <% in code review.

Dynamic Attributes

The dynamic-attributes attribute of the tag directive turns on the acceptance of attributes the tag did not declare. It names a page-scoped variable that receives a Map of names to values; the generated handler implements jakarta.servlet.jsp.tagext.DynamicAttributes, and the container must not reject any name. Only attributes without a namespace URI are in the map.

WEB-INF/tags/ui/button.tag
<%@ tag body-content="scriptless" dynamic-attributes="extra" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ attribute name="kind" %>
<button type="${empty kind ? 'button' : kind}"<c:forEach var="a" items="${extra}"> ${a.key}="<c:out value='${a.value}'/>"</c:forEach>><jsp:doBody/></button>
<ui:button kind="submit" class="borrow" data-id="7">Borrow</ui:button>
<button type="submit" data-id="7" class="borrow">Borrow</button>

The map is a hash map, so the order of data-id and class is not the order written. The values are escaped with c:out: a dynamic attribute is request data the moment it comes from an expression.

A tag that does not declare dynamic-attributes is strict: <ui:rating value="1" extra="no"/> stops the translation with Attribute [extra] invalid for tag [rating] according to TLD.

trimDirectiveWhitespaces can eat the space you need

With trimDirectiveWhitespaces="true" the container removes template text that consists only of whitespace. In the button tag the text before ${a.key} is a single space, and an expression splits the template text into pieces, so the space is a whitespace-only piece and is removed. The output of the same tag with the directive set to true:

<button type="submit"data-id="7"class="borrow">Borrow</button>

That is invalid HTML attribute spacing that browsers tolerate unpredictably. Either leave the directive out for such a tag, or write the space as an expression or entity that is not whitespace-only template text.

Tag Files in XML Syntax

A tag file whose extension is .tagx is in XML syntax, like a JSP document: it must be well formed, the jsp:root element may be present (in a .tag it may not), directives become jsp:directive.tag, jsp:directive.attribute and jsp:directive.variable, and the default is not to generate an XML declaration. A pageEncoding attribute in the tag directive of a .tagx is an error: the encoding follows the XML rules. If jsp:root carries a version, it must match the tag file’s JSP version. The JSP documents page covers the XML syntax in general.

WEB-INF/tags/ui/note.tagx
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" xmlns:c="jakarta.tags.core" version="4.0">
  <jsp:directive.tag body-content="scriptless" description="A note box in XML syntax"/>
  <jsp:directive.attribute name="level" required="false"/>
  <div class="note ${empty level ? 'info' : level}"><jsp:doBody/></div>
</jsp:root>

It is called like any other tag; the call <ui:note level="warn">Beta & <b>bold</b></ui:note> gives <div class="note warn">Beta & <b>bold</b></div>. The XML view of a tag file, the form passed to tag library validators, is built the same way as for a page.

Packaging Tag Files in a JAR

To reuse tag files across applications, put them in a JAR under /META-INF/tags/ and describe them in a tag library descriptor in the same JAR. A tag file in a JAR that no descriptor lists is ignored. The descriptor’s tag-file element has a name and a path, which must start with /META-INF/tags; other elements of the element override the defaults of the file’s tag directive. A library can mix tag (Java) and tag-file elements, and both share one namespace of names, so a duplicate name makes the library invalid. The JSP version of a tag file in a JAR is the version of the descriptor.

META-INF/bookshelf-ui.tld
<?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_4_0.xsd"
        version="4.0">
  <description>Reusable Bookshelf components packaged as tag files</description>
  <tlib-version>1.0</tlib-version>
  <short-name>bsui</short-name>
  <uri>https://example.org/bookshelf/ui</uri>
  <tag-file>
    <name>badge</name>
    <path>/META-INF/tags/badges/badge.tag</path>
  </tag-file>
</taglib>
META-INF/tags/badges/badge.tag
<%@ tag body-content="scriptless" description="A small label" trimDirectiveWhitespaces="true" %>
<%@ attribute name="tone" description="info, ok or warn" %>
<span class="badge ${empty tone ? 'info' : tone}"><jsp:doBody/></span>
Building the JAR and using the tag
jar cf src/main/webapp/WEB-INF/lib/bookshelf-ui.jar -C jarsrc META-INF
<%@ taglib prefix="bsui" uri="https://example.org/bookshelf/ui" %>
<p>Status: <bsui:badge tone="ok">on loan</bsui:badge></p>
<p>Status: <span class="badge ok">on loan</span></p>

Jasper compiled the tag into org/apache/jsp/tag/meta/... of the work directory, a separate tree from the tag/web/... of tag files in the web application. Tomcat finds the descriptor by scanning the JARs of WEB-INF/lib for TLD files, as the Tomcat JAR scanner page describes.

A third form, tag files compiled into Java classes and distributed as a binary tag library, is allowed but needs a tool that produces portable code; containers are not required to supply one. Precompiling with JspC at build time is a matter for the Jasper Configuration & Precompilation.

A Layout Template with Tag Files

Every page of the Bookshelf has the same skeleton: a head, a navigation bar, a main area, an optional second column and a footer. In JSP the clean way to write that skeleton once is a tag file whose body is the page content and whose optional fragments fill the other slots. This is the pattern called Composite View, which the JSP world also implemented with Tiles and SiteMesh, and which Thymeleaf calls a layout.

WEB-INF/tags/layout/page.tag
<%@ tag description="Bookshelf page layout" body-content="scriptless" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="ui" tagdir="/WEB-INF/tags/ui" %>
<%@ attribute name="title" required="true" description="Text of the title element" %>
<%@ attribute name="active" description="Name of the current menu entry" %>
<%@ attribute name="head" fragment="true" description="Extra markup for the head element" %>
<%@ attribute name="sidebar" fragment="true" description="Optional second column" %>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title><c:out value="${title}"/> - Bookshelf</title>
  <jsp:invoke fragment="head"/>
</head>
<body>
<nav>
  <ui:navLink href="${pageContext.request.contextPath}/index.jsp" label="Catalogue" current="${active == 'catalogue'}"/>
  <ui:navLink href="${pageContext.request.contextPath}/about.jsp" label="About" current="${active == 'about'}"/>
</nav>
<main>
  <jsp:doBody/>
</main>
<aside><jsp:invoke fragment="sidebar"/></aside>
<footer>Bookshelf, ${books.size()} titles</footer>
</body>
</html>
WEB-INF/tags/ui/navLink.tag
<%@ tag body-content="empty" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ attribute name="href" required="true" %>
<%@ attribute name="label" required="true" %>
<%@ attribute name="current" type="java.lang.Boolean" %>
<a href="<c:out value='${href}'/>"${current ? ' class="current" aria-current="page"' : ''}><c:out value="${label}"/></a>

The design decisions are all visible in that file. The page-specific content comes in through jsp:doBody. The optional slots are fragment attributes invoked with jsp:invoke, which does nothing for a slot the page did not fill. Everything the layout reads from the model comes from a scope (here books from application scope); a layout that needs a variable the page sets should ask for it as an attribute instead, since the caller’s page scope is not visible. The title is escaped with c:out because it is data. The navigation uses a component tag file (ui:navLink), a tag file calling a tag file from another directory through its own taglib directive.

A page uses the layout by wrapping its content:

index.jsp
<%@ page contentType="text/html; charset=UTF-8" session="false" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="layout" tagdir="/WEB-INF/tags/layout" %>
<%@ taglib prefix="ui" tagdir="/WEB-INF/tags/ui" %>
<layout:page title="Catalogue" active="catalogue">
  <jsp:attribute name="head"><link rel="stylesheet" href="${pageContext.request.contextPath}/bookshelf.css"></jsp:attribute>
  <jsp:attribute name="sidebar"><p>Sidebar for ${books.size()} books</p></jsp:attribute>
  <jsp:body>
    <ui:each items="${books}" var="b" varIndex="i">
      <ui:bookCard book="${b}" highlight="${i == 0}">
        <ui:button kind="submit" class="borrow" data-id="${b.id}">Borrow</ui:button>
      </ui:bookCard>
    </ui:each>
    <ui:summary items="${books}" var="total"/>
    <p>${total}</p>
  </jsp:body>
</layout:page>
Requesting the catalogue
curl http://localhost:8080/g6tagfiles/index.jsp
The response (blank lines and indentation removed)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Catalogue - Bookshelf</title>
<link rel="stylesheet" href="/g6tagfiles/bookshelf.css"></head>
<body>
<nav>
<a href="/g6tagfiles/index.jsp" class="current" aria-current="page">Catalogue</a>
<a href="/g6tagfiles/about.jsp">About</a>
</nav>
<main>
<article class="card featured" id="book-1">
<h2>Neuromancer</h2>
<p>William Gibson (1984)
<span class="rating" title="5 out of 5">5 / 5</span>
</p>
<div class="actions">
<button type="submit" data-id="1" class="borrow">Borrow</button>
</div>
</article>
<article class="card" id="book-2">
<h2>Dune</h2>
...
<p>3 book(s)</p>
</main>
<aside><p>Sidebar for 3 books</p></aside>
<footer>Bookshelf, 3 titles</footer>
</body>
</html>

The <head> fragment is optional: the about.jsp page of the application passes none and its response simply has an empty aside and no extra head markup.

Layouts built on layouts

A layout can wrap another layout, which is the JSP form of Tiles definition inheritance. The catalogue layout adds a section element around the body and a fixed "Filters" block in front of whatever sidebar the page supplies; it forwards its own sidebar fragment to layout:page by invoking it inside a jsp:attribute.

WEB-INF/tags/layout/catalogue.tag
<%@ tag description="Catalogue layout built on the page layout" body-content="scriptless" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="layout" tagdir="/WEB-INF/tags/layout" %>
<%@ attribute name="title" required="true" %>
<%@ attribute name="sidebar" fragment="true" %>
<layout:page title="${title}" active="catalogue">
  <jsp:attribute name="sidebar"><div class="filters">Filters</div><jsp:invoke fragment="sidebar"/></jsp:attribute>
  <jsp:body><section class="catalogue"><jsp:doBody/></section></jsp:body>
</layout:page>
nested.jsp
<%@ page contentType="text/html; charset=UTF-8" session="false" %>
<%@ taglib prefix="layout" tagdir="/WEB-INF/tags/layout" %>
<layout:catalogue title="Sci-fi">
  <jsp:attribute name="sidebar"><p>Page-specific sidebar</p></jsp:attribute>
  <jsp:body><p>Three books.</p></jsp:body>
</layout:catalogue>
<main>
  <section class="catalogue"><p>Three books.</p></section></main>
<aside><div class="filters">Filters</div><p>Page-specific sidebar</p></aside>

Inside catalogue.tag the tag sidebar that jsp:invoke names is the attribute of catalogue.tag itself, even though layout:page also has a sidebar: the two are in different page scopes. The diagram at the top of the page is this call.

Layout tag files, includes, Tiles and Thymeleaf

Table 12. Ways to build a common page skeleton
Approach How the skeleton is shared Trade-offs

Header and footer includes

Each page includes header.jspf and footer.jspf around its content

Simple; the skeleton is repeated in every page and the order is the page’s job. See the includes page.

include-prelude and include-coda

A property group adds a prelude and a coda to every page of a pattern

No page change; invisible, no per-page parameters. See the configuration page.

A layout page and jsp:include with a body parameter

The layout page includes the content page named by a parameter

Content is a whole page, parameters are strings, and the page scope is not shared. See the includes page.

Layout tag file

The page wraps its content in a tag, with fragments for optional slots

Typed attributes, body, nested layouts, compile-time checking of the call; the content stays in the page that owns it. Pure JSP.

Apache Tiles

A definition file composes pages with tiles:insert and attributes; definitions inherit

Retired by the Apache Attic. Struts 7.4 still ships the plugin; see Tiles & Page Layouts in Part II. A layout tag file replaces most uses.

Thymeleaf layouts

A native fragment mechanism or the Layout Dialect decorates a page

Another template engine entirely, see Layouts & the Layout Dialect.

A reader who knows Tiles can translate its vocabulary one to one.

Table 13. Tiles concepts and their tag-file counterparts
Tiles (Struts 1 and 2) Tag file

A layout JSP that inserts attributes at named slots

layout/page.tag with jsp:invoke and jsp:doBody

tiles:insert with tiles:put

A call <layout:page title="..."> with attributes and jsp:attribute

tiles:getAsString

${title} in the tag file, escaped with c:out

tiles:useAttribute

An attribute directive, which already exposes the value as a page-scoped variable

A definition that extends another

A layout tag file that wraps another (layout/catalogue.tag above)

A definition used as a forward target

A servlet or an action forwards to a JSP under WEB-INF that is just a call of the layout

Composite View (the pattern)

The layout tag plus its fragments

The older books of this section’s bibliography build a "channel" layout with c:import: a portal page includes survey, message-board and news channels, each a separate page, and the case study of the JSTL book keeps database and RSS access in those pages, with registration, login and a personalised welcome on top. The same result is better reached with a layout tag file for the frame (the navigation can read the signed-in reader from the session and show the login or the logout link), component tag files for the survey, the message board and each channel, and the data loaded by a servlet or a service before the view runs; a view that queries a database or fetches a feed belongs to the era the book describes, and is called out on the includes page and in MVC with Servlets & JSP.

Reusable Components

A component tag file renders one concept of the application: a book card, a rating, a navigation link, a form field. The same rules produce good ones.

  • Take typed attributes (book of type Book, not three strings) and a body for content the caller owns.

  • Read nothing from the request that the caller did not pass, except the context path or other genuinely global values.

  • Escape every value with c:out or fn:escapeXml: the tag file is the single place where the escaping is written.

  • Give optional behaviour a name (highlight), not a flag of unrelated meanings.

  • Keep logic to presentation. A loop that decides nothing is fine; a query, a transaction or a permission check is not.

  • Put each directory of related tags under one prefix (ui, layout, form), and keep the names short and in the camel case or hyphenated style the project agrees on.

The Bookshelf’s ui directory shows the range: rating (no body, one attribute), navLink (no body, a boolean), bookCard (typed attribute, boolean, body), each (collection, two named variables), summary (an AT_END result), button (dynamic attributes), shout and raw (body transformation) and note (XML syntax).

Worked Example: the Bookshelf Catalogue End to End

The pieces assembled: the servlet or listener puts the books in a scope, the page calls the layout, the layout renders the skeleton, and component tags render the cards. The call chain for one request of index.jsp is: Jasper translates and compiles index.jsp, then each tag file it uses (layout/page.tag, ui/each.tag, ui/bookCard.tag, ui/rating.tag, ui/button.tag, ui/summary.tag, ui/navLink.tag) the first time it is reached. At run time layout:page receives three things from the page: the attributes title and active, the two fragments head and sidebar, and the body. It writes the head, calls navLink twice, runs the body (which calls each, which for every book runs its own body, which calls bookCard, whose body is the button), and finally the aside and the footer. summary sets total once, at the end of its call, and the paragraph that follows prints it.

A change to a component, seen without a restart
# edit the deployed rating.tag, then ask for a page that uses it
sed -i '' 's|</span>|</span><!-- v2 -->|' webapps/g6tagfiles/WEB-INF/tags/ui/rating.tag
sleep 6
curl -s http://localhost:8080/g6tagfiles/index.jsp | grep -c 'v2'
3

The three matches are the three ratings of the three cards. A change to the layout file (<footer> to <footer data-v="2">) showed in index.jsp and in about.jsp six seconds later, so a tag file edit reaches every page that uses it.

Compilation, Reloading and Where the Generated Code Is

Jasper translates a tag file to Java the first time a page that uses it is translated, compiles it, and keeps the class in the web application’s work directory: org/apache/jsp/tag/web/<directory>/<name>_tag.java and .class for a file in WEB-INF/tags, and org/apache/jsp/tag/meta/<uri>/... for a file in a JAR. The generated class extends SimpleTagSupport; its setJspContext creates the JspContextWrapper that holds the clean page scope and the variable synchronization state, and doTag calls syncBeforeInvoke before it invokes the body.

In development mode (the default, development is true) Jasper checks a page and its dependent files for modification at most once per modificationTestInterval (4 seconds by default), and the tag files a page uses are among its dependents, as the reload test above showed. In production mode (development false) the pages and tag files are checked at the checkInterval or never, and precompiling with JspC removes the first-request cost. Compile errors name the tag file and line, as in the messages of this page, and runtime errors from EL name the tag file too:

jakarta.el.PropertyNotFoundException: Property [isbn] not found on type [org.example.bookshelf.Book]
org.apache.jasper.JasperException: An exception occurred processing [/WEB-INF/tags/bad/missingprop.tag] at line [2]

Security Notes

  • Tag file source is a secret like any JSP. Tag files in /WEB-INF/tags cannot be requested by a client. A tag file anywhere else is just content: a leak.tag containing <%@ attribute name="x" %> SECRET-TAG-SOURCE ${x} in the web application root (/tags/leak.tag) was served as is, source and ${x} unexpanded, with status 200. Keep tag files under WEB-INF/tags or META-INF/tags, always.

  • A tag file renders; it does not authorise. Escape attribute values in the tag file with c:out, as the cards do. Do not rely on the caller to escape. Output of jsp:doBody var="x" and jsp:invoke var="x" is already markup, so printing ${x} unescaped re-emits whatever the caller’s body contained; escape only values, not captured markup.

  • Dynamic attributes are open input. Names and values of a dynamic attribute may come from a request; the button tag escapes values, and a tag that writes attribute names from request data is an injection point.

  • Static attributes are constants. A name-from-attribute or rtexprvalue="false" attribute cannot take ${param.x}; that is a feature, since it fixes the names of scoped variables at translation time.

  • Property groups such as scripting-invalid do not apply to tag files. Review WEB-INF/tags for scriptlets separately.

  • A shared tag JAR runs with the application’s privileges, like any library. Prefer tag files from sources you build.

Pitfalls and Error Names

Table 14. Pitfalls of tag files, with the error or symptom seen on Tomcat 11.0.26
Mistake Symptom Fix

A tag file outside WEB-INF/tags

The file is served as content; tagdir rejects the directory with Tag file directory [/tags] does not start with "/WEB-INF/tags"

Move it under WEB-INF/tags.

<%@ page %> in a tag file

[<%@ page] directive cannot be used in a tag file

Use the tag directive.

<%@ tag %>, <%@ attribute %>, <%@ variable %> in a JSP

[<%@ tag] directive can only be used in a tag file

They exist only in tag files.

jsp:doBody or jsp:invoke in a JSP

[<jsp:doBody] action can be used in tag files only

Use them in a tag file.

body-content="JSP"

Invalid body-content [JSP] in tag directive

empty, tagdependent or scriptless.

A body in a call to an empty tag

According to TLD, tag [b:emptyTag] must be empty, but is not

Remove the body or change body-content.

A required attribute left out, or an unknown attribute

attribute [label] is mandatory for tag [emptyTag], Attribute [other] invalid for tag [emptyTag] according to TLD

Declare it; or add dynamic-attributes.

A fragment attribute given as text

Unable to convert string [...] to class [jakarta.servlet.jsp.tagext.JspFragment]

Use jsp:attribute.

Text outside jsp:body when jsp:attribute is used

Must use jsp:body to specify tag body for [<layout:page] if jsp:attribute is used.

Wrap the body in jsp:body.

fragment="true" plus type

Cannot specify both 'fragment' and 'type' attributes

Drop the type.

A primitive type

A javac error from the generated class, not a clean message

Use the wrapper type.

name-from-attribute pointing at an attribute that is not required or is an rtexprvalue

must be of type java.lang.String, must be "required" and must not be a "rtexprvalue"

Declare the attribute required="true" rtexprvalue="false".

Using the caller’s taglib prefixes in the tag file

<c:out> printed as text

Repeat the taglib directive in the tag file.

Expecting the caller’s page-scoped variables

${pageScope.callerVar} is empty

Pass them as attributes, or use request scope.

Expecting a tag file’s c:set to appear in the page

The variable is gone after the tag

Declare a variable directive.

${request} or ${jspContext} in EL

Empty

Use ${pageContext.request}, or a scriptlet or expression.

trimDirectiveWhitespaces="true" with whitespace-only text next to an expression

type="submit"data-id="7"

Remove the directive, or avoid a whitespace-only template piece.

A property that does not exist on the typed attribute

jakarta.el.PropertyNotFoundException: Property [isbn] not found on type [...Book] at the tag file’s line

Fix the property name; the message names the tag file.

deferredValue in a library of version 2.0 (the default)

Invalid JSP version defined for tag file at [/WEB-INF/tags/old/def.tag]

Add an implicit.tld with jsp-version 2.1 or later.

a.tag and a.tagx in one directory

Invalid by the specification; Tomcat silently used the .tag

Do not.

A scriptlet in a tag file, assuming a property group forbids it

The scriptlet ran; property groups do not apply to tag files

Search tag files in review.

What Changed Recently

Table 15. Tag files over the versions
Version Change

JSP 2.0 (2003)

Tag files, .tag/.tagx, jsp:invoke, jsp:doBody, jsp:attribute, jsp:body, the tag, attribute and variable directives, JspFragment and SimpleTag.

JSP 2.1 (2006)

deferredValue and deferredMethod attributes, deferredSyntaxAllowedAsLiteral, and implicit.tld to set the version of an implicit library.

Jakarta Pages 3.0 (Jakarta EE 9)

The package moved from javax.servlet.jsp to jakarta.servlet.jsp; tag files that import javax.* classes in import or scriptlets must change.

Jakarta Pages 3.1 (Jakarta EE 10)

An option to raise a PropertyNotFoundException for unknown identifiers (the errorOnELNotFound attribute, which the tag directive carries too); clarified meaning of scope for scripting variables of custom actions.

Jakarta Pages 4.0 (Jakarta EE 11)

Java 17 minimum, BodyTag.EVAL_BODY_TAG and the jakarta.servlet.jsp.el deprecated classes removed. The appendix of changes lists no change to the tag file chapter.

Where the Books Are Outdated

The Pages and JSTL books of the bibliography are from the JSP 1.2 era, which predates tag files. The JSP 1.2 book builds custom tags only as classic Tag and BodyTag handlers in Java with a JSP 1.2 TLD DTD, and has nothing like jsp:doBody or a page scope for a tag; the JSTL book evaluates EL in custom tag attributes by hand with an ExpressionEvaluatorManager, and builds its portal layout with c:import. Everything the books do with those tools is today either an EL expression, a tag file or a simple tag handler. The Struts books use Tiles for layouts, with tiles:insert and definition inheritance; a layout tag file expresses the same composition with jsp:attribute and nested layouts, and Tiles itself has been retired. Where the books use a custom tag to reach a database or a feed, the modern answer is to load data before the view.