JSP, JSTL & the View Layer

Reusing Views: Includes & Tag Files

18 min Lesson 9 of 13

Reusing Views: Includes & Tag Files

Every production JSP application faces a familiar problem: headers, footers, navigation bars, and sidebar widgets need to appear on dozens of pages — and keeping them consistent means centralising them. JSP provides three distinct mechanisms for this: the static include directive, the dynamic <jsp:include> action, and the more powerful custom tag file. Choosing the right mechanism for each situation is part of writing maintainable view code.

Static Include: The include Directive

The static include directive is resolved at translation time — when the JSP container compiles your page into a servlet. The included file's content is literally pasted into the generated Java source before compilation. Because of this, the included file can reference page-scope variables declared in the including page.

<%-- In header.jspf (JSP Fragment) --%> <header class="site-header"> <h1>My App</h1> <nav> <a href="${pageContext.request.contextPath}/home">Home</a> <a href="${pageContext.request.contextPath}/products">Products</a> </nav> </header> <%-- In main page --%> <%@ include file="/WEB-INF/fragments/header.jspf" %>
Convention: JSP fragment files are commonly given the .jspf extension to signal they are not standalone pages. Placing them under /WEB-INF/ prevents browsers from accessing them directly, which is considered best practice.

Because the include is resolved at compile time, if you update header.jspf you must force a recompile of every page that includes it. In development servers this usually happens automatically, but it is worth being aware of in environments with aggressive class caching.

Dynamic Include: <jsp:include>

The <jsp:include> action is resolved at request time. Each time the page is served, a sub-request is dispatched to the included resource, and its output is merged into the parent page's response stream. This means the included page runs as an independent unit — it cannot see the parent's page-scope variables, but it can accept parameters and it can be a fully dynamic resource (even a servlet).

<%-- Passing parameters to an included page --%> <jsp:include page="/WEB-INF/fragments/sidebar.jsp"> <jsp:param name="category" value="${currentCategory}" /> </jsp:include> <%-- In sidebar.jsp, read with request.getParameter --%> <aside> <h3>Browse: ${param.category}</h3> <!-- category-specific content --> </aside>

The key trade-off: dynamic includes are more flexible and always reflect the latest state of the included file without recompiling the parent, but they carry the overhead of a sub-dispatch per request.

Choosing Between Static and Dynamic Include

  • Use static (include directive) for truly constant fragments — copyright footers, static navigation that never changes at runtime, shared taglib declarations. Compile-time merging means zero runtime overhead.
  • Use dynamic (<jsp:include>) when the fragment needs its own independent scope, when you need to pass parameters, or when the fragment output varies per-request (e.g., a "currently logged-in user" widget).
Taglib declarations with static include: A common and elegant pattern is to create a single taglibs.jspf file containing all your <%@ taglib ... %> declarations and statically include it at the top of every page. Because it is a static include, the declarations become part of each page at compile time — no duplication, one place to add or remove a library.

Custom Tag Files

Static and dynamic includes give you reuse at the page fragment level. Tag files take this further: they let you define a custom JSP tag with named attributes, making your markup expressive and self-documenting. A tag file is a plain .tag file (or .tagx for XML syntax) stored in /WEB-INF/tags/ — no Java, no TLD descriptor required.

Suppose you have a "card" component repeated throughout your app. Define it once:

<%-- /WEB-INF/tags/card.tag --%> <%@ tag description="Bootstrap-style card component" pageEncoding="UTF-8" %> <%@ attribute name="title" required="true" rtexprvalue="true" %> <%@ attribute name="subtitle" required="false" rtexprvalue="true" %> <%@ taglib prefix="c" uri="jakarta.tags.core" %> <div class="card shadow-sm mb-3"> <div class="card-header"> <strong>${title}</strong> <c:if test="${not empty subtitle}"> <span class="text-muted ms-2">${subtitle}</span> </c:if> </div> <div class="card-body"> <jsp:doBody /> </div> </div>

