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


🗺️ Deine Position im Kurs

TagThemaStatus
1Java EE Überblick & HTTP ✅ Abgeschlossen
2HTTP-Protokoll Vertiefung & Zustandslosigkeit✅ Abgeschlossen
→ 3Servlets & Servlet API👉 DU BIST HIER!
4Deployment Descriptor & MVC vs Model 2🔒 Noch nicht freigeschaltet
5JSP & Expression Languages (Teil 1)🔒 Noch nicht freigeschaltet
6Java Beans, Actions, Scopes & Direktiven🔒 Noch nicht freigeschaltet
7Include-Action vs Include-Direktive🔒 Noch nicht freigeschaltet
8JSTL – Java Standard Tag Libraries🔒 Noch nicht freigeschaltet
9Java Web und Datenbanken – Datasource🔒 Noch nicht freigeschaltet
10Connection Pools & JDBC in Web-Umgebungen🔒 Noch nicht freigeschaltet

Modul: Java Web Basic
Gesamt-Dauer: 10 Arbeitstage (je 8 Stunden)
Dauer heute: 8 Stunden
Dein Ziel: Servlets von Grund auf verstehen und professionell einsetzen


📋 Voraussetzungen

Was du schon können solltest:

  • ✅ HTTP-Protokoll verstehen (GET, POST, Status-Codes)
  • ✅ Request/Response-Zyklus kennen
  • ✅ Sessions und Cookies verstehen
  • ✅ Maven-Projekt aufsetzen können
  • ✅ Payara Server nutzen können

Was du heute lernst:

  • ✅ Servlet-Lifecycle im Detail
  • ✅ Request und Response Objekte nutzen
  • ✅ Thread-Safety in Servlets
  • ✅ Filter-Chain verstehen
  • ✅ Listeners einsetzen
  • ✅ Production-Ready Servlets schreiben

⚡ 30-Sekunden-Überblick

Was sind Servlets? Servlets sind Java-Klassen, die HTTP-Requests verarbeiten und Responses erzeugen. Sie sind die erste Spezialklassenart in Java EE, die du lernst – und die Grundlage ALLER Java-Webanwendungen. Auch wenn du später Spring Boot nutzt, laufen unter der Haube Servlets.

Was lernst du heute? Du verstehst den kompletten Servlet-Lifecycle, arbeitest mit Request/Response-Objekten, lernst Thread-Safety kennen und baust production-ready Servlets.

Warum ist das wichtig? Ohne Servlets gibt es keine Java-Webanwendung. Punkt. Selbst moderne Frameworks basieren auf dieser Technologie. Im Java Web Basic Kurs dreht sich fast alles um Servlets – weitere Spezialklassen wie Filter, WebListener und TagHandler lernst du später im Aufbau-Kurs kennen.


👋 Willkommen zu Tag 3!

Hi! 👋

Elyndra hier. Heute wird es praktisch und technisch zugleich!

Gestern hast du HTTP verstanden – heute setzt du das um. Servlets sind das Herzstück jeder Java-Webanwendung. Auch wenn du später mit JSPs oder Spring Boot arbeitest – unter der Haube laufen immer Servlets.

Servlets – Deine erste Spezialklassenart in Java EE: Servlets sind die erste Spezialklassenart, die du in Java EE kennenlernst. Im Java Web Basic Kurs dreht sich fast alles um Servlets – sie sind dein Einstieg in die Java-Web-Welt.

Später im Java Web Aufbau Kurs lernst du weitere Spezialklassen kennen:

  • Filter (Request-Preprocessing)
  • WebListener (Event-Handling)
  • TagHandler (Custom JSP-Tags)
  • Und noch mehr…

Aber heute? Heute meisterst du Servlets von Grund auf!

Warum Servlets? Als Nova mich neulich fragte: „Kann ich nicht einfach Spring Boot lernen und Servlets überspringen?“ – musste ich lachen. Spring Boot IST Servlets! Der DispatcherServlet in Spring? Ein Servlet. Jeder Controller? Wird zu einem Servlet gemappt.

Du kannst kein Haus bauen, ohne das Fundament zu verstehen.

Los geht’s! 🔧


🟢 GRUNDLAGEN

Was ist ein Servlet genau?

Definition: Ein Servlet ist eine Java-Klasse, die:

  1. Von HttpServlet erbt
  2. HTTP-Requests empfängt
  3. HTTP-Responses zurückgibt
  4. Im Webcontainer läuft

Einfachstes Beispiel:

package com.javadeveloper.hellopayara;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        response.setContentType("text/html;charset=UTF-8");
        
        PrintWriter out = response.getWriter();
        out.println("<!DOCTYPE html>");
        out.println("<html>");
        out.println("<head><title>Hello Servlet</title></head>");
        out.println("<body>");
        out.println("<h1>Hello from Servlet!</h1>");
        out.println("</body>");
        out.println("</html>");
    }
}

