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

Listener

🗺️ Deine Position im Kurs

TagThemaStatus
1Filter im Webcontainer✅ Abgeschlossen
→ 2Listener im Webcontainer👉 DU BIST HIER!
3Authentifizierung gegenüber einer Datenbank🔒 Noch nicht freigeschaltet
4Container Base Security & Jakarta EE Security🔒 Noch nicht freigeschaltet
5Custom Tags & Tag Handler🔒 Noch nicht freigeschaltet
6Custom Tag Handler mit BodyTagSupport🔒 Noch nicht freigeschaltet
7JPA vs JDBC – Konfiguration & Persistence Provider🔒 Noch nicht freigeschaltet
8Relationen (1) in der JPA🔒 Noch nicht freigeschaltet
9Relationen (2) in der JPA🔒 Noch nicht freigeschaltet
10JSF Überblick🔒 Noch nicht freigeschaltet

Modul: Java Web Aufbau
Gesamt-Dauer: 10 Arbeitstage (je 8 Stunden)
Dein Ziel: Listener verstehen und für Application Lifecycle, Session-Tracking und Request-Monitoring einsetzen


📋 Voraussetzungen

Was du schon können solltest:

  • ✅ Filter verstehen und implementieren
  • ✅ Servlets kennen
  • ✅ Request/Response-Zyklus verstehen
  • ✅ Sessions nutzen
  • ✅ web.xml konfigurieren

Was du heute lernst:

  • ✅ 8 Listener-Typen kennenlernen
  • ✅ Application Lifecycle Events verstehen
  • ✅ Session-Tracking implementieren
  • ✅ Request-Monitoring aufbauen
  • ✅ Attribute-Listener nutzen
  • ✅ Production-Ready Listener schreiben

⚡ 30-Sekunden-Überblick

Was sind Listener? Listener sind Event-Handler im Webcontainer. Sie reagieren auf Lifecycle-Events: Application startet/stoppt, Sessions werden erstellt/zerstört, Attribute ändern sich. Sie sind die dritte Spezialklassenart in Jakarta EE – neben Servlets und Filtern.

Was lernst du heute? Du verstehst alle 8 Listener-Typen, implementierst Application-Startup-Logic, baust Session-Tracking für aktive User, monitored Request-Performance und nutzt Attribute-Listener für Change-Tracking.

Warum ist das wichtig? Listener sind essentiell für Application-Initialisierung, Monitoring und Resource-Management. Jede professionelle Webanwendung nutzt Listener für Startup-Logic, Session-Tracking und Performance-Monitoring.


👋 Willkommen zu Tag 2!

Hi! 👋

Elyndra hier. Willkommen zurück!

Erinnerst du dich an Tag 1?
Du hast Filter kennengelernt – Interceptors, die Requests VOR und Responses NACH Servlets verarbeiten. Heute lernst du Listener kennen!

Was ist der Unterschied zwischen Filtern und Listenern?

Filter:

  • Verarbeiten Requests und Responses
  • Werden pro Request aufgerufen
  • Sind Teil der Request-Chain
  • Beispiel: Encoding, Security, Logging

Listener:

  • Reagieren auf Events im Webcontainer
  • Werden bei Lifecycle-Events aufgerufen
  • Sind NICHT Teil der Request-Chain
  • Beispiel: Application Start, Session Created, Attribute Changed

Stell dir vor:

Filter = Türsteher
→ Prüft jeden Gast beim Reinkommen (Request)
→ Prüft jeden Gast beim Rausgehen (Response)

Listener = Hausmeister
→ Reagiert wenn Party startet (Application Start)
→ Reagiert wenn neue Gäste kommen (Session Created)
→ Reagiert wenn Party endet (Application Stop)

Real talk: Ohne Listener wäre Application-Initialisierung ein Chaos. Du müsstest in JEDEM Servlet prüfen, ob die Connection-Pool schon initialisiert ist. Listener machen das zentral beim Application-Start.

Heute lernst du Event-Driven Programming im Webcontainer!

Bist du bereit? Let’s go! 🚀


📚 Teil 1: Listener verstehen

Was sind Listener?

Listener sind Event-Handler, die auf Lifecycle-Events im Webcontainer reagieren.

Event-Typen:

  1. Application Lifecycle – Application startet/stoppt
  2. Session Lifecycle – Sessions erstellt/zerstört
  3. Request Lifecycle – Requests starten/enden
  4. Attribute Changes – Attribute werden gesetzt/geändert/gelöscht

Das Listener-Interface-Pattern:

Jeder Listener implementiert ein oder mehrere Listener-Interfaces:

public interface ServletContextListener {
    void contextInitialized(ServletContextEvent sce);
    void contextDestroyed(ServletContextEvent sce);
}

Der Container ruft diese Methoden automatisch auf!


Die 8 Listener-Typen

Übersicht:

ListenerEventUse-Case
ServletContextListenerApplication Start/StopDB Pool initialisieren, Config laden
ServletContextAttributeListenerApplication-Attribute ändernMonitoring, Debugging
HttpSessionListenerSession erstellt/zerstörtAktive User zählen
HttpSessionAttributeListenerSession-Attribute ändernSession-Monitoring
HttpSessionActivationListenerSession MigrationClustering, Passivation
HttpSessionBindingListenerObjekt zu Session hinzugefügtObject-Level Tracking
ServletRequestListenerRequest Start/EndePerformance-Monitoring
ServletRequestAttributeListenerRequest-Attribute ändernRequest-Debugging

Die wichtigsten 4 (heute Fokus):

  1. ServletContextListener
  2. HttpSessionListener
  3. ServletRequestListener
  4. ServletContextAttributeListener

Listener vs. Servlets vs. Filter

Die drei Spezialklassenarten im Vergleich:

AspektServletFilterListener
ZweckBusiness-LogicRequest/Response ProcessingEvent-Handling
AufrufPro RequestPro Request (vor/nach Servlet)Bei Lifecycle-Events
Instanzen1 pro Klasse1 pro Klasse1 pro Klasse
Thread-Safe?JaJaJa
Chain?NeinJa (Filter-Chain)Nein

Alle drei:

  • Werden einmal instanziiert
  • Sind thread-safe (müssen!)
  • Haben einen Lifecycle

Der Unterschied:

  • Servlet = Endpoint (verarbeitet Request)
  • Filter = Interceptor (vor/nach Request)
  • Listener = Observer (reagiert auf Events)

🟢 GRUNDLAGEN: Die wichtigsten Listener

ServletContextListener – Application Lifecycle

Der wichtigste Listener!

Use-Case: Beim Application-Start Ressourcen initialisieren, beim Stop aufräumen.

package com.javafleet.listeners;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebListener;
import java.util.logging.Logger;

@WebListener
public class AppInitializationListener implements ServletContextListener {
    
