Java Web Aufbau – Tag 6 von 10
Von Elyndra Valen, Senior Developer bei Java Fleet Systems Consulting

BodyTagSupport

📋 Deine Position im Kurs

TagThemaStatus
1Filter im Webcontainer✅ Abgeschlossen
2Listener im Webcontainer✅ Abgeschlossen
3Authentifizierung über Datenbank✅ Abgeschlossen
4Container-Managed Security & Jakarta Security API✅ Abgeschlossen
5Custom Tags & Tag Handler (SimpleTag)✅ Abgeschlossen
→ 6Custom Tag Handler mit BodyTagSupport👉 DU BIST HIER!
7JPA vs JDBC – Konfiguration & Provider🔒 Noch nicht freigeschaltet
8JPA Relationen (1): @OneToOne & @ManyToOne🔒 Noch nicht freigeschaltet
9JPA Relationen (2): @OneToMany & @ManyToMany🔒 Noch nicht freigeschaltet
10JSF Überblick – Component-Based UI🔒 Noch nicht freigeschaltet

Modul: Java Web Aufbau
Gesamt-Dauer: 10 Arbeitstage (je 8 Stunden)
Dein Ziel: Classic Tag Handler mit BodyTagSupport meistern und komplexe Iteration verstehen


📋 Voraussetzungen für diesen Tag

Du brauchst:

  • ✅ JDK 21 LTS installiert
  • ✅ Apache NetBeans 22 (oder neuer)
  • ✅ Payara Server 6.x konfiguriert
  • ✅ Tag 1-5 abgeschlossen
  • ✅ SimpleTag verstanden (Tag 5!)
  • ✅ TLD-Dateien erstellen können

Tag 5 verpasst?
Spring zurück zu Tag 5, um SimpleTag zu verstehen. BodyTagSupport baut darauf auf!

Setup-Probleme?
Schreib uns: support@java-developer.online


⚡ Das Wichtigste in 30 Sekunden

Heute lernst du:

  • ✅ Was BodyTagSupport ist und warum es existiert
  • ✅ Der Unterschied zwischen SimpleTag und Classic Tag Handler
  • ✅ doStartTag(), doEndTag(), doAfterBody() Lifecycle
  • ✅ Body Content mehrfach verarbeiten (Iteration!)
  • ✅ Return-Codes verstehen (EVAL_BODY_BUFFERED, SKIP_BODY, etc.)
  • ✅ Komplexe Tags wie <c:forEach> nachbauen

Am Ende des Tages kannst du:

  • Classic Tag Handler mit BodyTagSupport erstellen
  • Über Collections iterieren in Custom Tags
  • Body Content mehrfach durchlaufen
  • Den kompletten Tag Lifecycle verstehen
  • Entscheiden wann SimpleTag vs. BodyTagSupport

Zeit-Investment: ~8 Stunden
Schwierigkeitsgrad: Fortgeschritten (aber du packst das!)


👋 Willkommen zu Tag 6!

Hi! 👋

Elyndra hier. Heute wird’s richtig interessant!

Kurzwiederholung: Challenge von Tag 5

Gestern solltest du eine Tag Library mit 4 Tags erstellen:

  1. <fleet:greeting> – Personalisierte Grüße
  2. <fleet:badge> – Bootstrap-Badges
  3. <fleet:truncate> – Text kürzen
  4. <fleet:repeat> – Content wiederholen

Falls du das noch nicht gemacht hast – kein Problem! Du kannst die Lösung im GitHub-Projekt finden.

Was du heute lernst:

Gestern hast du SimpleTagSupport kennengelernt – die moderne, einfache Art Custom Tags zu erstellen. Heute tauchen wir tiefer ein: BodyTagSupport – die Classic Tag Handler API.

„Moment“, denkst du vielleicht, „warum soll ich veraltete Technologie lernen?“

Gute Frage! Drei Gründe:

1. Legacy-Code verstehen
Du wirst in der Praxis auf Custom Tags mit BodyTagSupport treffen – besonders in älteren Enterprise-Projekten. Du musst sie verstehen können, um sie zu warten.

2. Mehr Kontrolle
BodyTagSupport gibt dir feinere Kontrolle über den Tag-Lifecycle. Manche komplexe Use-Cases sind damit einfacher zu lösen als mit SimpleTag.

3. Tiefes Verständnis
Wenn du beide APIs kennst, verstehst du WARUM SimpleTag besser ist – und wann du doch BodyTagSupport brauchst.

Keine Sorge:

BodyTagSupport wirkt kompliziert mit seinen vielen Methoden und Return-Codes. Aber ich zeige dir Schritt für Schritt, wie alles zusammenpasst. Nach heute verstehst du den kompletten Tag-Lifecycle!

Los geht’s! 🚀


🟢 GRUNDLAGEN: Was ist BodyTagSupport?

Definition

BodyTagSupport ist eine Basis-Klasse für Classic Tag Handler – die ursprüngliche API für Custom Tags in JSP (vor JSP 2.0).

Wichtig zu verstehen:

SimpleTag (Tag 5) wurde in JSP 2.0 eingeführt, um die API zu vereinfachen.
BodyTagSupport ist die ältere, komplexere, aber mächtigere Variante.

SimpleTag vs. BodyTagSupport – Der direkte Vergleich

FeatureSimpleTagBodyTagSupport
EingeführtJSP 2.0 (2003)JSP 1.2 (2001)
Basis-KlasseSimpleTagSupportBodyTagSupport
Lifecycle1 Methode (doTag())3+ Methoden
Body verarbeiteninvoke()doAfterBody() Loop
IterationManuell im CodeMit Return-Codes
Komplexität⭐ Einfach⭐⭐⭐ Komplex
Use Cases90% aller TagsLegacy + Spezialfälle

Wann welches nutzen?

Nutze SimpleTag wenn:

  • ✅ Einfache Tags
  • ✅ Body einmal verarbeiten
  • ✅ Moderne Codebasis
  • ✅ Du die Wahl hast

Nutze BodyTagSupport wenn:

  • ✅ Legacy-Code warten
  • ✅ Body mehrfach iterieren (wie <c:forEach>)
  • ✅ Feinste Kontrolle über Lifecycle
  • ✅ Kompatibilität mit alten Servern

Der Classic Tag Lifecycle

Das ist der Kern von BodyTagSupport!

Wenn ein Tag ausgeführt wird, durchläuft er mehrere Phasen:

[1] setPageContext()      → Container setzt JSP-Context
[2] setParent()           → Container setzt Parent-Tag (falls nested)
[3] setAttribute()        → Container setzt alle Attribute
    ↓
[4] doStartTag()          → Tag beginnt
    ↓
[5] setBodyContent()      → Container gibt Body-Buffer
    ↓
[6] doInitBody()          → Body-Initialisierung
    ↓
[7] doAfterBody()         → Body wurde verarbeitet
    ↓                       (kann mehrfach aufgerufen werden!)
[8] doEndTag()            → Tag endet
    ↓
[9] release()             → Cleanup (optional)

Was macht dieser Lifecycle?

Der Lifecycle definiert die Reihenfolge, in der der Container deine Tag-Handler-Methoden aufruft.