Use it from any JSP by declaring the taglib prefix that points to your tags directory:

<%@ taglib prefix="ui" tagdir="/WEB-INF/tags" %> <ui:card title="User Profile" subtitle="Last updated today"> <p>Name: ${user.fullName}</p> <p>Email: ${user.email}</p> </ui:card> <ui:card title="Order History"> <p>You have ${orderCount} orders.</p> </ui:card>
<jsp:doBody /> is the tag file equivalent of a slot or yield — it renders whatever content the caller placed between the opening and closing tag. Omit it from your tag file and the body will be silently discarded.

Tag File Attributes in Depth

The <%@ attribute %> directive mirrors a method parameter. Its key options are:

  • name — the attribute name used in the calling markup.
  • requiredtrue or false; the container will reject the page at translation time if a required attribute is missing.
  • rtexprvaluetrue allows EL expressions (${...}) as the value; false restricts it to a literal string.
  • type — the Java type of the attribute (e.g., java.lang.Integer); the container performs automatic string conversion.
<%-- /WEB-INF/tags/paginator.tag --%> <%@ tag pageEncoding="UTF-8" %> <%@ attribute name="currentPage" required="true" type="java.lang.Integer" %> <%@ attribute name="totalPages" required="true" type="java.lang.Integer" %> <%@ attribute name="baseUrl" required="true" rtexprvalue="true" %> <%@ taglib prefix="c" uri="jakarta.tags.core" %> <nav aria-label="Page navigation"> <ul class="pagination"> <c:forEach var="i" begin="1" end="${totalPages}"> <li class="page-item ${i == currentPage ? 'active' : ''}"> <a class="page-link" href="${baseUrl}?page=${i}">${i}</a> </li> </c:forEach> </ul> </nav>
<%-- Calling the paginator in a product listing page --%> <ui:paginator currentPage="${currentPage}" totalPages="${totalPages}" baseUrl="${pageContext.request.contextPath}/products" />

Building a Layout Template with Tag Files

One of the most powerful patterns with tag files is a full page layout template — a single tag that wraps every page with a consistent shell, allowing child pages to inject their title and body.

<%-- /WEB-INF/tags/layout.tag --%> <%@ tag pageEncoding="UTF-8" %> <%@ attribute name="pageTitle" required="true" rtexprvalue="true" %> <%@ attribute name="cssClass" required="false" rtexprvalue="true" %> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>${pageTitle} | My App</title> <link rel="stylesheet" href="${pageContext.request.contextPath}/css/app.css"> </head> <body class="${not empty cssClass ? cssClass : 'default-layout'}"> <%@ include file="/WEB-INF/fragments/topbar.jspf" %> <main class="container"> <jsp:doBody /> </main> <%@ include file="/WEB-INF/fragments/footer.jspf" %> </body> </html>

Individual pages become lean and focused:

<%@ taglib prefix="ui" tagdir="/WEB-INF/tags" %> <ui:layout pageTitle="Dashboard" cssClass="dashboard-page"> <h2>Welcome back, ${user.firstName}!</h2> <ui:card title="Your Stats"> <p>Orders: ${stats.orderCount} | Reviews: ${stats.reviewCount}</p> </ui:card> </ui:layout>
Tag files vs. Tiles / Thymeleaf layout dialect: Tag files are a zero-dependency, pure JSP solution — ideal when you are building with vanilla Jakarta EE and want no extra frameworks. Once you move to Spring MVC, consider Thymeleaf's layout dialect or Apache Tiles for the same pattern with better IDE tooling support.

Summary

Three tools, three use cases: the static include directive merges content at compile time with no runtime cost, perfect for shared taglib declarations and unchanging fragments; <jsp:include> dispatches a sub-request at runtime, giving each fragment its own scope and allowing parameter passing; tag files elevate reuse to a component model — named attributes, body slots, and full JSTL support in a plain .tag file. Together they give you everything you need to build a consistent, maintainable view layer without duplicating a single line of layout markup.