    private static final Logger LOGGER = Logger.getLogger(
        AppInitializationListener.class.getName()
    );
    
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        LOGGER.info("=== APPLICATION STARTUP ===");
        
        ServletContext context = sce.getServletContext();
        
        // 1. Environment prüfen
        String environment = context.getInitParameter("environment");
        LOGGER.info("Environment: " + environment);
        
        // 2. Start-Zeit speichern
        long startTime = System.currentTimeMillis();
        context.setAttribute("appStartTime", startTime);
        
        // 3. Application-Config laden
        loadConfiguration(context);
        
        // 4. Connection Pool initialisieren (später mit JPA!)
        initializeConnectionPool(context);
        
        // 5. Scheduler starten (z.B. für Cleanup-Jobs)
        startBackgroundJobs(context);
        
        LOGGER.info("Application initialized successfully!");
    }
    
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        LOGGER.info("=== APPLICATION SHUTDOWN ===");
        
        ServletContext context = sce.getServletContext();
        
        // 1. Background-Jobs stoppen
        stopBackgroundJobs(context);
        
        // 2. Connection Pool schließen
        closeConnectionPool(context);
        
        // 3. Cleanup
        context.removeAttribute("appStartTime");
        
        LOGGER.info("Application shutdown complete. Goodbye!");
    }
    
    private void loadConfiguration(ServletContext context) {
        // Config laden (Properties-File, Environment Variables, etc.)
        context.setAttribute("maxUploadSize", 10 * 1024 * 1024); // 10MB
        context.setAttribute("sessionTimeout", 30); // 30 Minuten
    }
    
    private void initializeConnectionPool(ServletContext context) {
        // Connection Pool initialisieren
        // (In echten Apps: HikariCP, C3P0, etc.)
        LOGGER.info("Connection Pool initialized");
    }
    
    private void closeConnectionPool(ServletContext context) {
        // Connection Pool schließen
        LOGGER.info("Connection Pool closed");
    }
    
    private void startBackgroundJobs(ServletContext context) {
        // Scheduler starten (z.B. für tägliche Cleanup-Jobs)
        LOGGER.info("Background jobs started");
    }
    
    private void stopBackgroundJobs(ServletContext context) {
        // Scheduler stoppen
        LOGGER.info("Background jobs stopped");
    }
}

Was macht dieser Code?

Die contextInitialized-Methode:

@Override
public void contextInitialized(ServletContextEvent sce) {

Diese Methode wird einmal beim Application-Start aufgerufen.

ServletContext context = sce.getServletContext();

Das ServletContext-Objekt ist die Application-Scope:

  • Globale Attribute für alle Servlets/JSPs
  • Init-Parameter aus web.xml
  • Resource-Paths
context.setAttribute("appStartTime", startTime);

Attribute im ServletContext sind für alle verfügbar:

  • Alle Servlets können darauf zugreifen
  • Alle JSPs können darauf zugreifen
  • Alle Filter/Listener können darauf zugreifen

Die contextDestroyed-Methode:

@Override
public void contextDestroyed(ServletContextEvent sce) {

Diese Methode wird einmal beim Application-Stop aufgerufen.

Wichtig zu verstehen:

Der ServletContextListener ist perfekt für:

  • ✅ Connection Pool initialisieren
  • ✅ Config laden
  • ✅ Scheduler starten
  • ✅ Caches aufbauen
  • ✅ Resource-Cleanup beim Shutdown

In der Praxis bedeutet das:

Statt in JEDEM Servlet zu prüfen „Ist Connection Pool initialisiert?“, machst du es EINMAL im ServletContextListener!


HttpSessionListener – Session-Tracking

Use-Case: Aktive Sessions zählen (= aktive User).

package com.javafleet.listeners;

import jakarta.servlet.annotation.WebListener;
import jakarta.servlet.http.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;

@WebListener
public class SessionTrackingListener implements HttpSessionListener {
    
    private static final Logger LOGGER = Logger.getLogger(
        SessionTrackingListener.class.getName()
    );
    
    // Thread-safe Counter!
    private static final AtomicInteger activeSessions = new AtomicInteger(0);
    private static final AtomicInteger totalSessions = new AtomicInteger(0);
    
    @Override
    public void sessionCreated(HttpSessionEvent se) {
        HttpSession session = se.getSession();
        
        // Counter erhöhen
        int active = activeSessions.incrementAndGet();
        int total = totalSessions.incrementAndGet();
        
        // Timestamp
        String timestamp = LocalDateTime.now().format(
            DateTimeFormatter.ISO_LOCAL_DATE_TIME
        );
        
        LOGGER.info(String.format(
            "[%s] Session CREATED: ID=%s | Active=%d | Total=%d",
            timestamp, session.getId(), active, total
        ));
        
        // Im ServletContext speichern für alle Servlets
        session.getServletContext()
               .setAttribute("activeSessionCount", active);
        session.getServletContext()
               .setAttribute("totalSessionCount", total);
        
        // Session-Metadata
        session.setAttribute("createdAt", timestamp);
    }
    
    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        HttpSession session = se.getSession();
        
        // Counter verringern
        int active = activeSessions.decrementAndGet();
        
        // Timestamp
        String timestamp = LocalDateTime.now().format(
            DateTimeFormatter.ISO_LOCAL_DATE_TIME
        );
        
        // Session-Dauer berechnen
        String createdAt = (String) session.getAttribute("createdAt");
        
        LOGGER.info(String.format(
            "[%s] Session DESTROYED: ID=%s | Active=%d | Created=%s",
            timestamp, session.getId(), active, createdAt
        ));
        
        // Update ServletContext
        session.getServletContext()
               .setAttribute("activeSessionCount", active);
    }
    
    /**
     * Statische Methode für andere Komponenten.
     */
    public static int getActiveSessionCount() {
        return activeSessions.get();
    }
    
    public static int getTotalSessionCount() {
        return totalSessions.get();
    }
}

Was macht dieser Code?

Thread-Safety mit AtomicInteger:

private static final AtomicInteger activeSessions = new AtomicInteger(0);

Warum AtomicInteger?

Listener werden von mehreren Threads gleichzeitig aufgerufen!

// ❌ FALSCH - Race Condition!
private static int activeSessions = 0;

public void sessionCreated(HttpSessionEvent se) {
    activeSessions++;  // Nicht thread-safe!
}

// ✅ RICHTIG - Thread-safe!
private static final AtomicInteger activeSessions = new AtomicInteger(0);

public void sessionCreated(HttpSessionEvent se) {
    activeSessions.incrementAndGet();  // Atomic Operation!
}

Session-Counter im ServletContext:

session.getServletContext()
       .setAttribute("activeSessionCount", active);

Warum im ServletContext?

Damit ALLE Servlets den Counter abrufen können:

@WebServlet("/stats")
public class StatsServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws IOException {
        
        ServletContext context = getServletContext();
        Integer activeUsers = (Integer) context.getAttribute("activeSessionCount");
        
        response.getWriter().println("Active Users: " + activeUsers);
    }
}