Was passiert hier?

  1. @WebServlet("/hello") – Registriert das Servlet unter der URL /hello
  2. extends HttpServlet – Macht deine Klasse zu einem Servlet
  3. doGet() – Wird bei GET-Request aufgerufen
  4. response.getWriter() – Gibt HTML zurück

URL zum Testen:

http://localhost:8080/HelloPayara/hello

Der Servlet-Lifecycle

3 Phasen – super wichtig zu verstehen:

┌─────────────────────────────────────────────────┐
│  Phase 1: INIT (einmalig beim Start)            │
│  ├─ Constructor wird aufgerufen                 │
│  └─ init() wird aufgerufen                      │
├─────────────────────────────────────────────────┤
│  Phase 2: SERVICE (bei jedem Request)           │
│  ├─ service() entscheidet: GET oder POST?       │
│  ├─ doGet() oder doPost() wird aufgerufen       │
│  └─ Multithreading! Mehrere Requests parallel   │
├─────────────────────────────────────────────────┤
│  Phase 3: DESTROY (beim Herunterfahren)         │
│  └─ destroy() wird aufgerufen                   │
└─────────────────────────────────────────────────┘

Lifecycle-Demo:

@WebServlet("/lifecycle")
public class LifecycleServlet extends HttpServlet {
    
    // Phase 1: Constructor (nur einmal)
    public LifecycleServlet() {
        System.out.println("1. Constructor called");
    }
    
    // Phase 1: Init (nur einmal)
    @Override
    public void init() throws ServletException {
        System.out.println("2. init() called");
        // Hier: DB-Connection Pool initialisieren
        // Hier: Config laden
    }
    
    // Phase 2: Service (bei jedem Request)
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        System.out.println("3. doGet() called - Thread: " + 
                          Thread.currentThread().getName());
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.println("<h1>Request processed by: " + 
                   Thread.currentThread().getName() + "</h1>");
    }
    
    // Phase 3: Destroy (nur einmal)
    @Override
    public void destroy() {
        System.out.println("4. destroy() called");
        // Hier: DB-Connections schließen
        // Hier: Resources freigeben
    }
}

Was du sehen wirst:

1. Constructor called
2. init() called
3. doGet() called - Thread: http-thread-pool-1
3. doGet() called - Thread: http-thread-pool-2
3. doGet() called - Thread: http-thread-pool-3
... (bei weiteren Requests)
4. destroy() called (beim Server-Shutdown)

Wichtig:

  • Constructor + init() = nur 1x
  • doGet()/doPost() = bei jedem Request (parallel!)
  • destroy() = nur 1x beim Shutdown

Request-Objekt verstehen

Was enthält ein HttpServletRequest?

@WebServlet("/request-info")
public class RequestInfoServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        // 1. URL-Parameter lesen
        String name = request.getParameter("name");
        String age = request.getParameter("age");
        
        // 2. HTTP-Headers lesen
        String userAgent = request.getHeader("User-Agent");
        String acceptLanguage = request.getHeader("Accept-Language");
        
        // 3. Request-Informationen
        String method = request.getMethod();        // GET, POST, etc.
        String uri = request.getRequestURI();       // /HelloPayara/request-info
        String protocol = request.getProtocol();    // HTTP/1.1
        
        // 4. Client-Informationen
        String remoteAddr = request.getRemoteAddr();  // IP-Adresse
        String remoteHost = request.getRemoteHost();  // Hostname
        
        // 5. Session
        HttpSession session = request.getSession();
        
        // Antwort erstellen
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        
        out.println("<!DOCTYPE html>");
        out.println("<html><body>");
        out.println("<h1>Request Information</h1>");
        out.println("<p><strong>Name:</strong> " + name + "</p>");
        out.println("<p><strong>Age:</strong> " + age + "</p>");
        out.println("<p><strong>Method:</strong> " + method + "</p>");
        out.println("<p><strong>URI:</strong> " + uri + "</p>");
        out.println("<p><strong>User-Agent:</strong> " + userAgent + "</p>");
        out.println("<p><strong>Remote Address:</strong> " + remoteAddr + "</p>");
        out.println("</body></html>");
    }
}

Teste mit dieser URL:

http://localhost:8080/HelloPayara/request-info?name=Anna&age=25

Response-Objekt verstehen

Was kannst du mit HttpServletResponse machen?