Die wichtigsten Methoden im Detail:

doStartTag() – Der Anfang

public int doStartTag() throws JspException {
    // Wird aufgerufen, wenn Tag startet
    // Return-Wert entscheidet, was als nächstes passiert
    
    return EVAL_BODY_BUFFERED;  // Oder SKIP_BODY
}

Mögliche Return-Werte:

  • EVAL_BODY_BUFFERED (2011) → Body verarbeiten, Buffer erstellen
  • SKIP_BODY (0) → Body ignorieren
  • EVAL_BODY_INCLUDE (1) → Body direkt ausgeben (deprecated)

doAfterBody() – Nach dem Body

public int doAfterBody() throws JspException {
    // Wird aufgerufen, nachdem Body verarbeitet wurde
    // Hier kannst du Body-Content lesen und manipulieren
    
    return SKIP_BODY;  // Oder EVAL_BODY_AGAIN
}

Mögliche Return-Werte:

  • SKIP_BODY (0) → Fertig, weiter zu doEndTag()
  • EVAL_BODY_AGAIN (2) → Body nochmal verarbeiten! (Iteration!)

doEndTag() – Das Ende

public int doEndTag() throws JspException {
    // Wird aufgerufen, wenn Tag endet
    // Cleanup, finale Ausgabe
    
    return EVAL_PAGE;  // Oder SKIP_PAGE
}

Mögliche Return-Werte:

  • EVAL_PAGE (6) → JSP-Seite normal weiter verarbeiten
  • SKIP_PAGE (5) → Rest der JSP überspringen (selten genutzt!)

Wichtig zu verstehen:

Die Return-Codes sind Integer-Konstanten. Du gibst sie zurück, um dem Container zu sagen, was er als nächstes tun soll.

In der Praxis bedeutet das:

Der Container ruft deine Methoden auf und reagiert auf deine Return-Werte:

Container: "doStartTag() bitte!"
Du: "EVAL_BODY_BUFFERED" → Container erstellt Buffer
Container: "doAfterBody() bitte!"
Du: "EVAL_BODY_AGAIN" → Container ruft doAfterBody() nochmal auf!
Du: "SKIP_BODY" → Container geht zu doEndTag()
Container: "doEndTag() bitte!"
Du: "EVAL_PAGE" → Container macht normal weiter

🟢 GRUNDLAGEN: Dein erstes BodyTagSupport Tag

Ziel:
Ein einfaches Tag, das seinen Body in Großbuchstaben umwandelt (wie gestern mit SimpleTag, aber mit BodyTagSupport).

Implementation

1. Tag Handler Klasse:

package com.javafleet.tags;

import jakarta.servlet.jsp.tagext.BodyTagSupport;
import jakarta.servlet.jsp.tagext.BodyContent;
import jakarta.servlet.jsp.JspException;
import java.io.IOException;

public class UppercaseBodyTag extends BodyTagSupport {
    
    @Override
    public int doStartTag() throws JspException {
        // Tag startet - wir wollen den Body verarbeiten
        return EVAL_BODY_BUFFERED;  // Wichtig: Buffer erstellen!
    }
    
    @Override
    public int doAfterBody() throws JspException {
        try {
            // Body-Content holen
            BodyContent bodyContent = getBodyContent();
            String content = bodyContent.getString();
            
            // In Uppercase umwandeln
            String uppercased = content.toUpperCase();
            
            // In Output-Stream schreiben
            bodyContent.getEnclosingWriter().write(uppercased);
            
            // Fertig - keine weitere Iteration
            return SKIP_BODY;
            
        } catch (IOException e) {
            throw new JspException("Error in UppercaseBodyTag", e);
        }
    }
    
    @Override
    public int doEndTag() throws JspException {
        // Tag endet - normal weitermachen
        return EVAL_PAGE;
    }
}

Was macht dieser Code?

Das ist dein erster Classic Tag Handler! Lass uns jeden Teil verstehen:

Die Vererbung von BodyTagSupport
BodyTagSupport stellt uns alle Lifecycle-Methoden bereit. Wir überschreiben nur die, die wir brauchen.

doStartTag() – Der Start

return EVAL_BODY_BUFFERED;

Das sagt dem Container: „Erstelle einen Buffer für den Body-Content!“

Wichtig zu verstehen:
EVAL_BODY_BUFFERED erstellt einen BodyContent-Buffer. Der Body wird NICHT direkt ausgegeben, sondern erst in diesem Buffer gesammelt.

doAfterBody() – Die Hauptarbeit

BodyContent bodyContent = getBodyContent();

getBodyContent() gibt uns Zugriff auf den Buffer mit dem Body-Content.

Der Unterschied zu SimpleTag:
Bei SimpleTag hatten wir getJspBody().invoke(writer).
Bei BodyTagSupport haben wir ein BodyContent-Objekt mit mehr Möglichkeiten!

Den Content verarbeiten:

String content = bodyContent.getString();
String uppercased = content.toUpperCase();

Wir holen den Content als String und transformieren ihn.

Ausgabe in den richtigen Stream:

bodyContent.getEnclosingWriter().write(uppercased);

Kritisch: getEnclosingWriter() gibt den echten Output-Stream der JSP zurück!

bodyContent selbst ist nur der Buffer. Wir müssen in den „enclosing writer“ schreiben, damit es im finalen HTML landet.

Return SKIP_BODY:

return SKIP_BODY;

Das sagt: „Ich bin fertig mit dem Body, geh weiter zu doEndTag().“

doEndTag() – Das Ende

return EVAL_PAGE;

Das sagt: „JSP-Seite normal weitermachen.“

In der Praxis bedeutet das:

  1. JSP enthält <fleet:uppercaseBody>hello world</fleet:uppercaseBody>
  2. Container ruft doStartTag() → Wir geben EVAL_BODY_BUFFERED zurück
  3. Container erstellt Buffer und verarbeitet Body → „hello world“ landet im Buffer
  4. Container ruft doAfterBody() → Wir transformieren zu „HELLO WORLD“ und geben SKIP_BODY zurück
  5. Container ruft doEndTag() → Wir geben EVAL_PAGE zurück
  6. Fertig! User sieht „HELLO WORLD“

2. TLD-Eintrag:

<tag>
    <name>uppercaseBody</name>
    <tag-class>com.javafleet.tags.UppercaseBodyTag</tag-class>
    <body-content>scriptless</body-content>
    <info>Converts body content to uppercase using BodyTagSupport</info>
</tag>

3. In JSP verwenden:

<%@ taglib prefix="fleet" uri="http://javafleet.com/tags" %>

<fleet:uppercaseBody>
    this will be converted to uppercase!
</fleet:uppercaseBody>

<!-- Output: THIS WILL BE CONVERTED TO UPPERCASE! -->

🟡 PROFESSIONALS: Iteration mit EVAL_BODY_AGAIN

Konzept

Der große Vorteil von BodyTagSupport:
Du kannst EVAL_BODY_AGAIN zurückgeben, um den Body mehrfach zu verarbeiten!

Use Case: Ein <fleet:repeat times="3"> Tag, das seinen Body 3x wiederholt.

Implementation: Repeat Tag

Ziel:

<fleet:repeat times="5">
    <p>This line appears 5 times!</p>
</fleet:repeat>

Tag Handler:

package com.javafleet.tags;

import jakarta.servlet.jsp.tagext.BodyTagSupport;
import jakarta.servlet.jsp.tagext.BodyContent;
import jakarta.servlet.jsp.JspException;
import java.io.IOException;

public class RepeatTag extends BodyTagSupport {
    
    // Attribut
    private int times;
    
    // Counter für Iteration
    private int iteration;
    
    // Setter für Attribut (PFLICHT!)
    public void setTimes(int times) {
        this.times = times;
    }
    
    @Override
    public int doStartTag() throws JspException {
        // Initialisiere Counter
        iteration = 0;
        
        if (times > 0) {
            return EVAL_BODY_BUFFERED;  // Body verarbeiten
        } else {
            return SKIP_BODY;  // times <= 0: Body überspringen
        }
    }
    
    @Override
    public int doAfterBody() throws JspException {
        try {
            // Body-Content holen
            BodyContent bodyContent = getBodyContent();
            String content = bodyContent.getString();
            
            // Content ausgeben
            bodyContent.getEnclosingWriter().write(content);
            
            // Counter erhöhen
            iteration++;
            
            // Buffer leeren für nächste Iteration
            bodyContent.clearBody();
            
            // Noch mal wiederholen?
            if (iteration < times) {
                return EVAL_BODY_AGAIN;  // Ja, nochmal!
            } else {
                return SKIP_BODY;  // Nein, fertig!
            }
            
        } catch (IOException e) {
            throw new JspException("Error in RepeatTag", e);
        }
    }
    
    @Override
    public int doEndTag() throws JspException {
        // Cleanup
        iteration = 0;
        return EVAL_PAGE;
    }
}

Was macht dieser Code?

Das ist ein Iterations-Tag – der Kern von BodyTagSupport! Lass uns die wichtigen Teile verstehen:

Der Iteration Counter

private int iteration;

Wir speichern, wie oft wir den Body schon verarbeitet haben. Das ist eine Instanzvariable des Tags.

doStartTag() – Initialisierung

iteration = 0;

Wir setzen den Counter auf 0, bevor wir starten.

Wichtig zu verstehen:
Tag-Instanzen können wiederverwendet werden! Immer in doStartTag() initialisieren, nicht im Konstruktor!

doAfterBody() – Die Iterations-Logik

bodyContent.getEnclosingWriter().write(content);
iteration++;
bodyContent.clearBody();

Kritisch: bodyContent.clearBody() leert den Buffer!

Warum? Weil der Body nochmal evaluiert wird bei EVAL_BODY_AGAIN. Ohne clearBody() hätten wir den alten Content noch im Buffer!

Die Entscheidung:

if (iteration < times) {
    return EVAL_BODY_AGAIN;  // Nochmal!
} else {
    return SKIP_BODY;  // Fertig!
}

Das ist der Kern: Solange iteration < times, geben wir EVAL_BODY_AGAIN zurück.

Was passiert bei EVAL_BODY_AGAIN?

Der Container:

  1. Evaluiert den Body nochmal
  2. Ruft doAfterBody() nochmal auf
  3. Wiederholt das, bis SKIP_BODY zurückgegeben wird

In der Praxis bedeutet das:

<fleet:repeat times="3">
    <p>Hello!</p>
</fleet:repeat>

Lifecycle:

  1. doStartTag()EVAL_BODY_BUFFERED, iteration=0
  2. Body evaluiert → <p>Hello!</p> in Buffer
  3. doAfterBody() → Ausgeben, iteration=1, clearBuffer, EVAL_BODY_AGAIN
  4. Body evaluiert → <p>Hello!</p> in Buffer
  5. doAfterBody() → Ausgeben, iteration=2, clearBuffer, EVAL_BODY_AGAIN
  6. Body evaluiert → <p>Hello!</p> in Buffer
  7. doAfterBody() → Ausgeben, iteration=3, clearBuffer, SKIP_BODY
  8. doEndTag()EVAL_PAGE
  9. Ergebnis: Drei <p>Hello!</p> im HTML!

TLD-Eintrag:

<tag>
    <name>repeat</name>
    <tag-class>com.javafleet.tags.RepeatTag</tag-class>
    <body-content>scriptless</body-content>
    <info>Repeats body content n times</info>
    
    <attribute>
        <name>times</name>
        <required>true</required>
        <rtexprvalue>true</rtexprvalue>
        <type>int</type>
    </attribute>
</tag>

Verwendung:

<fleet:repeat times="5">
    <p>This appears 5 times!</p>
</fleet:repeat>

<!-- Mit EL: -->
<fleet:repeat times="${numberOfRepeats}">
    <p>Dynamic repetition!</p>
</fleet:repeat>

🟡 PROFESSIONALS: Iteration über Collections

Konzept

Der ultimative Use Case für BodyTagSupport:
Iteration über Collections – wie JSTL’s <c:forEach>!

Ziel:

<fleet:forEach items="${products}" var="product">
    <p>${product.name}: ${product.price}€</p>
</fleet:forEach>

Implementation: forEach Tag

Tag Handler:

package com.javafleet.tags;

import jakarta.servlet.jsp.tagext.BodyTagSupport;
import jakarta.servlet.jsp.tagext.BodyContent;
import jakarta.servlet.jsp.JspException;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;

public class ForEachTag extends BodyTagSupport {
    
    // Attribute
    private Collection<?> items;
    private String var;
    
    // Iterator für Collection
    private Iterator<?> iterator;
    
    // Setter
    public void setItems(Collection<?> items) {
        this.items = items;
    }
    
    public void setVar(String var) {
        this.var = var;
    }
    
    @Override
    public int doStartTag() throws JspException {
        // Validierung
        if (items == null || items.isEmpty()) {
            return SKIP_BODY;
        }
        
        // Iterator erstellen
        iterator = items.iterator();
        
        // Erstes Element in PageContext setzen
        if (iterator.hasNext()) {
            Object currentItem = iterator.next();
            pageContext.setAttribute(var, currentItem);
            return EVAL_BODY_BUFFERED;
        } else {
            return SKIP_BODY;
        }
    }
    
    @Override
    public int doAfterBody() throws JspException {
        try {
            // Body-Content ausgeben
            BodyContent bodyContent = getBodyContent();
            String content = bodyContent.getString();
            bodyContent.getEnclosingWriter().write(content);
            
            // Buffer leeren
            bodyContent.clearBody();
            
            // Nächstes Element?
            if (iterator.hasNext()) {
                // Nächstes Element in PageContext setzen
                Object currentItem = iterator.next();
                pageContext.setAttribute(var, currentItem);
                
                // Nochmal iterieren!
                return EVAL_BODY_AGAIN;
            } else {
                // Fertig - Cleanup
                pageContext.removeAttribute(var);
                return SKIP_BODY;
            }
            
        } catch (IOException e) {
            throw new JspException("Error in ForEachTag", e);
        }
    }
    
    @Override
    public int doEndTag() throws JspException {
        // Cleanup
        iterator = null;
        items = null;
        return EVAL_PAGE;
    }
}