In der Praxis bedeutet das:

Du kannst ein Live-Dashboard bauen, das zeigt:

  • Wie viele User online sind
  • Wie lange Sessions laufen
  • Wann Peak-Zeiten sind

ServletRequestListener – Request-Monitoring

Use-Case: Performance-Monitoring – wie lange dauert jeder Request?

package com.javafleet.listeners;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebListener;
import jakarta.servlet.http.*;
import java.util.logging.Logger;

@WebListener
public class RequestPerformanceListener implements ServletRequestListener {
    
    private static final Logger LOGGER = Logger.getLogger(
        RequestPerformanceListener.class.getName()
    );
    
    // Thread-Local für Request-spezifische Daten
    private static final ThreadLocal<Long> startTime = new ThreadLocal<>();
    
    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        // Start-Zeit speichern
        startTime.set(System.currentTimeMillis());
        
        ServletRequest request = sre.getServletRequest();
        
        if (request instanceof HttpServletRequest) {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            
            String method = httpRequest.getMethod();
            String uri = httpRequest.getRequestURI();
            String query = httpRequest.getQueryString();
            
            String fullUrl = uri + (query != null ? "?" + query : "");
            
            LOGGER.info(String.format(
                "REQUEST START: %s %s", method, fullUrl
            ));
        }
    }
    
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        // End-Zeit und Dauer berechnen
        Long start = startTime.get();
        long duration = System.currentTimeMillis() - start;
        
        ServletRequest request = sre.getServletRequest();
        
        if (request instanceof HttpServletRequest) {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            
            String method = httpRequest.getMethod();
            String uri = httpRequest.getRequestURI();
            
            // Performance-Warning bei langsamen Requests
            String level = duration > 1000 ? "SLOW" : "OK";
            
            LOGGER.info(String.format(
                "REQUEST END: %s %s | Duration=%dms | Performance=%s",
                method, uri, duration, level
            ));
        }
        
        // ThreadLocal cleanup!
        startTime.remove();
    }
}

Was macht dieser Code?

ThreadLocal für Request-Daten:

private static final ThreadLocal<Long> startTime = new ThreadLocal<>();

Was ist ThreadLocal?

ThreadLocal erstellt für jeden Thread eine eigene Variable:

Thread 1: startTime = 100
Thread 2: startTime = 150
Thread 3: startTime = 200

Jeder Thread sieht nur seinen eigenen Wert!

Warum ThreadLocal?

Listener werden von vielen Threads gleichzeitig aufgerufen:

// ❌ FALSCH - Alle Threads teilen sich eine Variable!
private long startTime;

public void requestInitialized(...) {
    startTime = System.currentTimeMillis();
    // Thread 2 überschreibt jetzt den Wert von Thread 1!
}

// ✅ RICHTIG - Jeder Thread hat seine eigene Variable!
private static final ThreadLocal<Long> startTime = new ThreadLocal<>();

public void requestInitialized(...) {
    startTime.set(System.currentTimeMillis());
    // Thread-safe!
}

ThreadLocal Cleanup:

startTime.remove();

Wichtig zu verstehen:

Nach jedem Request MUSST du ThreadLocal aufräumen, sonst Memory Leak!

In der Praxis bedeutet das:

Du siehst in deinen Logs:

REQUEST START: GET /products?page=1
REQUEST END: GET /products?page=1 | Duration=45ms | Performance=OK

REQUEST START: GET /api/export
REQUEST END: GET /api/export | Duration=2340ms | Performance=SLOW

Perfekt für Performance-Debugging! 🎯


ServletContextAttributeListener – Change-Tracking

Use-Case: Monitoring wenn Application-Attribute sich ändern.

package com.javafleet.listeners;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebListener;
import java.util.logging.Logger;

@WebListener
public class AttributeMonitoringListener 
        implements ServletContextAttributeListener {
    
    private static final Logger LOGGER = Logger.getLogger(
        AttributeMonitoringListener.class.getName()
    );
    
    @Override
    public void attributeAdded(ServletContextAttributeEvent scae) {
        String name = scae.getName();
        Object value = scae.getValue();
        
        LOGGER.info(String.format(
            "ATTRIBUTE ADDED: %s = %s", name, value
        ));
    }
    
    @Override
    public void attributeRemoved(ServletContextAttributeEvent scae) {
        String name = scae.getName();
        Object value = scae.getValue();
        
        LOGGER.info(String.format(
            "ATTRIBUTE REMOVED: %s (was: %s)", name, value
        ));
    }
    
    @Override
    public void attributeReplaced(ServletContextAttributeEvent scae) {
        String name = scae.getName();
        Object oldValue = scae.getValue(); // Old value!
        Object newValue = scae.getServletContext().getAttribute(name);
        
        LOGGER.info(String.format(
            "ATTRIBUTE REPLACED: %s | Old=%s | New=%s",
            name, oldValue, newValue
        ));
    }
}

Was macht dieser Code?

Diese drei Methoden werden aufgerufen wenn:

ServletContext context = request.getServletContext();

// attributeAdded() wird aufgerufen
context.setAttribute("config", "value1");

// attributeReplaced() wird aufgerufen
context.setAttribute("config", "value2");

// attributeRemoved() wird aufgerufen
context.removeAttribute("config");

Wichtig zu verstehen:

scae.getValue() gibt in attributeReplaced() den alten Wert!

Use-Case:

Perfect für Debugging und Auditing:

  • Wer ändert welche Config-Werte?
  • Wann werden Attribute gesetzt/gelöscht?
  • Was war der alte Wert?

🟡 PROFESSIONALS: Fortgeschrittene Listener-Patterns

Session-Attribute-Listener

Use-Case: Track wenn User einloggt/ausloggt.

package com.javafleet.listeners;

import jakarta.servlet.annotation.WebListener;
import jakarta.servlet.http.*;
import java.time.LocalDateTime;
import java.util.logging.Logger;

@WebListener
public class LoginTrackingListener implements HttpSessionAttributeListener {
    
    private static final Logger LOGGER = Logger.getLogger(
        LoginTrackingListener.class.getName()
    );
    
    @Override
    public void attributeAdded(HttpSessionBindingEvent se) {
        String attributeName = se.getName();
        Object attributeValue = se.getValue();
        
        // Interessiert uns nur "user"-Attribut
        if ("user".equals(attributeName)) {
            HttpSession session = se.getSession();
            String sessionId = session.getId();
            
            LOGGER.info(String.format(
                "USER LOGIN: %s | SessionID=%s | Time=%s",
                attributeValue, sessionId, LocalDateTime.now()
            ));
            
            // Statistik tracken
            incrementLoginCount(session.getServletContext());
        }
    }
    
