Java Web Basic – Tag 8 von 10
Von Elyndra Valen, Senior Developer bei Java Fleet Systems Consulting
🗺️ Deine Position im Kurs
| Tag | Thema | Status |
|---|---|---|
| 1 | Java EE Überblick & HTTP | ✅ Abgeschlossen |
| 2 | HTTP-Protokoll Vertiefung & Zustandslosigkeit | ✅ Abgeschlossen |
| 3 | Servlets & Servlet API | ✅ Abgeschlossen |
| 4 | Deployment Descriptor & MVC vs Model 2 | ✅ Abgeschlossen |
| 5 | JSP & Expression Languages | ✅ Abgeschlossen |
| 6 | Java Beans, Actions, Scopes & Direktiven | ✅ Abgeschlossen |
| 7 | Include-Action vs Include-Direktive | ✅ Abgeschlossen |
| → 8 | JSTL – Java Standard Tag Libraries | 👉 DU BIST HIER! |
| 9 | Java Web und Datenbanken – Datasource | 🔒 Noch nicht freigeschaltet |
| 10 | Connection Pools & JDBC in Web-Umgebungen | 🔒 Noch nicht freigeschaltet |
Modul: Java Web Basic
Gesamt-Dauer: 10 Arbeitstage (je 8 Stunden)
Dein Ziel: JSTL verstehen und produktiv einsetzen – endgültig Abschied von Scriptlets!
📋 Voraussetzungen für diesen Tag
Du brauchst:
- ✅ JDK 21 LTS installiert
- ✅ Apache NetBeans 22 (oder neuer)
- ✅ Payara Server 6.x konfiguriert
- ✅ Tag 1-7 abgeschlossen
- ✅ Expression Language (EL) beherrschen
- ✅ JSP-Direktiven verstanden
- ✅ Include-Action vs Include-Direktive kennen
Tag verpasst?
Spring zurück zu Tag 5, um Expression Language zu verstehen. JSTL baut darauf auf!
Setup-Probleme?
Schreib uns: support@java-developer.online
⚡ Das Wichtigste in 30 Sekunden
Heute lernst du:
- ✅ Was JSTL ist und warum es wichtig ist
- ✅ Core Tags (
<c:forEach>,<c:if>,<c:choose>) - ✅ Formatting Tags (
<fmt:formatNumber>,<fmt:formatDate>) - ✅ Functions Library (
fn:length,fn:toUpperCase, etc.) - ✅ URL & XML Tags (Basics)
- ✅ Best Practices für JSTL in modernen Anwendungen
Am Ende des Tages kannst du:
- JSTL-Tags sicher verwenden
- Schleifen und Bedingungen ohne Java-Code schreiben
- Daten professionell formatieren
- String-Operationen mit JSTL Functions durchführen
- Komplett scriptlet-freie JSPs erstellen
- Production-Ready Views bauen
Zeit-Investment: ~8 Stunden
Schwierigkeitsgrad: Mittel (viele praktische Beispiele!)
👋 Willkommen zu Tag 8!
Hi! 👋
Elyndra hier. Heute wird’s richtig praktisch – JSTL ist der Game-Changer für saubere JSPs!
Kurzwiederholung: Challenge von Tag 7
Gestern solltest du ein modulares Layout mit Include-Actions erstellen. Falls du es noch nicht gemacht hast, schau dir das Beispielprojekt an – es zeigt Best Practices für wiederverwendbare Komponenten!
Heute geht’s um das finale Puzzle-Stück:
JSTL – Java Standard Tag Libraries.
Erinnere dich an Tag 5, als wir über Scriptlets gesprochen haben? Die bösen <% %> Tags, die deinen Code unleserlich machen?
Mit JSTL wirst du sie NIE wieder brauchen!
Real talk: Als ich bei Java Fleet anfing, hab ich noch Scriptlets geschrieben. Nova (unsere Junior Dev) hat mich mal gefragt: „Warum schreibst du Java-Code in HTML? Isn’t that, like, total chaos?“
Sie hatte recht. 😅
JSTL ist die Lösung. Es gibt dir alle Kontrollstrukturen (Schleifen, Bedingungen) als saubere XML-Tags. Keine Scriptlets mehr. Keine Java-Imports in JSPs. Nur sauberer, wartbarer Code.
Let’s do this!
🟢 GRUNDLAGEN: Was ist JSTL?
Das Problem: Scriptlets sind böse
Erinnerst du dich an das hier?
<%@ page import="java.util.List, com.shop.model.Product" %>
<%
List<Product> products = (List<Product>) request.getAttribute("products");
if (products != null && !products.isEmpty()) {
for (Product product : products) {
%>
<div class="product">
<h3><%= product.getName() %></h3>
<p>$<%= product.getPrice() %></p>
<% if (product.getStock() > 0) { %>
<button>Add to Cart</button>
<% } else { %>
<p class="out-of-stock">Out of Stock</p>
<% } %>
</div>
<%
}
} else {
%>
<p>No products found.</p>
<%
}
%>
Das ist Spaghetti-Code!
❌ Java und HTML gemischt
❌ Unleserlich
❌ Schwer zu warten
❌ Designer können nicht damit arbeiten
❌ XSS-anfällig
❌ Debugging-Albtraum
Die Lösung: JSTL!
Was ist JSTL?
JSTL = Jakarta Standard Tag Library (früher: JSP Standard Tag Library)
Eine Sammlung von wiederverwendbaren Tag-Bibliotheken, die häufige Aufgaben in JSPs vereinfachen.
Denk an JSTL wie an:
- Eine „Standardbibliothek“ für JSPs (wie
java.utilfür Java) - Fertige Tags für Schleifen, Bedingungen, Formatierung
- Die moderne Alternative zu Scriptlets
Mit JSTL wird der obige Code zu:
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<c:choose>
<c:when test="${not empty products}">
<c:forEach var="product" items="${products}">
<div class="product">
<h3>${product.name}</h3>
<p><fmt:formatNumber value="${product.price}" type="currency" /></p>
<c:choose>
<c:when test="${product.stock > 0}">
<button>Add to Cart</button>
</c:when>
<c:otherwise>
<p class="out-of-stock">Out of Stock</p>
</c:otherwise>
</c:choose>
</div>
</c:forEach>
</c:when>
<c:otherwise>
<p>No products found.</p>
</c:otherwise>
</c:choose>
Viel besser, oder? ✨
Die JSTL-Bibliotheken
JSTL besteht aus 5 Tag-Libraries:
| Library | Prefix | URI | Zweck |
|---|---|---|---|
| Core | c | jakarta.tags.core | Schleifen, Bedingungen, Variablen |
| Formatting | fmt | jakarta.tags.fmt | Zahlen, Datum, Internationalisierung |
| Functions | fn | jakarta.tags.functions | String-Operationen |
| XML | x | jakarta.tags.xml | XML-Verarbeitung (selten genutzt) |
| SQL | sql | jakarta.tags.sql | Datenbank-Zugriff (NICHT empfohlen!) |
Heute fokussieren wir uns auf:
- Core (die wichtigste!)
- Formatting
- Functions
XML und SQL sind für moderne Anwendungen weniger relevant.
JSTL einrichten
Schritt 1: Dependency hinzufügen (Maven)
<!-- pom.xml -->
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jakarta.servlet.jsp.jstl</artifactId>
<version>3.0.1</version>
</dependency>
Wichtig:
- Ab Jakarta EE 10:
jakarta.servlet.jsp.jstl - Alte Projekte (Java EE 8 und früher):
javax.servlet.jsp.jstl
Schritt 2: Taglib in JSP importieren
<%@ taglib prefix="c" uri="jakarta.tags.core" %> <%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %> <%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
Schritt 3: Tags verwenden!
<c:forEach var="item" items="${products}">
<p>${item.name}</p>
</c:forEach>
Das war’s! Easy, oder?
🟢 GRUNDLAGEN: Core Tags – Die Basis
Die Core Tag Library ist die wichtigste JSTL-Bibliothek. Sie ersetzt die meisten Scriptlet-Anwendungsfälle.
<c:out> – Sichere Ausgabe
Syntax:
<c:out value="${expression}" default="defaultValue" escapeXml="true" />
Beispiel:
<!-- Ausgabe mit Fallback -->
<c:out value="${user.name}" default="Guest" />
<!-- Äquivalent zu EL mit null-check -->
${not empty user.name ? user.name : 'Guest'}
Wann <c:out> nutzen?
Option 1: EL direkt (meist besser):
${product.name} <!-- Escapet automatisch HTML -->
Option 2: <c:out> (wenn explizite Kontrolle gewünscht):
<c:out value="${product.name}" escapeXml="true" />
Wichtig: escapeXml="false" nur verwenden, wenn du HTML-Code bewusst ausgeben willst!
<!-- ⚠️ VORSICHT: XSS-Gefahr! -->
<c:out value="${content.html}" escapeXml="false" />
Best Practice: Nutze EL direkt (${...}), außer du brauchst explizites escapeXml-Handling!
<c:set> – Variablen setzen
Syntax:
<c:set var="variableName" value="value" scope="scope" />
Beispiel:
<!-- Variable im Page-Scope setzen -->
<c:set var="discount" value="0.1" />
<p>Discount: ${discount * 100}%</p>
<!-- Variable im Request-Scope setzen -->
<c:set var="totalPrice" value="${product.price * quantity}" scope="request" />
<!-- Property eines Objekts setzen -->
<c:set target="${user}" property="loggedIn" value="true" />
Scopes:
page(default)requestsessionapplication
Wann verwenden?
✅ Zwischenergebnisse speichern
✅ Komplexe Berechnungen vereinfachen
✅ Wiederverwendbare Werte
Beispiel:
<!-- Berechnung einmal durchführen -->
<c:set var="total" value="${product.price * quantity * (1 - discount)}" />
<!-- Mehrfach verwenden -->
<p>Subtotal: ${product.price * quantity}</p>
<p>Discount: ${discount * 100}%</p>
<p>Total: ${total}</p>
<c:remove> – Variablen löschen
Syntax:
<c:remove var="variableName" scope="scope" />
Beispiel:
<c:set var="tempData" value="some data" /> <!-- Use tempData --> <c:remove var="tempData" /> <!-- Cleanup -->
Selten gebraucht – aber nützlich für Cleanup!
<c:if> – Einfache Bedingungen
Syntax:
<c:if test="${condition}">
<!-- Content wenn true -->
</c:if>
Beispiel:
<c:if test="${user.admin}">
<a href="/admin">Admin Panel</a>
</c:if>
<c:if test="${product.stock > 0}">
<button>Add to Cart</button>
</c:if>
<c:if test="${not empty errorMessage}">
<div class="error">${errorMessage}</div>
</c:if>
Wichtig:
❌ Kein else! <c:if> hat keine else-Klausel!
Für if-else brauchst du <c:choose>!
<c:choose>, <c:when>, <c:otherwise> – If-Else-Konstrukte
Syntax:
<c:choose>
<c:when test="${condition1}">
<!-- Content wenn condition1 true -->
</c:when>
<c:when test="${condition2}">
<!-- Content wenn condition2 true -->
</c:when>
<c:otherwise>
<!-- Content wenn keine Bedingung true (else) -->
</c:otherwise>
</c:choose>
Beispiel:
<c:choose>
<c:when test="${user.role == 'ADMIN'}">
<p>Welcome, Admin!</p>
<a href="/admin">Admin Dashboard</a>
</c:when>
<c:when test="${user.role == 'MODERATOR'}">
<p>Welcome, Moderator!</p>
<a href="/moderate">Moderation Panel</a>
</c:when>
<c:otherwise>
<p>Welcome, User!</p>
<a href="/profile">My Profile</a>
</c:otherwise>
</c:choose>
Real-World Beispiel: Product Stock Status
<c:choose>
<c:when test="${product.stock > 10}">
<span class="badge badge-success">In Stock</span>
</c:when>
<c:when test="${product.stock > 0}">
<span class="badge badge-warning">Low Stock (${product.stock} left)</span>
</c:when>
<c:otherwise>
<span class="badge badge-danger">Out of Stock</span>
</c:otherwise>
</c:choose>
<c:forEach> – Schleifen (Das Wichtigste!)
Syntax:
<c:forEach var="item" items="${collection}">
<!-- Content wird für jedes Element wiederholt -->
</c:forEach>
Beispiel 1: Liste iterieren
<c:forEach var="product" items="${products}">
<div class="product-card">
<h3>${product.name}</h3>
<p>${product.description}</p>
<p class="price">$${product.price}</p>
</div>
</c:forEach>
Beispiel 2: Mit Index
<c:forEach var="product" items="${products}" varStatus="status">
<div class="product-card">
<span class="number">${status.index + 1}.</span>
<h3>${product.name}</h3>
<c:if test="${status.first}">
<span class="badge">New!</span>
</c:if>
<c:if test="${status.last}">
<span class="badge">Last Chance!</span>
</c:if>
</div>
</c:forEach>
varStatus Properties:
| Property | Beschreibung | Typ |
|---|---|---|
index | Aktueller Index (0-basiert) | int |
count | Aktuelle Iteration (1-basiert) | int |
first | Ist das erste Element? | boolean |
last | Ist das letzte Element? | boolean |
current | Aktuelles Element | Typ des Elements |
Beispiel 3: Range (Zahlen-Loop)
<!-- Von 1 bis 10 -->
<c:forEach var="i" begin="1" end="10">
<p>Number ${i}</p>
</c:forEach>
<!-- Mit Schrittweite -->
<c:forEach var="i" begin="0" end="100" step="10">
<p>${i}%</p>
</c:forEach>
Beispiel 4: Verschachtelte Schleifen
<c:forEach var="category" items="${categories}">
<h2>${category.name}</h2>
<ul>
<c:forEach var="product" items="${category.products}">
<li>${product.name} - $${product.price}</li>
</c:forEach>
</ul>
</c:forEach>
<c:forTokens> – String-Splitting
Syntax:
<c:forTokens var="token" items="${string}" delims="delimiter">
<!-- Process each token -->
</c:forTokens>
Beispiel:
<c:set var="tags" value="java,web,jstl,tutorial" />
<c:forTokens var="tag" items="${tags}" delims=",">
<span class="tag">${tag}</span>
</c:forTokens>
<!-- Ergebnis: -->
<!-- <span class="tag">java</span> -->
<!-- <span class="tag">web</span> -->
<!-- <span class="tag">jstl</span> -->
<!-- <span class="tag">tutorial</span> -->
Mehrere Delimiters:
<c:forTokens var="word" items="apple;banana,cherry;date" delims=",;">
<p>${word}</p>
</c:forTokens>
<c:url> – URLs erstellen
Syntax:
<c:url var="variableName" value="/path" />
Beispiel:
<!-- URL mit Context-Path -->
<c:url var="homeUrl" value="/home" />
<a href="${homeUrl}">Home</a>
<!-- Mit Query-Parametern -->
<c:url var="productUrl" value="/product">
<c:param name="id" value="${product.id}" />
<c:param name="category" value="${product.category}" />
</c:url>
<a href="${productUrl}">View Product</a>
<!-- Ergebnis: /myapp/product?id=42&category=electronics -->
Warum <c:url> verwenden?
✅ Automatischer Context-Path: /myapp/product statt nur /product
✅ URL-Encoding: Sonderzeichen werden automatisch encoded
✅ Session-ID-Rewriting: Falls Cookies deaktiviert sind
Best Practice:
<!-- ❌ FALSCH: Manueller Context-Path -->
<a href="${pageContext.request.contextPath}/product?id=${product.id}">...</a>
<!-- ✅ RICHTIG: Mit <c:url> -->
<c:url var="productUrl" value="/product">
<c:param name="id" value="${product.id}" />
</c:url>
<a href="${productUrl}">...</a>
<c:redirect> – Redirect durchführen
Syntax:
<c:redirect url="/path" />
Beispiel:
<!-- Einfacher Redirect -->
<c:if test="${not user.loggedIn}">
<c:redirect url="/login" />
</c:if>
<!-- Mit Parametern -->
<c:redirect url="/search">
<c:param name="query" value="${searchTerm}" />
<c:param name="page" value="1" />
</c:redirect>
Unterschied zu Servlet-Redirect:
// Servlet:
response.sendRedirect("/login");
<!-- JSP: --> <c:redirect url="/login" />
Wann verwenden?
⚠️ Selten! Meist sollte der Controller (Servlet) Redirects durchführen, nicht die View!
<c:import> – Content importieren
Syntax:
<c:import url="/path" var="variableName" />
Beispiel:
<!-- Include externe Seite -->
<c:import url="https://example.com/data.xml" var="xmlData" />
<c:out value="${xmlData}" />
<!-- Include interne Resource -->
<c:import url="/WEB-INF/fragments/sidebar.jsp" />
Unterschied zu <jsp:include>:
| Feature | <jsp:include> | <c:import> |
|---|---|---|
| Externe URLs | ❌ Nein | ✅ Ja |
| Variablen speichern | ❌ Nein | ✅ Ja (var-Attribut) |
| Relative Pfade | ✅ Ja | ✅ Ja |
Best Practice: Nutze <jsp:include> für interne JSPs, <c:import> nur für externe Resources!
<c:catch> – Exception Handling
Syntax:
<c:catch var="exceptionVariable">
<!-- Code, der Exception werfen könnte -->
</c:catch>
<c:if test="${not empty exceptionVariable}">
<p>Error: ${exceptionVariable.message}</p>
</c:if>
Beispiel:
<c:catch var="error">
${product.price / product.quantity} <!-- Kann Division by Zero sein -->
</c:catch>
<c:if test="${not empty error}">
<div class="alert alert-danger">
<p>Error occurred: ${error.message}</p>
</div>
</c:if>
Wann verwenden?
⚠️ Selten! Exceptions sollten im Controller behandelt werden, nicht in der View!
🟡 PROFESSIONALS: Formatting Tags
Die Formatting Tag Library hilft bei der Formatierung von Zahlen, Datumswerten und Internationalisierung.
<fmt:formatNumber> – Zahlen formatieren
Syntax:
<fmt:formatNumber value="${number}" type="type" pattern="pattern" />
Typen:
number(default)currencypercent
Beispiel:
<!-- Währung -->
<fmt:formatNumber value="${product.price}" type="currency" />
<!-- Ergebnis: $99.99 -->
<!-- Prozent -->
<fmt:formatNumber value="${discount}" type="percent" />
<!-- Ergebnis: 10% -->
<!-- Dezimalstellen -->
<fmt:formatNumber value="${product.rating}" maxFractionDigits="1" />
<!-- Ergebnis: 4.5 -->
<!-- Tausender-Trennzeichen -->
<fmt:formatNumber value="${views}" pattern="#,###" />
<!-- Ergebnis: 1,234,567 -->
Custom Pattern:
<!-- 2 Dezimalstellen, immer angezeigt -->
<fmt:formatNumber value="${product.price}" pattern="0.00" />
<!-- 9.5 → 9.50 -->
<!-- Optional Dezimalstellen -->
<fmt:formatNumber value="${product.price}" pattern="#.##" />
<!-- 9.5 → 9.5 -->
<fmt:parseNumber> – String zu Zahl
Syntax:
<fmt:parseNumber var="variableName" value="${string}" type="type" />
Beispiel:
<!-- String zu Zahl parsen -->
<c:set var="priceString" value="99.99" />
<fmt:parseNumber var="price" value="${priceString}" type="number" />
<!-- Jetzt als Zahl verfügbar -->
<p>Double Price: ${price * 2}</p>
<fmt:formatDate> – Datum formatieren
Syntax:
<fmt:formatDate value="${date}" type="type" dateStyle="style" timeStyle="style" pattern="pattern" />
Typen:
date– Nur Datumtime– Nur Zeitboth– Datum und Zeit
Styles:
shortmediumlongfull
Beispiel:
<!-- Datum - verschiedene Stile -->
<fmt:formatDate value="${product.createdAt}" type="date" dateStyle="short" />
<!-- 10/31/25 -->
<fmt:formatDate value="${product.createdAt}" type="date" dateStyle="medium" />
<!-- Oct 31, 2025 -->
<fmt:formatDate value="${product.createdAt}" type="date" dateStyle="long" />
<!-- October 31, 2025 -->
<fmt:formatDate value="${product.createdAt}" type="date" dateStyle="full" />
<!-- Friday, October 31, 2025 -->
Custom Pattern:
<!-- DD.MM.YYYY -->
<fmt:formatDate value="${order.date}" pattern="dd.MM.yyyy" />
<!-- 31.10.2025 -->
<!-- YYYY-MM-DD HH:mm:ss -->
<fmt:formatDate value="${order.timestamp}" pattern="yyyy-MM-dd HH:mm:ss" />
<!-- 2025-10-31 14:30:45 -->
<!-- Weekday, Month Day -->
<fmt:formatDate value="${event.date}" pattern="EEEE, MMMM d" />
<!-- Friday, October 31 -->
Pattern Symbols:
| Symbol | Bedeutung | Beispiel |
|---|---|---|
y | Jahr | 2025 |
M | Monat (Zahl) | 10 |
MM | Monat (2-stellig) | 10 |
MMM | Monat (kurz) | Oct |
MMMM | Monat (lang) | October |
d | Tag | 31 |
dd | Tag (2-stellig) | 31 |
E | Wochentag (kurz) | Fri |
EEEE | Wochentag (lang) | Friday |
H | Stunde (0-23) | 14 |
HH | Stunde (2-stellig) | 14 |
m | Minute | 30 |
mm | Minute (2-stellig) | 30 |
s | Sekunde | 45 |
ss | Sekunde (2-stellig) | 45 |
<fmt:parseDate> – String zu Datum
Syntax:
<fmt:parseDate var="variableName" value="${string}" pattern="pattern" />
Beispiel:
<!-- User gibt Datum als String ein -->
<c:set var="dateString" value="2025-10-31" />
<!-- String zu Date parsen -->
<fmt:parseDate var="parsedDate" value="${dateString}" pattern="yyyy-MM-dd" />
<!-- Jetzt als Date verfügbar -->
<fmt:formatDate value="${parsedDate}" pattern="MMMM dd, yyyy" />
<!-- October 31, 2025 -->
<fmt:setLocale> – Locale setzen
Syntax:
<fmt:setLocale value="locale" />
Beispiel:
<!-- Deutsch -->
<fmt:setLocale value="de_DE" />
<fmt:formatNumber value="1234.56" type="currency" />
<!-- 1.234,56 € -->
<fmt:formatDate value="${now}" type="date" dateStyle="long" />
<!-- 31. Oktober 2025 -->
<!-- English -->
<fmt:setLocale value="en_US" />
<fmt:formatNumber value="1234.56" type="currency" />
<!-- $1,234.56 -->
<fmt:formatDate value="${now}" type="date" dateStyle="long" />
<!-- October 31, 2025 -->
Wichtige Locales:
de_DE– Deutsch (Deutschland)en_US– Englisch (USA)en_GB– Englisch (UK)fr_FR– Französisch (Frankreich)es_ES– Spanisch (Spanien)it_IT– Italienisch (Italien)ja_JP– Japanisch (Japan)zh_CN– Chinesisch (China)
<fmt:timeZone> – Zeitzone setzen
Syntax:
<fmt:timeZone value="timeZone">
<!-- Content mit dieser Zeitzone -->
</fmt:timeZone>
Beispiel:
<!-- UTC -->
<fmt:timeZone value="UTC">
<fmt:formatDate value="${order.timestamp}" pattern="yyyy-MM-dd HH:mm:ss" />
</fmt:timeZone>
<!-- Europe/Berlin -->
<fmt:timeZone value="Europe/Berlin">
<fmt:formatDate value="${order.timestamp}" pattern="yyyy-MM-dd HH:mm:ss" />
</fmt:timeZone>
<!-- America/New_York -->
<fmt:timeZone value="America/New_York">
<fmt:formatDate value="${order.timestamp}" pattern="yyyy-MM-dd HH:mm:ss" />
</fmt:timeZone>
🟡 PROFESSIONALS: Functions Library
Die Functions Library bietet String- und Collection-Operationen.
Wichtig: Functions sind KEINE Tags, sondern EL-Funktionen!
Import:
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
String-Funktionen
fn:length – Länge bestimmen
${fn:length(product.name)}
<!-- Ergebnis: 10 -->
<c:if test="${fn:length(user.password) < 8}">
<p class="error">Password too short!</p>
</c:if>
fn:toUpperCase / fn:toLowerCase – Case konvertieren
${fn:toUpperCase(product.name)}
<!-- "JAVA WEB BASIC" -->
${fn:toLowerCase(user.email)}
<!-- "user@example.com" -->
fn:substring – Teilstring
<!-- Substring von Index 0 bis 10 -->
${fn:substring(product.description, 0, 10)}...
<!-- "Java Web B..." -->
fn:trim – Whitespace entfernen
${fn:trim(user.name)}
<!-- " John Doe " → "John Doe" -->
fn:replace – Ersetzen
${fn:replace(product.name, "Java", "Python")}
<!-- "Java Web Basic" → "Python Web Basic" -->
fn:contains – Enthält String?
<c:if test="${fn:contains(user.email, '@gmail.com')}">
<p>Gmail user detected!</p>
</c:if>
fn:startsWith / fn:endsWith – Beginnt/Endet mit?
<c:if test="${fn:startsWith(file.name, 'invoice')}">
<span class="badge">Invoice</span>
</c:if>
<c:if test="${fn:endsWith(file.name, '.pdf')}">
<img src="/icons/pdf.png" alt="PDF" />
</c:if>
fn:indexOf – Index finden
${fn:indexOf(product.sku, "-")}
<!-- "PROD-12345" → 4 -->
fn:split – String splitten
<c:set var="tags" value="${fn:split(product.tags, ',')}" />
<c:forEach var="tag" items="${tags}">
<span class="tag">${tag}</span>
</c:forEach>
fn:join – Strings verbinden
<c:set var="words" value="${['Java', 'Web', 'JSTL']}" />
${fn:join(words, " - ")}
<!-- "Java - Web - JSTL" -->
fn:escapeXml – HTML/XML escapen
${fn:escapeXml(userInput)}
<!-- "<script>alert('XSS')</script>" → "<script>alert('XSS')</script>" -->
Collection-Funktionen
fn:length – Collection-Größe
<p>You have ${fn:length(cart.items)} items in your cart.</p>
<c:if test="${fn:length(products) == 0}">
<p>No products found.</p>
</c:if>
fn:contains – Collection enthält Element?
<c:if test="${fn:contains(user.roles, 'ADMIN')}">
<a href="/admin">Admin Panel</a>
</c:if>
🎯 REAL-WORLD: Komplettes Beispiel
Lass uns alles zusammenführen in einem realistischen Beispiel!
Szenario: E-Commerce Product Listing Page
ProductServlet.java:
@WebServlet("/products")
public class ProductServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Produkte aus Datenbank holen (vereinfacht)
List<Product> products = List.of(
new Product(1, "Java Web Basic Course", "Learn Java Web Development", 99.99, 15, "ACTIVE"),
new Product(2, "Spring Boot Masterclass", "Master Spring Boot Framework", 149.99, 3, "ACTIVE"),
new Product(3, "Legacy Course", "Old outdated content", 29.99, 0, "DISCONTINUED")
);
request.setAttribute("products", products);
request.setAttribute("lastUpdated", new Date());
request.setAttribute("discountRate", 0.15);
request.getRequestDispatcher("/WEB-INF/views/products.jsp")
.forward(request, response);
}
}
products.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Products - Java Fleet Shop</title>
<style>
.product-card { border: 1px solid #ddd; padding: 1rem; margin: 1rem; }
.badge { padding: 0.25rem 0.5rem; border-radius: 3px; font-size: 0.875rem; }
.badge-success { background: #28a745; color: white; }
.badge-warning { background: #ffc107; color: black; }
.badge-danger { background: #dc3545; color: white; }
.badge-secondary { background: #6c757d; color: white; }
.price { font-size: 1.5rem; font-weight: bold; color: #28a745; }
.discount-price { text-decoration: line-through; color: #999; }
.out-of-stock { color: #dc3545; }
</style>
</head>
<body>
<header>
<h1>Java Fleet Shop - Products</h1>
<p>
Last Updated:
<fmt:formatDate value="${lastUpdated}" pattern="MMMM dd, yyyy 'at' HH:mm" />
</p>
</header>
<main>
<c:choose>
<c:when test="${not empty products}">
<p>Showing ${fn:length(products)} products</p>
<!-- Current Discount Banner -->
<c:if test="${discountRate > 0}">
<div class="alert alert-info">
🎉 Special Offer:
<fmt:formatNumber value="${discountRate}" type="percent" />
discount on all active courses!
</div>
</c:if>
<!-- Products Grid -->
<div class="products-grid">
<c:forEach var="product" items="${products}" varStatus="status">
<!-- Skip discontinued products -->
<c:if test="${product.status != 'DISCONTINUED'}">
<div class="product-card">
<!-- Product Number -->
<span class="product-number">#${status.count}</span>
<!-- New Badge für erste 2 Produkte -->
<c:if test="${status.index < 2}">
<span class="badge badge-success">NEW</span>
</c:if>
<!-- Product Title -->
<h2>${product.name}</h2>
<!-- Description (Max 50 chars) -->
<p>
<c:choose>
<c:when test="${fn:length(product.description) > 50}">
${fn:substring(product.description, 0, 50)}...
</c:when>
<c:otherwise>
${product.description}
</c:otherwise>
</c:choose>
</p>
<!-- Price with Discount -->
<div class="pricing">
<c:set var="discountedPrice" value="${product.price * (1 - discountRate)}" />
<c:if test="${discountRate > 0}">
<span class="discount-price">
<fmt:formatNumber value="${product.price}" type="currency" />
</span>
<br/>
</c:if>
<span class="price">
<fmt:formatNumber value="${discountedPrice}" type="currency" />
</span>
</div>
<!-- Stock Status -->
<div class="stock-status">
<c:choose>
<c:when test="${product.stock > 10}">
<span class="badge badge-success">
✓ In Stock (${product.stock} available)
</span>
</c:when>
<c:when test="${product.stock > 0}">
<span class="badge badge-warning">
⚠ Low Stock (${product.stock} left)
</span>
</c:when>
<c:otherwise>
<span class="badge badge-danger">
✗ Out of Stock
</span>
</c:otherwise>
</c:choose>
</div>
<!-- Action Buttons -->
<div class="actions">
<c:choose>
<c:when test="${product.stock > 0}">
<c:url var="addToCartUrl" value="/cart/add">
<c:param name="productId" value="${product.id}" />
</c:url>
<a href="${addToCartUrl}" class="btn btn-primary">
Add to Cart
</a>
</c:when>
<c:otherwise>
<button class="btn btn-secondary" disabled>
Sold Out
</button>
</c:otherwise>
</c:choose>
<c:url var="detailsUrl" value="/products/details">
<c:param name="id" value="${product.id}" />
</c:url>
<a href="${detailsUrl}" class="btn btn-outline">
View Details
</a>
</div>
</div>
</c:if>
</c:forEach>
</div>
</c:when>
<c:otherwise>
<div class="empty-state">
<h2>No products found</h2>
<p>Check back later for new courses!</p>
</div>
</c:otherwise>
</c:choose>
</main>
<footer>
<p>© 2025 Java Fleet Systems Consulting</p>
</footer>
</body>
</html>
Das Beispiel zeigt:
✅ <c:forEach> mit varStatus für Zähler und first/last
✅ <c:if> für einfache Bedingungen
✅ <c:choose> für if-else-Konstrukte
✅ <fmt:formatNumber> für Preise und Prozente
✅ <fmt:formatDate> für Zeitstempel
✅ fn:length für Collection-Größe
✅ fn:substring für Text-Truncation
✅ <c:url> für saubere URLs
✅ <c:set> für Zwischenberechnungen
✅ Keine einzige Scriptlet! 🎉
🟡 PROFESSIONALS: HTTP-Fehlerbehandlung mit JSP & JSTL
Das Problem: Fehlerseiten in Webanwendungen
Was passiert, wenn etwas schief geht?
- 404 – Seite nicht gefunden
- 500 – Internal Server Error
- 403 – Zugriff verweigert
- 400 – Bad Request
Ohne Fehlerbehandlung:
HTTP Status 500 – Internal Server Error Type: Exception Report Message: java.lang.NullPointerException Description: The server encountered an internal error that prevented it from fulfilling this request.
Sieht unprofessionell aus! 😬
Mit Fehlerbehandlung:
Eine schöne, gebrandete Fehlerseite mit hilfreichen Informationen!
Fehlerbehandlung konfigurieren (web.xml)
Option 1: Nach Error-Code
<!-- web.xml -->
<web-app 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-app_6_0.xsd"
version="6.0">
<!-- 404 - Not Found -->
<error-page>
<error-code>404</error-code>
<location>/WEB-INF/errors/404.jsp</location>
</error-page>
<!-- 500 - Internal Server Error -->
<error-page>
<error-code>500</error-code>
<location>/WEB-INF/errors/500.jsp</location>
</error-page>
<!-- 403 - Forbidden -->
<error-page>
<error-code>403</error-code>
<location>/WEB-INF/errors/403.jsp</location>
</error-page>
<!-- 400 - Bad Request -->
<error-page>
<error-code>400</error-code>
<location>/WEB-INF/errors/400.jsp</location>
</error-page>
</web-app>
Option 2: Nach Exception-Typ
<!-- web.xml -->
<web-app ...>
<!-- NullPointerException -->
<error-page>
<exception-type>java.lang.NullPointerException</exception-type>
<location>/WEB-INF/errors/null-pointer.jsp</location>
</error-page>
<!-- SQLException -->
<error-page>
<exception-type>java.sql.SQLException</exception-type>
<location>/WEB-INF/errors/database-error.jsp</location>
</error-page>
<!-- Alle anderen Exceptions -->
<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/WEB-INF/errors/general-error.jsp</location>
</error-page>
</web-app>
Option 3: Generische Fehlerseite (Fallback)
<!-- web.xml -->
<web-app ...>
<!-- Catch-All für alle Error-Codes -->
<error-page>
<location>/WEB-INF/errors/error.jsp</location>
</error-page>
</web-app>
Implizite Error-Objekte in JSP
In einer Error-JSP stehen folgende implizite Objekte zur Verfügung:
| Objekt | Typ | Beschreibung |
|---|---|---|
exception | Throwable | Die geworfene Exception (nur bei exception-type) |
${requestScope['jakarta.servlet.error.status_code']} | Integer | HTTP Status Code |
${requestScope['jakarta.servlet.error.exception_type']} | Class | Exception-Klasse |
${requestScope['jakarta.servlet.error.message']} | String | Error-Message |
${requestScope['jakarta.servlet.error.exception']} | Throwable | Exception-Objekt |
${requestScope['jakarta.servlet.error.request_uri']} | String | URI die den Fehler verursacht hat |
${requestScope['jakarta.servlet.error.servlet_name']} | String | Name des Servlets |
Wichtig: Ab Jakarta EE 9+ heißen die Attribute jakarta.servlet.error.* (nicht mehr javax.*!)
Error-JSP mit JSTL erstellen
404.jsp – Page Not Found:
<%@ page contentType="text/html;charset=UTF-8" language="java" isErrorPage="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>404 - Page Not Found | Java Fleet</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 0;
padding: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.error-container {
background: white;
border-radius: 10px;
padding: 3rem;
max-width: 600px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
text-align: center;
}
.error-code {
font-size: 6rem;
font-weight: bold;
color: #667eea;
margin: 0;
}
.error-title {
font-size: 2rem;
color: #333;
margin: 1rem 0;
}
.error-message {
color: #666;
margin: 1rem 0 2rem;
line-height: 1.6;
}
.error-details {
background: #f5f5f5;
padding: 1rem;
border-radius: 5px;
margin: 1rem 0;
font-size: 0.9rem;
color: #666;
}
.btn {
display: inline-block;
padding: 12px 30px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 5px;
margin: 0.5rem;
transition: background 0.3s;
}
.btn:hover {
background: #5568d3;
}
</style>
</head>
<body>
<div class="error-container">
<div class="error-code">404</div>
<h1 class="error-title">Oops! Page Not Found</h1>
<p class="error-message">
The page you're looking for doesn't exist or has been moved.
</p>
<c:if test="${not empty requestScope['jakarta.servlet.error.request_uri']}">
<div class="error-details">
<strong>Requested URL:</strong><br/>
<code>${requestScope['jakarta.servlet.error.request_uri']}</code>
</div>
</c:if>
<div>
<c:url var="homeUrl" value="/" />
<a href="${homeUrl}" class="btn">🏠 Back to Home</a>
<c:url var="contactUrl" value="/contact" />
<a href="${contactUrl}" class="btn">📧 Contact Support</a>
</div>
</div>
</body>
</html>
500.jsp – Internal Server Error:
<%@ page contentType="text/html;charset=UTF-8" language="java" isErrorPage="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>500 - Server Error | Java Fleet</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
margin: 0;
padding: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.error-container {
background: white;
border-radius: 10px;
padding: 3rem;
max-width: 800px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.error-code {
font-size: 6rem;
font-weight: bold;
color: #f5576c;
margin: 0;
text-align: center;
}
.error-title {
font-size: 2rem;
color: #333;
margin: 1rem 0;
text-align: center;
}
.error-message {
color: #666;
margin: 1rem 0 2rem;
line-height: 1.6;
text-align: center;
}
.error-details {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 5px;
margin: 1rem 0;
border-left: 4px solid #f5576c;
}
.error-details h3 {
margin-top: 0;
color: #333;
}
.error-details code {
display: block;
background: #fff;
padding: 1rem;
border-radius: 3px;
margin: 0.5rem 0;
overflow-x: auto;
font-size: 0.9rem;
}
.btn {
display: inline-block;
padding: 12px 30px;
background: #f5576c;
color: white;
text-decoration: none;
border-radius: 5px;
margin: 0.5rem;
transition: background 0.3s;
}
.btn:hover {
background: #e04858;
}
.btn-secondary {
background: #6c757d;
}
.btn-secondary:hover {
background: #5a6268;
}
.actions {
text-align: center;
margin-top: 2rem;
}
.stack-trace {
max-height: 300px;
overflow-y: auto;
background: #fff;
padding: 1rem;
border-radius: 3px;
font-size: 0.85rem;
line-height: 1.4;
}
</style>
</head>
<body>
<div class="error-container">
<div class="error-code">500</div>
<h1 class="error-title">Internal Server Error</h1>
<p class="error-message">
Something went wrong on our end. We're working on fixing it!
</p>
<div class="error-details">
<h3>Error Information</h3>
<c:if test="${not empty requestScope['jakarta.servlet.error.request_uri']}">
<p>
<strong>Requested URL:</strong><br/>
<code>${requestScope['jakarta.servlet.error.request_uri']}</code>
</p>
</c:if>
<c:set var="errorException" value="${requestScope['jakarta.servlet.error.exception']}" />
<c:if test="${not empty errorException}">
<p>
<strong>Exception Type:</strong><br/>
<code>${errorException['class'].name}</code>
</p>
<c:if test="${not empty errorException.message}">
<p>
<strong>Error Message:</strong><br/>
<code>${fn:escapeXml(errorException.message)}</code>
</p>
</c:if>
<!-- Stack Trace (nur für Development!) -->
<c:if test="${pageContext.servletContext.getInitParameter('displayStackTrace') eq 'true'}">
<p>
<strong>Stack Trace:</strong>
</p>
<div class="stack-trace">
<c:forEach var="element" items="${errorException.stackTrace}">
${fn:escapeXml(element)}<br/>
</c:forEach>
</div>
</c:if>
</c:if>
<c:if test="${not empty requestScope['jakarta.servlet.error.servlet_name']}">
<p>
<strong>Servlet:</strong><br/>
<code>${requestScope['jakarta.servlet.error.servlet_name']}</code>
</p>
</c:if>
</div>
<div class="actions">
<c:url var="homeUrl" value="/" />
<a href="${homeUrl}" class="btn">🏠 Back to Home</a>
<a href="javascript:history.back()" class="btn btn-secondary">← Go Back</a>
<c:url var="supportUrl" value="/support" />
<a href="${supportUrl}" class="btn btn-secondary">📧 Report Issue</a>
</div>
</div>
</body>
</html>
403.jsp – Forbidden / Access Denied:
<%@ page contentType="text/html;charset=UTF-8" language="java" isErrorPage="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>403 - Access Denied | Java Fleet</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
margin: 0;
padding: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.error-container {
background: white;
border-radius: 10px;
padding: 3rem;
max-width: 600px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
text-align: center;
}
.error-code {
font-size: 6rem;
font-weight: bold;
color: #fa709a;
margin: 0;
}
.error-title {
font-size: 2rem;
color: #333;
margin: 1rem 0;
}
.error-message {
color: #666;
margin: 1rem 0 2rem;
line-height: 1.6;
}
.btn {
display: inline-block;
padding: 12px 30px;
background: #fa709a;
color: white;
text-decoration: none;
border-radius: 5px;
margin: 0.5rem;
transition: background 0.3s;
}
.btn:hover {
background: #e85d87;
}
</style>
</head>
<body>
<div class="error-container">
<div class="error-code">403</div>
<h1 class="error-title">🚫 Access Denied</h1>
<p class="error-message">
You don't have permission to access this resource.
</p>
<c:choose>
<c:when test="${not empty sessionScope.user}">
<p>Logged in as: <strong>${sessionScope.user.username}</strong></p>
<p>Your role doesn't allow access to this page.</p>
</c:when>
<c:otherwise>
<p>You need to be logged in to access this page.</p>
<c:url var="loginUrl" value="/login" />
<a href="${loginUrl}" class="btn">🔐 Login</a>
</c:otherwise>
</c:choose>
<c:url var="homeUrl" value="/" />
<a href="${homeUrl}" class="btn">🏠 Back to Home</a>
</div>
</body>
</html>
Generische Error-Seite (error.jsp)
Eine Error-Seite für alle Fehler:
<%@ page contentType="text/html;charset=UTF-8" language="java" isErrorPage="true" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Error | Java Fleet</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
margin: 0;
padding: 2rem;
}
.error-container {
background: white;
border-radius: 10px;
padding: 2rem;
max-width: 900px;
margin: 0 auto;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.error-header {
text-align: center;
padding-bottom: 2rem;
border-bottom: 2px solid #f0f0f0;
}
.error-code {
font-size: 4rem;
font-weight: bold;
color: #e74c3c;
margin: 0;
}
.error-title {
font-size: 1.5rem;
color: #333;
margin: 1rem 0;
}
.error-info {
margin: 2rem 0;
}
.info-row {
padding: 0.75rem;
border-bottom: 1px solid #f0f0f0;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-weight: bold;
color: #555;
display: inline-block;
width: 150px;
}
.info-value {
color: #666;
word-break: break-all;
}
.btn {
display: inline-block;
padding: 10px 20px;
background: #3498db;
color: white;
text-decoration: none;
border-radius: 5px;
margin: 0.5rem 0.5rem 0 0;
}
.btn:hover {
background: #2980b9;
}
</style>
</head>
<body>
<div class="error-container">
<div class="error-header">
<c:set var="statusCode" value="${requestScope['jakarta.servlet.error.status_code']}" />
<c:set var="errorMessage" value="${requestScope['jakarta.servlet.error.message']}" />
<c:choose>
<c:when test="${not empty statusCode}">
<div class="error-code">${statusCode}</div>
</c:when>
<c:otherwise>
<div class="error-code">ERROR</div>
</c:otherwise>
</c:choose>
<h1 class="error-title">
<c:choose>
<c:when test="${statusCode == 404}">Page Not Found</c:when>
<c:when test="${statusCode == 500}">Internal Server Error</c:when>
<c:when test="${statusCode == 403}">Access Denied</c:when>
<c:when test="${statusCode == 400}">Bad Request</c:when>
<c:otherwise>An Error Occurred</c:otherwise>
</c:choose>
</h1>
</div>
<div class="error-info">
<c:if test="${not empty statusCode}">
<div class="info-row">
<span class="info-label">Status Code:</span>
<span class="info-value">${statusCode}</span>
</div>
</c:if>
<c:if test="${not empty errorMessage}">
<div class="info-row">
<span class="info-label">Message:</span>
<span class="info-value">${fn:escapeXml(errorMessage)}</span>
</div>
</c:if>
<c:set var="requestUri" value="${requestScope['jakarta.servlet.error.request_uri']}" />
<c:if test="${not empty requestUri}">
<div class="info-row">
<span class="info-label">Requested URL:</span>
<span class="info-value">${fn:escapeXml(requestUri)}</span>
</div>
</c:if>
<c:set var="servletName" value="${requestScope['jakarta.servlet.error.servlet_name']}" />
<c:if test="${not empty servletName}">
<div class="info-row">
<span class="info-label">Servlet:</span>
<span class="info-value">${fn:escapeXml(servletName)}</span>
</div>
</c:if>
<c:set var="errorException" value="${requestScope['jakarta.servlet.error.exception']}" />
<c:if test="${not empty errorException}">
<div class="info-row">
<span class="info-label">Exception:</span>
<span class="info-value">${errorException['class'].name}</span>
</div>
<c:if test="${not empty errorException.message}">
<div class="info-row">
<span class="info-label">Exception Message:</span>
<span class="info-value">${fn:escapeXml(errorException.message)}</span>
</div>
</c:if>
</c:if>
</div>
<div style="text-align: center; margin-top: 2rem;">
<c:url var="homeUrl" value="/" />
<a href="${homeUrl}" class="btn">🏠 Home</a>
<a href="javascript:history.back()" class="btn">← Back</a>
</div>
</div>
</body>
</html>
Stack Trace Display konfigurieren
Für Development: Stack Traces anzeigen
web.xml:
<web-app ...>
<!-- Context Parameter für Stack Trace Display -->
<context-param>
<param-name>displayStackTrace</param-name>
<param-value>true</param-value> <!-- true = Development, false = Production -->
</context-param>
<!-- Error Pages -->
<error-page>
<error-code>500</error-code>
<location>/WEB-INF/errors/500.jsp</location>
</error-page>
</web-app>
In der Error-JSP:
<c:if test="${pageContext.servletContext.getInitParameter('displayStackTrace') eq 'true'}">
<div class="stack-trace">
<h3>Stack Trace (Development Mode)</h3>
<c:forEach var="element" items="${errorException.stackTrace}">
${fn:escapeXml(element)}<br/>
</c:forEach>
</div>
</c:if>
⚠️ WICHTIG: In Production IMMER displayStackTrace=false setzen!
Best Practices für Error-Handling
1. Niemals Stack Traces in Production zeigen
❌ GEFÄHRLICH:
<%= exception.printStackTrace() %>
✅ SICHER:
<c:if test="${developmentMode}">
<!-- Stack Trace nur in Dev -->
</c:if>
2. User-Friendly Messages
❌ SCHLECHT:
NullPointerException at line 42 in ProductDAO.java
✅ GUT:
Sorry, we couldn't load the product. Please try again later.
3. Log Errors serverseitig
// Im Servlet oder Filter:
try {
// Code
} catch (Exception e) {
logger.error("Error processing request: " + request.getRequestURI(), e);
throw e; // Weitergeben an Error-Page
}
4. Different Error Pages für verschiedene Bereiche
<!-- Admin-Bereich -->
<error-page>
<error-code>403</error-code>
<location>/WEB-INF/errors/admin/403.jsp</location>
</error-page>
<!-- Public-Bereich -->
<error-page>
<error-code>403</error-code>
<location>/WEB-INF/errors/public/403.jsp</location>
</error-page>
5. Hilfreiche Actions anbieten
<!-- Kontakt-Button -->
<c:url var="supportUrl" value="/support">
<c:param name="error" value="${statusCode}" />
<c:param name="url" value="${requestUri}" />
</c:url>
<a href="${supportUrl}" class="btn">Report Issue</a>
<!-- Retry-Button -->
<a href="javascript:location.reload()" class="btn">🔄 Try Again</a>
Error-Handler Servlet (Alternative zu web.xml)
Für komplexere Error-Handling-Logik:
@WebServlet("/error-handler")
public class ErrorHandlerServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Error-Informationen auslesen
Integer statusCode = (Integer) request.getAttribute("jakarta.servlet.error.status_code");
String requestUri = (String) request.getAttribute("jakarta.servlet.error.request_uri");
Throwable throwable = (Throwable) request.getAttribute("jakarta.servlet.error.exception");
// Logging
if (throwable != null) {
logger.error("Error occurred at " + requestUri, throwable);
}
// Custom Logic basierend auf Error-Code
String errorPage;
switch (statusCode != null ? statusCode : 0) {
case 404:
errorPage = "/WEB-INF/errors/404.jsp";
break;
case 403:
errorPage = "/WEB-INF/errors/403.jsp";
break;
case 500:
// Bei 500: Email an Admins senden (in Production)
if (isProduction()) {
notifyAdmins(throwable, requestUri);
}
errorPage = "/WEB-INF/errors/500.jsp";
break;
default:
errorPage = "/WEB-INF/errors/error.jsp";
}
request.getRequestDispatcher(errorPage).forward(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
private boolean isProduction() {
// Check Environment
return !"true".equals(getServletContext().getInitParameter("displayStackTrace"));
}
private void notifyAdmins(Throwable error, String url) {
// Email-Notification an Admins
// Oder: Push-Notification, Slack-Message, etc.
}
}
web.xml:
<web-app ...>
<error-page>
<location>/error-handler</location>
</error-page>
</web-app>
Custom Error Attributes
Im Servlet Fehler-Kontext hinzufügen:
@WebServlet("/products/*")
public class ProductServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
String productId = request.getPathInfo().substring(1);
Product product = productDAO.findById(productId);
if (product == null) {
// Custom Error-Daten setzen
request.setAttribute("errorTitle", "Product Not Found");
request.setAttribute("errorDetail", "Product with ID " + productId + " doesn't exist.");
request.setAttribute("suggestedAction", "Browse our catalog");
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
request.getRequestDispatcher("/WEB-INF/errors/404.jsp").forward(request, response);
return;
}
// Normal processing...
} catch (Exception e) {
logger.error("Error loading product", e);
throw new ServletException(e);
}
}
}
404.jsp mit custom Attributen:
<h1>${not empty errorTitle ? errorTitle : 'Page Not Found'}</h1>
<p>${not empty errorDetail ? errorDetail : 'The page you requested does not exist.'}</p>
<c:if test="${not empty suggestedAction}">
<div class="suggestion">
<strong>Suggested action:</strong> ${suggestedAction}
</div>
</c:if>
🟢 GRUNDLAGEN: Best Practices
1. Niemals Scriptlets!
❌ FALSCH:
<%
List<Product> products = (List<Product>) request.getAttribute("products");
for (Product p : products) {
%>
<div><%= p.getName() %></div>
<%
}
%>
✅ RICHTIG:
<c:forEach var="product" items="${products}">
<div>${product.name}</div>
</c:forEach>
2. Business-Logik im Controller, nicht in JSP!
❌ FALSCH:
<!-- Komplexe Berechnung in JSP -->
<c:set var="total" value="0" />
<c:forEach var="item" items="${cart.items}">
<c:set var="itemTotal" value="${item.price * item.quantity * (1 - item.discount)}" />
<c:set var="total" value="${total + itemTotal}" />
</c:forEach>
✅ RICHTIG:
// Servlet:
double total = cart.calculateTotal();
request.setAttribute("cartTotal", total);
<!-- JSP: -->
<p>Total: <fmt:formatNumber value="${cartTotal}" type="currency" /></p>
Regel: JSP ist für Darstellung, nicht für Berechnungen!
3. Null-Checks mit empty
✅ RICHTIG:
<c:if test="${not empty products}">
<c:forEach var="product" items="${products}">
...
</c:forEach>
</c:if>
<c:if test="${empty cart.items}">
<p>Your cart is empty.</p>
</c:if>
empty prüft:
null- Leerer String (
"") - Leere Collection (
size() == 0) - Leere Map (
size() == 0)
4. Formatierung mit fmt:formatNumber/Date
❌ FALSCH:
$${product.price} <!-- $99.9900000001 -->
✅ RICHTIG:
<fmt:formatNumber value="${product.price}" type="currency" />
<!-- $99.99 -->
5. URL-Encoding mit <c:url>
❌ FALSCH:
<a href="/product?name=${product.name}">...</a>
<!-- Probleme mit Sonderzeichen und Spaces -->
✅ RICHTIG:
<c:url var="productUrl" value="/product">
<c:param name="name" value="${product.name}" />
</c:url>
<a href="${productUrl}">...</a>
<!-- Automatisches URL-Encoding -->
6. Wiederverwendbare Variablen mit <c:set>
✅ RICHTIG:
<!-- Einmal berechnen -->
<c:set var="hasItems" value="${not empty cart.items}" />
<!-- Mehrfach verwenden -->
<c:if test="${hasItems}">
<button>Checkout</button>
</c:if>
<c:if test="${not hasItems}">
<p>Cart is empty</p>
</c:if>
❌ ANTI-PATTERNS: Was du NICHT tun solltest
1. Scriptlets mit JSTL mischen
❌ NIEMALS:
<%
List<Product> products = (List<Product>) request.getAttribute("products");
request.setAttribute("productCount", products.size());
%>
<c:forEach var="product" items="${products}">
<p>${product.name}</p>
</c:forEach>
Warum schlecht?
- Inkonsistenter Stil
- Schwer zu lesen
- Zwei verschiedene Programmier-Paradigmen gemischt
2. SQL-Tags verwenden
❌ NIEMALS:
<%@ taglib prefix="sql" uri="jakarta.tags.sql" %>
<sql:query var="products" dataSource="${myDS}">
SELECT * FROM products
</sql:query>
<c:forEach var="product" items="${products.rows}">
<p>${product.name}</p>
</c:forEach>
Warum schlecht?
- Datenbank-Logik gehört NICHT in die View!
- Keine Separation of Concerns
- Schwer zu testen
- Security-Probleme (SQL-Injection!)
Richtig: DAO-Pattern verwenden!
3. Zu viele verschachtelte <c:choose>
❌ SCHLECHT:
<c:choose>
<c:when test="${user.role == 'ADMIN'}">
<c:choose>
<c:when test="${user.superAdmin}">
<c:choose>
<c:when test="${user.canDeleteUsers}">
...
</c:when>
...
</c:choose>
</c:when>
...
</c:choose>
</c:when>
...
</c:choose>
Besser:
// Im Servlet: Bereite View-spezifische Daten vor
boolean canDeleteUsers = user.isAdmin() && user.isSuperAdmin() && user.hasPermission("DELETE_USERS");
request.setAttribute("canDeleteUsers", canDeleteUsers);
<!-- JSP: -->
<c:if test="${canDeleteUsers}">
<button>Delete User</button>
</c:if>
4. Komplexe EL-Expressions
❌ SCHLECHT:
${product.stock > 10 && product.price < 100 && product.rating >= 4.0 && product.category == 'Electronics' ? 'Hot Deal!' : product.stock > 0 ? 'Available' : 'Out of Stock'}
Besser:
// Servlet:
String stockStatus = productService.getStockStatus(product);
request.setAttribute("stockStatus", stockStatus);
<!-- JSP: -->
<span class="badge">${stockStatus}</span>
💬 Real Talk: JSTL im echten Leben
Java Fleet Büro, 15:30 Uhr. Nova zeigt Elyndra stolz ihre neue JSP – komplett ohne Scriptlets!
Nova: „Elyndra! Schau mal! Meine neue Product-List-JSP – zero Scriptlets!“
Elyndra (scrollt durch den Code): „Yo, das sieht mega clean aus! JSTL only, formatierte Preise, conditional rendering… You’re leveling up!“
Nova: „Right? Und honestly, jetzt versteh ich, warum Scriptlets so trash sind. JSTL ist SO viel lesbarer!“
Elyndra: „Exactly! Designer können damit arbeiten, du kannst es warten, und XSS ist kein Problem mehr.“
Kofi (kommt dazu): „Nova hat’s verstanden? Yo, das ist character development!“
Nova (grinst): „Ha ha, sehr funny. Aber real talk – ich hab eine Frage. Wann sollte ich <c:if> vs. <c:choose> verwenden?“
Elyndra: „Gute Frage! <c:if> für einzelne Bedingungen. <c:choose> wenn du if-else-if brauchst. Simple.“
Nova: „Und was ist mit Performance? Macht JSTL meine App langsamer?“
Elyndra: „Nah. JSTL wird zur Translation-Zeit kompiliert – also kein Runtime-Overhead. Chill.“
Cassian (schaut vorbei): „Und selbst wenn – readable code beats micro-optimizations any day. Trust me.“
Nova: „Okay, eine Frage noch: <fmt:formatNumber> vs. DecimalFormat im Servlet – was ist besser?“
Elyndra: „Kommt drauf an. Simple Formatierung? JSTL. Komplexe Logik oder Wiederverwendung? Mach’s im Servlet.“
Nova: „Got it. Regel: Business-Logik im Controller, Darstellung in JSP mit JSTL.“
Elyndra: „Exactly! Separation of Concerns – das ist der Way.“
Nova: „Und lowkey… ich fühl mich jetzt wie ein richtiger Java-Web-Dev!“
Kofi: „You basically are! JSTL ist der endgültige Beweis: You left Scriptlet-Land behind!“
Elyndra: „Welcome to the JSTL side, Nova. We have clean code.“
Nova (lacht): „Best side ever!“
✅ Checkpoint: Hast du es verstanden?
Zeit für einen Reality-Check! Beantworte diese Fragen, um zu prüfen, ob du heute alles verstanden hast.
Quiz:
Frage 1: Welche JSTL-Bibliotheken gibt es? Nenne mindestens 3.
Frage 2: Was ist der Unterschied zwischen <c:if> und <c:choose>?
Frage 3: Wie iterierst du über eine Liste mit JSTL? Zeige ein Beispiel.
Frage 4: Was macht varStatus in <c:forEach>? Nenne 3 Properties.
Frage 5: Wie formatierst du eine Zahl als Währung mit JSTL?
Frage 6: Was ist der Unterschied zwischen <jsp:include> und <c:import>?
Frage 7: Warum solltest du <c:url> für Links verwenden?
Frage 8: Nenne 5 JSTL Functions aus der Functions Library.
Frage 9: Warum solltest du NIE die SQL-Tags verwenden?
Frage 10: Wann solltest du komplexe Logik im Servlet vs. in JSP mit JSTL machen?
🎯 Mini-Challenge
Aufgabe: Erstelle eine Shopping-Cart-Page mit JSTL.
Requirements:
- Servlet bereitet vor:
List<CartItem> cartItems = cart.getItems(); double subtotal = cart.getSubtotal(); double tax = cart.getTax(); double total = cart.getTotal(); request.setAttribute("cartItems", cartItems); request.setAttribute("subtotal", subtotal); request.setAttribute("tax", tax); request.setAttribute("total", total); request.setAttribute("lastUpdated", new Date()); - JSP soll anzeigen:
- Liste aller Cart-Items (Product Name, Price, Quantity, Subtotal)
- Subtotal (formatiert als Währung)
- Tax (formatiert als Währung)
- Total (formatiert als Währung)
- „Empty Cart“ Message wenn keine Items
- Checkout-Button nur wenn Items vorhanden
- Last Updated Timestamp
- Verwende:
<c:forEach>für Items<c:if>oder<c:choose>für Bedingungen<fmt:formatNumber>für Preise<fmt:formatDate>für Timestampfn:lengthfür Item-Count<c:url>für Checkout-Link- Keine Scriptlets!
- Bonus:
- Zeige Item-Nummer mit varStatus
- Highlight erste und letzte Items
- Berechne Subtotal pro Item (price * quantity)
- Zeige „Free Shipping“ wenn total > $50
Lösung:
Die Lösung zu dieser Challenge findest du am Anfang von Tag 9 als Kurzwiederholung! 🚀
Alternativ kannst du die Musterlösung im GitHub-Projekt checken: https://github.com/java-fleet/java-web-basic-examples
Geschafft? 🎉
Dann bist du bereit für die FAQ-Sektion!
❓ Häufig gestellte Fragen
Frage 1: JSTL vs. Thymeleaf vs. JSF – was sollte ich lernen?
Antwort:
Kurzversion: Lerne JSTL – es ist die Basis!
Im Detail:
JSTL:
- ✅ Standard in Jakarta EE
- ✅ Einfach zu lernen
- ✅ Überall verfügbar
- ✅ Legacy-Code nutzt es
- ⚠️ Nur Template-Engine, kein Framework
Thymeleaf:
- ✅ Modern
- ✅ Natürliche Templates (HTML bleibt HTML)
- ✅ Gut mit Spring Boot
- ⚠️ Nicht Jakarta EE Standard
JSF (JavaServer Faces):
- ✅ Component-Based
- ✅ Jakarta EE Standard
- ⚠️ Komplexer als JSTL
- ⚠️ Weniger populär als früher
Empfehlung:
- Lerne JSTL (jetzt!)
- Später Thymeleaf wenn du Spring Boot machst
- JSF nur wenn du es beruflich brauchst
Real Talk: In bestehenden Enterprise-Projekten wirst du JSTL überall sehen. It’s worth learning!
Frage 2: Warum werden meine JSTL-Tags nicht erkannt?
Symptom:
<c:forEach var="item" items="${products}">
<p>${item.name}</p>
</c:forEach>
Wird als Text im Browser angezeigt, nicht ausgeführt!
Ursachen & Lösungen:
1. Taglib nicht importiert:
<!-- ❌ Vergessen! -->
<c:forEach var="item" items="${products}">
<!-- ✅ RICHTIG: -->
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:forEach var="item" items="${products}">
2. Falsche URI:
<!-- ❌ FALSCH: Alte Java EE Version --> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!-- ✅ RICHTIG: Jakarta EE 10+ --> <%@ taglib prefix="c" uri="jakarta.tags.core" %>
3. JSTL-Dependency fehlt:
<!-- pom.xml -->
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jakarta.servlet.jsp.jstl</artifactId>
<version>3.0.1</version>
</dependency>
4. web.xml Version zu alt:
<!-- ❌ Zu alt --> <web-app version="2.3" ...> <!-- ✅ Mindestens 2.4+ --> <web-app version="6.0" ...>
Frage 3: <c:forEach> zeigt nichts an – was ist falsch?
Mögliche Ursachen:
1. Attribut ist null oder leer:
// Servlet:
List<Product> products = productDAO.getAll(); // Könnte null sein!
request.setAttribute("products", products);
<!-- JSP: Prüfe auf empty -->
<c:if test="${empty products}">
<p>No products found.</p>
</c:if>
<c:forEach var="product" items="${products}">
<p>${product.name}</p>
</c:forEach>
2. Scope falsch:
// Servlet:
session.setAttribute("products", products); // Session-Scope!
<!-- JSP: -->
<c:forEach var="product" items="${sessionScope.products}">
<p>${product.name}</p>
</c:forEach>
3. Forward vs. Redirect:
// ❌ FALSCH: Redirect verliert Request-Attribute!
response.sendRedirect("/products.jsp");
// ✅ RICHTIG: Forward
request.getRequestDispatcher("/products.jsp").forward(request, response);
Frage 4: Wie kann ich in JSTL auf Java-Methoden zugreifen?
Option 1: Getter nutzen (Standard)
public class Product {
private String name;
public String getName() { // Getter!
return name;
}
}
${product.name} <!-- Ruft getName() -->
Option 2: Custom EL Functions (für statische Methoden)
// StringUtils.java
public class StringUtils {
public static String capitalize(String text) {
return text.substring(0, 1).toUpperCase() + text.substring(1);
}
}
<!-- WEB-INF/custom.tld -->
<function>
<name>capitalize</name>
<function-class>com.shop.utils.StringUtils</function-class>
<function-signature>
java.lang.String capitalize(java.lang.String)
</function-signature>
</function>
<%@ taglib prefix="util" uri="/WEB-INF/custom.tld" %>
${util:capitalize(product.name)}
Option 3: Im Servlet vorbereiten (empfohlen!)
// Servlet:
String formattedName = StringUtils.capitalize(product.getName());
request.setAttribute("formattedName", formattedName);
<!-- JSP: -->
${formattedName}
Best Practice: Komplexe Logik gehört ins Servlet, nicht in JSP!
Frage 5: Performance – ist JSTL langsam?
Antwort:
Nein! JSTL ist NICHT langsam!
Warum?
- Compile-Time Translation:
- JSTL-Tags werden zur JSP-Compile-Zeit in Java-Code übersetzt
- Zur Runtime läuft optimierter Java-Bytecode
- Kein Parsing zur Laufzeit!
- Vergleich Scriptlets vs. JSTL:
<!-- Scriptlet: -->
<%
for (Product p : products) {
out.print("<p>" + p.getName() + "</p>");
}
%>
<!-- JSTL: -->
<c:forEach var="p" items="${products}">
<p>${p.name}</p>
</c:forEach>
Beide werden zu ähnlichem Java-Code kompiliert → gleiche Performance!
Wann könnte Performance ein Thema werden?
⚠️ Bei extremen Szenarien:
- 1000+ Elemente in
<c:forEach> - Tiefe verschachtelte Loops
- Sehr komplexe EL-Expressions
Lösung: Pagination, Lazy Loading, Caching – aber das sind Architektur-Probleme, nicht JSTL-Probleme!
Real Talk: In 99% der Fälle ist JSTL-Performance kein Issue. Focus auf Readability und Maintainability!
🎉 Tag 8 geschafft!
Main Character Energy! Du rockst! ✨
Real talk: JSTL ist ein absoluter Game-Changer. Du kannst jetzt komplett scriptlet-freie JSPs schreiben!
Das hast du heute gelernt:
- ✅ JSTL Core Tags (forEach, if, choose, set, etc.)
- ✅ Formatting Tags (formatNumber, formatDate, locale)
- ✅ Functions Library (string & collection operations)
- ✅ Best Practices für saubere JSPs
- ✅ Anti-Patterns vermeiden
- ✅ Real-World Beispiele
- ✅ Troubleshooting häufiger Probleme
Du kannst jetzt:
- Komplett ohne Scriptlets arbeiten
- Listen und Collections elegant iterieren
- Daten professionell formatieren
- Bedingungen sauber umsetzen
- Production-Ready JSPs schreiben
- Legacy-Code mit JSTL refactoren
Honestly? Das ist HUGE! Du verstehst jetzt, wie man moderne, wartbare Java-Webanwendungen baut.
Und lowkey: Viele Entwickler nutzen immer noch Scriptlets, weil sie JSTL nicht kennen. Mit diesem Wissen kannst du echten Impact haben! 💪
🔮 Wie geht’s weiter?
Morgen (Tag 9): Java Web und Datenbanken – Datasource & Connection Pools
Was dich erwartet:
- Datasource-Objekte verstehen
- Connection Pools konfigurieren
- JNDI-Namen nutzen
- Best Practices für Datenbankzugriff
- Endlich echte Daten in deiner Anwendung! 🔥
Brauchst du eine Pause?
Mach sie! JSTL braucht Zeit zum Verinnerlichen.
Tipp für heute Abend:
Nimm eine deiner alten JSPs mit Scriptlets und refactore sie zu JSTL. Siehst du den Unterschied?
Learning by refactoring! 🎨
📧 Troubleshooting
Problem: <c:forEach> zeigt nichts an
Lösung 1: Prüfe, ob Daten vorhanden sind
<p>Debug: ${fn:length(products)} products</p>
<p>Debug: ${products}</p>
<c:forEach var="product" items="${products}">
<p>${product.name}</p>
</c:forEach>
Lösung 2: Prüfe Scope
<!-- Explizit Scope angeben -->
<c:forEach var="product" items="${requestScope.products}">
<p>${product.name}</p>
</c:forEach>
Problem: <fmt:formatDate> wirft Exception
Symptom:
<fmt:formatDate value="${product.createdAt}" pattern="yyyy-MM-dd" />
<!-- java.lang.IllegalArgumentException -->
Ursache: createdAt ist kein java.util.Date, sondern LocalDateTime oder String!
Lösung 1: Im Servlet konvertieren
// Servlet:
Date date = Date.from(product.getCreatedAt().atZone(ZoneId.systemDefault()).toInstant());
request.setAttribute("createdAtDate", date);
<!-- JSP: -->
<fmt:formatDate value="${createdAtDate}" pattern="yyyy-MM-dd" />
Lösung 2: Als String formatieren
// Servlet:
String formattedDate = product.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
request.setAttribute("formattedDate", formattedDate);
<!-- JSP: -->
${formattedDate}
Problem: <c:url> erzeugt falsche URLs
Symptom:
<c:url var="homeUrl" value="/home" /> <!-- Ergebnis: /myapp//home (doppelter Slash!) -->
Ursache: Context-Path wird automatisch hinzugefügt!
Lösung:
<!-- ✅ RICHTIG: Ohne führenden Slash --> <c:url var="homeUrl" value="home" /> <!-- Ergebnis: /myapp/home --> <!-- ODER: Mit führendem Slash --> <c:url var="homeUrl" value="/home" /> <!-- Ergebnis: /myapp/home -->
JSTL ist hier tolerant – beides funktioniert!
Problem: JSTL-Tags funktionieren nach Server-Restart nicht mehr
Symptom:
Nach Payara-Restart werden JSTL-Tags als Text angezeigt.
Lösung:
- Clean & Build:
- NetBeans: Rechtsklick auf Projekt → Clean and Build
- Server-Cache löschen:
C:\payara6\glassfish\domains\domain1\generated\ C:\payara6\glassfish\domains\domain1\osgi-cache\ - Dependency Check:
<!-- pom.xml: Prüfe JSTL-Version --> <dependency> <groupId>jakarta.servlet.jsp.jstl</groupId> <artifactId>jakarta.servlet.jsp.jstl-api</artifactId> <version>3.0.0</version> </dependency> - Server neu starten:
- Stop → Start (nicht nur Restart!)
Viel Erfolg beim Lernen! 🚀
Wenn du Fragen hast, schreib uns: support@java-developer.online
„JSTL ist wie ein guter Freund – immer da, wenn du ihn brauchst, und macht dein Leben einfacher!“ – Elyndra Valen