Was macht dieser Code?

Das ist ein vollwertiger Collection-Iterator! Lass uns die Magie verstehen:

Die Attribute

private Collection<?> items;  // Die Collection zum Iterieren
private String var;            // Der Variablenname in der JSP
private Iterator<?> iterator;  // Unser Iterator

Wichtig zu verstehen:
items ist die Collection (z.B. List<Product>).
var ist der Name, unter dem das aktuelle Element in der JSP verfügbar ist.
iterator behalten wir zwischen den doAfterBody() Aufrufen!

doStartTag() – Setup

iterator = items.iterator();
Object currentItem = iterator.next();
pageContext.setAttribute(var, currentItem);

Das ist der Trick: Wir setzen das erste Element in den PageContext unter dem Namen var!

Was ist PageContext?
Der PageContext ist eine Art „Map“, die während der JSP-Verarbeitung verfügbar ist. Wenn wir dort pageContext.setAttribute("product", productObject) setzen, können wir in der JSP mit ${product} darauf zugreifen!

doAfterBody() – Die Iterations-Schleife

if (iterator.hasNext()) {
    Object currentItem = iterator.next();
    pageContext.setAttribute(var, currentItem);
    return EVAL_BODY_AGAIN;
}

Bei jedem Durchlauf:

  1. Nächstes Element holen
  2. In PageContext setzen (überschreibt vorheriges!)
  3. EVAL_BODY_AGAIN zurückgeben
  4. Container evaluiert Body nochmal → diesmal mit neuem Element!

In der Praxis bedeutet das:

<fleet:forEach items="${products}" var="product">
    <p>${product.name}</p>
</fleet:forEach>

Lifecycle mit 3 Produkten:

  1. doStartTag() → product[0] in PageContext, EVAL_BODY_BUFFERED
  2. Body evaluiert → <p>Laptop</p> (product[0].name)
  3. doAfterBody() → product[1] in PageContext, EVAL_BODY_AGAIN
  4. Body evaluiert → <p>Mouse</p> (product[1].name)
  5. doAfterBody() → product[2] in PageContext, EVAL_BODY_AGAIN
  6. Body evaluiert → <p>Keyboard</p> (product[2].name)
  7. doAfterBody() → Keine weiteren Elemente, SKIP_BODY
  8. doEndTag()EVAL_PAGE

Ergebnis:

<p>Laptop</p>
<p>Mouse</p>
<p>Keyboard</p>

Das ist genau wie JSTL’s <c:forEach> funktioniert!


TLD-Eintrag:

<tag>
    <name>forEach</name>
    <tag-class>com.javafleet.tags.ForEachTag</tag-class>
    <body-content>scriptless</body-content>
    <info>Iterates over a collection (like JSTL c:forEach)</info>
    
    <attribute>
        <name>items</name>
        <required>true</required>
        <rtexprvalue>true</rtexprvalue>
        <type>java.util.Collection</type>
    </attribute>
    
    <attribute>
        <name>var</name>
        <required>true</required>
        <rtexprvalue>false</rtexprvalue>
        <type>java.lang.String</type>
    </attribute>
</tag>

Verwendung:

<%
// Im Servlet:
List<Product> products = productService.getAll();
request.setAttribute("products", products);
%>

<!-- In JSP: -->
<fleet:forEach items="${products}" var="product">
    <div class="product-card">
        <h3>${product.name}</h3>
        <p>Price: ${product.price}€</p>
        <p>Stock: ${product.stock}</p>
    </div>
</fleet:forEach>

🔵 BONUS: Nested Tags – Parent-Child Kommunikation

Konzept

Advanced Feature:
Tags können verschachtelt sein und miteinander kommunizieren!

Use Case:
Ein <fleet:table> Tag mit <fleet:column> Child-Tags:

<fleet:table items="${users}">
    <fleet:column property="name" header="Name" />
    <fleet:column property="email" header="Email" />
    <fleet:column property="age" header="Age" />
</fleet:table>

Implementation

Parent Tag – TableTag:

package com.javafleet.tags;

import jakarta.servlet.jsp.tagext.BodyTagSupport;
import jakarta.servlet.jsp.JspException;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.List;

public class TableTag extends BodyTagSupport {
    
    private Collection<?> items;
    private Iterator<?> iterator;
    private Object currentItem;
    
    // Columns sammeln von Child-Tags
    private List<ColumnTag> columns = new ArrayList<>();
    
    public void setItems(Collection<?> items) {
        this.items = items;
    }
    
    // Child-Tags registrieren sich hier!
    public void addColumn(ColumnTag column) {
        columns.add(column);
    }
    
    // Child-Tags können aktuelles Item holen
    public Object getCurrentItem() {
        return currentItem;
    }
    
    @Override
    public int doStartTag() throws JspException {
        if (items == null || items.isEmpty()) {
            return SKIP_BODY;
        }
        
        iterator = items.iterator();
        
        // Body erstmal evaluieren, um Columns zu sammeln
        return EVAL_BODY_BUFFERED;
    }
    
    @Override
    public int doAfterBody() throws JspException {
        try {
            // Beim ersten Mal: Table-Header schreiben
            if (iterator.hasNext() && currentItem == null) {
                writeTableHeader();
                currentItem = iterator.next();
                return EVAL_BODY_AGAIN;  // Erste Row
            }
            
            // Row schreiben
            writeTableRow();
            
            // Body-Content verwerfen (wir generieren eigenes HTML)
            getBodyContent().clearBody();
            
            // Nächstes Item?
            if (iterator.hasNext()) {
                currentItem = iterator.next();
                return EVAL_BODY_AGAIN;
            } else {
                writeTableFooter();
                return SKIP_BODY;
            }
            
        } catch (IOException e) {
            throw new JspException("Error in TableTag", e);
        }
    }
    
    private void writeTableHeader() throws IOException {
        StringBuilder html = new StringBuilder();
        html.append("<table class='fleet-table'>\n");
        html.append("  <thead>\n");
        html.append("    <tr>\n");
        
        for (ColumnTag column : columns) {
            html.append("      <th>").append(column.getHeader()).append("</th>\n");
        }
        
        html.append("    </tr>\n");
        html.append("  </thead>\n");
        html.append("  <tbody>\n");
        
        getBodyContent().getEnclosingWriter().write(html.toString());
    }
    
    private void writeTableRow() throws IOException {
        StringBuilder html = new StringBuilder();
        html.append("    <tr>\n");
        
        for (ColumnTag column : columns) {
            String value = column.getValueFromItem(currentItem);
            html.append("      <td>").append(value).append("</td>\n");
        }
        
        html.append("    </tr>\n");
        
        getBodyContent().getEnclosingWriter().write(html.toString());
    }
    
    private void writeTableFooter() throws IOException {
        String html = "  </tbody>\n</table>\n";
        getBodyContent().getEnclosingWriter().write(html);
    }
    
    @Override
    public int doEndTag() throws JspException {
        // Cleanup
        columns.clear();
        iterator = null;
        currentItem = null;
        return EVAL_PAGE;
    }
}

Child Tag – ColumnTag:

package com.javafleet.tags;