    @Override
    public void attributeRemoved(HttpSessionBindingEvent se) {
        String attributeName = se.getName();
        Object attributeValue = se.getValue();
        
        if ("user".equals(attributeName)) {
            HttpSession session = se.getSession();
            String sessionId = session.getId();
            
            LOGGER.info(String.format(
                "USER LOGOUT: %s | SessionID=%s | Time=%s",
                attributeValue, sessionId, LocalDateTime.now()
            ));
        }
    }
    
    @Override
    public void attributeReplaced(HttpSessionBindingEvent se) {
        // User-Objekt wird ersetzt (z.B. Role-Change)
        if ("user".equals(se.getName())) {
            Object oldUser = se.getValue();
            Object newUser = se.getSession().getAttribute("user");
            
            LOGGER.info(String.format(
                "USER CHANGED: Old=%s | New=%s",
                oldUser, newUser
            ));
        }
    }
    
    private void incrementLoginCount(ServletContext context) {
        synchronized (context) {
            Integer count = (Integer) context.getAttribute("totalLogins");
            count = (count == null) ? 1 : count + 1;
            context.setAttribute("totalLogins", count);
        }
    }
}

Was macht dieser Code?

Session-Attribute-Tracking:

// Irgendwo im Login-Servlet:
session.setAttribute("user", username);
// → attributeAdded() wird aufgerufen!

// Im Logout-Servlet:
session.removeAttribute("user");
// → attributeRemoved() wird aufgerufen!

Synchronization bei ServletContext:

synchronized (context) {
    Integer count = (Integer) context.getAttribute("totalLogins");
    count = (count == null) ? 1 : count + 1;
    context.setAttribute("totalLogins", count);
}

Warum synchronized?

Mehrere Threads können gleichzeitig auf ServletContext zugreifen:

Thread 1: read count=10 → write count=11
Thread 2: read count=10 → write count=11
Result: count=11 (should be 12!)

Mit synchronized wird der Block für andere Threads gesperrt!

In der Praxis bedeutet das:

Du siehst jeden Login/Logout in deinen Logs:

USER LOGIN: john@example.com | SessionID=ABC123 | Time=2025-10-25T14:30:00
USER LOGOUT: john@example.com | SessionID=ABC123 | Time=2025-10-25T15:45:00

Perfect für Security-Auditing! 🔒


Request-Attribute-Listener für Debugging

Use-Case: Track alle Attribute, die während Request-Verarbeitung gesetzt werden.

package com.javafleet.listeners;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebListener;
import jakarta.servlet.http.*;
import java.util.logging.Logger;

@WebListener
public class RequestAttributeDebugListener 
        implements ServletRequestAttributeListener {
    
    private static final Logger LOGGER = Logger.getLogger(
        RequestAttributeDebugListener.class.getName()
    );
    
    // Flag um Listener zu aktivieren/deaktivieren
    private static final String DEBUG_MODE_KEY = "requestDebugMode";
    
    @Override
    public void attributeAdded(ServletRequestAttributeEvent srae) {
        if (!isDebugMode(srae.getServletRequest())) return;
        
        String name = srae.getName();
        Object value = srae.getValue();
        String requestURI = getRequestURI(srae.getServletRequest());
        
        LOGGER.fine(String.format(
            "[%s] Attribute ADDED: %s = %s",
            requestURI, name, value
        ));
    }
    
    @Override
    public void attributeRemoved(ServletRequestAttributeEvent srae) {
        if (!isDebugMode(srae.getServletRequest())) return;
        
        String name = srae.getName();
        String requestURI = getRequestURI(srae.getServletRequest());
        
        LOGGER.fine(String.format(
            "[%s] Attribute REMOVED: %s",
            requestURI, name
        ));
    }
    
    @Override
    public void attributeReplaced(ServletRequestAttributeEvent srae) {
        if (!isDebugMode(srae.getServletRequest())) return;
        
        String name = srae.getName();
        Object oldValue = srae.getValue();
        Object newValue = srae.getServletRequest().getAttribute(name);
        String requestURI = getRequestURI(srae.getServletRequest());
        
        LOGGER.fine(String.format(
            "[%s] Attribute REPLACED: %s | Old=%s | New=%s",
            requestURI, name, oldValue, newValue
        ));
    }
    
    private boolean isDebugMode(ServletRequest request) {
        ServletContext context = request.getServletContext();
        Boolean debugMode = (Boolean) context.getAttribute(DEBUG_MODE_KEY);
        return debugMode != null && debugMode;
    }
    
    private String getRequestURI(ServletRequest request) {
        if (request instanceof HttpServletRequest) {
            return ((HttpServletRequest) request).getRequestURI();
        }
        return "unknown";
    }
}

Was macht dieser Code?

Conditional Logging:

private boolean isDebugMode(ServletRequest request) {
    ServletContext context = request.getServletContext();
    Boolean debugMode = (Boolean) context.getAttribute(DEBUG_MODE_KEY);
    return debugMode != null && debugMode;
}

Warum conditional?

Request-Attribute-Listener werden SEHR oft aufgerufen:

  • Jedes request.setAttribute() triggert Event
  • Jedes Framework-Attribut (Spring, JSF, etc.)
  • Kann sehr viele Logs erzeugen!

Mit Debug-Flag kannst du es an/ausschalten:

// Debug-Modus aktivieren
servletContext.setAttribute("requestDebugMode", true);

// Debug-Modus deaktivieren
servletContext.setAttribute("requestDebugMode", false);

In der Praxis bedeutet das:

Du siehst ALLE Attribute während Request-Processing:

[/products] Attribute ADDED: category = electronics
[/products] Attribute ADDED: productList = [Product@123, Product@456]
[/products] Attribute ADDED: totalCount = 42

Perfect für Debugging komplexer Request-Flows! 🔍


Listener-Kombination: Complete Monitoring

Use-Case: Ein Listener, der ALLES überwacht!

package com.javafleet.listeners;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebListener;
import jakarta.servlet.http.*;
import java.time.LocalDateTime;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;