@WebServlet("/response-demo")
public class ResponseDemoServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        // 1. Content-Type setzen (WICHTIG!)
        response.setContentType("text/html;charset=UTF-8");
        
        // 2. Status-Code setzen (optional)
        response.setStatus(HttpServletResponse.SC_OK); // 200
        
        // 3. Headers setzen
        response.setHeader("X-Custom-Header", "My Value");
        response.setHeader("Cache-Control", "no-cache, no-store");
        
        // 4. Cookie setzen
        Cookie cookie = new Cookie("username", "anna");
        cookie.setMaxAge(3600); // 1 Stunde
        response.addCookie(cookie);
        
        // 5. HTML-Output schreiben
        PrintWriter out = response.getWriter();
        out.println("<!DOCTYPE html>");
        out.println("<html><body>");
        out.println("<h1>Response Demo</h1>");
        out.println("<p>Check your browser's developer tools!</p>");
        out.println("</body></html>");
    }
}

Content-Types – Die wichtigsten:

// HTML
response.setContentType("text/html;charset=UTF-8");

// JSON
response.setContentType("application/json;charset=UTF-8");

// Plain Text
response.setContentType("text/plain;charset=UTF-8");

// XML
response.setContentType("application/xml;charset=UTF-8");

// Binary (Download)
response.setContentType("application/octet-stream");

Redirects und Forwards

2 Arten, den User weiterzuleiten:

1. Redirect (Client-Side)

@WebServlet("/redirect-demo")
public class RedirectServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        // Client macht neuen Request an andere URL
        response.sendRedirect("https://www.google.com");
        
        // Oder intern:
        // response.sendRedirect("/HelloPayara/hello");
    }
}

Was passiert?

  1. Server sendet Status-Code 302 (Found)
  2. Header „Location: https://www.google.com
  3. Browser macht neuen Request an neue URL

Wann nutzen?

  • Nach erfolgreicher POST-Operation (PRG-Pattern!)
  • Weiterleitung zu externer URL
  • User soll neue URL sehen

2. Forward (Server-Side)

@WebServlet("/forward-demo")
public class ForwardServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        // Daten vorbereiten
        request.setAttribute("message", "Hello from ForwardServlet!");
        
        // An anderes Servlet weiterleiten (Server-intern)
        RequestDispatcher dispatcher = 
            request.getRequestDispatcher("/hello");
        dispatcher.forward(request, response);
    }
}

Was passiert?

  1. Request wird server-intern weitergeleitet
  2. Browser merkt nichts davon
  3. URL im Browser bleibt gleich

Wann nutzen?

  • MVC-Pattern: Controller → View
  • Request-Attribute weitergeben
  • Server-interne Verarbeitung

🟡 PROFESSIONALS

Thread-Safety – Das MUSS du verstehen!

KRITISCH: Ein Servlet ist EINE Instanz für ALLE Requests!

Der Webcontainer erstellt beim Start genau eine Instanz deines Servlets. Alle eingehenden Requests werden von verschiedenen Threads verarbeitet, die aber alle auf dieselbe Servlet-Instanz zugreifen.

// ❌ FALSCH - NICHT THREAD-SAFE!
@WebServlet("/counter-bad")
public class CounterBadServlet extends HttpServlet {
    
    private int counter = 0;  // ❌ Shared State!
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        counter++;  // ❌ Race Condition!
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.println("<h1>Counter: " + counter + "</h1>");
    }
}

Was ist das Problem?

Zeit T1: Thread 1 liest counter = 0
Zeit T2: Thread 2 liest counter = 0
Zeit T3: Thread 1 schreibt counter = 1
Zeit T4: Thread 2 schreibt counter = 1  (sollte 2 sein!)
  • 1000 User = 1000 verschiedene Threads
  • Alle Threads greifen auf DIESELBE Servlet-Instanz zu
  • Alle teilen sich die Instance-Variable counter
  • Race Conditions sind garantiert!

✅ RICHTIG – Thread-Safe Variante 1: AtomicInteger

@WebServlet("/counter-atomic")
public class CounterAtomicServlet extends HttpServlet {
    
    private final AtomicInteger counter = new AtomicInteger(0);
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        int currentValue = counter.incrementAndGet();
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.println("<h1>Counter: " + currentValue + "</h1>");
    }
}

✅ RICHTIG – Thread-Safe Variante 2: Lokale Variablen

@WebServlet("/counter-local")
public class CounterLocalServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        // Lokale Variable = jeder Thread hat seine eigene!
        int counter = 42;
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.println("<h1>Counter: " + counter + "</h1>");
    }
}

✅ RICHTIG – Thread-Safe Variante 3: Session

@WebServlet("/counter-session")
public class CounterSessionServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        HttpSession session = request.getSession();
        
        // Pro User ein eigener Counter
        Integer counter = (Integer) session.getAttribute("counter");
        if (counter == null) {
            counter = 0;
        }
        counter++;
        session.setAttribute("counter", counter);
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.println("<h1>Your Counter: " + counter + "</h1>");
    }
}

Regel:

  • ✅ Lokale Variablen = thread-safe
  • ✅ Request-Attribute = thread-safe (pro Request)
  • ✅ Session-Attribute = thread-safe (pro User)
  • ✅ AtomicInteger/AtomicLong = thread-safe
  • ❌ Instance-Variablen = NICHT thread-safe!