import jakarta.servlet.jsp.tagext.TagSupport;
import jakarta.servlet.jsp.JspException;
import java.lang.reflect.Method;

public class ColumnTag extends TagSupport {
    
    private String property;
    private String header;
    
    public void setProperty(String property) {
        this.property = property;
    }
    
    public void setHeader(String header) {
        this.header = header;
    }
    
    public String getHeader() {
        return header != null ? header : property;
    }
    
    @Override
    public int doStartTag() throws JspException {
        // Parent-Tag finden
        TableTag parent = (TableTag) findAncestorWithClass(this, TableTag.class);
        
        if (parent == null) {
            throw new JspException("ColumnTag must be nested inside TableTag!");
        }
        
        // Bei Parent registrieren
        parent.addColumn(this);
        
        // Kein Body - nur Config!
        return SKIP_BODY;
    }
    
    public String getValueFromItem(Object item) {
        try {
            // Reflection: getProperty() auf item aufrufen
            String methodName = "get" + 
                property.substring(0, 1).toUpperCase() + 
                property.substring(1);
            
            Method method = item.getClass().getMethod(methodName);
            Object value = method.invoke(item);
            
            return value != null ? value.toString() : "";
            
        } catch (Exception e) {
            return "[Error: " + e.getMessage() + "]";
        }
    }
}

Was macht dieser Code?

Das ist Parent-Child-Kommunikation zwischen Tags! Lass uns verstehen, wie das funktioniert:

Das Konzept:
<fleet:table> ist das Parent-Tag.
<fleet:column> sind Child-Tags, die INS IDE des Table-Tags stehen.

Die Kommunikation:
Child-Tags rufen findAncestorWithClass() auf, um ihr Parent zu finden, und registrieren sich dort.

ColumnTag’s doStartTag():

TableTag parent = (TableTag) findAncestorWithClass(this, TableTag.class);
parent.addColumn(this);

findAncestorWithClass() sucht in der Tag-Hierarchie nach einem Parent vom Typ TableTag.

Dann registrieren wir uns: Das Parent speichert uns in seiner columns-Liste!

TableTag sammelt Columns:

private List<ColumnTag> columns = new ArrayList<>();

public void addColumn(ColumnTag column) {
    columns.add(column);
}

TableTag generiert HTML:
Beim ersten doAfterBody() Aufruf haben wir alle Columns gesammelt (weil der Body evaluiert wurde und alle <fleet:column> Tags sich registriert haben).

Jetzt iterieren wir über items und für jede Row über columns, um die Tabelle zu generieren!

In der Praxis bedeutet das:

<fleet:table items="${users}">
    <fleet:column property="name" header="Name" />
    <fleet:column property="email" header="Email" />
</fleet:table>

Lifecycle:

  1. TableTag.doStartTag()EVAL_BODY_BUFFERED
  2. Body evaluiert → Beide ColumnTags registrieren sich bei TableTag
  3. TableTag.doAfterBody() → Generiert <table><thead>... mit gesammelten Columns
  4. Iteriert über items, für jede Row ruft EVAL_BODY_AGAIN
  5. Jede Iteration generiert <tr><td>...</td></tr>
  6. Am Ende </tbody></table>

Das ist wie Bootstrap-Table-Components arbeiten!


TLD-Einträge:

<tag>
    <name>table</name>
    <tag-class>com.javafleet.tags.TableTag</tag-class>
    <body-content>scriptless</body-content>
    <info>Generates HTML table from collection</info>
    
    <attribute>
        <name>items</name>
        <required>true</required>
        <rtexprvalue>true</rtexprvalue>
        <type>java.util.Collection</type>
    </attribute>
</tag>

<tag>
    <name>column</name>
    <tag-class>com.javafleet.tags.ColumnTag</tag-class>
    <body-content>empty</body-content>
    <info>Defines a column in table tag</info>
    
    <attribute>
        <name>property</name>
        <required>true</required>
        <rtexprvalue>true</rtexprvalue>
    </attribute>
    
    <attribute>
        <name>header</name>
        <required>false</required>
        <rtexprvalue>true</rtexprvalue>
    </attribute>
</tag>

🟡 PROFESSIONALS: Best Practices

1. Return-Code Constants verwenden

✅ DO:

return EVAL_BODY_BUFFERED;
return EVAL_BODY_AGAIN;
return SKIP_BODY;

❌ DON’T:

return 2011;  // Was bedeutet das?!
return 2;     // Unleserlich!

Die Konstanten sind selbst-dokumentierend!


2. Immer in doStartTag() initialisieren

✅ DO:

@Override
public int doStartTag() throws JspException {
    iteration = 0;  // Reset!
    // ...
}

❌ DON’T:

public RepeatTag() {
    iteration = 0;  // Falsch!
}

Warum? Tag-Instanzen werden vom Container wiederverwendet (Pooling)!


3. bodyContent.clearBody() nicht vergessen

✅ DO:

@Override
public int doAfterBody() throws JspException {
    // ... Content verarbeiten ...
    
    bodyContent.clearBody();  // Wichtig!
    
    return EVAL_BODY_AGAIN;
}

Sonst akkumuliert der Buffer bei jeder Iteration!


4. Cleanup in doEndTag()

✅ DO:

@Override
public int doEndTag() throws JspException {
    // Cleanup
    iterator = null;
    items = null;
    columns.clear();
    
    return EVAL_PAGE;
}

Verhindert Memory Leaks bei Tag-Pooling!


5. Error Handling

✅ DO:

@Override
public int doAfterBody() throws JspException {
    try {
        // ... Code ...
    } catch (IOException e) {
        throw new JspException("Error in MyTag: " + e.getMessage(), e);
    }
}

Wrappen von checked Exceptions in JspException!


💬 Real Talk: Warum BodyTagSupport wenn SimpleTag einfacher ist?

Java Fleet Küche, 14:00 Uhr. Nova hat gerade 2 Stunden mit BodyTagSupport gekämpft.

Nova: „Elyndra, ich verstehe nicht. Warum lernen wir BodyTagSupport? SimpleTag war doch SO viel einfacher!“

Elyndra: „Gute Frage! Drei Szenarien aus der Praxis:

Szenario 1: Legacy-Code
Letzten Monat habe ich ein 15 Jahre altes E-Commerce-System gewartet. Tausende Custom Tags – alle mit BodyTagSupport. Wenn ich die nicht verstehe, kann ich den Code nicht warten.

Szenario 2: Komplexe Iteration
Wir hatten einen Customer, der wollte ein komplexes Reporting-Tag. Iteration über mehrere Collections gleichzeitig, verschachtelte Loops, conditional rendering. Mit SimpleTag hätten wir das in 500 Zeilen Code machen müssen. Mit BodyTagSupport in 150 – weil der Lifecycle-Management vom Container übernommen wird.

Szenario 3: Tiefes Verständnis
Wenn du verstehst, wie JSTL’s <c:forEach> unter der Haube funktioniert, verstehst du auch, warum manche Performance-Probleme auftreten. Und wie du sie löst.“

Nova: „Okay, aber für neue Projekte nutze ich SimpleTag, right?“