@WebListener
public class ApplicationMonitoringListener 
        implements ServletContextListener,
                   HttpSessionListener,
                   ServletRequestListener {
    
    private static final Logger LOGGER = Logger.getLogger(
        ApplicationMonitoringListener.class.getName()
    );
    
    // Metrics
    private static final AtomicInteger activeSessions = new AtomicInteger(0);
    private static final AtomicInteger totalRequests = new AtomicInteger(0);
    private static final ThreadLocal<Long> requestStartTime = new ThreadLocal<>();
    
    // ===== ServletContextListener =====
    
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        LOGGER.info("╔════════════════════════════════════╗");
        LOGGER.info("║  APPLICATION STARTUP COMPLETE     ║");
        LOGGER.info("╚════════════════════════════════════╝");
        
        ServletContext context = sce.getServletContext();
        context.setAttribute("appStartTime", LocalDateTime.now());
        context.setAttribute("appVersion", "1.0.0");
        
        // Initialize monitoring
        resetMetrics(context);
    }
    
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        ServletContext context = sce.getServletContext();
        
        LocalDateTime startTime = (LocalDateTime) context.getAttribute("appStartTime");
        LocalDateTime endTime = LocalDateTime.now();
        
        LOGGER.info("╔════════════════════════════════════╗");
        LOGGER.info("║  APPLICATION SHUTDOWN             ║");
        LOGGER.info("╠════════════════════════════════════╣");
        LOGGER.info("║  Started:  " + startTime);
        LOGGER.info("║  Stopped:  " + endTime);
        LOGGER.info("║  Total Requests: " + totalRequests.get());
        LOGGER.info("╚════════════════════════════════════╝");
    }
    
    // ===== HttpSessionListener =====
    
    @Override
    public void sessionCreated(HttpSessionEvent se) {
        int count = activeSessions.incrementAndGet();
        
        HttpSession session = se.getSession();
        session.setAttribute("sessionCreatedAt", LocalDateTime.now());
        
        LOGGER.info(String.format(
            "➕ Session CREATED | ID=%s | Active=%d",
            session.getId().substring(0, 8) + "...", count
        ));
        
        updateMetrics(se.getSession().getServletContext());
    }
    
    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        int count = activeSessions.decrementAndGet();
        
        HttpSession session = se.getSession();
        LocalDateTime created = (LocalDateTime) session.getAttribute("sessionCreatedAt");
        LocalDateTime destroyed = LocalDateTime.now();
        
        LOGGER.info(String.format(
            "➖ Session DESTROYED | ID=%s | Active=%d | Duration=%s",
            session.getId().substring(0, 8) + "...", count,
            java.time.Duration.between(created, destroyed)
        ));
        
        updateMetrics(session.getServletContext());
    }
    
    // ===== ServletRequestListener =====
    
    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        requestStartTime.set(System.currentTimeMillis());
        totalRequests.incrementAndGet();
        
        if (sre.getServletRequest() instanceof HttpServletRequest) {
            HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
            
            LOGGER.fine(String.format(
                "→ Request START: %s %s",
                req.getMethod(), req.getRequestURI()
            ));
        }
    }
    
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        long duration = System.currentTimeMillis() - requestStartTime.get();
        requestStartTime.remove();
        
        if (sre.getServletRequest() instanceof HttpServletRequest) {
            HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
            
            String performance = duration < 100 ? "⚡" : 
                               duration < 500 ? "✓" : 
                               duration < 1000 ? "⚠" : "🐌";
            
            LOGGER.fine(String.format(
                "← Request END: %s %s | %dms %s",
                req.getMethod(), req.getRequestURI(), duration, performance
            ));
        }
    }
    
    // ===== Helper Methods =====
    
    private void resetMetrics(ServletContext context) {
        context.setAttribute("activeSessions", 0);
        context.setAttribute("totalRequests", 0);
        context.setAttribute("peakSessions", 0);
    }
    
    private void updateMetrics(ServletContext context) {
        int active = activeSessions.get();
        int total = totalRequests.get();
        int peak = (Integer) context.getAttribute("peakSessions");
        
        if (active > peak) {
            context.setAttribute("peakSessions", active);
        }
        
        context.setAttribute("activeSessions", active);
        context.setAttribute("totalRequests", total);
    }
}

Was macht dieser Code?

Multi-Interface-Listener:

public class ApplicationMonitoringListener 
        implements ServletContextListener,
                   HttpSessionListener,
                   ServletRequestListener {

Das Prinzip:

Ein Listener kann MEHRERE Interfaces implementieren!

Metrics Dashboard:

private static final AtomicInteger activeSessions = new AtomicInteger(0);
private static final AtomicInteger totalRequests = new AtomicInteger(0);

Diese Metrics kannst du in einem Admin-Servlet anzeigen:

@WebServlet("/admin/stats")
public class StatsServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws IOException {
        
        ServletContext context = getServletContext();
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        
        out.println("<h1>Application Metrics</h1>");
        out.println("<p>Active Sessions: " + 
                   context.getAttribute("activeSessions") + "</p>");
        out.println("<p>Peak Sessions: " + 
                   context.getAttribute("peakSessions") + "</p>");
        out.println("<p>Total Requests: " + 
                   context.getAttribute("totalRequests") + "</p>");
    }
}

In der Praxis bedeutet das:

Du hast ein Complete-Monitoring-System für deine Application:

  • Application Uptime
  • Active Sessions
  • Peak Sessions
  • Total Requests
  • Request Performance

Alles in einem Listener! 🎯


🔵 BONUS: Session-Migration & Binding-Listener

HttpSessionActivationListener

Use-Case: Clustering – Sessions werden zwischen Servern verschoben.

package com.javafleet.listeners;

import jakarta.servlet.http.*;
import java.io.Serializable;
import java.util.logging.Logger;

public class ShoppingCart 
        implements Serializable, HttpSessionActivationListener {
    
    private static final Logger LOGGER = Logger.getLogger(
        ShoppingCart.class.getName()
    );
    
    private static final long serialVersionUID = 1L;
    
    private List<Product> items = new ArrayList<>();
    
    @Override
    public void sessionWillPassivate(HttpSessionEvent se) {
        // Session wird auf Disk geschrieben (Passivation)
        LOGGER.info("ShoppingCart wird passiviert: " + items.size() + " items");
    }
    
    @Override
    public void sessionDidActivate(HttpSessionEvent se) {
        // Session wird von Disk geladen (Activation)
        LOGGER.info("ShoppingCart wurde aktiviert: " + items.size() + " items");
    }
}

Was macht dieser Code?

Object-Level Listener:

Dieses Interface wird NICHT auf Listener-Klassen, sondern auf Session-Attribute implementiert!

Passivation/Activation:

In Clustern:

  1. Server 1 speichert Session auf Disk (Passivate)
  2. Session wird zu Server 2 transferiert
  3. Server 2 lädt Session von Disk (Activate)

In der Praxis bedeutet das:

Wenn du komplexe Objekte in Sessions speicherst (Warenkorb, User-Profile), kannst du tracken, wenn sie zwischen Servern verschoben werden.


HttpSessionBindingListener

Use-Case: Track wenn Objekt zu Session hinzugefügt/entfernt wird.

package com.javafleet.listeners;

import jakarta.servlet.http.*;
import java.util.logging.Logger;

public class UserSession implements HttpSessionBindingListener {
    
    private static final Logger LOGGER = Logger.getLogger(
        UserSession.class.getName()
    );
    
    private String username;
    private String role;
    
    public UserSession(String username, String role) {
        this.username = username;
        this.role = role;
    }
    
