JSP, JSTL & the View Layer

Passing Data to Views

18 min Lesson 8 of 13

Passing Data to Views

In the MVC pattern the Servlet is the controller: it fetches data from the model (a service, DAO, or database), packages that data into named attributes, and then forwards the request to a JSP that renders HTML. The JSP itself never talks to the database — it only reads what the Servlet has placed in one of the servlet-managed scopes: request, session, application, or page. Understanding how those scopes work, and choosing the right one every time, is the core skill this lesson teaches.

The Four JSP Scopes

Every attribute you set is stored in one of four objects, each with a different lifetime:

  • Page scope — exists only within the current JSP page. Rarely used directly; internal to tag libraries.
  • Request scope (HttpServletRequest) — lives for the duration of a single HTTP request/response cycle. The most common choice for view data.
  • Session scope (HttpSession) — survives across multiple requests from the same browser session. Correct for user-identity data: who is logged in, their preferences, a shopping cart.
  • Application scope (ServletContext) — shared across all users and all requests for the lifetime of the web application. Use only for truly global, read-mostly data (e.g. a lookup table loaded at startup).
Default EL scope search order: when you write ${username} in a JSP, the EL engine searches page → request → session → application and returns the first match. This is convenient but can hide bugs when you accidentally set the same attribute name in two scopes. Always set attributes in the narrowest scope that satisfies your need.

Setting Request Attributes in a Servlet

The pattern is always the same: (1) obtain data, (2) call request.setAttribute("name", value), (3) forward to the JSP with RequestDispatcher.

package com.example.web; import com.example.model.Product; import com.example.service.ProductService; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; @WebServlet("/products") public class ProductListServlet extends HttpServlet { private final ProductService productService = new ProductService(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. Fetch data from the model layer List<Product> products = productService.findAll(); int totalCount = products.size(); String categoryLabel = "All Products"; // 2. Store data as request attributes request.setAttribute("products", products); request.setAttribute("totalCount", totalCount); request.setAttribute("categoryLabel", categoryLabel); // 3. Forward to the JSP view RequestDispatcher rd = request.getRequestDispatcher("/WEB-INF/views/product-list.jsp"); rd.forward(request, response); } }
Always place JSPs under /WEB-INF/views/. Files inside WEB-INF are not directly accessible via a URL — the browser cannot request them directly. This means users can only see a JSP through a Servlet, which is exactly what you want: the controller always runs first.

Rendering Request Attributes with EL

In the JSP you simply reference the attribute name inside ${ }. EL handles null safely — a missing attribute renders as an empty string, not a NullPointerException.

<!-- /WEB-INF/views/product-list.jsp --> <%@ taglib prefix="c" uri="jakarta.tags.core" %> <!DOCTYPE html> <html lang="en"> <head><title>${categoryLabel}</title></head> <body> <h1>${categoryLabel}</h1> <p>Showing ${totalCount} products</p> <ul> <c:forEach var="p" items="${products}"> <li> <strong>${p.name}</strong> — $${p.price} </li> </c:forEach> </ul> </body> </html>

EL dot-notation (p.name) calls the getter getName() on the Product object. Bracket notation (p["name"]) is equivalent and required when the property name contains hyphens or is stored in a variable.

Setting Session Attributes

For data that must persist across requests — a logged-in user's profile, a shopping cart — use the HttpSession:

@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userService.authenticate(username, password); if (user != null) { // Store the authenticated user in the session HttpSession session = request.getSession(); // creates a session if none exists session.setAttribute("currentUser", user); session.setAttribute("role", user.getRole()); response.sendRedirect(request.getContextPath() + "/dashboard"); } else { request.setAttribute("error", "Invalid credentials"); request.getRequestDispatcher("/WEB-INF/views/login.jsp").forward(request, response); } } }

In the JSP, session attributes are accessed with exactly the same EL syntax as request attributes:

<c:if test="${not empty currentUser}"> <p>Welcome back, ${currentUser.firstName}!</p> </c:if>
Do not store large or non-serializable objects in the session. Sessions must be serializable when the container clusters or persists them. Heavy objects also consume server-side memory for every active user. Store just what you need — typically a small UserSession DTO, not an entire JPA entity graph.

Removing Attributes and Invalidating Sessions

Use removeAttribute to delete a single attribute, and invalidate() to destroy the entire session (essential on logout):

// Remove one attribute from request request.removeAttribute("tempError"); // Remove one attribute from session session.removeAttribute("cart"); // Destroy the session entirely (logout) HttpSession session = request.getSession(false); // false = do NOT create if absent if (session != null) { session.invalidate(); } response.sendRedirect(request.getContextPath() + "/login");

Passing false to getSession() is an important habit: if no session exists yet (the user is already logged out), it avoids creating a pointless new empty session just to invalidate it.

Passing Collections and Maps

EL handles nested structures naturally. Suppose you set a Map<String, List<Product>> grouped by category:

// Servlet Map<String, List<Product>> productsByCategory = productService.groupedByCategory(); request.setAttribute("productsByCategory", productsByCategory);
<!-- JSP --> <c:forEach var="entry" items="${productsByCategory}"> <h2>${entry.key}</h2> <ul> <c:forEach var="p" items="${entry.value}"> <li>${p.name} — $${p.price}</li> </c:forEach> </ul> </c:forEach>

entry.key and entry.value are EL property lookups on a Map.Entry object — the EL engine calls getKey() and getValue() automatically.

A Complete Round-Trip Example

Here is the full flow for a product-detail page, showing how request and session data co-exist in the same view:

@WebServlet("/product") public class ProductDetailServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String idParam = request.getParameter("id"); Product product = productService.findById(Long.parseLong(idParam)); if (product == null) { response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } // Request-scoped: this product for this one page request.setAttribute("product", product); // Session-scoped: already set at login, just read it in the JSP via ${currentUser} request.getRequestDispatcher("/WEB-INF/views/product-detail.jsp") .forward(request, response); } }
<!-- product-detail.jsp --> <p>Logged in as: ${currentUser.email}</p> <!-- from session --> <h1>${product.name}</h1> <!-- from request --> <p>${product.description}</p> <p>Price: $${product.price}</p>

Summary

The Servlet-to-JSP data flow is straightforward: the Servlet sets named attributes on the appropriate scope object, calls forward(), and the JSP reads them with EL. Use request scope for everything that belongs to a single page response. Use session scope for identity and short-lived cross-request state. Avoid application scope unless the data is truly global and read-only. These discipline rules keep your views simple, your Servlets testable, and your memory footprint predictable.