Elyndra: „Zu 90% ja! SimpleTag für einfache bis mittlere Komplexität. BodyTagSupport nur wenn:

  • Du Legacy-Code wartest
  • Du sehr komplexe Iteration brauchst
  • Du maximale Performance-Kontrolle willst

Real talk: Ich habe in den letzten 2 Jahren nur EINMAL ein neues BodyTagSupport-Tag geschrieben. Aber ich habe HUNDERTE gewartet.

Das ist wie Manual vs. Automatic Getriebe. Du fährst wahrscheinlich Automatic – aber du solltest wissen, wie Manual funktioniert.“

Nova: „Makes sense. Also lernen für Legacy, aber nutzen SimpleTag für Neues?“

Elyndra: „Exactly! Und jetzt weißt du, warum <c:forEach> so performant ist – es ist mit BodyTagSupport gebaut!“


✅ Checkpoint & Quiz

Mini-Challenge:

Erstelle ein <fleet:if> Tag mit BodyTagSupport, das:

  • Ein test Attribut vom Typ boolean hat
  • Den Body nur anzeigt, wenn test == true
  • Mit EVAL_BODY_BUFFERED arbeitet

Quiz:

  1. Was ist der Unterschied zwischen EVAL_BODY_BUFFERED und SKIP_BODY?
  2. Wann gibt man EVAL_BODY_AGAIN zurück?
  3. Was macht bodyContent.clearBody() und warum ist es wichtig?
  4. Warum muss man in doStartTag() initialisieren und nicht im Konstruktor?
  5. Wie kommunizieren Child-Tags mit Parent-Tags?
  6. Was ist der Unterschied zwischen bodyContent.getEnclosingWriter() und bodyContent.getWriter()?

Lösung: Am Anfang von Tag 7!


❓ FAQ

F1: Wann sollte ich BodyTagSupport statt SimpleTag nutzen?

Antwort:

Nutze BodyTagSupport wenn:

  • ✅ Du Legacy-Code wartest (häufigster Fall!)
  • ✅ Du sehr komplexe Iteration über mehrere Collections brauchst
  • ✅ Du feinste Kontrolle über den Lifecycle willst
  • ✅ Performance-kritische Tags mit vielen Iterationen

Nutze SimpleTag wenn:

  • ✅ Du neuen Code schreibst
  • ✅ Body einmal oder gar nicht verarbeitet wird
  • ✅ Einfachere Wartbarkeit wichtiger ist
  • ✅ Du die Wahl hast (90% der Fälle!)

Regel: SimpleTag first, BodyTagSupport nur wenn nötig!


F2: Warum heißt es EVAL_BODY_BUFFERED und nicht einfach PROCESS_BODY?

Historical Context:

Die Konstanten stammen aus JSP 1.2 (2001). Damals gab es drei Optionen:

  • EVAL_BODY_INCLUDE (1) – Body direkt ausgeben
  • EVAL_BODY_BUFFERED (2011) – Body in Buffer
  • SKIP_BODY (0) – Body ignorieren

„EVAL“ = „Evaluate“ (auswerten)
„BUFFERED“ = Im Buffer sammeln

EVAL_BODY_INCLUDE ist deprecated – deshalb nutzen wir nur noch BUFFERED.

Die Zahlen (1, 2011, 0) sind die tatsächlichen Integer-Werte der Konstanten!


F3: Was passiert, wenn ich bodyContent.clearBody() vergesse?

Problem:

@Override
public int doAfterBody() throws JspException {
    String content = bodyContent.getString();
    // Content ausgeben...
    
    // bodyContent.clearBody();  ← VERGESSEN!
    
    return EVAL_BODY_AGAIN;  // Body nochmal!
}

Resultat:

Bei EVAL_BODY_AGAIN evaluiert der Container den Body nochmal und HÄNGT den neuen Content an den alten an!

Beispiel:

<fleet:repeat times="3">
    <p>Hello</p>
</fleet:repeat>

Ohne clearBody():

Iteration 1: "<p>Hello</p>"
Iteration 2: "<p>Hello</p><p>Hello</p>" (verdoppelt!)
Iteration 3: "<p>Hello</p><p>Hello</p><p>Hello</p>" (verdreifacht!)

Ergebnis: 6x statt 3x Hello! 😱

Lösung: IMMER clearBody() vor EVAL_BODY_AGAIN!


F4: Kann ich BodyTagSupport und SimpleTag mischen?

Ja, absolut!

In derselben TLD kannst du beides haben:

<taglib>
    <!-- SimpleTag -->
    <tag>
        <name>uppercase</name>
        <tag-class>com.javafleet.tags.UppercaseTag</tag-class>
        <body-content>scriptless</body-content>
    </tag>
    
    <!-- BodyTagSupport -->
    <tag>
        <name>forEach</name>
        <tag-class>com.javafleet.tags.ForEachTag</tag-class>
        <body-content>scriptless</body-content>
    </tag>
</taglib>

Best Practice: Nutze das richtige Tool für jeden Use-Case!


F5: Wie debugge ich BodyTagSupport Tags?

Strategie:

1. Logging in Lifecycle-Methoden:

@Override
public int doStartTag() throws JspException {
    System.out.println("RepeatTag.doStartTag() - times=" + times);
    return EVAL_BODY_BUFFERED;
}

@Override
public int doAfterBody() throws JspException {
    System.out.println("RepeatTag.doAfterBody() - iteration=" + iteration);
    // ...
    int result = (iteration < times) ? EVAL_BODY_AGAIN : SKIP_BODY;
    System.out.println("  → returning " + result);
    return result;
}

2. Content inspizieren:

String content = bodyContent.getString();
System.out.println("Body content: " + content);

3. PageContext dumpen:

Enumeration<String> attrs = pageContext.getAttributeNamesInScope(PageContext.PAGE_SCOPE);
while (attrs.hasMoreElements()) {
    String name = attrs.nextElement();
    System.out.println("  " + name + " = " + pageContext.getAttribute(name));
}

4. Breakpoints setzen: In NetBeans: Breakpoint in doAfterBody() setzen, dann Request ausführen und durch Lifecycle steppen!


F6: Kann ich mit BodyTagSupport auch über Maps iterieren?

Ja! Erweiterte ForEachTag-Version:

public class ForEachMapTag extends BodyTagSupport {
    
    private Map<?, ?> items;
    private String keyVar;
    private String valueVar;
    private Iterator<?> keyIterator;
    
    public void setItems(Map<?, ?> items) {
        this.items = items;
    }
    
    public void setKeyVar(String keyVar) {
        this.keyVar = keyVar;
    }
    
    public void setValueVar(String valueVar) {
        this.valueVar = valueVar;
    }
    
    @Override
    public int doStartTag() throws JspException {
        if (items == null || items.isEmpty()) {
            return SKIP_BODY;
        }
        
        keyIterator = items.keySet().iterator();
        
        if (keyIterator.hasNext()) {
            setCurrentEntry();
            return EVAL_BODY_BUFFERED;
        } else {
            return SKIP_BODY;
        }
    }
    