POST-Request mit Formular

HTML-Formular:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>User Registration</title>
</head>
<body>
    <h1>Register</h1>
    <form action="/HelloPayara/register" method="POST">
        <label>Username:</label>
        <input type="text" name="username" required><br><br>
        
        <label>Email:</label>
        <input type="email" name="email" required><br><br>
        
        <label>Age:</label>
        <input type="number" name="age" required><br><br>
        
        <button type="submit">Register</button>
    </form>
</body>
</html>

Servlet:

@WebServlet("/register")
public class RegisterServlet extends HttpServlet {
    
    @Override
    protected void doPost(HttpServletRequest request, 
                         HttpServletResponse response) 
                         throws ServletException, IOException {
        
        // 1. Form-Daten lesen
        String username = request.getParameter("username");
        String email = request.getParameter("email");
        String ageStr = request.getParameter("age");
        
        // 2. Validierung
        if (username == null || username.trim().isEmpty()) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, 
                              "Username is required");
            return;
        }
        
        int age = 0;
        try {
            age = Integer.parseInt(ageStr);
        } catch (NumberFormatException e) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, 
                              "Invalid age");
            return;
        }
        
        // 3. Business-Logic (z.B. in DB speichern)
        // userService.saveUser(username, email, age);
        
        // 4. PRG-Pattern: POST → Redirect → GET
        HttpSession session = request.getSession();
        session.setAttribute("message", 
                            "Registration successful! Welcome, " + username);
        
        response.sendRedirect("/HelloPayara/success");
    }
}

Success-Servlet:

@WebServlet("/success")
public class SuccessServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        HttpSession session = request.getSession();
        String message = (String) session.getAttribute("message");
        session.removeAttribute("message");  // Flash-Message
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        
        out.println("<!DOCTYPE html>");
        out.println("<html><body>");
        out.println("<h1>✅ " + message + "</h1>");
        out.println("<a href='/HelloPayara'>Back to home</a>");
        out.println("</body></html>");
    }
}

PRG-Pattern (Post-Redirect-Get):

POST /register
    ↓
  [Daten verarbeiten]
    ↓
  [In DB speichern]
    ↓
Redirect → GET /success
    ↓
  [Erfolg anzeigen]

Warum PRG?

  • ✅ Kein doppeltes Submit beim F5 (Reload)
  • ✅ Saubere URL nach POST
  • ✅ Bessere User-Experience

🔵 BONUS

Async Servlets – Nicht-blockierend arbeiten

Normal (blockierend):

Client → Request → [Servlet wartet] → Response
                        ↓
                   [Thread blockiert]

Async (nicht-blockierend):

Client → Request → [Servlet gibt Thread frei] → Async-Processing
                            ↓                           ↓
                   [Thread verfügbar]          [Complete später]
                                                        ↓
                                                   Response

Beispiel:

@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        // 1. Async-Context starten
        AsyncContext asyncContext = request.startAsync();
        asyncContext.setTimeout(5000);  // 5 Sekunden Timeout
        
        // 2. Langwierige Operation in separatem Thread
        asyncContext.start(() -> {
            try {
                // Simuliere lange Datenbank-Abfrage
                Thread.sleep(3000);
                
                // Response schreiben
                ServletResponse resp = asyncContext.getResponse();
                resp.setContentType("text/html;charset=UTF-8");
                PrintWriter out = resp.getWriter();
                out.println("<h1>Async Response after 3 seconds</h1>");
                
                // Async-Context abschließen
                asyncContext.complete();
                
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        
        // doGet() endet sofort - Thread ist frei!
    }
}

Wann nutzen?

  • Lange DB-Queries
  • API-Calls zu externen Services
  • File-Processing
  • Lange Berechnungen

Vorteil:

  • Threads bleiben nicht blockiert
  • Bessere Skalierung
  • Mehr gleichzeitige Requests möglich

Error Handling – Fehler elegant behandeln

Globaler Error-Handler:

@WebServlet("/error")
public class ErrorHandlerServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        // Error-Informationen aus Request
        Integer statusCode = 
            (Integer) request.getAttribute("jakarta.servlet.error.status_code");
        String message = 
            (String) request.getAttribute("jakarta.servlet.error.message");
        Throwable throwable = 
            (Throwable) request.getAttribute("jakarta.servlet.error.exception");
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        
        out.println("<!DOCTYPE html>");
        out.println("<html><body>");
        out.println("<h1>⚠️ Error " + statusCode + "</h1>");
        out.println("<p>" + message + "</p>");
        
        if (throwable != null) {
            out.println("<h2>Stack Trace:</h2>");
            out.println("<pre>");
            throwable.printStackTrace(out);
            out.println("</pre>");
        }
        
        out.println("</body></html>");
    }
}

