Java Web Basic – Tag 10 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 | ✅ Abgeschlossen |
| 3 | Servlets & Servlet API | ✅ Abgeschlossen |
| 4 | Deployment Descriptor & MVC vs Model 2 | ✅ Abgeschlossen |
| 5 | JSP & Expression Languages | ✅ Abgeschlossen |
| 6 | Java Beans, Actions, Scopes & Direktiven | ✅ Abgeschlossen |
| 7 | Include-Action vs Include-Direktive | ✅ Abgeschlossen |
| 8 | JSTL – Java Standard Tag Libraries | ✅ Abgeschlossen |
| 9 | Java Web und Datenbanken – Datasource | ✅ Abgeschlossen |
| → 10 | Connection Pools & JDBC Performance | 👉 DU BIST HIER! 🎉 |
Modul: Java Web Basic
Gesamt-Dauer: 10 Arbeitstage (je 8 Stunden)
Dein Ziel: JDBC Performance meistern und Production-Ready sein!
📋 Voraussetzungen für diesen Tag
Du brauchst:
- ✅ JDK 21 LTS installiert
- ✅ Apache NetBeans 22 (oder neuer)
- ✅ Payara Server 6.x konfiguriert
- ✅ MariaDB (oder MySQL) installiert und lauffähig
- ✅ Tag 1-9 abgeschlossen
- ✅ Datasource & Connection Pools verstanden
- ✅ DAO-Pattern beherrschen
- ✅ JSTL & JSP sitzen
Tag verpasst?
Spring zurück zu Tag 9, um Datasources zu verstehen. Heute bauen wir darauf auf!
Setup-Probleme?
Schreib uns: support@java-developer.online
⚡ Das Wichtigste in 30 Sekunden
Heute lernst du:
- ✅ Prepared Statements vs. Statement – Performance & Security
- ✅ Batch Operations für Massen-Inserts
- ✅ Transaction Management im Detail
- ✅ Connection Pool Tuning für Production
- ✅ Stored Procedures aufrufen
- ✅ JDBC Best Practices & Anti-Patterns
- ✅ Performance-Optimierung & Monitoring
Am Ende des Tages kannst du:
- Production-Ready JDBC-Code schreiben
- Performance-Probleme identifizieren & lösen
- Transaktionen korrekt handhaben
- Batch-Operations nutzen
- Connection Pools optimal konfigurieren
- SQL-Injection verhindern
- Du bist ein Java Web Developer! 🎓
Zeit-Investment: ~6-8 Stunden
Schwierigkeitsgrad: Fortgeschritten (aber du schaffst das – es ist der letzte Tag!)
👋 Willkommen zu Tag 10 – Der finale Tag!
Hi! 👋
Elyndra hier. Das ist es – der letzte Tag des Java Web Basic Kurses!
Real talk: Wenn du bis hierher gekommen bist, bin ich wirklich stolz auf dich! 💪
Kurzwiederholung: Challenge von Tag 9
Gestern hast du Datasources und Connection Pools konfiguriert. Heute machen wir die Anwendung Production-Ready!
Was dich heute erwartet:
- Performance-Optimierung
- Security Best Practices
- Advanced JDBC Features
- Production-Ready Code schreiben
Und am Ende: Du bist ein vollwertiger Java Web Developer! 🎓
Franz-Martin (unser Tech Lead) sagt immer: „Die Grundlagen sind das Fundament. Aber Performance und Security machen den Unterschied zwischen einem Junior und einem Professional.“
Let’s finish this strong! 🚀
🟢 GRUNDLAGEN: Statement vs. PreparedStatement
Das Problem mit Statement
Erinnerst du dich an Java SE?
// ❌ Statement: String Concatenation
String name = request.getParameter("name");
String sql = "SELECT * FROM products WHERE name = '" + name + "'";
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
// Daten verarbeiten...
}
Was ist das Problem?
Problem 1: SQL-Injection! 🔴
// User gibt ein: ' OR '1'='1
String name = "' OR '1'='1";
// Resultierende Query:
SELECT * FROM products WHERE name = '' OR '1'='1'
↑
Dies ist IMMER true!
Ergebnis: Alle Produkte werden zurückgegeben, nicht nur das gesuchte!
Noch schlimmer:
// User gibt ein: '; DROP TABLE products; --
String name = "'; DROP TABLE products; --";
// Resultierende Query:
SELECT * FROM products WHERE name = ''; DROP TABLE products; --'
↑
Löscht die Tabelle!
Deine Datenbank ist weg! 💥
Problem 2: Performance!
Jedes Mal wenn du eine Query ausführst:
1. SQL-String wird geparst 2. Execution Plan wird erstellt 3. Query wird optimiert 4. Query wird ausgeführt
Bei gleichen Queries mit unterschiedlichen Parametern wiederholt sich jedes Mal Schritt 1-3!
Das ist langsam und ineffizient!
Die Lösung: PreparedStatement
// ✅ PreparedStatement: Parameter-Binding
String sql = "SELECT * FROM products WHERE name = ?";
↑
Placeholder!
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, name); // Parameter wird escaped!
try (ResultSet rs = pstmt.executeQuery()) {
// Daten verarbeiten...
}
}
Was macht PreparedStatement?
1. Compilation & Caching:
PreparedStatement pstmt = conn.prepareStatement(sql); // → Query wird einmal kompiliert und gecacht
2. Parameter-Binding:
pstmt.setString(1, name); // → Wert wird escaped und sicher eingesetzt
3. Wiederverwendung:
// Gleiche Query, andere Parameter: pstmt.setString(1, "Laptop"); pstmt.executeQuery(); pstmt.setString(1, "Mouse"); pstmt.executeQuery(); // Query-Plan wird wiederverwendet! (schneller!)
PreparedStatement Syntax im Detail
Basis-Syntax:
String sql = "SELECT * FROM products WHERE category = ? AND price < ?"; PreparedStatement pstmt = conn.prepareStatement(sql); // Parameter setzen (1-basiert!) pstmt.setString(1, "Electronics"); // Erster ? pstmt.setDouble(2, 100.0); // Zweiter ? ResultSet rs = pstmt.executeQuery();
Wichtig: Parameter-Indizes starten bei 1, nicht bei 0!
Alle setXXX() Methoden:
// Strings
pstmt.setString(1, "text");
// Zahlen
pstmt.setInt(1, 42);
pstmt.setLong(1, 123456789L);
pstmt.setDouble(1, 99.99);
pstmt.setBigDecimal(1, new BigDecimal("19.99"));
// Boolean
pstmt.setBoolean(1, true);
// Datum & Zeit
pstmt.setDate(1, java.sql.Date.valueOf("2025-01-15"));
pstmt.setTime(1, java.sql.Time.valueOf("14:30:00"));
pstmt.setTimestamp(1, java.sql.Timestamp.valueOf("2025-01-15 14:30:00"));
// NULL-Werte
pstmt.setNull(1, java.sql.Types.VARCHAR);
// Binary Data
pstmt.setBytes(1, byteArray);
pstmt.setBlob(1, inputStream);
Beispiel: Komplexe Query mit mehreren Parametern
String sql = "SELECT * FROM products " +
"WHERE category = ? " +
"AND price BETWEEN ? AND ? " +
"AND in_stock = ? " +
"ORDER BY name";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, "Electronics"); // category
pstmt.setDouble(2, 50.0); // min price
pstmt.setDouble(3, 200.0); // max price
pstmt.setBoolean(4, true); // in_stock
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
// Daten verarbeiten...
}
}
}
Das Prinzip: Jeder ? bekommt einen Wert über setXXX(index, value)!
Sicherheit: SQL-Injection Prevention
Wie PreparedStatement SQL-Injection verhindert:
// User-Eingabe: String maliciousInput = "'; DROP TABLE products; --"; // Mit Statement (UNSICHER!): String sql = "SELECT * FROM products WHERE name = '" + maliciousInput + "'"; // Resultat: SELECT * FROM products WHERE name = ''; DROP TABLE products; --' // → Tabelle wird gelöscht! 💥 // Mit PreparedStatement (SICHER!): String sql = "SELECT * FROM products WHERE name = ?"; pstmt.setString(1, maliciousInput); // Resultat: SELECT * FROM products WHERE name = '\'; DROP TABLE products; --' // ↑ // Escaped! Wird als String behandelt!
Der Unterschied:
- Statement: String wird direkt in Query eingefügt
- PreparedStatement: String wird escaped und als Literal behandelt
Best Practice: NIEMALS String-Concatenation für SQL-Queries verwenden!
Performance-Vergleich
Benchmark: 1000 Queries ausführen
// ❌ Statement: 1000ms
long start = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
String sql = "SELECT * FROM products WHERE id = " + i;
Statement stmt = conn.createStatement();
stmt.executeQuery(sql);
stmt.close();
}
long duration = System.currentTimeMillis() - start;
// Dauer: ~1000ms (1 Sekunde)
// ✅ PreparedStatement: 400ms
start = System.currentTimeMillis();
String sql = "SELECT * FROM products WHERE id = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
for (int i = 0; i < 1000; i++) {
pstmt.setInt(1, i);
pstmt.executeQuery();
}
pstmt.close();
duration = System.currentTimeMillis() - start;
// Dauer: ~400ms (0.4 Sekunden)
PreparedStatement ist 2.5x schneller! 🚀
Warum?
- Query wird nur einmal kompiliert
- Execution Plan wird gecacht
- Datenbank kann optimieren
🟢 GRUNDLAGEN: Batch Operations
Das Problem: Viele Inserts
Szenario: 1000 Produkte in Datenbank einfügen
// ❌ LANGSAM: Einzelne Inserts
String sql = "INSERT INTO products (name, price, stock) VALUES (?, ?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
for (int i = 0; i < 1000; i++) {
pstmt.setString(1, "Product " + i);
pstmt.setDouble(2, 99.99);
pstmt.setInt(3, 10);
pstmt.executeUpdate(); // ← Jedes Mal ein DB-Roundtrip!
}
}
// Dauer: ~5000ms (5 Sekunden!)
Problem: 1000 Roundtrips zur Datenbank!
App → DB: INSERT Product 1 DB → App: OK App → DB: INSERT Product 2 DB → App: OK ... (1000x wiederholt)
Das ist extrem langsam!
Die Lösung: Batch Operations
// ✅ SCHNELL: Batch-Insert
String sql = "INSERT INTO products (name, price, stock) VALUES (?, ?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
for (int i = 0; i < 1000; i++) {
pstmt.setString(1, "Product " + i);
pstmt.setDouble(2, 99.99);
pstmt.setInt(3, 10);
pstmt.addBatch(); // ← Fügt zur Batch hinzu (kein DB-Zugriff!)
// Alle 100 Statements ausführen
if (i % 100 == 0) {
pstmt.executeBatch(); // ← 100 Inserts auf einmal!
pstmt.clearBatch();
}
}
// Rest ausführen
pstmt.executeBatch();
}
// Dauer: ~500ms (0.5 Sekunden!)
10x schneller! 🚀
Was macht addBatch()?
App sammelt 100 Statements: - INSERT Product 1 - INSERT Product 2 - INSERT Product 3 - ... - INSERT Product 100 executeBatch() → Schickt alle 100 auf einmal zur DB! DB verarbeitet alle 100 zusammen DB → App: OK (alle 100 erfolgreich)
Nur 10 Roundtrips statt 1000!
Batch Operations im Detail
Basic Pattern:
PreparedStatement pstmt = conn.prepareStatement(sql);
for (Item item : items) {
pstmt.setXXX(1, item.getValue1());
pstmt.setXXX(2, item.getValue2());
pstmt.addBatch(); // Fügt zur Batch hinzu
}
int[] results = pstmt.executeBatch(); // Führt alle aus
executeBatch() Rückgabewert:
int[] results = pstmt.executeBatch(); // results[i] enthält: // - Anzahl betroffener Rows für Statement i // - Statement.SUCCESS_NO_INFO (-2) wenn unbekannt // - Statement.EXECUTE_FAILED (-3) bei Fehler
Beispiel:
int[] results = pstmt.executeBatch(); // results = [1, 1, 1, -2, 1, 1, ...] // ↑ ↑ ↑ ↑ // OK OK OK Info nicht verfügbar
Best Practice: Batch Size
// ✅ Empfohlene Batch-Größe: 50-100
final int BATCH_SIZE = 100;
for (int i = 0; i < items.size(); i++) {
Item item = items.get(i);
pstmt.setString(1, item.getName());
pstmt.setDouble(2, item.getPrice());
pstmt.addBatch();
// Batch nach jedem 100. Element ausführen
if ((i + 1) % BATCH_SIZE == 0) {
pstmt.executeBatch();
pstmt.clearBatch(); // ✅ Wichtig: Batch leeren!
}
}
// Rest ausführen (falls items.size() nicht durch BATCH_SIZE teilbar)
pstmt.executeBatch();
Warum 50-100?
- Zu klein (10): Viele Roundtrips, langsam
- Zu groß (10000): Großer Memory-Verbrauch, Timeout-Risiko
- Sweet Spot (100): Balance zwischen Performance und Ressourcen
Batch mit Transaktionen kombinieren
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false); // Transaction starten
String sql = "INSERT INTO products (name, price, stock) VALUES (?, ?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
for (int i = 0; i < 1000; i++) {
pstmt.setString(1, "Product " + i);
pstmt.setDouble(2, 99.99);
pstmt.setInt(3, 10);
pstmt.addBatch();
if (i % 100 == 0) {
pstmt.executeBatch();
pstmt.clearBatch();
}
}
pstmt.executeBatch();
pstmt.close();
conn.commit(); // ✅ Alle Inserts erfolgreich
} catch (SQLException e) {
if (conn != null) {
conn.rollback(); // ❌ Fehler: Alle Inserts rückgängig
}
throw new RuntimeException("Batch insert failed", e);
} finally {
if (conn != null) {
conn.setAutoCommit(true);
conn.close();
}
}
Das Prinzip: Batch + Transaction = Atomare Massen-Operation!
🟡 PROFESSIONALS: Transaction Management
ACID-Eigenschaften verstehen
Was sind Transaktionen?
Eine Transaction ist eine Gruppe von Operationen, die entweder komplett ausgeführt werden oder gar nicht.
ACID:
A – Atomicity (Atomarität):
- Alles oder nichts
- Keine Teil-Ausführung
C – Consistency (Konsistenz):
- Daten bleiben valide
- Business Rules werden eingehalten
I – Isolation (Isolation):
- Transaktionen beeinflussen sich nicht gegenseitig
- Keine Race Conditions
D – Durability (Dauerhaftigkeit):
- Nach Commit sind Daten persistent
- Überleben System-Crash
Transaction Isolation Levels
Das Problem: Concurrent Access
User A startet Transaction: Überweist 100€ von Konto 1 → Konto 2 User B startet Transaction: Liest Kontostand von Konto 1
Was soll User B sehen?
- Alten Stand (vor Überweisung)?
- Neuen Stand (nach Überweisung)?
- Zwischenstand (während Überweisung)?
Das regeln Isolation Levels!
Die 4 Isolation Levels:
| Level | Dirty Read | Non-Repeatable Read | Phantom Read | Performance |
|---|---|---|---|---|
| READ_UNCOMMITTED | ✅ Möglich | ✅ Möglich | ✅ Möglich | 🚀 Schnellst |
| READ_COMMITTED | ❌ Verhindert | ✅ Möglich | ✅ Möglich | ⚡ Schnell |
| REPEATABLE_READ | ❌ Verhindert | ❌ Verhindert | ✅ Möglich | 🐢 Langsam |
| SERIALIZABLE | ❌ Verhindert | ❌ Verhindert | ❌ Verhindert | 🐌 Langsamst |
Was bedeuten die Begriffe?
Dirty Read:
- Transaction A ändert Daten, committed aber noch nicht
- Transaction B liest die nicht-committeten Daten
- Transaction A macht Rollback
- Problem: B hat „schmutzige“ Daten gelesen!
Non-Repeatable Read:
- Transaction A liest Daten (z.B. Kontostand = 100€)
- Transaction B ändert die Daten (Kontostand = 50€) und committed
- Transaction A liest nochmal
- Problem: Unterschiedliche Werte innerhalb einer Transaction!
Phantom Read:
- Transaction A liest alle Produkte mit price < 100 (findet 5 Produkte)
- Transaction B fügt neues Produkt mit price = 50 ein und committed
- Transaction A liest nochmal
- Problem: Plötzlich 6 Produkte statt 5! („Phantom“)
Isolation Level setzen:
Connection conn = dataSource.getConnection(); // Isolation Level setzen conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); // Mögliche Werte: // Connection.TRANSACTION_READ_UNCOMMITTED // Connection.TRANSACTION_READ_COMMITTED ← Standard in meisten DBs // Connection.TRANSACTION_REPEATABLE_READ // Connection.TRANSACTION_SERIALIZABLE
Best Practice:
// ✅ Standard: READ_COMMITTED // - Verhindert Dirty Reads // - Gute Performance // - Ausreichend für die meisten Anwendungen conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
Transaction Best Practices
1. Kurze Transaktionen
// ❌ SCHLECHT: Lange Transaction
conn.setAutoCommit(false);
// Schritt 1: DB-Operation (5ms)
updateAccount(accountId, amount);
// Schritt 2: External API Call (2000ms!)
boolean approved = paymentGateway.charge(amount);
// Schritt 3: DB-Operation (5ms)
if (approved) {
conn.commit();
} else {
conn.rollback();
}
// Transaction dauert 2010ms!
// → Verbindung ist 2010ms geblockt! 😱
// ✅ BESSER: Kurze Transaction
// Schritt 1: External API Call (OHNE Transaction)
boolean approved = paymentGateway.charge(amount);
// Schritt 2: Kurze Transaction
conn.setAutoCommit(false);
try {
updateAccount(accountId, amount);
logPayment(transactionId, approved);
conn.commit(); // Transaction dauert nur ~10ms!
} catch (SQLException e) {
conn.rollback();
}
conn.setAutoCommit(true);
Das Prinzip: Transaction nur für DB-Operationen, nicht für externe Calls!
2. Immer Finally-Block nutzen
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false);
// Operationen...
conn.commit();
} catch (SQLException e) {
if (conn != null) {
try {
conn.rollback();
} catch (SQLException ex) {
// Log, aber nicht re-throw
}
}
throw new RuntimeException("Transaction failed", e);
} finally {
if (conn != null) {
try {
conn.setAutoCommit(true); // ✅ Wichtig!
conn.close();
} catch (SQLException e) {
// Log
}
}
}
Warum setAutoCommit(true) in finally?
- Connection geht zurück in Pool
- Nächster User bekommt Connection mit korrektem Auto-Commit!
3. Savepoints für Teil-Rollbacks
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
Savepoint savepoint1 = null;
try {
// Operation 1: Produkt erstellen
insertProduct(conn, product);
savepoint1 = conn.setSavepoint("AfterProductInsert");
// Operation 2: Bilder hochladen (könnte fehlschlagen)
uploadProductImages(conn, productId, images);
// Alles OK
conn.commit();
} catch (ImageUploadException e) {
// ✅ Nur Bilder-Upload rückgängig machen
if (savepoint1 != null) {
conn.rollback(savepoint1);
}
// Produkt bleibt bestehen!
conn.commit();
} catch (SQLException e) {
// ❌ Alles rückgängig machen
conn.rollback();
throw new RuntimeException(e);
} finally {
conn.setAutoCommit(true);
conn.close();
}
Use Case: Teil-Operationen können fehlschlagen, ohne ganze Transaction zu verwerfen!
🟡 PROFESSIONALS: Connection Pool Tuning
Connection Pool Parameter verstehen
Die wichtigsten Parameter:
Wichtig: Die Parameter-Namen sind unterschiedlich zwischen Tomcat und Payara!
Tomcat (context.xml):
<Resource
name="jdbc/ShopDB"
<!-- Pool-Größe (Tomcat-Syntax) -->
initialSize="10" <!-- Verbindungen beim Start -->
minIdle="10" <!-- Minimum immer verfügbar -->
maxTotal="100" <!-- Maximum gleichzeitig -->
maxIdle="50" <!-- Maximum im Leerlauf -->
<!-- Timeouts -->
maxWaitMillis="30000" <!-- Max Wartezeit: 30 Sek -->
<!-- Validation -->
testOnBorrow="true" <!-- Teste vor Benutzung -->
testWhileIdle="true" <!-- Teste Idle-Connections -->
validationQuery="SELECT 1"
<!-- Eviction (Aufräumen) -->
timeBetweenEvictionRunsMillis="30000" <!-- Alle 30 Sek -->
minEvictableIdleTimeMillis="60000" <!-- Nach 1 Min Idle -->
<!-- Connection Leak Detection -->
removeAbandonedOnBorrow="true"
removeAbandonedTimeout="300" <!-- Nach 5 Min -->
logAbandoned="true"
/>
Payara (Admin Console):
JDBC Connection Pools → [Pool Name] → General Tab: Pool Settings: - Initial Pool Size: 10 - Minimum Pool Size: 10 - Maximum Pool Size: 100 - Pool Resize Quantity: 2 - Idle Timeout: 300 seconds - Max Wait Time: 30000 milliseconds Advanced Tab: - Connection Validation: Required - Validation Method: table - Validation Table Name: dual (oder eigene Tabelle) - Validate Atmost Once: 30 seconds
Wichtig zu verstehen:
| Konzept | Tomcat (XML) | Payara (Admin Console) |
|---|---|---|
| Konfiguration | META-INF/context.xml | Web-basierte GUI |
| Format | XML-Attribute | Formular-Felder |
| Beispiel „Max Size“ | maxTotal="100" | „Maximum Pool Size: 100“ |
Was bedeuten die Parameter im Detail?
Wichtiger Hinweis: Die folgenden Erklärungen verwenden Tomcat-Syntax (XML), da das konzeptionell einfacher zu zeigen ist. In Payara konfigurierst du diese Werte über die Admin Console (GUI), nicht über XML-Dateien!
Pool-Größe:
initialSize: 10
↓
Beim Start: ██████████ (10 Connections erstellt)
minIdle: 10
↓
Minimum: ██████████ (immer mindestens 10 verfügbar)
maxTotal: 100
↓
Maximum: ████████████████████████████ (max 100 gleichzeitig)
maxIdle: 50
↓
Wenn mehr als 50 idle: älteste werden geschlossen
Timeouts:
maxWaitMillis="30000"
User Request → Pool: "Gib mir Connection!"
Pool: "Alle busy, warte..."
Nach 30 Sekunden:
Pool: "Timeout! Keine Connection verfügbar!"
→ SQLException: Cannot get connection, wait time exceeded
Validation:
testOnBorrow="true"
validationQuery="SELECT 1"
User Request → Pool: "Gib mir Connection!"
Pool: "OK, aber erst testen..."
Pool → DB: SELECT 1
DB → Pool: OK
Pool → User: "Hier, Connection ist OK!"
Wenn Test fehlschlägt:
Pool → DB: SELECT 1
DB: (keine Antwort - Connection tot)
Pool: "Connection kaputt, erstelle neue..."
Pool → User: "Hier, neue Connection!"
Verhindert „Stale Connection“ Fehler!
Eviction (Aufräumen):
timeBetweenEvictionRunsMillis="30000" minEvictableIdleTimeMillis="60000" Alle 30 Sekunden läuft "Eviction Thread": 1. Prüft alle Idle-Connections 2. Schließt Connections, die > 60 Sek idle sind 3. Hält Pool sauber
Warum wichtig?
- Spart DB-Ressourcen
- Verhindert „Connection Timeout“ in DB
- Pool passt sich an Last an
Production-Ready Pool-Konfiguration
Empfohlene Settings für verschiedene Szenarien:
Szenario 1: Kleine App (< 100 User gleichzeitig)
<Resource
name="jdbc/ShopDB"
initialSize="5"
minIdle="5"
maxTotal="20"
maxIdle="10"
maxWaitMillis="10000"
testOnBorrow="true"
testWhileIdle="true"
validationQuery="SELECT 1"
timeBetweenEvictionRunsMillis="60000"
minEvictableIdleTimeMillis="300000"
/>
Szenario 2: Mittlere App (100-1000 User)
<Resource
name="jdbc/ShopDB"
initialSize="20"
minIdle="20"
maxTotal="100"
maxIdle="50"
maxWaitMillis="30000"
testOnBorrow="true"
testWhileIdle="true"
validationQuery="SELECT 1"
validationQueryTimeout="3"
timeBetweenEvictionRunsMillis="30000"
minEvictableIdleTimeMillis="180000"
removeAbandonedOnBorrow="true"
removeAbandonedTimeout="300"
logAbandoned="true"
/>
Szenario 3: Große App (> 1000 User)
<Resource
name="jdbc/ShopDB"
initialSize="50"
minIdle="50"
maxTotal="200"
maxIdle="100"
maxWaitMillis="60000"
testOnBorrow="true"
testWhileIdle="true"
validationQuery="SELECT 1"
validationQueryTimeout="5"
timeBetweenEvictionRunsMillis="30000"
minEvictableIdleTimeMillis="120000"
removeAbandonedOnBorrow="true"
removeAbandonedOnMaintenance="true"
removeAbandonedTimeout="180"
logAbandoned="true"
<!-- Performance-Tuning -->
fairQueue="false"
jmxEnabled="true"
/>
Monitoring & Diagnostics
Payara Monitoring aktivieren:
Admin Console → Monitoring → Configure Monitoring → JDBC Connection Pool: HIGH
Metrics anzeigen:
Admin Console → Monitoring → Server → Resources → JDBC Connection Pools → ShopDBPool
Was du siehst:
numConnUsed: 15 ← Aktuell in Benutzung numConnFree: 35 ← Verfügbar numConnCreated: 50 ← Gesamt erstellt waitTime: 2ms ← Durchschnittliche Wartezeit numConnTimedOut: 0 ← Timeouts
JMX Monitoring (für Production):
// JConsole / VisualVM nutzen
// → MBeans → java.sql → ShopDBPool
// Oder programmatisch:
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
ObjectName poolName = new ObjectName(
"Catalina:type=DataSource,class=javax.sql.DataSource,name=\"jdbc/ShopDB\""
);
int numActive = (Integer) mbs.getAttribute(poolName, "numActive");
int numIdle = (Integer) mbs.getAttribute(poolName, "numIdle");
System.out.println("Active: " + numActive + ", Idle: " + numIdle);
Logging aktivieren:
Payara Admin Console → Configurations → server-config → Logger Settings Log Levels: jakarta.enterprise.resource.resourceadapter: FINE org.apache.tomcat.jdbc.pool: FINE
Log-Ausgabe:
INFO: Created connection pool 'ShopDBPool'
FINE: Borrowing connection from pool (active: 5, idle: 15)
FINE: Returning connection to pool (active: 4, idle: 16)
WARNING: Connection leak detected! Stack trace:
at com.shop.dao.ProductDAOImpl.findAll()
...
🟡 PROFESSIONALS: Stored Procedures
Was sind Stored Procedures?
Stored Procedures sind vorkompilierte SQL-Funktionen, die in der Datenbank gespeichert sind.
Beispiel: Stored Procedure in MariaDB
-- Stored Procedure erstellen
DELIMITER $$
CREATE PROCEDURE getProductsByPriceRange(
IN minPrice DECIMAL(10,2),
IN maxPrice DECIMAL(10,2)
)
BEGIN
SELECT * FROM products
WHERE price BETWEEN minPrice AND maxPrice
ORDER BY price;
END$$
DELIMITER ;
Aufrufen aus JDBC:
String sql = "{CALL getProductsByPriceRange(?, ?)}";
try (CallableStatement cstmt = conn.prepareCall(sql)) {
cstmt.setDouble(1, 50.0); // minPrice
cstmt.setDouble(2, 200.0); // maxPrice
try (ResultSet rs = cstmt.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getString("name") + ": " + rs.getDouble("price"));
}
}
}
Stored Procedures mit OUT-Parametern
Beispiel: Berechnung in Datenbank
DELIMITER $$
CREATE PROCEDURE calculateOrderTotal(
IN orderId INT,
OUT totalAmount DECIMAL(10,2),
OUT itemCount INT
)
BEGIN
SELECT SUM(price * quantity), COUNT(*)
INTO totalAmount, itemCount
FROM order_items
WHERE order_id = orderId;
END$$
DELIMITER ;
Aufrufen aus JDBC:
String sql = "{CALL calculateOrderTotal(?, ?, ?)}";
try (CallableStatement cstmt = conn.prepareCall(sql)) {
// IN-Parameter
cstmt.setInt(1, orderId);
// OUT-Parameter registrieren
cstmt.registerOutParameter(2, java.sql.Types.DECIMAL);
cstmt.registerOutParameter(3, java.sql.Types.INTEGER);
// Ausführen
cstmt.execute();
// OUT-Parameter lesen
BigDecimal totalAmount = cstmt.getBigDecimal(2);
int itemCount = cstmt.getInt(3);
System.out.println("Total: " + totalAmount);
System.out.println("Items: " + itemCount);
}
Wann Stored Procedures nutzen?
✅ Vorteile:
- Performance (vorkompiliert, DB-seitig optimiert)
- Weniger Netzwerk-Traffic
- Wiederverwendbar
- Zentralisierte Business-Logik
❌ Nachteile:
- Schwer zu testen
- Nicht portabel (DB-spezifisch)
- Versionierung kompliziert
- Debugging schwierig
Best Practice:
- Nutze Stored Procedures für komplexe Berechnungen
- Nutze Stored Procedures für Performance-kritische Operationen
- Aber: Halte Business-Logik in Java, nicht in DB!
🔵 BONUS: Performance-Optimierung
1. Connection Pooling Tuning
Problem identifizieren:
// Monitoring zeigt: waitTime: 500ms ← User warten lange! numConnTimedOut: 15 ← Viele Timeouts! numConnUsed: 100 ← Pool immer voll!
Lösung 1: Pool vergrößern
<!-- Vorher --> maxTotal="100" <!-- Nachher --> maxTotal="200"
Problem: Connection Leaks
// Monitoring zeigt: numConnUsed: 95 ← Fast alle used numConnFree: 5 ← Kaum noch frei Aber: Niedrige Last! ← Widerspruch!
Ursache: Connections werden nicht geschlossen!
Lösung:
// ❌ FALSCH: Leak!
Connection conn = dataSource.getConnection();
// ... vergessen zu schließen
// ✅ RICHTIG: Try-with-resources
try (Connection conn = dataSource.getConnection()) {
// ...
} // Automatisch geschlossen
2. Query-Optimierung
Problem: Langsame Queries
// Slow Query Log zeigt: SELECT * FROM products WHERE name LIKE '%laptop%' Time: 5000ms ← 5 Sekunden! 😱
Lösung 1: Index erstellen
-- Index auf name-Spalte CREATE INDEX idx_products_name ON products(name); -- Query jetzt: Time: 50ms ← 100x schneller!
Lösung 2: Query umschreiben
// ❌ LANGSAM: Wildcard am Anfang
SELECT * FROM products WHERE name LIKE '%laptop%'
// Index kann nicht genutzt werden!
// ✅ SCHNELLER: Wildcard am Ende
SELECT * FROM products WHERE name LIKE 'laptop%'
// Index kann genutzt werden!
// ✅ ODER: Full-Text-Search nutzen
SELECT * FROM products WHERE MATCH(name) AGAINST('laptop')
Problem: N+1 Queries
// ❌ SCHLECHT: N+1 Query Problem
List<Order> orders = orderDAO.findAll(); // 1 Query
for (Order order : orders) {
List<Item> items = orderDAO.getItems(order.getId()); // N Queries!
order.setItems(items);
}
// Gesamt: 1 + N Queries (N = Anzahl Orders)
// ✅ BESSER: JOIN verwenden
String sql = """
SELECT o.*, i.*
FROM orders o
LEFT JOIN order_items i ON o.id = i.order_id
""";
// Nur 1 Query!
3. Caching-Strategien
Problem: Gleiche Daten immer wieder geladen
// Jeder Request lädt Kategorien neu (ineffizient!)
@WebServlet("/products")
public class ProductServlet extends HttpServlet {
protected void doGet(...) {
List<Category> categories = categoryDAO.findAll(); // DB-Zugriff!
// ...
}
}
Lösung 1: Application-Scope Cache
@WebServlet("/products")
public class ProductServlet extends HttpServlet {
@Override
public void init() throws ServletException {
// Beim Start laden
List<Category> categories = categoryDAO.findAll();
getServletContext().setAttribute("categories", categories);
}
protected void doGet(...) {
// Aus Cache holen (kein DB-Zugriff!)
List<Category> categories =
(List<Category>) getServletContext().getAttribute("categories");
// ...
}
}
Lösung 2: LRU-Cache mit Guava
<!-- pom.xml -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.0.0-jre</version>
</dependency>
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
public class ProductDAOImpl implements ProductDAO {
private LoadingCache<Integer, Product> cache;
public ProductDAOImpl() {
cache = CacheBuilder.newBuilder()
.maximumSize(1000) // Max 1000 Einträge
.expireAfterWrite(10, TimeUnit.MINUTES) // Nach 10 Min invalidieren
.build(new CacheLoader<Integer, Product>() {
@Override
public Product load(Integer id) throws Exception {
return loadFromDatabase(id); // Bei Cache-Miss: DB laden
}
});
}
@Override
public Optional<Product> findById(int id) {
try {
return Optional.of(cache.get(id)); // Aus Cache (oder lädt automatisch)
} catch (Exception e) {
return Optional.empty();
}
}
private Product loadFromDatabase(int id) {
// Echter DB-Zugriff
try (Connection conn = dataSource.getConnection()) {
// ...
}
}
}
🔵 BONUS: Security Best Practices
1. SQL-Injection Prevention (nochmal betont!)
// ❌ NIEMALS String-Concatenation! String sql = "SELECT * FROM users WHERE username = '" + username + "'"; // → SQL-Injection möglich! // ✅ IMMER PreparedStatement! String sql = "SELECT * FROM users WHERE username = ?"; pstmt.setString(1, username); // → Sicher!
2. Credentials Management
// ❌ NIEMALS Passwörter im Code!
String dbPassword = "secret123";
// ❌ NIEMALS Passwörter in Konfigurationsdateien (Git!)
<Resource password="secret123" />
// ✅ Environment Variables
String dbPassword = System.getenv("DB_PASSWORD");
// ✅ Payara Password Aliases
asadmin create-password-alias db_password
<Resource password="${ALIAS=db_password}" />
3. Least Privilege Principle
-- ❌ FALSCH: Application-User mit Admin-Rechten GRANT ALL PRIVILEGES ON *.* TO 'app_user'@'%'; -- ✅ RICHTIG: Nur nötige Rechte GRANT SELECT, INSERT, UPDATE, DELETE ON shopdb.* TO 'app_user'@'%'; -- Kein DROP, CREATE, ALTER!
4. Connection Strings absichern
// ❌ UNSICHER: Credentials in URL
String url = "jdbc:mariadb://localhost:3306/shopdb?user=root&password=secret";
// ✅ SICHER: Credentials separat
Properties props = new Properties();
props.setProperty("user", System.getenv("DB_USER"));
props.setProperty("password", System.getenv("DB_PASSWORD"));
props.setProperty("useSSL", "true");
Connection conn = DriverManager.getConnection(url, props);
Weiterführende Ressourcen
JavaBeans Spec:
JSP Actions:
Bean Validation:
🎯 Häufig gestellte Fragen (FAQ)
Frage 1: Wann PreparedStatement vs. CallableStatement?
Antwort:
PreparedStatement:
- ✅ Für normale SQL-Queries (SELECT, INSERT, UPDATE, DELETE)
- ✅ Performance durch Query-Caching
- ✅ SQL-Injection Prevention
- ✅ 99% der Anwendungsfälle
String sql = "SELECT * FROM products WHERE category = ?"; PreparedStatement pstmt = conn.prepareStatement(sql);
CallableStatement:
- ✅ Nur für Stored Procedures
- ✅ Wenn DB-seitige Logik gewünscht
- ✅ OUT-Parameter benötigt
String sql = "{CALL getProductsByCategory(?)}";
CallableStatement cstmt = conn.prepareCall(sql);
Best Practice: Nutze PreparedStatement, außer du hast explizite Stored Procedures!
Frage 2: Wie groß soll mein Connection Pool sein?
Antwort:
Faustregel:
Pool Size = (Anzahl CPU-Kerne * 2) + Anzahl Festplatten Beispiel: 4 CPU-Kerne, 1 HDD Pool Size = (4 * 2) + 1 = 9
Aber: Das ist nur ein Startpunkt!
In der Praxis:
Min Pool Size: 10-20
Max Pool Size: 50-100 (für mittlere Apps)
100-200 (für große Apps)
Wichtig: Mit Monitoring optimieren!
Wenn: waitTime > 100ms Dann: Pool vergrößern Wenn: numConnUsed < 50% von maxTotal Dann: Pool verkleinern
Frage 3: Wie vermeide ich Connection Leaks?
Antwort:
1. IMMER Try-with-resources:
// ✅ RICHTIG
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
// ...
} // Automatisch geschlossen
2. Leak Detection aktivieren:
<Resource
removeAbandonedOnBorrow="true"
removeAbandonedTimeout="300"
logAbandoned="true"
/>
3. Code-Review:
- Suche nach
getConnection()ohne Try-with-resources - Suche nach
close()in catch-Block (kann übersprungen werden!)
Frage 4: Batch vs. einzelne Statements – wann was?
Antwort:
Einzelne Statements:
- ✅ 1-10 Operationen
- ✅ Sofortiges Feedback gewünscht
- ✅ Operationen sind unabhängig
for (Product p : products) {
pstmt.setString(1, p.getName());
pstmt.executeUpdate();
}
Batch Operations:
- ✅ 100+ Operationen
- ✅ Performance kritisch
- ✅ Operationen sind ähnlich
for (Product p : products) {
pstmt.setString(1, p.getName());
pstmt.addBatch();
}
pstmt.executeBatch();
Breaking Point: Ab ~50 Operationen lohnt sich Batch meist!
Frage 5: Wie teste ich DAO-Code mit echter DB?
Antwort:
Option 1: Testcontainers (empfohlen!)
<!-- pom.xml -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mariadb</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
import org.testcontainers.containers.MariaDBContainer;
public class ProductDAOTest {
static MariaDBContainer<?> mariadb = new MariaDBContainer<>("mariadb:11.2");
@BeforeAll
static void setUp() {
mariadb.start();
// DataSource mit Testcontainer-URL erstellen
DataSource dataSource = createDataSource(
mariadb.getJdbcUrl(),
mariadb.getUsername(),
mariadb.getPassword()
);
// Test-Daten einfügen
initTestData(dataSource);
}
@Test
void testFindAll() {
ProductDAO dao = new ProductDAOImpl(dataSource);
List<Product> products = dao.findAll();
assertNotNull(products);
assertTrue(products.size() > 0);
}
@AfterAll
static void tearDown() {
mariadb.stop();
}
}
Vorteile:
- ✅ Echter DB-Test (keine Mocks)
- ✅ Isoliert (jeder Test eigene DB)
- ✅ Automatisch gestartet/gestoppt
Option 2: H2 In-Memory-DB
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
<scope>test</scope>
</dependency>
@BeforeEach
void setUp() {
DataSource dataSource = new JdbcDataSource();
dataSource.setURL("jdbc:h2:mem:testdb");
// Schema erstellen
try (Connection conn = dataSource.getConnection()) {
conn.createStatement().execute(
"CREATE TABLE products (...)"
);
}
}
Vorteile:
- ✅ Sehr schnell
- ✅ Kein Setup nötig
Nachteile:
- ⚠️ H2 ≠ MariaDB (SQL-Dialekt unterschiedlich)
🎉 Tag 10 geschafft – Du bist ein Java Web Developer! 🎓
LEGENDARY! Du hast es geschafft! 🔥🎉
Real talk: 10 Tage Java Web Basic sind durch. Du hast jeden einzelnen Tag gemeistert. Das ist HUGE!
Das hast du in diesem Kurs gelernt:
Woche 1: Grundlagen
- ✅ Java EE Architektur & HTTP-Protokoll
- ✅ Servlets & Servlet API meistern
- ✅ MVC & Model 2 Architektur
- ✅ JSP & Expression Language
- ✅ Java Beans, Actions, Scopes & Direktiven
Woche 2: Production-Ready
- ✅ Include-Action vs Include-Direktive
- ✅ JSTL – Komplett scriptlet-freie JSPs
- ✅ Datasources & Connection Pools
- ✅ Connection Pool Tuning
- ✅ JDBC Performance & Best Practices
Du kannst jetzt:
- 🚀 Vollständige Web-Anwendungen von Grund auf bauen
- 🚀 Datasources & Connection Pools konfigurieren
- 🚀 Production-Ready JDBC-Code schreiben
- 🚀 SQL-Injection verhindern
- 🚀 Performance-Probleme identifizieren & lösen
- 🚀 Transaktionen korrekt handhaben
- 🚀 Batch-Operations nutzen
- 🚀 Code schreiben, der in Production läuft!
Honestly? Das ist ein riesiger Meilenstein! 💪
Du bist jetzt ein Java Web Developer. Nicht Junior. Nicht „in Ausbildung“. Ein echter Developer, der Production-Code schreiben kann.
Was jetzt?
- Projekt bauen: Bau eine echte Anwendung (Shop, Blog, Task-Manager)
- Aufbau-Kurs: Lerne Filter, Listeners, Sessions im Detail
- Frameworks: Spring Boot, Hibernate, REST APIs
- Bewirb dich: Du hast die Skills für Junior-Positionen!
Ein letzter Gedanke:
Als ich bei Java Fleet anfing, hab ich auch mit diesen Grundlagen gestartet. Legacy-Code refactoren, Performance-Probleme lösen, Connection Leaks fixen – das sind die Daily Tasks eines Developers.
Du hast jetzt die Skills dafür. 🎓
Stolz sein ist angesagt! Du hast 80 Stunden in diesen Kurs investiert. Das zeigt Commitment und Durchhaltevermögen. Genau das, was in der IT zählt.
Lowkey: Die meisten brechen bei Tag 3 ab. Du bist bis zum Ende geblieben. Das macht dich besonders! ✨
🔮 Wie geht’s weiter?
Java Web Aufbau-Kurs (10 weitere Tage)
Was dich erwartet:
- Filter & Filter-Chains im Detail
- Listeners (ServletContext, Session, Request)
- Session Management & Session-Tracking
- Cookies & Session Security
- File Upload & Download
- WebSockets für Real-Time Apps
- RESTful Services mit JAX-RS
- JSON Processing
- Authentication & Authorization
- Deployment & Production-Setup
Und dann:
- Spring Boot Grundlagen
- Hibernate/JPA
- Microservices
- Docker & Kubernetes
- CI/CD Pipelines
Der Weg ist klar – du bist bereit dafür! 🚀
📧 Abschluss-Troubleshooting
Problem: PreparedStatement mit ORDER BY funktioniert nicht
// ❌ FALSCH: ORDER BY mit ? String sql = "SELECT * FROM products ORDER BY ?"; pstmt.setString(1, "price"); // Funktioniert NICHT!
Ursache: Parameter können nur für Werte, nicht für Spaltennamen!
Lösung 1: Whitelist
String column = request.getParameter("sort");
// Validierung gegen Whitelist
if (!List.of("name", "price", "stock").contains(column)) {
column = "name"; // Fallback
}
String sql = "SELECT * FROM products ORDER BY " + column;
Statement stmt = conn.createStatement(); // Kein PreparedStatement nötig
Lösung 2: Switch/Case
String sort = request.getParameter("sort");
String sql = switch (sort) {
case "price" -> "SELECT * FROM products ORDER BY price";
case "stock" -> "SELECT * FROM products ORDER BY stock";
default -> "SELECT * FROM products ORDER BY name";
};
Problem: ResultSet already closed
// ❌ FEHLER
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
// ...
}
// ResultSet hier geschlossen!
rs.next(); // SQLException: ResultSet closed
Lösung: Daten INNERHALB des Try-Blocks verarbeiten!
List<Product> products = new ArrayList<>();
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
products.add(mapRowToProduct(rs)); // ✅ Innerhalb Try
}
}
// ✅ Liste nutzen (außerhalb Try-Block)
return products;
Problem: Auto-increment ID nach Insert holen
String sql = "INSERT INTO products (name, price) VALUES (?, ?)";
// ✅ Mit RETURN_GENERATED_KEYS
try (PreparedStatement pstmt = conn.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS)) {
pstmt.setString(1, "Laptop");
pstmt.setDouble(2, 999.99);
int affectedRows = pstmt.executeUpdate();
if (affectedRows > 0) {
try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
if (generatedKeys.next()) {
int id = generatedKeys.getInt(1);
System.out.println("Neue ID: " + id);
}
}
}
}
Viel Erfolg auf deinem weiteren Weg! 🚀
Wenn du Fragen hast oder Unterstützung brauchst, schreib uns: support@java-developer.online
🎖️ Dein Zertifikat
Du hast den Java Web Basic Kurs abgeschlossen!
Hier ist dein (imaginäres, aber wohlverdientes) Zertifikat:
╔══════════════════════════════════════════════════════════╗ ║ ║ ║ 🎓 JAVA WEB BASIC ZERTIFIKAT 🎓 ║ ║ ║ ║ Dieses Zertifikat bestätigt, dass ║ ║ ║ ║ [DEIN NAME] ║ ║ ║ ║ erfolgreich den Java Web Basic Kurs ║ ║ (10 Tage / 80 Stunden) abgeschlossen hat. ║ ║ ║ ║ Skills erworben: ║ ║ ✅ Servlets & Servlet API ║ ║ ✅ JSP & JSTL ║ ║ ✅ MVC/Model 2 Architektur ║ ║ ✅ Datasources & Connection Pools ║ ║ ✅ JDBC Best Practices ║ ║ ✅ Production-Ready Code ║ ║ ║ ║ Du bist jetzt ein Java Web Developer! 🚀 ║ ║ ║ ║ Ausgestellt am: [DATUM] ║ ║ Von: Java Fleet Systems Consulting ║ ║ Instruktor: Elyndra Valen ║ ║ ║ ╚══════════════════════════════════════════════════════════╝
„Der Weg zum Meister beginnt mit dem ersten Schritt. Du hast 10 Tage lang jeden Tag einen neuen Schritt gemacht. Sei stolz auf dich!“ – Elyndra Valen
Jetzt geh raus und bau etwas Großartiges! 🌟