    @Override
    public int doAfterBody() throws JspException {
        try {
            bodyContent.getEnclosingWriter().write(bodyContent.getString());
            bodyContent.clearBody();
            
            if (keyIterator.hasNext()) {
                setCurrentEntry();
                return EVAL_BODY_AGAIN;
            } else {
                pageContext.removeAttribute(keyVar);
                pageContext.removeAttribute(valueVar);
                return SKIP_BODY;
            }
        } catch (IOException e) {
            throw new JspException("Error in ForEachMapTag", e);
        }
    }
    
    private void setCurrentEntry() {
        Object key = keyIterator.next();
        Object value = items.get(key);
        
        pageContext.setAttribute(keyVar, key);
        pageContext.setAttribute(valueVar, value);
    }
}

Verwendung:

<fleet:forEachMap items="${userAges}" keyVar="name" valueVar="age">
    <p>${name} is ${age} years old</p>
</fleet:forEachMap>

F7: Bernd meinte, „BodyTagSupport ist deprecated“. Stimmt das?

Nein, nicht ganz! 😄

Real talk: Bernd verwechselt das mit EVAL_BODY_TAG (die alte Konstante), die tatsächlich deprecated ist.

BodyTagSupport selbst ist NICHT deprecated!

Es ist Teil der Jakarta Servlet Spec und wird weiterhin unterstützt. Aber:

Was Bernd meint:

  • SimpleTag (JSP 2.0+) ist der modernere Ansatz
  • Für neue Projekte wird SimpleTag empfohlen
  • BodyTagSupport ist „Legacy-Style“ aber nicht veraltet

Analogie:

  • BodyTagSupport = Manuelle Schaltung
  • SimpleTag = Automatik

Beide funktionieren, beide haben ihren Platz. Automatic ist moderner, aber Manual ist nicht verboten!

Fun Fact: JSTL’s <c:forEach> nutzt BodyTagSupport – und JSTL ist alles andere als deprecated! 🎯


📚 Quiz-Lösungen

Hier sind die Antworten zum Quiz von oben:


Frage 1: Was ist der Unterschied zwischen EVAL_BODY_BUFFERED und SKIP_BODY?

Antwort:

EVAL_BODY_BUFFERED (2011):

  • Container erstellt einen BodyContent-Buffer
  • Body wird evaluiert und in Buffer geschrieben
  • Tag kann Body-Content lesen und manipulieren
  • doAfterBody() wird aufgerufen

SKIP_BODY (0):

  • Body wird komplett ignoriert
  • Kein Buffer erstellt
  • doAfterBody() wird NICHT aufgerufen
  • Container geht direkt zu doEndTag()

Use Cases:

// Body verarbeiten:
return EVAL_BODY_BUFFERED;

// Body ignorieren (z.B. bei times=0):
return SKIP_BODY;

Frage 2: Wann gibt man EVAL_BODY_AGAIN zurück?

Antwort:

EVAL_BODY_AGAIN (2) gibt man in doAfterBody() zurück, wenn man den Body nochmal evaluieren will.

Use Cases:

  • Iteration: Body mehrfach wiederholen
  • Collection-Iteration: Für jedes Element Body evaluieren
  • Conditional Loops: Solange Bedingung true, Body wiederholen

Beispiel:

@Override
public int doAfterBody() throws JspException {
    iteration++;
    
    if (iteration < times) {
        return EVAL_BODY_AGAIN;  // Nochmal!
    } else {
        return SKIP_BODY;  // Fertig!
    }
}

Wichtig: Bei EVAL_BODY_AGAIN wird der Body neu evaluiert und doAfterBody() nochmal aufgerufen!


Frage 3: Was macht bodyContent.clearBody() und warum ist es wichtig?

Antwort:

bodyContent.clearBody() leert den Body-Buffer.

Warum wichtig bei Iteration?

Ohne clearBody():

Iteration 1: Body = "Hello"
Iteration 2: Body = "HelloHello" (angehängt!)
Iteration 3: Body = "HelloHelloHello" (nochmal angehängt!)

Mit clearBody():

Iteration 1: Body = "Hello" → clearBody()
Iteration 2: Body = "Hello" → clearBody()
Iteration 3: Body = "Hello" → clearBody()

Best Practice:

@Override
public int doAfterBody() throws JspException {
    // Content holen und ausgeben
    String content = bodyContent.getString();
    bodyContent.getEnclosingWriter().write(content);
    
    // KRITISCH: Buffer leeren!
    bodyContent.clearBody();
    
    return EVAL_BODY_AGAIN;
}

Regel: Vor jedem EVAL_BODY_AGAINclearBody() aufrufen!


Frage 4: Warum muss man in doStartTag() initialisieren und nicht im Konstruktor?

Antwort:

Tag-Pooling!

Der Container erstellt NICHT für jeden Request neue Tag-Instanzen. Stattdessen:

Mit Pooling:

Request 1: Tag-Instanz erstellt → verwendet → in Pool
Request 2: Instanz aus Pool → wiederverwendet!
Request 3: Dieselbe Instanz → wiederverwendet!

Problem mit Konstruktor:

public class RepeatTag extends BodyTagSupport {
    private int iteration = 0;  // Konstruktor-Init
    
    // Konstruktor wird nur EINMAL aufgerufen!
    public RepeatTag() {
        iteration = 0;  // Zu spät!
    }
}

Bei Request 2 hat iteration noch den Wert vom Ende von Request 1!

Lösung: doStartTag():

@Override
public int doStartTag() throws JspException {
    iteration = 0;  // Bei jedem Request neu!
    return EVAL_BODY_BUFFERED;
}

doStartTag() wird bei jedem Request aufgerufen – perfekt für Initialisierung!


Frage 5: Wie kommunizieren Child-Tags mit Parent-Tags?

Antwort:

Mit findAncestorWithClass():

// In Child-Tag:
@Override
public int doStartTag() throws JspException {
    // Parent finden
    TableTag parent = (TableTag) findAncestorWithClass(
        this,           // Von mir aus suchen
        TableTag.class  // Nach diesem Typ suchen
    );
    
    if (parent == null) {
        throw new JspException("Must be nested in TableTag!");
    }
    
    // Bei Parent registrieren
    parent.addColumn(this);
    
    return SKIP_BODY;
}

Parent-Tag:

public class TableTag extends BodyTagSupport {
    private List<ColumnTag> columns = new ArrayList<>();
    
    // Child-Tags registrieren sich hier!
    public void addColumn(ColumnTag column) {
        columns.add(column);
    }
}

Der Ablauf:

  1. Container evaluiert Body von <fleet:table>
  2. Dabei werden alle <fleet:column> Tags evaluiert
  3. Jedes Column-Tag findet sein Table-Parent
  4. Jedes Column-Tag registriert sich beim Parent
  5. Table-Tag hat jetzt alle Columns gesammelt!

Frage 6: Was ist der Unterschied zwischen bodyContent.getEnclosingWriter() und bodyContent.getWriter()?

Antwort:

getEnclosingWriter():

  • Gibt den echten Output-Writer der JSP zurück
  • Content, der hier geschrieben wird, landet im finalen HTML
  • Das willst du normalerweise!

getWriter():

  • Gibt den Buffer-Writer zurück
  • Content wird nur im Buffer gesammelt, nicht ausgegeben
  • Für spezielle Use-Cases (mehrfache Verarbeitung)

Best Practice:

@Override
public int doAfterBody() throws JspException {
    try {
        String content = bodyContent.getString();
        
        // In echten Output schreiben:
        bodyContent.getEnclosingWriter().write(content);
        
        // NICHT:
        // bodyContent.getWriter().write(content);  ← Landet nur im Buffer!
        
        bodyContent.clearBody();
        return SKIP_BODY;
        
    } catch (IOException e) {
        throw new JspException(e);
    }
}

Merksatz: „Enclosing“ = „umschließend“ = der Writer der JSP, die das Tag umschließt!


🎉 Tag 6 geschafft!

Slay! Du hast es geschafft! 🚀

Das hast du heute gerockt:

  • ✅ BodyTagSupport von Grund auf verstanden
  • ✅ Den kompletten Classic Tag Lifecycle gemeistert
  • ✅ Return-Codes (EVAL_BODY_BUFFERED, EVAL_BODY_AGAIN, SKIP_BODY) beherrscht
  • ✅ Iteration über Collections implementiert
  • ✅ Parent-Child-Kommunikation zwischen Tags
  • ✅ Unterschied zwischen SimpleTag und BodyTagSupport klar

Von einfachen Uppercase-Tags zu vollwertigen Collection-Iteratoren!

Main Character Energy: Unlocked!

Dein neues Skillset:

  • Classic Tag Handler Expertise
  • Body Content mehrfach verarbeiten
  • Komplexe Tag-Hierarchien bauen
  • Legacy-Code verstehen und warten

Real talk:
BodyTagSupport ist komplex – aber du hast es geschafft! Du verstehst jetzt, wie JSTL’s <c:forEach> unter der Haube funktioniert. Das ist hardcore Jakarta EE Knowledge!


Wie geht’s weiter?

Morgen (Tag 7): JPA vs JDBC – Konfiguration & Provider

Was dich erwartet:

  • Was ist JPA und warum brauchen wir es?
  • Der Unterschied zwischen JPA und JDBC
  • persistence.xml konfigurieren
  • EntityManager verstehen
  • JPA-Provider (Hibernate, EclipseLink) einrichten
  • Erste Entity-Klassen erstellen – das wird dein Game-Changer für Datenbank-Arbeit! 🔥

Brauchst du eine Pause?
Absolut! BodyTagSupport ist intensive Kost. Lass das heute sacken, spiel mit den Beispielen, und morgen geht’s mit JPA weiter.

Tipp für heute Abend:
Nimm die ForEachTag-Implementation und erweitere sie:

  • Füge ein start und end Attribut hinzu (wie bei echtem <c:forEach>)
  • Füge ein step Attribut hinzu (jedes n-te Element)
  • Zeige einen Index an (varStatus=“status“)

Learning by doing! 🔧


🔧 Troubleshooting

Problem: IllegalStateException – Cannot buffer a body content once it has been flushed

Ursache: Body wurde bereits ausgegeben, bevor EVAL_BODY_BUFFERED zurückgegeben wurde.

Lösung:

@Override
public int doStartTag() throws JspException {
    // Erst Return-Code, DANN nichts mehr ausgeben!
    return EVAL_BODY_BUFFERED;
}

Nichts vor return EVAL_BODY_BUFFERED in den Output schreiben!


Problem: Body wird mehrfach ausgegeben, obwohl SKIP_BODY zurückgegeben wird

Ursache: clearBody() vergessen vor EVAL_BODY_AGAIN.

Lösung:

@Override
public int doAfterBody() throws JspException {
    bodyContent.getEnclosingWriter().write(bodyContent.getString());
    bodyContent.clearBody();  // KRITISCH!
    
    return someCondition ? EVAL_BODY_AGAIN : SKIP_BODY;
}

Problem: NullPointerException bei getBodyContent()

Ursache: getBodyContent() gibt null zurück, wenn EVAL_BODY_BUFFERED nicht zurückgegeben wurde.

Lösung:

@Override
public int doStartTag() throws JspException {
    // MUSS EVAL_BODY_BUFFERED sein für Body-Verarbeitung!
    return EVAL_BODY_BUFFERED;
}

Problem: Parent-Tag wird nicht gefunden (findAncestorWithClass() gibt null)

Ursache: Child-Tag ist nicht korrekt verschachtelt.

Lösung:

<!-- FALSCH: -->
<fleet:column property="name" />
<fleet:table items="${users}" />

<!-- RICHTIG: Column INSIDE Table! -->
<fleet:table items="${users}">
    <fleet:column property="name" />
</fleet:table>

Problem: Attribute-Werte sind null in doAfterBody()

Ursache: Setter wurden vom Container nicht aufgerufen (Namenskonvention falsch).

Lösung:

// Attribut "times" → Setter "setTimes()"
// FALSCH:
public void settimes(int t) { ... }

// RICHTIG:
public void setTimes(int times) { ... }

JavaBeans-Konvention beachten!


📚 Resources & Links

Offizielle Dokumentation:

  • Jakarta Tag Library Spec: https://jakarta.ee/specifications/tags/
  • JSP Specification: https://jakarta.ee/specifications/pages/
  • BodyTagSupport JavaDoc: https://jakarta.ee/specifications/tags/3.0/apidocs/jakarta/servlet/jsp/tagext/BodyTagSupport.html

Tutorials:

  • Baeldung – JSP Custom Tags: https://www.baeldung.com/jsp-custom-tags
  • Oracle – Tag Library Development: https://docs.oracle.com/javaee/5/tutorial/doc/bnalj.html
  • TutorialsPoint – JSP Custom Tags: https://www.tutorialspoint.com/jsp/jsp_custom_tags.htm

Legacy aber lehrreich:

  • Java EE 5 Tutorial – Classic Tags: https://docs.oracle.com/javaee/5/tutorial/doc/bnama.html
  • Core Servlets – Custom Tags: http://www.coreservlets.com/JSF-Tutorial/jsf-tags.html

Best Practices:

  • IBM Developer – Writing Custom JSP Tags: https://developer.ibm.com/articles/wa-jsptags/
  • DZone – JSP Tag Development: https://dzone.com/articles/jsp-custom-tags

Vergleich SimpleTag vs. BodyTagSupport:

  • Stack Overflow Discussion: https://stackoverflow.com/questions/tagged/bodytag+jsp

💬 Feedback?

War Tag 6 zu komplex? Zu viele Return-Codes? Mehr Beispiele gewünscht?

Schreib uns: feedback@java-developer.online

Wir wollen, dass du erfolgreich lernst!


Bis morgen! 👋

Elyndra

elyndra@java-developer.online
Senior Developer bei Java Fleet Systems Consulting


Java Web Aufbau – Tag 6 von 10
© 2025 Java Fleet Systems Consulting
Website: java-developer.online

Autor

  • Elyndra Valen

    28 Jahre alt, wurde kürzlich zur Senior Entwicklerin befördert nach 4 Jahren intensiver Java-Entwicklung. Elyndra kennt die wichtigsten Frameworks und Patterns, beginnt aber gerade erst, die tieferen Zusammenhänge und Architektur-Entscheidungen zu verstehen. Sie ist die Brücke zwischen Junior- und Senior-Welt im Team.