In web.xml registrieren:

<error-page>
    <error-code>404</error-code>
    <location>/error</location>
</error-page>

<error-page>
    <error-code>500</error-code>
    <location>/error</location>
</error-page>

JSON-Response zurückgeben

Moderne Web-APIs:

@WebServlet("/api/user")
public class UserApiServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        // JSON-Response
        response.setContentType("application/json;charset=UTF-8");
        
        PrintWriter out = response.getWriter();
        
        // Manuell JSON erstellen (für einfache Fälle)
        out.println("{");
        out.println("  \"id\": 1,");
        out.println("  \"username\": \"anna\",");
        out.println("  \"email\": \"anna@example.com\",");
        out.println("  \"age\": 25");
        out.println("}");
        
        // Production: Jackson oder Gson verwenden!
        // ObjectMapper mapper = new ObjectMapper();
        // User user = new User(1, "anna", "anna@example.com", 25);
        // mapper.writeValue(out, user);
    }
}

Mit Gson (Maven-Dependency):

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.10.1</version>
</dependency>
@WebServlet("/api/users")
public class UsersApiServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        // User-Liste erstellen
        List<User> users = Arrays.asList(
            new User(1, "anna", "anna@example.com", 25),
            new User(2, "bob", "bob@example.com", 30),
            new User(3, "charlie", "charlie@example.com", 35)
        );
        
        // JSON-Response mit Gson
        response.setContentType("application/json;charset=UTF-8");
        Gson gson = new Gson();
        String json = gson.toJson(users);
        
        PrintWriter out = response.getWriter();
        out.print(json);
    }
}

class User {
    private int id;
    private String username;
    private String email;
    private int age;
    
    public User(int id, String username, String email, int age) {
        this.id = id;
        this.username = username;
        this.email = email;
        this.age = age;
    }
    
    // Getters (für Gson)
    public int getId() { return id; }
    public String getUsername() { return username; }
    public String getEmail() { return email; }
    public int getAge() { return age; }
}

Response:

[
  {
    "id": 1,
    "username": "anna",
    "email": "anna@example.com",
    "age": 25
  },
  {
    "id": 2,
    "username": "bob",
    "email": "bob@example.com",
    "age": 30
  },
  {
    "id": 3,
    "username": "charlie",
    "email": "charlie@example.com",
    "age": 35
  }
]

💬 Real Talk: Thread-Safety ist kein Nice-to-Have

Java Fleet Büro, 15:30 Uhr. Nova sitzt frustriert vor ihrem Code. Elyndra kommt mit Kaffee vorbei.


Nova: „Ely, mein Counter-Servlet macht komische Sachen. Manchmal zeigt es 5, dann plötzlich 3, dann wieder 7. Total random!“

Elyndra: schaut auf den Code „Ah. Instance-Variable, oder?“

Nova: „Ja, private int counter = 0; – wo ist das Problem?“

Elyndra: setzt sich neben Nova „Das Problem heißt Race Condition. Du hast eine Servlet-Instanz für alle Requests. 100 User = 100 Threads greifen auf dieselbe Variable zu.“

Nova: „Aber… ich dachte, Java ist thread-safe?“

Elyndra: „Java gibt dir die Werkzeuge, aber du musst sie nutzen. int counter++ ist nicht atomar. Das sind drei Operationen: lesen, incrementieren, schreiben.“

Kat kommt aus ihrem Büro

Kat: „Ich hatte das gleiche Problem letzten Monat. Hab gedacht, ich teste mit nur einem User – alles lief perfekt. Dann ging die App live und es war Chaos.“

Nova: „Was ist die Lösung?“

Elyndra: „Drei Optionen: Erstens, AtomicInteger statt int. Zweitens, lokale Variablen statt Instance-Variablen. Drittens, Session-Attribute für User-spezifische Daten.“

Kat: „Ich nutze jetzt die Regel: Instance-Variablen in Servlets = nur final und immutable. Alles andere ist Session oder Request-Scope.“

Nova: „Real talk, das hätte man mir am Anfang sagen sollen!“

Elyndra: lacht „Deshalb lernst du’s jetzt. Thread-Safety ist kein Nice-to-Have – es ist kritisch für Production.“


✅ Checkpoint: Hast du es verstanden?

Zeit zu testen, was du gelernt hast!

Quiz:

Frage 1: Wie viele Instanzen eines Servlets erstellt der Webcontainer?

Frage 2: Welche Methode wird nur einmal beim Start aufgerufen?

Frage 3: Was ist der Unterschied zwischen sendRedirect() und forward()?

Frage 4: Warum sind Instance-Variablen in Servlets problematisch?

Frage 5: Was ist das PRG-Pattern und warum ist es wichtig?


Die Lösungen findest du weiter unten! 👇