    @Override
    public void valueBound(HttpSessionBindingEvent event) {
        // Objekt wurde zu Session hinzugefügt
        LOGGER.info(String.format(
            "UserSession BOUND: %s (Role=%s) | SessionID=%s",
            username, role, event.getSession().getId()
        ));
        
        // Statistik
        incrementActiveUsers(event.getSession().getServletContext());
    }
    
    @Override
    public void valueUnbound(HttpSessionBindingEvent event) {
        // Objekt wurde von Session entfernt
        LOGGER.info(String.format(
            "UserSession UNBOUND: %s (Role=%s) | SessionID=%s",
            username, role, event.getSession().getId()
        ));
        
        // Statistik
        decrementActiveUsers(event.getSession().getServletContext());
    }
    
    private void incrementActiveUsers(ServletContext context) {
        // Aktive User zählen
    }
    
    private void decrementActiveUsers(ServletContext context) {
        // Aktive User verringern
    }
}

Was macht dieser Code?

Automatisches Tracking:

// Im Login-Servlet:
UserSession userSession = new UserSession("john", "admin");
session.setAttribute("user", userSession);
// → valueBound() wird automatisch aufgerufen!

// Im Logout-Servlet:
session.removeAttribute("user");
// → valueUnbound() wird automatisch aufgerufen!

Der Unterschied zu HttpSessionAttributeListener:

  • HttpSessionAttributeListener = Listener-Klasse, trackt ALLE Attribute
  • HttpSessionBindingListener = Auf Objekt implementiert, trackt nur DIESES Objekt

In der Praxis bedeutet das:

Du kannst User-spezifische Tracking-Logic direkt im User-Objekt implementieren!


💬 Real Talk

Warum sind Listener so wichtig?

Honestly, ich verstehe wenn du denkst: „Ich könnte doch einfach im ersten Servlet prüfen ob initialisiert ist?“

Real talk: Das skaliert nicht!

Beispiel ohne Listener:

@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    protected void doGet(...) {
        // JEDES Servlet braucht das:
        if (getServletContext().getAttribute("dbPool") == null) {
            initializeDBPool();
        }
        // Business-Logic...
    }
}

@WebServlet("/users")
public class UserServlet extends HttpServlet {
    protected void doGet(...) {
        // Nochmal das gleiche!
        if (getServletContext().getAttribute("dbPool") == null) {
            initializeDBPool();
        }
        // Business-Logic...
    }
}

Problem:

  • ❌ Code-Duplizierung in JEDEM Servlet
  • ❌ Initialisierung passiert erst beim ersten Request
  • ❌ Race Conditions möglich
  • ❌ Kein zentraler Cleanup beim Shutdown

Mit ServletContextListener:

@WebListener
public class AppInitListener implements ServletContextListener {
    public void contextInitialized(...) {
        initializeDBPool();  // EINMAL beim Start!
    }
    
    public void contextDestroyed(...) {
        closeDBPool();  // EINMAL beim Stop!
    }
}

@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    protected void doGet(...) {
        // DB Pool ist garantiert verfügbar!
        // Keine Checks nötig!
    }
}

Vorteile:

  • ✅ Zentrale Initialisierung
  • ✅ Garantiert beim Start (nicht bei erstem Request)
  • ✅ Kein Code-Duplikat
  • ✅ Cleanup beim Shutdown

Bottom Line:

Listener sind essentiell für saubere Enterprise-Architektur. Lowkey ein echter Unterschied zwischen Junior- und Mid-Level-Entwicklern! 💪


✅ Checkpoint: Hast du es verstanden?

Quiz:

Frage 1: Was ist der Unterschied zwischen Filter und Listener?

Frage 2: Welcher Listener eignet sich, um beim Application-Start eine Datenbank-Verbindung zu initialisieren?

Frage 3: Warum muss man AtomicInteger für Session-Counter verwenden?

Frage 4: Was ist der Unterschied zwischen HttpSessionAttributeListener und HttpSessionBindingListener?

Frage 5: Wann wird ServletRequestListener.requestDestroyed() aufgerufen?


Mini-Challenge:

Aufgabe: Implementiere ein Complete-Monitoring-System mit Listenern.

Requirements:

  1. Application-Startup:
    • Logge Start-Zeit
    • Initialisiere Config
    • Setze „appVersion“ im ServletContext
  2. Session-Tracking:
    • Zähle aktive Sessions
    • Track Peak-Sessions
    • Berechne Session-Duration
  3. Request-Performance:
    • Messe Request-Duration
    • Kategorisiere: Fast (<100ms), OK (<500ms), Slow (>500ms)
    • Zähle Requests pro Kategorie
  4. Admin-Dashboard:
    • Servlet unter /admin/monitor
    • Zeige alle Metrics als HTML

Bonus:

  • Export Metrics als JSON unter /admin/api/metrics
  • Alert bei mehr als 1000 aktiven Sessions
  • Histogram für Request-Durations

Lösung:

Die Lösung zu dieser Challenge findest du am Anfang von Tag 3 als Kurzwiederholung! 🚀

Alternativ kannst du die Musterlösung im GitHub-Projekt checken: Java Fleet – Tag 2 Challenge Solution


Geschafft? 🎉

Dann bist du bereit für die FAQ-Sektion!


❓ Häufig gestellte Fragen

Frage 1: Kann ich mehrere Listener-Interfaces in einer Klasse implementieren?

Ja! Du kannst beliebig viele Interfaces kombinieren:

@WebListener
public class AllInOneListener 
        implements ServletContextListener,
                   HttpSessionListener,
                   ServletRequestListener {
    // Alle Methoden implementieren
}

Vorteil: Zentrales Monitoring in einer Klasse
Nachteil: Klasse kann groß werden

Best Practice: Für Production: Separate Listener für separate Concerns (Single Responsibility Principle)


Frage 2: Werden Listener in einer bestimmten Reihenfolge aufgerufen?

Bei @WebListener: Reihenfolge ist undefiniert!

Bei web.xml: Reihenfolge der <listener>-Definitionen!

<listener>
    <listener-class>com.javafleet.FirstListener</listener-class>
</listener>
<listener>
    <listener-class>com.javafleet.SecondListener</listener-class>
</listener>

→ FirstListener wird VOR SecondListener aufgerufen

Best Practice: Wenn Reihenfolge wichtig ist, nutze web.xml!


Frage 3: Wie unterscheiden sich ServletContext-Attribute von Session-Attributen?

Scope-Vergleich:

ScopeLebensdauerSichtbarkeit
ServletContextApplication-LifetimeALLE User
HttpSessionSession-LifetimeEIN User
ServletRequestRequest-LifetimeEIN Request

Beispiel:

// Application-Scope (für ALLE!)
servletContext.setAttribute("dbPool", pool);

// Session-Scope (für EINEN User)
session.setAttribute("user", username);

// Request-Scope (für EINEN Request)
request.setAttribute("products", productList);

