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

🗺️ Deine Position im Kurs
| Tag | Thema | Status |
|---|---|---|
| 1 | Filter im Webcontainer | ✅ Abgeschlossen |
| → 2 | Listener im Webcontainer | 👉 DU BIST HIER! |
| 3 | Authentifizierung gegenüber einer Datenbank | 🔒 Noch nicht freigeschaltet |
| 4 | Container Base Security & Jakarta EE Security | 🔒 Noch nicht freigeschaltet |
| 5 | Custom Tags & Tag Handler | 🔒 Noch nicht freigeschaltet |
| 6 | Custom Tag Handler mit BodyTagSupport | 🔒 Noch nicht freigeschaltet |
| 7 | JPA vs JDBC – Konfiguration & Persistence Provider | 🔒 Noch nicht freigeschaltet |
| 8 | Relationen (1) in der JPA | 🔒 Noch nicht freigeschaltet |
| 9 | Relationen (2) in der JPA | 🔒 Noch nicht freigeschaltet |
| 10 | JSF Ü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:
- Application Lifecycle – Application startet/stoppt
- Session Lifecycle – Sessions erstellt/zerstört
- Request Lifecycle – Requests starten/enden
- 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:
| Listener | Event | Use-Case |
|---|---|---|
| ServletContextListener | Application Start/Stop | DB Pool initialisieren, Config laden |
| ServletContextAttributeListener | Application-Attribute ändern | Monitoring, Debugging |
| HttpSessionListener | Session erstellt/zerstört | Aktive User zählen |
| HttpSessionAttributeListener | Session-Attribute ändern | Session-Monitoring |
| HttpSessionActivationListener | Session Migration | Clustering, Passivation |
| HttpSessionBindingListener | Objekt zu Session hinzugefügt | Object-Level Tracking |
| ServletRequestListener | Request Start/Ende | Performance-Monitoring |
| ServletRequestAttributeListener | Request-Attribute ändern | Request-Debugging |
Die wichtigsten 4 (heute Fokus):
- ServletContextListener
- HttpSessionListener
- ServletRequestListener
- ServletContextAttributeListener
Listener vs. Servlets vs. Filter
Die drei Spezialklassenarten im Vergleich:
| Aspekt | Servlet | Filter | Listener |
|---|---|---|---|
| Zweck | Business-Logic | Request/Response Processing | Event-Handling |
| Aufruf | Pro Request | Pro Request (vor/nach Servlet) | Bei Lifecycle-Events |
| Instanzen | 1 pro Klasse | 1 pro Klasse | 1 pro Klasse |
| Thread-Safe? | Ja | Ja | Ja |
| Chain? | Nein | Ja (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:
- Server 1 speichert Session auf Disk (Passivate)
- Session wird zu Server 2 transferiert
- 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:
- Application-Startup:
- Logge Start-Zeit
- Initialisiere Config
- Setze „appVersion“ im ServletContext
- Session-Tracking:
- Zähle aktive Sessions
- Track Peak-Sessions
- Berechne Session-Duration
- Request-Performance:
- Messe Request-Duration
- Kategorisiere: Fast (<100ms), OK (<500ms), Slow (>500ms)
- Zähle Requests pro Kategorie
- Admin-Dashboard:
- Servlet unter
/admin/monitor - Zeige alle Metrics als HTML
- Servlet unter
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:
| Scope | Lebensdauer | Sichtbarkeit |
|---|---|---|
| ServletContext | Application-Lifetime | ALLE User |
| HttpSession | Session-Lifetime | EIN User |
| ServletRequest | Request-Lifetime | EIN 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:
- Servlet Listeners sind der Standard in Jakarta EE
- Spring Boot nutzt sie intern (z.B. ContextLoaderListener)
- Für pure Servlet/JSP Apps sind sie essentiell
- Legacy-Code den du maintainen musst
- 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 (
returnohnechain.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 aufgerufencontextDestroyed()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
@WebListenerregistriert
@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:
- @WebListener fehlt
// ❌ FALSCH: public class MyListener implements ServletContextListener { // ✅ RICHTIG: @WebListener public class MyListener implements ServletContextListener { - Interface nicht implementiert
@WebListener public class MyListener { // ❌ Kein Interface! - web.xml überschreibt Annotation
- Prüfe ob
metadata-complete="true"in web.xml - Falls ja: Annotations werden ignoriert!
- Prüfe ob
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?
- 📧 elyndra@java-developer.online
- 💬 Kommentare unten im Blog
- 📱 Folge uns: @java_developer_online
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