Versuch erst selbst zu antworten – das hilft beim Lernen!


🎯 Mini-Challenge

Aufgabe:

Erstelle ein vollständiges Login-System mit:

  1. Login-Formular (HTML)
  2. LoginServlet (POST) – mit statischer Userliste zur Authentifizierung
  3. ProtectedServlet (nur für eingeloggte User – prüft Session selbst!)
  4. LogoutServlet
  5. DashboardServlet (zeigt Username aus Session)

Anforderungen:

  • Username + Password validieren gegen eine statische HashMap/Liste im Servlet
  • NICHT request.login() verwenden! Das lernen wir später
  • Session nutzen für User-Info
  • ProtectedServlet prüft selbst ob User eingeloggt (Session-Check!)
  • Redirect zu Login falls nicht eingeloggt
  • Logout löscht Session
  • Fehlerbehandlung bei falschen Credentials

Hinweise:

  1. Login-Formular sendet POST an /login
  2. Erstelle eine statische Map im LoginServlet:private static final Map<String, String> USERS = Map.of( "admin", "password", "user", "test123" );
  3. Bei Erfolg: Session-Attribut user setzen
  4. ProtectedServlet: Prüfe am Anfang ob session.getAttribute("user") existiert
  5. Falls nicht eingeloggt: response.sendRedirect("/HelloPayara/login")
  6. Logout macht session.invalidate()

Bonus:

  • Zähle fehlgeschlagene Login-Versuche in der Session
  • Zeige Fehlermeldung im Login-Formular
  • Nach 3 fehlgeschlagenen Versuchen: Account temporär sperren (z.B. für 5 Minuten)

Lösung:

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

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


Geschafft? 🎉

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


❓ Häufig gestellte Fragen

Frage 1: Kann ich mehrere Servlets unter derselben URL registrieren?

Nein. Jede URL kann nur zu einem Servlet gemappt werden. Wenn du mehrere @WebServlet("/test") hast, wird nur eines verwendet (welches ist undefiniert).

Lösung: Verwende verschiedene URLs oder implementiere ein zentrales Servlet mit Routing-Logic.


Frage 2: Warum funktionieren Umlaute nicht in meinem Servlet?

Du hast vergessen, das Character-Encoding zu setzen:

request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");

Setze das am Anfang von doGet() oder doPost() in deinem Servlet.

Besser: Globale Konfiguration in payara-web.xml

Erstelle die Datei src/main/webapp/WEB-INF/payara-web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE payara-web-app PUBLIC "-//Payara.fish//DTD Payara Server 4 Servlet 3.0//EN" 
    "https://docs.payara.fish/schemas/payara-web-app_4.dtd">
<payara-web-app error-url="">
    <parameter-encoding default-charset="UTF-8"/>
</payara-web-app>

Jetzt wird automatisch UTF-8 für alle Request-Parameter verwendet – du musst es nicht mehr in jedem Servlet setzen! 🎯

Im Java Web Aufbau Kurs lernst du Filter kennen, die das ebenfalls für alle Requests machen können.


Frage 3: Was ist der Unterschied zwischen request.getSession() und request.getSession(false)?

  • getSession() → Erstellt Session, falls keine existiert
  • getSession(false) → Gibt null zurück, falls keine Session existiert

Nutze getSession(false) in Login-Check-Logik, um nicht ungewollt Sessions zu erstellen!


Frage 4: Wie kann ich File-Uploads in Servlets verarbeiten?

Mit @MultipartConfig:

@WebServlet("/upload")
@MultipartConfig
public class UploadServlet extends HttpServlet {
    
    @Override
    protected void doPost(HttpServletRequest request, 
                         HttpServletResponse response) 
                         throws ServletException, IOException {
        
        Part filePart = request.getPart("file");
        String fileName = filePart.getSubmittedFileName();
        
        InputStream fileContent = filePart.getInputStream();
        // Speichern...
    }
}

HTML-Formular braucht enctype="multipart/form-data"!


Frage 5: Sollte ich HTML direkt in Servlets schreiben?

Nein! Für größere Projekte:

  • ✅ Servlet = Controller (Business-Logic)
  • ✅ JSP = View (Presentation)

Servlets sollten nur JSON-APIs oder kleine Responses erzeugen.

Ab Tag 5 lernst du JSPs kennen! 🚀


Frage 6: Wie debugge ich Servlets?

  1. Logging:
System.out.println("Debug: " + variable);
  1. IDE-Debugger:
  • Payara Server in Debug-Mode starten
  • Breakpoints setzen
  • Step-through im Code
  1. Browser DevTools:
  • F12 → Network-Tab
  • Requests/Responses inspizieren
  • Headers checken

Frage 7: Bernd meinte mal, Servlets wären „oldschool“ und ich sollte direkt Spring Boot lernen. Hat er recht?