Best Practice:

  • ServletContext: Config, Caches, Connection Pools
  • Session: User-Daten, Warenkorb
  • Request: Request-spezifische Daten

Frage 4: Muss ich ThreadLocal immer cleanup machen?

JA! Absolut essentiell!

@Override
public void requestDestroyed(ServletRequestEvent sre) {
    // ... Request-Verarbeitung
    
    startTime.remove();  // ← PFLICHT!
}

Warum?

Thread-Pools wiederverwenden Threads:

Thread-1: Request A → ThreadLocal.set(100) → Request Done
Thread-1: Request B → ThreadLocal.get() → NOCH 100! (FALSCH!)

Ohne remove() bleibt der alte Wert im Thread → Memory Leak!

Best Practice: IMMER im finally-Block oder am Ende von requestDestroyed() aufräumen!


Frage 5: Kann ich Listener dynamisch zur Laufzeit registrieren?

Ja, aber nur in ServletContextListener!

@WebListener
public class DynamicListenerRegistration implements ServletContextListener {
    
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        ServletContext context = sce.getServletContext();
        
        // Dynamisch Listener registrieren
        context.addListener(new MyCustomListener());
        
        // Oder per Klasse
        context.addListener(AnotherListener.class);
    }
}

Limitation: Nur während contextInitialized() möglich!

Use-Case: Plugin-Systeme, conditional Listener basierend auf Config


Frage 6: Was passiert bei Exception in einem Listener?

ServletContextListener:

  • Exception in contextInitialized() → Application startet NICHT!
  • Exception in contextDestroyed() → Wird geloggt, andere Listener laufen weiter

Andere Listener:

  • Exception wird geloggt
  • Request/Session-Verarbeitung läuft weiter
  • Andere Listener werden trotzdem aufgerufen

Best Practice: IMMER try-catch in Listenern!

@Override
public void sessionCreated(HttpSessionEvent se) {
    try {
        // Listener-Logic
    } catch (Exception e) {
        LOGGER.severe("Error in sessionCreated: " + e.getMessage());
        // Don't rethrow!
    }
}

Frage 7: Bernd sagte mal, „Listener sind old-school, moderne Apps nutzen Spring ApplicationListener“. Hat er recht?

Lowkey ja, aber mit Context! 😄

Bernd’s Perspektive:

Er arbeitet viel mit Spring Boot:

  • Spring hat sein eigenes Event-System
  • ApplicationListener, @EventListener
  • Mehr Features (conditional, async, ordering)
  • „The vibes“ von modernem Spring

In Spring Boot-Apps nutzt man tatsächlich eher Spring Events als Servlet Listeners.

Aber:

  1. Servlet Listeners sind der Standard in Jakarta EE
  2. Spring Boot nutzt sie intern (z.B. ContextLoaderListener)
  3. Für pure Servlet/JSP Apps sind sie essentiell
  4. Legacy-Code den du maintainen musst
  5. Understanding matters – auch wenn du Spring nutzt

Die Realität:

  • Spring Boot-Projekt? → Nutze Spring Events
  • Jakarta EE-Projekt? → Nutze Servlet Listeners
  • Beide verstehen? → Pro-Move! 🎯

Bottom Line:

Lern beides! Servlet Listeners für Jakarta EE, Spring Events für Spring Boot. It’s not either/or! Real Talk: Die Konzepte sind ähnlich, das Wissen ist transferierbar.


📚 Quiz-Lösungen

Hier sind die Antworten zum Quiz von oben:


Frage 1: Was ist der Unterschied zwischen Filter und Listener?

Antwort:

Filter:

  • Verarbeiten Requests und Responses
  • Werden pro Request aufgerufen
  • Sind Teil der Filter-Chain
  • Können Request/Response modifizieren
  • Können Chain stoppen (return ohne chain.doFilter())

Use-Cases:

  • Encoding setzen
  • Authentication prüfen
  • Logging
  • Performance-Messung

Listener:

  • Reagieren auf Lifecycle-Events
  • Werden bei Events aufgerufen (nicht pro Request!)
  • Sind NICHT Teil einer Chain
  • Können nur beobachten, nicht verhindern

Use-Cases:

  • Application-Initialisierung
  • Session-Tracking
  • Resource-Cleanup
  • Monitoring

Merksatz:

  • Filter = Request-Processing
  • Listener = Event-Handling

Frage 2: Welcher Listener eignet sich, um beim Application-Start eine Datenbank-Verbindung zu initialisieren?

Antwort:

ServletContextListener!

Dieser Listener reagiert auf Application Start/Stop:

@WebListener
public class DatabaseInitListener implements ServletContextListener {
    
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        // Connection Pool initialisieren
        DataSource ds = createConnectionPool();
        sce.getServletContext().setAttribute("dataSource", ds);
    }
    
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        // Connection Pool schließen
        DataSource ds = (DataSource) sce.getServletContext()
                                         .getAttribute("dataSource");
        ds.close();
    }
}

Warum ServletContextListener?

  • contextInitialized() wird einmal beim Application-Start aufgerufen
  • contextDestroyed() wird einmal beim Application-Stop aufgerufen
  • Perfect für Ressourcen mit Application-Lifetime

Alternative (falsch!):

❌ Nicht im Servlet-Constructor (zu früh, kein ServletContext)
❌ Nicht in Filter.init() (Filter für andere Zwecke)
❌ Nicht bei erstem Request (Race Conditions!)


Frage 3: Warum muss man AtomicInteger für Session-Counter verwenden?

Antwort:

Wegen Thread-Safety!

Problem mit normalem int:

private static int count = 0;  // ❌ NICHT thread-safe!

public void sessionCreated(HttpSessionEvent se) {
    count++;  // Race Condition!
}

Was passiert:

Thread 1: read count=10 → calculate 11 → write 11
Thread 2: read count=10 → calculate 11 → write 11
Result: count=11 (sollte 12 sein!)

Mit AtomicInteger:

private static final AtomicInteger count = new AtomicInteger(0);

public void sessionCreated(HttpSessionEvent se) {
    count.incrementAndGet();  // Atomic Operation!
}

Was passiert:

Thread 1: atomicIncrement(10) → 11
Thread 2: atomicIncrement(11) → 12
Result: count=12 (korrekt!)

Warum AtomicInteger statt synchronized?

  • ✅ Performanter (Lock-free)
  • ✅ Einfacher zu nutzen
  • ✅ Kein Deadlock-Risk

Best Practice:

Für einfache Counter: AtomicInteger
Für komplexe Operations: synchronized


Frage 4: Was ist der Unterschied zwischen HttpSessionAttributeListener und HttpSessionBindingListener?

Antwort:

HttpSessionAttributeListener:

  • Ist ein Listener (separate Klasse)
  • Überwacht ALLE Session-Attribute
  • Mit @WebListener registriert
