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


Connection Pools

🗺️ Deine Position im Kurs

TagThemaStatus
1Java EE Überblick & HTTP ✅ Abgeschlossen
2HTTP-Protokoll Vertiefung & Zustandslosigkeit✅ Abgeschlossen
3Servlets & Servlet API✅ Abgeschlossen
4Deployment Descriptor & MVC vs Model 2✅ Abgeschlossen
5JSP & Expression Languages✅ Abgeschlossen
6Java Beans, Actions, Scopes & Direktiven✅ Abgeschlossen
7Include-Action vs Include-Direktive✅ Abgeschlossen
8JSTL – Java Standard Tag Libraries✅ Abgeschlossen
9Java Web und Datenbanken – Datasource✅ Abgeschlossen
→ 10Connection 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:

LevelDirty ReadNon-Repeatable ReadPhantom ReadPerformance
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:

KonzeptTomcat (XML)Payara (Admin Console)
KonfigurationMETA-INF/context.xmlWeb-basierte GUI
FormatXML-AttributeFormular-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?

  1. Projekt bauen: Bau eine echte Anwendung (Shop, Blog, Task-Manager)
  2. Aufbau-Kurs: Lerne Filter, Listeners, Sessions im Detail
  3. Frameworks: Spring Boot, Hibernate, REST APIs
  4. 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! 🌟

Autor

  • Jamal Hassan

    💻 Luca Santoro – Der IT-Ninja

    IT-Support & DevOps Assistant | 29 Jahre | „Ich löse Dinge, bevor sie eskalieren.“

    Wenn bei Java Fleet der Drucker spinnt, das WLAN streikt oder der neue Mitarbeiter kein VPN-Zugriff hat – ist Luca schon unterwegs.
    Er ist der stille Systemretter, der dafür sorgt, dass alle anderen arbeiten können.
    Luca ist nicht laut, nicht hektisch, nicht übertrieben heroisch.
    Er ist einfach da – und das rechtzeitig.

    Seit 2023 unterstützt er das Team im Bereich IT-Support, Netzwerkmanagement und DevOps-Automatisierung.
    Er ist das, was man in der IT selten findet: zuverlässig, gelassen und serviceorientiert – mit technischem Tiefgang und menschlicher Geduld.

    💻 Die Tech-Seite

    Luca denkt in Systemen, nicht in Symptomen.
    Er betreut Hardware, richtet Arbeitsplätze ein, pflegt Benutzerkonten, überwacht Backups und unterstützt das DevOps-Team bei kleineren Deployments.
    Er arbeitet mit Linux, Docker, Active Directory, Ansible und klassischen Office-Netzwerkstrukturen.

    Was ihn besonders macht: Er versteht, dass IT-Support nicht nur Technik ist, sondern Kommunikation.
    Er erklärt, was er tut – klar, freundlich, ohne Fachjargon.
    Und er schreibt sich jeden Fehler auf, um ihn beim nächsten Mal schneller zu beheben.

    „Ich bin kein Feuerwehrmann. Ich bin der, der Rauchmelder installiert.“

    Wenn Franz-Martin über Stabilität redet, meint er oft Systeme, die Luca im Hintergrund pflegt.
    Er hat ein gutes Auge für Details, liebt klare Strukturen und hält Ordnung, wo Chaos droht.

    🌿 Die menschliche Seite

    Luca hat italienisch-deutsche Wurzeln, liebt guten Espresso und hat die entspannte Art eines Menschen, der Probleme ernst nimmt, aber nie dramatisch macht.
    Er ist höflich, hilfsbereit und humorvoll – der Typ Kollege, der leise lacht, wenn etwas schiefläuft, und sagt:

    „Kein Stress. Ich schau’s mir kurz an.“

    Er trägt oft ein leichtes Headset, hört Musik beim Arbeiten und findet in kleinen Routinen seinen Flow.
    Wenn andere nach Feierabend den Laptop zuklappen, prüft er noch schnell den Serverstatus – „nur zur Sicherheit“.

    Cassian sagt: „Er ist die Ruhe im System.“
    Kat nennt ihn „unseren stillen Lifesaver“.
    Und Franz-Martin beschreibt ihn mit einem Augenzwinkern:

    „Luca ist der Grund, warum der Kaffeeautomat läuft – und unser Git-Server auch.“

    🧠 Seine Rolle im Team

    Luca ist das stille Fundament der Java Fleet – derjenige, der sicherstellt, dass alle Systeme, Geräte und Prozesse ineinandergreifen.
    Er ist kein Entwickler im klassischen Sinne, aber ohne ihn gäbe es keine funktionierende Umgebung für die, die entwickeln.
    Seine Arbeit bleibt meist unsichtbar, doch seine Wirkung ist täglich spürbar.

    Er ist der Ansprechpartner, wenn Probleme auftauchen – und noch lieber: bevor sie auftauchen.
    Er dokumentiert, strukturiert, denkt voraus – und erinnert das Team daran, dass Zuverlässigkeit kein Feature ist, sondern eine Haltung.

    ⚡ Superkraft

    Ruhe im System.
    Luca erkennt Fehler, bevor sie eskalieren – und löst sie, bevor sie jemand bemerkt.

    💬 Motto

    „Wenn’s funktioniert und keiner weiß warum – dann hab ich’s repariert.“