Lowkey ja, aber auch nein. Real talk:

Spring Boot ist moderner und produktiver – keine Frage. Aber Spring Boot IST Servlets! Der DispatcherServlet ist das Herzstück von Spring MVC.

Wenn du Servlets verstehst:

  • ✅ Du verstehst, wie Spring Boot unter der Haube funktioniert
  • ✅ Du kannst Probleme besser debuggen
  • ✅ Du weißt, was Filter, Listeners und Request-Scopes sind
  • ✅ Du bist nicht nur „Framework-User“, sondern verstehst die Grundlagen

Für neue Projekte würde ich auch Spring Boot nehmen. Aber die Servlet-Basics zu kennen? Unverzichtbar für jeden Java-Web-Dev.

Bernd hat’s mal so gesagt: „Du kannst kein Auto tunen, wenn du nicht weißt, wie ein Motor funktioniert.“ Same here. 🔧


📚 Quiz-Lösungen

Hier sind die Antworten zum Quiz von oben:


Frage 1: Wie viele Instanzen eines Servlets erstellt der Webcontainer?

Antwort:

Genau eine Instanz pro Servlet-Klasse!

Das ist super wichtig zu verstehen:

  • Der Webcontainer erstellt beim Start eine Instanz
  • Diese Instanz wird von allen Threads/Requests geteilt
  • Deshalb sind Instance-Variablen gefährlich (Race Conditions!)

Beispiel:

@WebServlet("/counter")
public class CounterServlet extends HttpServlet {
    private int counter = 0;  // ❌ Shared State!
    // Alle 1000 User teilen sich diese Variable!
}

Frage 2: Welche Methode wird nur einmal beim Start aufgerufen?

Antwort:

init() – Die Init-Methode!

Der Lifecycle:

  1. Constructor (1x beim Laden der Klasse)
  2. init() (1x nach dem Constructor)
  3. doGet()/doPost() (bei jedem Request, parallel!)
  4. destroy() (1x beim Shutdown)

Nutze init() für:

  • DB-Connection Pool aufbauen
  • Config laden
  • Cache initialisieren

Nutze destroy() für:

  • Connections schließen
  • Resources freigeben

Frage 3: Was ist der Unterschied zwischen sendRedirect() und forward()?

Antwort:

sendRedirect() – Client-Side:

  • Browser macht neuen Request
  • URL im Browser ändert sich
  • Kann zu externen URLs gehen
  • Status-Code 302 (Found)
  • Request-Attribute gehen verloren
response.sendRedirect("/success");  // Neuer Request

forward() – Server-Side:

  • Request wird server-intern weitergeleitet
  • URL im Browser bleibt gleich
  • Nur interne URLs möglich
  • Kein neuer Request
  • Request-Attribute bleiben erhalten
request.getRequestDispatcher("/success").forward(request, response);

Wann was?

  • Nach POST → sendRedirect (PRG-Pattern!)
  • MVC: Controller → View → forward()
  • Externe URLs → sendRedirect()
  • Request-Attribute weitergeben → forward()

Frage 4: Warum sind Instance-Variablen in Servlets problematisch?

Antwort:

Weil alle Threads die gleiche Servlet-Instanz teilen!

Problem:

@WebServlet("/user")
public class UserServlet extends HttpServlet {
    private String username;  // ❌ GEFÄHRLICH!
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) {
        username = request.getParameter("username");
        // Thread 1 setzt "Anna"
        // Thread 2 setzt "Bob" → überschreibt "Anna"!
        // Thread 1 liest plötzlich "Bob" statt "Anna"!
    }
}

Race Condition garantiert!

Lösungen:

  1. Lokale Variablen (thread-safe)
  2. Request-Attribute (pro Request)
  3. Session-Attribute (pro User)
  4. AtomicInteger/AtomicLong (thread-safe)
  5. Synchronization (langsam!)

Frage 5: Was ist das PRG-Pattern und warum ist es wichtig?

Antwort:

PRG = Post-Redirect-Get Pattern

Problem ohne PRG:

1. User füllt Formular aus
2. Klick auf "Submit" → POST /register
3. Server verarbeitet → 200 OK mit HTML
4. User drückt F5 (Reload)
5. Browser fragt: "Form nochmal senden?"
6. User klickt "Ja"
7. Daten werden DOPPELT gespeichert! ❌

Lösung mit PRG:

1. User füllt Formular aus
2. Klick auf "Submit" → POST /register
3. Server verarbeitet Daten
4. Server macht sendRedirect("/success") → 302
5. Browser macht GET /success
6. Success-Seite wird angezeigt
7. User drückt F5 (Reload)
8. Nur GET /success wird wiederholt → kein Problem! ✅

Implementation:

@WebServlet("/register")
public class RegisterServlet extends HttpServlet {
    