@WebListener
public class AllAttributesListener implements HttpSessionAttributeListener {
    public void attributeAdded(HttpSessionBindingEvent event) {
        // Wird für JEDES Attribut aufgerufen
        String name = event.getName();
        Object value = event.getValue();
    }
}

HttpSessionBindingListener:

  • Ist ein Interface auf dem Attribut-Objekt selbst
  • Überwacht nur DIESES Objekt
  • Keine separate Registrierung nötig
public class UserSession implements HttpSessionBindingListener {
    public void valueBound(HttpSessionBindingEvent event) {
        // Wird nur aufgerufen wenn DIESES UserSession-Objekt
        // zur Session hinzugefügt wird
    }
}

Use-Cases:

HttpSessionAttributeListener:

  • Generisches Monitoring
  • Auditing aller Attribute-Changes
  • Security-Logging

HttpSessionBindingListener:

  • Object-spezifische Logic
  • Resource-Management im Objekt selbst
  • Tight coupling zwischen Objekt und Tracking

Merksatz:

  • HttpSessionAttributeListener = Global Monitoring
  • HttpSessionBindingListener = Object-Level Tracking

Frage 5: Wann wird ServletRequestListener.requestDestroyed() aufgerufen?

Antwort:

Nach Abschluss der kompletten Request-Verarbeitung!

Der Request-Lifecycle:

1. Browser sendet Request
2. Container empfängt Request
3. requestInitialized() wird aufgerufen ← START
4. Filter-Chain startet
5. Servlet verarbeitet Request
6. Filter-Chain endet
7. Response wird an Browser gesendet
8. requestDestroyed() wird aufgerufen ← END

Wichtig zu verstehen:

requestDestroyed() wird aufgerufen nachdem:

  • ✅ Servlet fertig ist
  • ✅ Alle Filter durchlaufen sind
  • ✅ Response an Browser gesendet wurde

Auch bei Exceptions!

public void requestDestroyed(ServletRequestEvent sre) {
    // Wird IMMER aufgerufen, selbst wenn Exception!
    // Perfect für Cleanup & Logging
}

Use-Cases:

  • Performance-Messung (End-Zeit)
  • Resource-Cleanup
  • Logging
  • Request-Counter

Timing:

requestInitialized()
    ↓ +45ms
Servlet-Processing
    ↓ +12ms
requestDestroyed()

Total Duration = 57ms (von initialized bis destroyed)


🔧 Troubleshooting

Problem 1: „Listener wird nicht aufgerufen“

Mögliche Ursachen:

  1. @WebListener fehlt // ❌ FALSCH: public class MyListener implements ServletContextListener { // ✅ RICHTIG: @WebListener public class MyListener implements ServletContextListener {
  2. Interface nicht implementiert @WebListener public class MyListener { // ❌ Kein Interface!
  3. web.xml überschreibt Annotation
    • Prüfe ob metadata-complete="true" in web.xml
    • Falls ja: Annotations werden ignoriert!

Debug:

@Override
public void contextInitialized(ServletContextEvent sce) {
    System.out.println("========================================");
    System.out.println("LISTENER STARTED!");
    System.out.println("========================================");
}

Restart Server und check Console!


Problem 2: „ConcurrentModificationException bei Session-Counter“

Ursache: Nicht thread-safe!

// ❌ FALSCH:
private static int count = 0;
count++;

// ✅ RICHTIG:
private static final AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();

Problem 3: „Memory Leak bei ThreadLocal“

Ursache: ThreadLocal nicht cleanup!

// ❌ FALSCH:
public void requestDestroyed(...) {
    long duration = System.currentTimeMillis() - startTime.get();
    // Fehlt: startTime.remove();
}

// ✅ RICHTIG:
public void requestDestroyed(...) {
    long duration = System.currentTimeMillis() - startTime.get();
    startTime.remove();  // Cleanup!
}

Problem 4: „NullPointerException bei ServletContext-Attribut“

Ursache: Attribut wurde noch nicht gesetzt.

// ❌ FALSCH:
Integer count = (Integer) context.getAttribute("sessionCount");
count++;  // NPE wenn noch nicht gesetzt!

// ✅ RICHTIG:
Integer count = (Integer) context.getAttribute("sessionCount");
count = (count == null) ? 1 : count + 1;
context.setAttribute("sessionCount", count);

Problem 5: „Listener-Reihenfolge stimmt nicht“

Lösung: Nutze web.xml statt @WebListener!

<listener>
    <listener-class>com.javafleet.FirstListener</listener-class>
</listener>
<listener>
    <listener-class>com.javafleet.SecondListener</listener-class>
</listener>

Reihenfolge in web.xml = Reihenfolge der Aufrufe!


📚 Resources & Links

Offizielle Dokumentation:

Tutorials:

Best Practices:

GitHub:


💬 Feedback

Wie war Tag 2 für dich?

Was können wir verbessern? Dein Feedback hilft uns!


🎉 Tag 2 geschafft!

Wow, das war intensiv! 💪

Du hast heute richtig was gelernt:

  • ✅ 8 Listener-Typen verstanden
  • ✅ Application Lifecycle gemeistert
  • ✅ Session-Tracking implementiert
  • ✅ Request-Monitoring aufgebaut
  • ✅ Attribute-Listener genutzt
  • ✅ Thread-Safety mit AtomicInteger & ThreadLocal

Real talk: Listener sind die heimlichen Helden jeder Enterprise-App. Ohne ServletContextListener wäre Application-Initialisierung ein Chaos. Ohne HttpSessionListener kein Session-Tracking. Du hast heute die dritte Spezialklassenart in Jakarta EE gemeistert!

Slay! Du bist jetzt ein Listener-Pro! 🎯


🚀 Wie geht’s weiter?

Morgen (Tag 3): Authentifizierung gegenüber einer Datenbank

Was dich erwartet:

  • User-Verwaltung in Datenbank aufbauen
  • Login-System implementieren
  • Password-Hashing mit BCrypt
  • Session-basierte Authentication
  • Remember-Me-Funktionalität
  • Das wird dein Game-Changer für sichere Webanwendungen! 🔥

Besonderheit: Morgen kombinierst du Filter + Listener + Session-Management für ein Complete-Authentication-System!


Brauchst du eine Pause?

Gönn sie dir! Listener sind komplex, besonders Thread-Safety. Lass das heute sacken.

Tipp für heute Abend:

Experimentiere mit der Mini-Challenge! Bau das Monitoring-System. Teste verschiedene Szenarien:

  • Was passiert bei 100 parallelen Sessions?
  • Wie lange laufen deine Requests?
  • Siehst du alle Events in den Logs?

Learning by doing! 🔧


Bis morgen! 👋

Elyndra

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


Java Web Aufbau – Tag 2 von 10
Teil der Java Fleet Learning-Serie
© 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.