    @Override
    protected void doPost(HttpServletRequest request, 
                         HttpServletResponse response) {
        // 1. Daten verarbeiten
        String username = request.getParameter("username");
        // ... in DB speichern ...
        
        // 2. Flash-Message in Session
        HttpSession session = request.getSession();
        session.setAttribute("message", "Registration successful!");
        
        // 3. REDIRECT zu GET-Seite
        response.sendRedirect("/success");
    }
}

@WebServlet("/success")
public class SuccessServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) {
        // Flash-Message anzeigen
        HttpSession session = request.getSession();
        String message = (String) session.getAttribute("message");
        session.removeAttribute("message");  // Nur einmal anzeigen!
        
        // HTML-Output...
    }
}

Vorteile:

  • ✅ Kein doppeltes Submit bei F5
  • ✅ Saubere URL nach POST
  • ✅ Browser-History freundlich
  • ✅ Bessere UX

Always use PRG after POST! 🎯


🎉 Tag 3 geschafft!

Wow, das war heftig! 💪

Du hast heute richtig was gelernt:

  • ✅ Servlet-Lifecycle verstanden
  • ✅ Request/Response-Objekte gemeistert
  • ✅ Thread-Safety kennengelernt
  • ✅ Filter und Listener eingesetzt
  • ✅ Production-Ready Code geschrieben

Real talk: Servlets sind die Basis von ALLEM in Java-Web. Selbst Spring Boot nutzt unter der Haube die Servlet-API. Du hast heute das Fundament gelegt!

Slay! Du bist jetzt ein Servlet-Pro! 🎯


🚀 Wie geht’s weiter?

Morgen (Tag 4): Deployment Descriptor & MVC vs Model 2

Was dich erwartet:

  • web.xml im Detail verstehen
  • MVC-Pattern vs. Model 2 Pattern
  • Servlet-Mapping ohne Annotations
  • Context-Parameter und Init-Parameter
  • Security-Constraints konfigurieren
  • Das wird dein Game-Changer für Enterprise-Projekte! 🔥

Besonderheit: Morgen lernst du, wie große Enterprise-Projekte konfiguriert werden – ohne Annotations! Das ist kritisch für Legacy-Code und große Projekte.


Brauchst du eine Pause?

Mach sie! Servlets sind komplex. Lass das heute sacken.

Tipp für heute Abend:

Spiel mit der Mini-Challenge! Bau das Login-System. Teste verschiedene Szenarien:

  • Was passiert bei falschem Passwort?
  • Was passiert, wenn du /protected direkt aufrufst?
  • Was passiert nach Logout?

Learning by breaking things! 🔧


🔧 Troubleshooting

Problem 1: „Cannot find HttpServlet“

Lösung: Maven-Dependency fehlt:

<dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <version>6.0.0</version>
    <scope>provided</scope>
</dependency>

Problem 2: „404 – Servlet not found“

Mögliche Ursachen:

  1. Falsche URL (z.B. /hello statt /HelloPayara/hello)
  2. @WebServlet Annotation fehlt
  3. Servlet nicht deployed
  4. Context-Root falsch

Lösung: Überprüfe:

http://localhost:8080/[CONTEXT-ROOT]/[SERVLET-PATH]
                       ↑               ↑
                       pom.xml         @WebServlet

Problem 3: „IllegalStateException: Response already committed“

Grund: Du versuchst, Response-Header zu setzen, nachdem du schon Output geschrieben hast.

Lösung: Setze IMMER zuerst Headers, dann Content:

// ✅ RICHTIG:
response.setContentType("text/html");  // 1. Content-Type
response.addCookie(cookie);            // 2. Cookies
PrintWriter out = response.getWriter(); // 3. Output
out.println("<html>...</html>");        // 4. Content

Problem 4: Umlaute sind kaputt

Lösung: Character-Encoding setzen:

request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");

Am besten in einem Filter für alle Requests!


Problem 5: Session ist immer null

Grund: Du rufst request.getSession(false) auf, ohne dass eine Session existiert.

Lösung:

// Session erstellen, falls keine existiert:
HttpSession session = request.getSession();  // ← OHNE false

// Oder: Erst prüfen
HttpSession session = request.getSession(false);
if (session == null) {
    // Keine Session vorhanden
}

Offizielle Dokumentation:

Payara Server:

Best Practices:

Tools:

Unsere GitHub-Repos:


💬 Feedback

Wie war Tag 3 für dich?

Was können wir verbessern? Dein Feedback hilft uns, den Kurs besser zu machen!


👋 Bis morgen!

Das war Tag 3 – einer der härtesten, aber auch wichtigsten Tage!

Servlets sind die Basis von allem. Du hast heute das Fundament gelegt, auf dem du morgen aufbauen wirst.

Bis morgen! 👋

Elyndra


Java Web Basic – Tag 3 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.