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


Datasource

🗺️ 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👉 DU BIST HIER!
10Connection Pools & JDBC in Web-Umgebungen🔒 Noch nicht freigeschaltet

Modul: Java Web Basic
Dein Ziel: Datenbankzugriff in Web-Anwendungen professionell umsetzen!


📋 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-8 abgeschlossen
  • ✅ JSTL beherrschen
  • ✅ JDBC-Grundkenntnisse aus Java SE
  • ✅ SQL-Grundkenntnisse

Tag verpasst?
Spring zurück zu Tag 8, um JSTL zu verstehen. Heute bauen wir darauf auf!

Setup-Probleme?
Schreib uns: support@java-developer.online


⚡ Das Wichtigste in 30 Sekunden

Heute lernst du:

  • ✅ Der Unterschied: JDBC in Java SE vs. Java Web
  • ✅ Was Datasource-Objekte sind und warum sie wichtig sind
  • ✅ Connection Pools verstehen und konfigurieren
  • ✅ JNDI-Namen nutzen und verstehen
  • ✅ Datasource in Payara Server einrichten
  • ✅ Best Practices für Datenbankzugriff in Web-Anwendungen
  • ✅ Echte Daten in deine JSPs bringen!

Am Ende des Tages kannst du:

  • Connection Pools in Payara konfigurieren
  • Datasources per JNDI nutzen
  • Datenbankverbindungen professionell managen
  • DAO-Pattern mit Datasources implementieren
  • Production-Ready Datenbank-Code schreiben
  • Häufige Fehler vermeiden

Zeit-Investment: ~8 Stunden
Schwierigkeitsgrad: Mittel bis Fortgeschritten (aber du schaffst das!)


👋 Willkommen zu Tag 9!

Hi! 👋

Elyndra hier. Heute wird’s richtig praktisch – endlich echte Daten in deiner Webanwendung!

Kurzwiederholung: Challenge von Tag 8

Gestern solltest du JSTL nutzen, um komplett scriptlet-freie JSPs zu erstellen. Falls du es noch nicht gemacht hast, schau dir das nochmal an – heute bauen wir darauf auf!

Heute geht’s um etwas Fundamentales:

Datenbankzugriff in Web-Anwendungen – aber RICHTIG!

Real talk: Als ich bei Java Fleet anfing, hab ich noch DriverManager.getConnection() in meinen Servlets verwendet. Franz-Martin (unser Tech Lead) hat mich zur Seite genommen: „Elyndra, das skaliert nicht. In Production würde das in Minuten zusammenbrechen.“

Er hatte recht. 😅

Das Problem mit klassischem JDBC:

In Java SE machst du das hier:

Connection conn = DriverManager.getConnection(url, user, password);

In Web-Anwendungen ist das eine Katastrophe!

Warum?

  1. Performance: Jede DB-Verbindung dauert 50-200ms
  2. Skalierung: Bei 100 gleichzeitigen Usern = 100 offene Verbindungen
  3. Ressourcen: Datenbanken haben Connection-Limits
  4. Fehleranfälligkeit: Vergessene close()-Aufrufe = Memory Leaks

Die Lösung: Datasources & Connection Pools!

Heute lernst du, wie professionelle Anwendungen Datenbankzugriffe managen.

Let’s do this! 🚀


🟢 GRUNDLAGEN: JDBC in Java SE vs. Java Web

Wie JDBC in Java SE funktioniert

Erinnerst du dich an Java SE?

// Java SE - Standalone Application
public class DatabaseDemo {
    public static void main(String[] args) {
        String url = "jdbc:mariadb://localhost:3306/shopdb";
        String user = "root";
        String password = "secret";
        
        try (Connection conn = DriverManager.getConnection(url, user, password);
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery("SELECT * FROM products")) {
            
            while (rs.next()) {
                System.out.println(rs.getString("name"));
            }
            
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

Was passiert hier?

  1. Verbindung öffnen: DriverManager.getConnection() erstellt eine neue DB-Verbindung
  2. Query ausführen: Statement erstellen und ausführen
  3. Ergebnisse verarbeiten: ResultSet durchlaufen
  4. Verbindung schließen: Try-with-resources schließt alles automatisch

Das funktioniert gut für:

  • Standalone-Anwendungen
  • Batch-Jobs
  • CLI-Tools
  • Desktop-Anwendungen

Aber NICHT für Web-Anwendungen!


Das Problem: JDBC in Web-Anwendungen

Stell dir vor:

@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        // ❌ SCHLECHT: Neue Verbindung bei jedem Request!
        String url = "jdbc:mariadb://localhost:3306/shopdb";
        String user = "root";
        String password = "secret";
        
        try (Connection conn = DriverManager.getConnection(url, user, password)) {
            // ... Daten holen ...
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

Was ist das Problem?

Szenario 1: Niedrige Last (10 User gleichzeitig)

User 1 → Request → Neue Verbindung (50ms) → Query (10ms) → Schließen
User 2 → Request → Neue Verbindung (50ms) → Query (10ms) → Schließen
User 3 → Request → Neue Verbindung (50ms) → Query (10ms) → Schließen
...

Gesamt-Zeit pro Request: 50ms (Verbindung) + 10ms (Query) = 60ms

Problem: 83% der Zeit wird für Verbindungsaufbau verschwendet!


Szenario 2: Hohe Last (1000 User gleichzeitig)

User   1 → Verbindung  1
User   2 → Verbindung  2
User   3 → Verbindung  3
...
User 1000 → Verbindung 1000

Probleme:

  1. Datenbank überlastet: MariaDB/MySQL hat Standard-Limit von ~150 Verbindungen
  2. Server-Crash: Out of Memory wegen zu vielen Verbindungen
  3. Langsame Response: Verbindungen müssen warten
  4. Resource-Verschwendung: Jede Verbindung braucht RAM (~1-2 MB)

Real Talk: In Production würde deine Anwendung nach wenigen Minuten zusammenbrechen! 💥


Die Lösung: Connection Pools

Was ist ein Connection Pool?

Ein Connection Pool ist eine Sammlung von wiederverwendbaren Datenbankverbindungen.

Das Prinzip:

Beim Server-Start:
┌─────────────────────────────────┐
│   Connection Pool (10 Conns)    │
├─────────────────────────────────┤
│ ● Verbindung 1 (idle)           │
│ ● Verbindung 2 (idle)           │
│ ● Verbindung 3 (idle)           │
│ ● Verbindung 4 (idle)           │
│ ● Verbindung 5 (idle)           │
│ ● Verbindung 6 (idle)           │
│ ● Verbindung 7 (idle)           │
│ ● Verbindung 8 (idle)           │
│ ● Verbindung 9 (idle)           │
│ ● Verbindung 10 (idle)          │
└─────────────────────────────────┘

Bei einem Request:

1. User macht Request
2. Servlet fragt Pool: "Gib mir eine Verbindung!"
3. Pool gibt Verbindung 1 (idle → active)
4. Servlet nutzt Verbindung für Query
5. Servlet gibt Verbindung zurück an Pool (active → idle)
6. Nächster User kann Verbindung 1 wiederverwenden!

Die Vorteile:

Schnell: Keine Zeit für Verbindungsaufbau (Verbindungen sind schon offen!)
Skalierbar: Pool hat festes Limit (z.B. 20 Verbindungen max)
Effizient: Verbindungen werden wiederverwendet
Stabil: Keine Out-of-Memory Fehler
Managebar: Application Server überwacht Pool

Beispiel:

1000 User gleichzeitig mit Pool (20 Verbindungen):
- 20 User bekommen sofort eine Verbindung
- Restliche 980 User warten kurz (Queue)
- Sobald eine Verbindung frei wird, bekommt sie der nächste
- KEINE neuen Verbindungen werden erstellt!

Das Ergebnis: Stabile, schnelle Anwendung! ✨


Datasource: Der Zugriff auf den Pool

In Java Web nutzt du keinen DriverManager mehr, sondern eine DataSource!

Was ist eine DataSource?

Eine DataSource ist ein Java-Interface, das Zugriff auf einen Connection Pool bietet.

Wichtig zu verstehen:

DriverManager (Java SE):     → Erstellt neue Verbindung
DataSource (Java Web/EE):    → Gibt Verbindung aus Pool

Der Code mit DataSource:

@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    
    // ✅ RICHTIG: DataSource per Dependency Injection
    @Resource(name = "jdbc/ShopDB")
    private DataSource dataSource;
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        // Hole Verbindung aus Pool (SCHNELL!)
        try (Connection conn = dataSource.getConnection()) {
            // ... Query ausführen ...
        } catch (SQLException e) {
            e.printStackTrace();
        }
        // Connection wird automatisch zurück in Pool gegeben!
    }
}

Was macht dataSource.getConnection()?

  1. Pool prüft: Ist eine Verbindung frei?
  2. Falls ja: Gib sie zurück (dauert ~1ms statt 50ms!)
  3. Falls nein: Warte, bis eine frei wird (oder erstelle neue, wenn unter Max-Limit)

Beim close():

conn.close();  // Gibt Verbindung zurück an Pool (schließt sie NICHT!)

Wichtig: close() schließt die Verbindung NICHT wirklich, sondern gibt sie nur zurück in den Pool zur Wiederverwendung!


🟢 GRUNDLAGEN: JNDI – Java Naming and Directory Interface

Was ist JNDI?

JNDI = Java Naming and Directory Interface

Denk an JNDI wie an ein Telefonbuch für Ressourcen:

Telefonbuch:
"Pizza-Haus"    → 030-12345678
"Franz-Martin"  → 0176-9876543
"Dentist"       → 030-55555555

JNDI:
"jdbc/ShopDB"           → DataSource für ShopDB
"jdbc/UserDB"           → DataSource für UserDB
"mail/MailSession"      → Mail-Session

JNDI ermöglicht:

  • Ressourcen einen Namen geben (z.B. jdbc/ShopDB)
  • Ressourcen per Namen abrufen (statt Hardcoding)
  • Zentrale Konfiguration im Application Server

Warum JNDI verwenden?

Ohne JNDI (schlecht!):

// ❌ Hardcoded in jedem Servlet!
String url = "jdbc:mariadb://localhost:3306/shopdb";
String user = "root";
String password = "secret123";
Connection conn = DriverManager.getConnection(url, user, password);

Probleme:

  1. Sicherheit: Passwörter im Code! 😱
  2. Wartbarkeit: Passwort ändern = Code in 50 Klassen ändern
  3. Umgebungen: Dev, Test, Production haben unterschiedliche URLs
  4. Deployment: Code neu kompilieren bei DB-Änderung

Mit JNDI (richtig!):

// ✅ JNDI-Lookup per Name
@Resource(name = "jdbc/ShopDB")
private DataSource dataSource;

// Oder manuell:
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("jdbc/ShopDB");
Connection conn = ds.getConnection();

Vorteile:

  1. Sicherheit: Keine Passwörter im Code
  2. Wartbarkeit: Passwort im Server ändern, nicht im Code
  3. Flexibilität: Verschiedene Datasources per Umgebung
  4. Deployment: Kein Code-Rebuild bei DB-Änderung

JNDI-Namen-Konventionen:

jdbc/...     → Datasources (Datenbanken)
jms/...      → JMS Queues/Topics (Messaging)
mail/...     → Mail-Sessions
java:comp/env/...  → Environment Entries

Standard: Datasource-Namen beginnen mit jdbc/, z.B.:

  • jdbc/ShopDB
  • jdbc/UserDB
  • jdbc/ProductionDB

JNDI-Lookup: Wie funktioniert das?

Der Prozess:

1. Application Server startet
   ↓
2. Liest server-config.xml (z.B. domain.xml in Payara)
   ↓
3. Erstellt Connection Pools
   ↓
4. Registriert Datasources in JNDI unter Namen
   (z.B. "jdbc/ShopDB" → Datasource-Objekt)
   ↓
5. Deine Anwendung macht Lookup:
   @Resource(name = "jdbc/ShopDB")
   ↓
6. Application Server injiziert Datasource
   ↓
7. Du benutzt dataSource.getConnection()

Was macht die Annotation @Resource?

@Resource(name = "jdbc/ShopDB")
private DataSource dataSource;

Beim Deployment:

  1. Payara scannt deine Klasse
  2. Sieht @Resource mit name=“jdbc/ShopDB“
  3. Macht JNDI-Lookup: lookup("jdbc/ShopDB")
  4. Injiziert DataSource-Objekt in dataSource-Variable

Das ist Dependency Injection! Der Application Server übernimmt die Initialisierung für dich.


🟢 GRUNDLAGEN: Connection Pool in Payara einrichten

Jetzt wird’s praktisch! Wir erstellen einen Connection Pool und eine Datasource in Payara.

Schritt 1: Datenbank vorbereiten

MariaDB starten (XAMPP oder Standalone):

# XAMPP Control Panel: Start MySQL/MariaDB

# ODER: Standalone MariaDB starten
sudo systemctl start mariadb

Datenbank erstellen:

-- Mit MySQL Workbench, phpMyAdmin oder CLI verbinden

CREATE DATABASE shopdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

USE shopdb;

CREATE TABLE products (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    description TEXT,
    price DECIMAL(10, 2) NOT NULL,
    stock INT NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Testdaten einfügen
INSERT INTO products (name, description, price, stock) VALUES
('Laptop', 'High-performance laptop for developers', 1299.99, 15),
('Mouse', 'Wireless ergonomic mouse', 29.99, 50),
('Keyboard', 'Mechanical keyboard with RGB', 89.99, 30),
('Monitor', '27-inch 4K display', 399.99, 10),
('Headphones', 'Noise-cancelling headphones', 199.99, 25);

Prüfen:

SELECT * FROM products;

Du solltest 5 Produkte sehen!


Schritt 2: JDBC-Treiber in Payara installieren

Payara braucht den MariaDB JDBC-Treiber!

Download:

  1. Gehe zu: https://mariadb.com/downloads/connectors/connectors-data-access/java8-connector
  2. Lade MariaDB Connector/J herunter (z.B. mariadb-java-client-3.3.0.jar)

ODER: Maven Dependency:

<!-- pom.xml -->
<dependency>
    <groupId>org.mariadb.jdbc</groupId>
    <artifactId>mariadb-java-client</artifactId>
    <version>3.3.0</version>
</dependency>

Aber: Du musst den Treiber auch in Payara verfügbar machen!


Installation in Payara:

Option 1: Global (für alle Anwendungen)

1. Stoppe Payara Server
2. Kopiere mariadb-java-client-3.3.0.jar nach:
   C:\payara6\glassfish\lib\
   (oder /opt/payara6/glassfish/lib/ auf Linux)
3. Starte Payara neu

Option 2: Per Anwendung (empfohlen für Entwicklung)

<!-- pom.xml: Dependency mit scope "provided" -->
<dependency>
    <groupId>org.mariadb.jdbc</groupId>
    <artifactId>mariadb-java-client</artifactId>
    <version>3.3.0</version>
    <scope>runtime</scope>
</dependency>

Dann wird der Treiber in dein WAR-File gepackt (WEB-INF/lib/).

Für heute: Nutze Option 2 (einfacher für Entwicklung).


Schritt 3: Payara Admin Console öffnen

Payara Server starten:

NetBeans → Services → Servers → Payara Server → Rechtsklick → Start

Admin Console öffnen:

  1. Browser öffnen: http://localhost:4848
  2. Login: (Standard: kein Passwort erforderlich)

Du siehst jetzt das Payara Admin Console Dashboard!


Schritt 4: JDBC Connection Pool erstellen

Navigation in Admin Console:

Resources → JDBC → JDBC Connection Pools → [New]

Pool-Konfiguration:

FeldWert
Pool NameShopDBPool
Resource Typejavax.sql.DataSource
Database Driver VendorMariaDB (oder wähle „MySQL“ wenn MariaDB nicht aufgelistet)

Klick auf „Next“


Erweiterte Eigenschaften:

Scrolle runter zu „Additional Properties“ und setze:

PropertyValue
URLjdbc:mariadb://localhost:3306/shopdb
Userroot (dein DB-User)
PassworddeinPasswort (dein DB-Passwort)
ServerNamelocalhost (kann auch leer bleiben)
PortNumber3306
DatabaseNameshopdb

Wichtige Pool-Einstellungen:

EinstellungWertErklärung
Initial Pool Size5Verbindungen beim Start
Min Pool Size5Minimum immer offen
Max Pool Size20Maximum gleichzeitig
Idle Timeout300 (Sekunden)Verbindung schließen nach 5 Min Inaktivität

Klick auf „Finish“


Pool testen:

  1. Gehe zurück zu „JDBC Connection Pools“
  2. Klick auf „ShopDBPool“
  3. Klick auf Tab „Advanced“
  4. Scroll runter und klick auf „Ping“ Button

Erfolgsmeldung: „Ping Succeeded!“
Fehlermeldung? Prüfe URL, User, Passwort!


Schritt 5: JDBC Datasource (JNDI-Ressource) erstellen

Navigation:

Resources → JDBC → JDBC Resources → [New]

Datasource-Konfiguration:

FeldWert
JNDI Namejdbc/ShopDB
Pool NameShopDBPool (aus Dropdown wählen)
StatusEnabled (✓)

Klick auf „OK“

Das war’s! 🎉


Was haben wir gerade gemacht?

1. Connection Pool erstellt:
   - Name: ShopDBPool
   - 5-20 Verbindungen zu MariaDB
   - Verbindung zu localhost:3306/shopdb

2. JNDI-Ressource erstellt:
   - Name: jdbc/ShopDB
   - Verknüpft mit ShopDBPool

3. Jetzt können wir in Code:
   @Resource(name = "jdbc/ShopDB")
   private DataSource dataSource;

In der Praxis bedeutet das:

Deine Anwendung kann jetzt per jdbc/ShopDB auf den Connection Pool zugreifen, ohne die DB-Details (URL, User, Passwort) zu kennen!


🟢 GRUNDLAGEN: Datasource in Java nutzen

Jetzt nutzen wir die Datasource im Code!

Variante 1: @Resource Annotation (empfohlen!)

package com.shop.servlet;

import jakarta.annotation.Resource;
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 javax.sql.DataSource;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.SQLException;

@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    
    // ✅ Datasource per Dependency Injection
    @Resource(name = "jdbc/ShopDB")
    private DataSource dataSource;
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery("SELECT * FROM products")) {
            
            // Liste für Produkte erstellen
            java.util.List<String> productNames = new java.util.ArrayList<>();
            
            while (rs.next()) {
                productNames.add(rs.getString("name"));
            }
            
            // An JSP weiterleiten
            request.setAttribute("productNames", productNames);
            request.getRequestDispatcher("/WEB-INF/views/products.jsp")
                   .forward(request, response);
            
        } catch (SQLException e) {
            throw new ServletException("Database error", e);
        }
    }
}

Was macht dieser Code?

Die Annotation @Resource:

@Resource(name = "jdbc/ShopDB")
private DataSource dataSource;
  • Beim Deployment macht Payara einen JNDI-Lookup nach jdbc/ShopDB
  • Injiziert das DataSource-Objekt in die dataSource-Variable
  • Du musst nichts tun! Payara übernimmt alles

Der Datenbankzugriff:

try (Connection conn = dataSource.getConnection();
     Statement stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery("SELECT * FROM products")) {
    
    while (rs.next()) {
        productNames.add(rs.getString("name"));
    }
}

Was passiert hier?

  1. dataSource.getConnection(): Holt Verbindung aus Pool (~1ms!)
  2. Statement erstellen: Wie in Java SE
  3. Query ausführen: Wie in Java SE
  4. Ergebnisse verarbeiten: Wie in Java SE
  5. Try-with-resources: Gibt Verbindung zurück in Pool

Wichtig: conn.close() gibt die Verbindung zurück an den Pool (schließt sie NICHT wirklich)!


Variante 2: Manuelle JNDI-Lookup (falls nötig)

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    
    private DataSource dataSource;
    
    @Override
    public void init() throws ServletException {
        try {
            // JNDI-Lookup manuell
            Context ctx = new InitialContext();
            dataSource = (DataSource) ctx.lookup("java:comp/env/jdbc/ShopDB");
        } catch (NamingException e) {
            throw new ServletException("JNDI lookup failed", e);
        }
    }
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        try (Connection conn = dataSource.getConnection()) {
            // ... wie vorher ...
        } catch (SQLException e) {
            throw new ServletException("Database error", e);
        }
    }
}

Wann diese Variante nutzen?

  • Wenn @Resource nicht funktioniert (selten)
  • In älteren Projekten
  • Wenn dynamische JNDI-Namen benötigt werden

Best Practice: Nutze @Resource! Einfacher und moderner.


🟡 PROFESSIONALS: DAO-Pattern mit Datasource

Jetzt implementieren wir das DAO-Pattern professionell!

Was ist das Problem?

// ❌ SCHLECHT: DB-Code direkt im Servlet!
@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    
    @Resource(name = "jdbc/ShopDB")
    private DataSource dataSource;
    
    protected void doGet(...) {
        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery("SELECT * FROM products")) {
            
            List<Product> products = new ArrayList<>();
            while (rs.next()) {
                Product p = new Product();
                p.setId(rs.getInt("id"));
                p.setName(rs.getString("name"));
                p.setPrice(rs.getDouble("price"));
                products.add(p);
            }
            
            request.setAttribute("products", products);
            // ...
        }
    }
}

Probleme:

  1. Vermischte Verantwortlichkeiten: Servlet macht Controller UND Data Access
  2. Code-Duplikation: Gleicher Code in jedem Servlet
  3. Schwer testbar: Kann Servlet nicht ohne DB testen
  4. Nicht wiederverwendbar: Nur in diesem Servlet nutzbar

Die Lösung: DAO-Pattern!


DAO-Struktur erstellen

1. Model (POJO/JavaBean):

package com.shop.model;

import java.math.BigDecimal;
import java.time.LocalDateTime;

public class Product {
    
    private int id;
    private String name;
    private String description;
    private BigDecimal price;
    private int stock;
    private LocalDateTime createdAt;
    
    // Default Constructor (wichtig für JSP!)
    public Product() {}
    
    // Constructor mit allen Feldern
    public Product(int id, String name, String description, 
                   BigDecimal price, int stock, LocalDateTime createdAt) {
        this.id = id;
        this.name = name;
        this.description = description;
        this.price = price;
        this.stock = stock;
        this.createdAt = createdAt;
    }
    
    // Getters & Setters
    public int getId() { 
        return id; 
    }
    
    public void setId(int id) { 
        this.id = id; 
    }
    
    public String getName() { 
        return name; 
    }
    
    public void setName(String name) { 
        this.name = name; 
    }
    
    public String getDescription() { 
        return description; 
    }
    
    public void setDescription(String description) { 
        this.description = description; 
    }
    
    public BigDecimal getPrice() { 
        return price; 
    }
    
    public void setPrice(BigDecimal price) { 
        this.price = price; 
    }
    
    public int getStock() { 
        return stock; 
    }
    
    public void setStock(int stock) { 
        this.stock = stock; 
    }
    
    public LocalDateTime getCreatedAt() { 
        return createdAt; 
    }
    
    public void setCreatedAt(LocalDateTime createdAt) { 
        this.createdAt = createdAt; 
    }
    
    @Override
    public String toString() {
        return "Product{id=" + id + ", name='" + name + "', price=" + price + "}";
    }
}

Warum BigDecimal für Preis?

// ❌ FALSCH: double für Geld
double price = 19.99;
double total = price * 3;  // 59.97000000000001 (Rundungsfehler!)

// ✅ RICHTIG: BigDecimal
BigDecimal price = new BigDecimal("19.99");
BigDecimal total = price.multiply(new BigDecimal("3"));  // 59.97 (genau!)

Best Practice: Nutze IMMER BigDecimal für Geldbeträge!


2. DAO-Interface (optional, aber empfohlen):

package com.shop.dao;

import com.shop.model.Product;
import java.util.List;
import java.util.Optional;

public interface ProductDAO {
    
    /**
     * Findet alle Produkte
     */
    List<Product> findAll();
    
    /**
     * Findet Produkt per ID
     */
    Optional<Product> findById(int id);
    
    /**
     * Sucht Produkte per Name
     */
    List<Product> searchByName(String query);
    
    /**
     * Speichert neues Produkt
     */
    void save(Product product);
    
    /**
     * Aktualisiert bestehendes Produkt
     */
    void update(Product product);
    
    /**
     * Löscht Produkt per ID
     */
    void deleteById(int id);
}

Warum ein Interface?

Austauschbarkeit: JDBC → JPA einfach möglich
Testbarkeit: Mock-Implementierung für Tests
Verträge: Definiert klare API
Best Practice: Standard in Java EE


3. DAO-Implementierung:

package com.shop.dao;

import com.shop.model.Product;
import jakarta.annotation.Resource;
import jakarta.ejb.Stateless;
import javax.sql.DataSource;
import java.math.BigDecimal;
import java.sql.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Stateless  // Macht diese Klasse zu einem Managed Bean
public class ProductDAOImpl implements ProductDAO {
    
    // Datasource per Dependency Injection
    @Resource(name = "jdbc/ShopDB")
    private DataSource dataSource;
    
    @Override
    public List<Product> findAll() {
        List<Product> products = new ArrayList<>();
        String sql = "SELECT * FROM products ORDER BY name";
        
        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {
            
            while (rs.next()) {
                products.add(mapRowToProduct(rs));
            }
            
        } catch (SQLException e) {
            throw new RuntimeException("Error fetching all products", e);
        }
        
        return products;
    }
    
    @Override
    public Optional<Product> findById(int id) {
        String sql = "SELECT * FROM products WHERE id = ?";
        
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            
            pstmt.setInt(1, id);
            
            try (ResultSet rs = pstmt.executeQuery()) {
                if (rs.next()) {
                    return Optional.of(mapRowToProduct(rs));
                }
            }
            
        } catch (SQLException e) {
            throw new RuntimeException("Error fetching product by id: " + id, e);
        }
        
        return Optional.empty();
    }
    
    @Override
    public List<Product> searchByName(String query) {
        List<Product> products = new ArrayList<>();
        String sql = "SELECT * FROM products WHERE name LIKE ? ORDER BY name";
        
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            
            pstmt.setString(1, "%" + query + "%");
            
            try (ResultSet rs = pstmt.executeQuery()) {
                while (rs.next()) {
                    products.add(mapRowToProduct(rs));
                }
            }
            
        } catch (SQLException e) {
            throw new RuntimeException("Error searching products by name: " + query, e);
        }
        
        return products;
    }
    
    @Override
    public void save(Product product) {
        String sql = "INSERT INTO products (name, description, price, stock) VALUES (?, ?, ?, ?)";
        
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
            
            pstmt.setString(1, product.getName());
            pstmt.setString(2, product.getDescription());
            pstmt.setBigDecimal(3, product.getPrice());
            pstmt.setInt(4, product.getStock());
            
            int affectedRows = pstmt.executeUpdate();
            
            if (affectedRows == 0) {
                throw new SQLException("Creating product failed, no rows affected.");
            }
            
            // ID des neuen Produkts holen
            try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
                if (generatedKeys.next()) {
                    product.setId(generatedKeys.getInt(1));
                } else {
                    throw new SQLException("Creating product failed, no ID obtained.");
                }
            }
            
        } catch (SQLException e) {
            throw new RuntimeException("Error saving product", e);
        }
    }
    
    @Override
    public void update(Product product) {
        String sql = "UPDATE products SET name = ?, description = ?, price = ?, stock = ? WHERE id = ?";
        
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            
            pstmt.setString(1, product.getName());
            pstmt.setString(2, product.getDescription());
            pstmt.setBigDecimal(3, product.getPrice());
            pstmt.setInt(4, product.getStock());
            pstmt.setInt(5, product.getId());
            
            int affectedRows = pstmt.executeUpdate();
            
            if (affectedRows == 0) {
                throw new SQLException("Updating product failed, product not found.");
            }
            
        } catch (SQLException e) {
            throw new RuntimeException("Error updating product", e);
        }
    }
    
    @Override
    public void deleteById(int id) {
        String sql = "DELETE FROM products WHERE id = ?";
        
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            
            pstmt.setInt(1, id);
            
            int affectedRows = pstmt.executeUpdate();
            
            if (affectedRows == 0) {
                throw new SQLException("Deleting product failed, product not found.");
            }
            
        } catch (SQLException e) {
            throw new RuntimeException("Error deleting product by id: " + id, e);
        }
    }
    
    /**
     * Hilfsmethode: Konvertiert ResultSet-Row zu Product-Objekt
     */
    private Product mapRowToProduct(ResultSet rs) throws SQLException {
        Product product = new Product();
        product.setId(rs.getInt("id"));
        product.setName(rs.getString("name"));
        product.setDescription(rs.getString("description"));
        product.setPrice(rs.getBigDecimal("price"));
        product.setStock(rs.getInt("stock"));
        
        // Timestamp zu LocalDateTime konvertieren
        Timestamp timestamp = rs.getTimestamp("created_at");
        if (timestamp != null) {
            product.setCreatedAt(timestamp.toLocalDateTime());
        }
        
        return product;
    }
}

Was macht dieser Code?

Die Annotation @Stateless:

@Stateless
public class ProductDAOImpl implements ProductDAO {
  • Macht die Klasse zu einem Enterprise JavaBean (EJB)
  • Der Application Server managed die Instanz
  • Ermöglicht Dependency Injection in anderen Klassen

Ohne @Stateless müsstest du manuell Instanzen erstellen!


Die mapRowToProduct() Hilfsmethode:

private Product mapRowToProduct(ResultSet rs) throws SQLException {
    Product product = new Product();
    product.setId(rs.getInt("id"));
    product.setName(rs.getString("name"));
    // ... rest ...
    return product;
}

Warum eine separate Methode?

DRY-Prinzip: Code nicht wiederholen
Wartbarkeit: Änderungen nur an einer Stelle
Lesbarkeit: Mapping-Logik klar getrennt

Das Prinzip: Jede Methode macht genau eine Sache!


Prepared Statements vs. Statement:

// ❌ UNSICHER: SQL-Injection möglich!
String sql = "SELECT * FROM products WHERE name = '" + userInput + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);

// ✅ SICHER: PreparedStatement
String sql = "SELECT * FROM products WHERE name = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, userInput);  // Escaped automatisch!
ResultSet rs = pstmt.executeQuery();

Best Practice: IMMER PreparedStatement verwenden, nie String-Concatenation!


4. Servlet mit DAO:

package com.shop.servlet;

import com.shop.dao.ProductDAO;
import com.shop.model.Product;
import jakarta.ejb.EJB;
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.util.List;

@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    
    // DAO per Dependency Injection
    @EJB
    private ProductDAO productDAO;
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        // Parameter für Suche (optional)
        String search = request.getParameter("search");
        
        List<Product> products;
        
        if (search != null && !search.trim().isEmpty()) {
            // Suche nach Name
            products = productDAO.searchByName(search);
        } else {
            // Alle Produkte
            products = productDAO.findAll();
        }
        
        // An JSP weiterleiten
        request.setAttribute("products", products);
        request.getRequestDispatcher("/WEB-INF/views/products.jsp")
               .forward(request, response);
    }
}

Was macht @EJB?

@EJB
private ProductDAO productDAO;
  • Injiziert die ProductDAOImpl-Instanz
  • Application Server managt die Instanz
  • Du musst kein new ProductDAOImpl() schreiben!

Das Servlet ist jetzt SAUBER:

✅ Keine DB-Logik im Controller
✅ Wiederverwendbarer DAO
✅ Testbar (Mock-DAO möglich)
✅ Klare Verantwortlichkeiten


5. JSP mit JSTL:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Products - Shop</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            max-width: 1200px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f5f5f5;
        }
        h1 {
            color: #333;
            border-bottom: 3px solid #007bff;
            padding-bottom: 10px;
        }
        .search-form {
            margin: 20px 0;
        }
        .search-form input[type="text"] {
            padding: 10px;
            width: 300px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        .search-form button {
            padding: 10px 20px;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        .search-form button:hover {
            background-color: #0056b3;
        }
        .product-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
            gap: 20px;
            margin-top: 20px;
        }
        .product-card {
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        .product-card h3 {
            color: #007bff;
            margin-top: 0;
        }
        .product-card .description {
            color: #666;
            font-size: 0.9em;
            margin: 10px 0;
        }
        .product-card .price {
            font-size: 1.5em;
            color: #28a745;
            font-weight: bold;
        }
        .product-card .stock {
            color: #666;
            font-size: 0.9em;
            margin-top: 10px;
        }
        .product-card .stock.low {
            color: #dc3545;
            font-weight: bold;
        }
        .no-products {
            text-align: center;
            padding: 40px;
            background: white;
            border-radius: 8px;
            color: #666;
        }
    </style>
</head>
<body>
    <h1>Our Products</h1>
    
    <!-- Suchformular -->
    <form action="products" method="get" class="search-form">
        <input type="text" name="search" placeholder="Search products..." 
               value="${param.search}">
        <button type="submit">Search</button>
        <c:if test="${not empty param.search}">
            <a href="products">Clear</a>
        </c:if>
    </form>
    
    <!-- Produktliste -->
    <c:choose>
        <c:when test="${not empty products}">
            <div class="product-grid">
                <c:forEach var="product" items="${products}">
                    <div class="product-card">
                        <h3>${product.name}</h3>
                        <p class="description">${product.description}</p>
                        <p class="price">
                            <fmt:formatNumber value="${product.price}" 
                                            type="currency" 
                                            currencySymbol="€" />
                        </p>
                        <p class="stock ${product.stock < 10 ? 'low' : ''}">
                            <c:choose>
                                <c:when test="${product.stock == 0}">
                                    Out of Stock
                                </c:when>
                                <c:when test="${product.stock < 10}">
                                    Only ${product.stock} left!
                                </c:when>
                                <c:otherwise>
                                    ${product.stock} in stock
                                </c:otherwise>
                            </c:choose>
                        </p>
                    </div>
                </c:forEach>
            </div>
        </c:when>
        <c:otherwise>
            <div class="no-products">
                <p>No products found.</p>
                <c:if test="${not empty param.search}">
                    <p>Try a different search term or 
                       <a href="products">view all products</a>.
                    </p>
                </c:if>
            </div>
        </c:otherwise>
    </c:choose>
</body>
</html>

Was macht diese JSP?

  1. Suchformular: Ermöglicht Suche nach Produktnamen
  2. Produktliste: Zeigt alle Produkte in Grid-Layout
  3. Formatierung: Preis als Währung, Stock-Status farbig
  4. Kein Java-Code: Nur JSTL und EL!

Das Ergebnis: Eine professionelle, produktionsreife Produktliste! ✨


🟡 PROFESSIONALS: Connection Management Best Practices

1. IMMER Try-with-Resources nutzen

// ✅ RICHTIG: Try-with-resources
try (Connection conn = dataSource.getConnection();
     PreparedStatement pstmt = conn.prepareStatement(sql);
     ResultSet rs = pstmt.executeQuery()) {
    
    // Daten verarbeiten
}
// Connection wird AUTOMATISCH geschlossen (zurück in Pool)

// ❌ FALSCH: Manuelles close()
Connection conn = null;
try {
    conn = dataSource.getConnection();
    // ...
} finally {
    if (conn != null) {
        conn.close();  // Vergessen = Memory Leak!
    }
}

Warum Try-with-Resources?

Automatisch: Ressourcen werden IMMER geschlossen
Sicherer: Auch bei Exceptions
Lesbarer: Weniger Boilerplate-Code


2. Connection NICHT als Instanzvariable speichern!

// ❌ FALSCH: Connection als Field
@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    
    @Resource(name = "jdbc/ShopDB")
    private DataSource dataSource;
    
    private Connection conn;  // ❌ FALSCH!
    
    @Override
    public void init() {
        try {
            conn = dataSource.getConnection();  // ❌ SEHR FALSCH!
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    
    protected void doGet(...) {
        // conn nutzen...  ❌ NICHT THREAD-SAFE!
    }
}

Warum ist das falsch?

  1. Thread-Safety: Servlets sind NICHT thread-safe für Fields!
  2. Connection-Leak: Verbindung wird nie zurückgegeben
  3. Performance: Pool kann keine Verbindungen wiederverwenden

✅ RICHTIG:

@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    
    @Resource(name = "jdbc/ShopDB")
    private DataSource dataSource;  // ✅ DataSource als Field OK!
    
    protected void doGet(...) {
        // ✅ Connection nur in Methode holen!
        try (Connection conn = dataSource.getConnection()) {
            // ...
        }
    }
}

Das Prinzip:

  • DataSource als Field: ✅ OK (thread-safe, stateless)
  • Connection als Field: ❌ NIEMALS (nicht thread-safe!)

3. Transaktionen richtig handhaben

Szenario: Zwei DB-Operationen sollen atomar sein (beide erfolgreich oder beide rückgängig).

@Override
public void transferMoney(int fromAccount, int toAccount, BigDecimal amount) {
    Connection conn = null;
    
    try {
        conn = dataSource.getConnection();
        
        // ✅ Auto-Commit deaktivieren
        conn.setAutoCommit(false);
        
        // Operation 1: Geld abbuchen
        try (PreparedStatement pstmt1 = conn.prepareStatement(
                "UPDATE accounts SET balance = balance - ? WHERE id = ?")) {
            pstmt1.setBigDecimal(1, amount);
            pstmt1.setInt(2, fromAccount);
            pstmt1.executeUpdate();
        }
        
        // Operation 2: Geld gutschreiben
        try (PreparedStatement pstmt2 = conn.prepareStatement(
                "UPDATE accounts SET balance = balance + ? WHERE id = ?")) {
            pstmt2.setBigDecimal(1, amount);
            pstmt2.setInt(2, toAccount);
            pstmt2.executeUpdate();
        }
        
        // ✅ Commit: Beide Operationen erfolgreich
        conn.commit();
        
    } catch (SQLException e) {
        // ✅ Rollback: Fehler passiert, alles rückgängig
        if (conn != null) {
            try {
                conn.rollback();
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        }
        throw new RuntimeException("Transfer failed", e);
        
    } finally {
        // ✅ Auto-Commit wieder aktivieren & Connection schließen
        if (conn != null) {
            try {
                conn.setAutoCommit(true);
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

Was macht dieser Code?

  1. Auto-Commit aus: conn.setAutoCommit(false)
  2. Operationen ausführen: Beide SQL-Statements
  3. Bei Erfolg: conn.commit() – Alles wird gespeichert
  4. Bei Fehler: conn.rollback() – Alles wird rückgängig gemacht
  5. Immer: Auto-Commit wieder an, Connection schließen

Das Prinzip: Transaktionen für kritische Multi-Step-Operationen!


4. Connection Timeouts konfigurieren

In der Admin Console:

JDBC Connection Pool → ShopDBPool → Advanced Tab

Connection Validation:
- Validation Method: table
- Validation Table Name: dual  (oder eine eigene Test-Tabelle)

Pool Settings:
- Idle Timeout: 300 seconds (5 Minuten)
- Max Wait Time: 30000 milliseconds (30 Sekunden)

Was bedeuten diese Einstellungen?

Idle Timeout:

  • Verbindungen, die länger als 5 Min nicht genutzt werden, werden geschlossen
  • Spart Ressourcen bei niedriger Last

Max Wait Time:

  • Wenn Pool voll ist (alle Verbindungen in Benutzung), wie lange wartet ein Request?
  • Nach 30 Sek → Timeout-Exception

Connection Validation:

  • Prüft ob Verbindung noch aktiv (z.B. SELECT 1 FROM dual)
  • Verhindert „Stale Connection“ Fehler

5. Monitoring & Logging

Connection Pool überwachen:

Payara Admin Console → Monitoring → Server (server) → Resources → JDBC

Du siehst:

  • Current Pool Size: Aktuell offene Verbindungen
  • Num Connections Used: In Benutzung
  • Num Connections Free: Verfügbar
  • Wait Time: Durchschnittliche Wartezeit

Logging aktivieren:

Payara Admin Console → Configurations → server-config → Logger Settings

Log Levels:
- jakarta.enterprise.resource.resourceadapter: INFO
- org.glassfish.jdbcruntime: FINE  (für Debug)

In Code loggen:

import java.util.logging.Logger;

public class ProductDAOImpl implements ProductDAO {
    
    private static final Logger LOGGER = 
        Logger.getLogger(ProductDAOImpl.class.getName());
    
    @Override
    public List<Product> findAll() {
        LOGGER.info("Fetching all products");
        
        try {
            // ...
            LOGGER.info("Found " + products.size() + " products");
            return products;
        } catch (SQLException e) {
            LOGGER.severe("Error fetching products: " + e.getMessage());
            throw new RuntimeException(e);
        }
    }
}

Best Practice: Logging für Production-Debugging!


🟡 PROFESSIONALS: Alternative – Connection Pool in Apache Tomcat

Tomcat vs. Payara – Die Unterschiede

Wichtig zu verstehen:

Apache Tomcat ist ein Servlet Container (nur Web-Tier), während Payara/GlassFish ein vollständiger Application Server ist (Web + EJB + JMS + mehr).

Das bedeutet für Connection Pools:

AspektPayara/GlassFishApache Tomcat
KonfigurationAdmin Console (GUI)XML-Dateien
JNDI-Namejdbc/ShopDBjava:comp/env/jdbc/ShopDB
Pool-ImplementierungHikariCP (default)Apache Commons DBCP2 oder Tomcat JDBC Pool
EJB-Support✅ Ja (@EJB, @Stateless)❌ Nein (nur Servlets)
Dependency Injection@Resource, @Inject@Resource (mit Einschränkungen)

Wann Tomcat nutzen?

✅ Kleinere Web-Anwendungen ohne EJBs
✅ Microservices (leichtgewichtig)
✅ Wenn nur Servlets + JSP gebraucht werden
✅ Entwicklungsumgebung (schneller Start)

Wann Payara/GlassFish nutzen?

✅ Enterprise-Anwendungen
✅ Wenn EJBs, JMS, CDI benötigt werden
✅ Full Jakarta EE Features
✅ Production mit komplexer Architektur


Connection Pool in Tomcat einrichten

Schritt 1: JDBC-Treiber bereitstellen

Kopiere den MariaDB JDBC-Treiber nach:

C:\apache-tomcat-10.1.x\lib\mariadb-java-client-3.3.0.jar

ODER (für Maven-Projekte):

<!-- pom.xml -->
<dependency>
    <groupId>org.mariadb.jdbc</groupId>
    <artifactId>mariadb-java-client</artifactId>
    <version>3.3.0</version>
    <scope>runtime</scope>
</dependency>

Schritt 2: context.xml konfigurieren

Option A: Globale Konfiguration (für alle Anwendungen)

Datei: $CATALINA_HOME/conf/context.xml

<?xml version="1.0" encoding="UTF-8"?>
<Context>

    <!-- WatchedResource wird von Tomcat überwacht -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
    <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>

    <!-- Connection Pool Definition -->
    <Resource 
        name="jdbc/ShopDB"
        auth="Container"
        type="javax.sql.DataSource"
        
        driverClassName="org.mariadb.jdbc.Driver"
        url="jdbc:mariadb://localhost:3306/shopdb"
        username="root"
        password="secret"
        
        maxTotal="20"
        maxIdle="10"
        minIdle="5"
        maxWaitMillis="30000"
        
        testOnBorrow="true"
        validationQuery="SELECT 1"
        validationQueryTimeout="3"
        
        removeAbandonedOnBorrow="true"
        removeAbandonedTimeout="60"
        logAbandoned="true"
    />

</Context>

Option B: Anwendungs-spezifische Konfiguration (empfohlen!)

Datei: src/main/webapp/META-INF/context.xml (in deinem Projekt)

<?xml version="1.0" encoding="UTF-8"?>
<Context>

    <!-- Connection Pool für diese Anwendung -->
    <Resource 
        name="jdbc/ShopDB"
        auth="Container"
        type="javax.sql.DataSource"
        
        factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
        
        driverClassName="org.mariadb.jdbc.Driver"
        url="jdbc:mariadb://localhost:3306/shopdb?serverTimezone=UTC"
        username="root"
        password="secret"
        
        <!-- Pool-Größe -->
        initialSize="5"
        maxTotal="20"
        maxIdle="10"
        minIdle="5"
        maxWaitMillis="30000"
        
        <!-- Verbindungs-Validierung -->
        testOnBorrow="true"
        testOnReturn="false"
        testWhileIdle="true"
        validationQuery="SELECT 1"
        validationQueryTimeout="3"
        timeBetweenEvictionRunsMillis="30000"
        
        <!-- Connection Leak Detection -->
        removeAbandonedOnBorrow="true"
        removeAbandonedOnMaintenance="true"
        removeAbandonedTimeout="60"
        logAbandoned="true"
        
        <!-- Performance -->
        jdbcInterceptors="org.apache.tomcat.jdbc.pool.interceptor.ConnectionState;
                         org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer"
    />

</Context>

Was bedeuten die Parameter?

Pool-Größen-Einstellungen:

ParameterBedeutungBeispiel
initialSizeVerbindungen beim Start5
maxTotalMaximum gleichzeitig20
maxIdleMaximum im Leerlauf10
minIdleMinimum im Leerlauf5
maxWaitMillisMax Wartezeit in ms30000 (30 Sek)

Validierungs-Einstellungen:

ParameterBedeutung
testOnBorrowPrüfe Verbindung vor Nutzung
testOnReturnPrüfe beim Zurückgeben
testWhileIdlePrüfe Leerlauf-Verbindungen
validationQuerySQL für Validierung (SELECT 1)
timeBetweenEvictionRunsMillisPrüfintervall für Idle-Connections

Connection Leak Detection:

ParameterBedeutung
removeAbandonedOnBorrowEntferne verwaiste Connections beim Holen
removeAbandonedTimeoutNach X Sekunden als „abandoned“ markieren
logAbandonedLogge Stack Trace von Leak

Schritt 3: web.xml anpassen

Wichtig: In Tomcat MUSST du die Datasource in web.xml deklarieren!

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

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee 
                             https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">

    <!-- Application Settings -->
    <display-name>ShopApplication</display-name>

    <!-- Datasource Reference -->
    <resource-ref>
        <description>Shop Database Connection Pool</description>
        <res-ref-name>jdbc/ShopDB</res-ref-name>
        <res-type>javax.sql.DataSource</res-type>
        <res-auth>Container</res-auth>
    </resource-ref>

</web-app>

Was macht <resource-ref>?

  • Deklariert die externe Ressource für die Anwendung
  • Verlinkt JNDI-Name mit DataSource
  • Container-Managed: Tomcat übernimmt die Verwaltung

Ohne <resource-ref>: Lookup schlägt fehl! (In Payara ist es oft optional)


Der kritische Unterschied: JNDI-Namen

Das ist der wichtigste Unterschied zwischen Tomcat und Payara!

JNDI-Name in Payara/GlassFish

// ✅ In Payara: Direkter Name
@Resource(name = "jdbc/ShopDB")
private DataSource dataSource;

// ODER manuell:
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("jdbc/ShopDB");

Der JNDI-Name: jdbc/ShopDB (direkt, ohne Prefix)


JNDI-Name in Tomcat

// ✅ In Tomcat: Mit java:comp/env/ Prefix!
@Resource(name = "jdbc/ShopDB")
private DataSource dataSource;  // Annotation findet es automatisch

// ABER bei manuellem Lookup:
Context ctx = new InitialContext();
Context envCtx = (Context) ctx.lookup("java:comp/env");
DataSource ds = (DataSource) envCtx.lookup("jdbc/ShopDB");

// ODER direkt:
DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/ShopDB");

Der JNDI-Name: java:comp/env/jdbc/ShopDB (mit Prefix!)


Was bedeutet java:comp/env?

  • java:comp = Java Component (die aktuelle Anwendung)
  • env = Environment Naming Context
  • jdbc/ShopDB = Die Ressource

Das Prinzip:

Tomcat organisiert JNDI hierarchisch:

java:
  └── comp:              ← Component (Anwendung)
       └── env:          ← Environment
            ├── jdbc/    ← Datasources
            │    └── ShopDB
            │    └── UserDB
            ├── mail/    ← Mail-Sessions
            └── jms/     ← JMS-Queues

Payara/GlassFish erlaubt direkten Zugriff (jdbc/ShopDB), Tomcat erfordert den vollständigen Pfad (java:comp/env/jdbc/ShopDB).

Aber: Bei @Resource kümmert sich Tomcat automatisch darum! Du kannst trotzdem name = "jdbc/ShopDB" schreiben.


Code-Beispiel: Tomcat-kompatibles Servlet

package com.shop.servlet;

import jakarta.annotation.Resource;
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 javax.sql.DataSource;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    
    // ✅ Funktioniert in BEIDEN: Tomcat + Payara
    @Resource(name = "jdbc/ShopDB")
    private DataSource dataSource;
    
    @Override
    protected void doGet(HttpServletRequest request, 
                        HttpServletResponse response) 
                        throws ServletException, IOException {
        
        List<String> productNames = new ArrayList<>();
        
        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery("SELECT name FROM products")) {
            
            while (rs.next()) {
                productNames.add(rs.getString("name"));
            }
            
            request.setAttribute("productNames", productNames);
            request.getRequestDispatcher("/WEB-INF/views/products.jsp")
                   .forward(request, response);
            
        } catch (SQLException e) {
            throw new ServletException("Database error", e);
        }
    }
}

Dieser Code funktioniert auf beiden Application Servern!


Manueller JNDI-Lookup in Tomcat

Wenn du manuell auf die Datasource zugreifen willst:

package com.shop.servlet;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;

@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    
    private DataSource dataSource;
    
    @Override
    public void init() throws ServletException {
        try {
            // ✅ TOMCAT: Mit java:comp/env/ Prefix
            Context initCtx = new InitialContext();
            Context envCtx = (Context) initCtx.lookup("java:comp/env");
            dataSource = (DataSource) envCtx.lookup("jdbc/ShopDB");
            
            // ODER direkt:
            // dataSource = (DataSource) initCtx.lookup("java:comp/env/jdbc/ShopDB");
            
        } catch (NamingException e) {
            throw new ServletException("Cannot retrieve DataSource", e);
        }
    }
    
    // ... doGet, doPost ...
}

Vergleich:

// PAYARA/GLASSFISH:
DataSource ds = (DataSource) ctx.lookup("jdbc/ShopDB");

// TOMCAT:
DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/ShopDB");

Tomcat-spezifische Features

1. Tomcat JDBC Pool (empfohlen für Tomcat)

<Resource 
    name="jdbc/ShopDB"
    factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
    type="javax.sql.DataSource"
    
    driverClassName="org.mariadb.jdbc.Driver"
    url="jdbc:mariadb://localhost:3306/shopdb"
    username="root"
    password="secret"
    
    initialSize="10"
    maxActive="100"
    maxIdle="50"
    minIdle="10"
    maxWait="30000"
    
    <!-- Tomcat-spezifische Features -->
    jmxEnabled="true"
    fairQueue="true"
    abandonWhenPercentageFull="50"
    
    jdbcInterceptors="org.apache.tomcat.jdbc.pool.interceptor.ConnectionState;
                     org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer;
                     org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReport(threshold=5000)"
/>

Tomcat-Extras:

  • jmxEnabled="true" – JMX Monitoring
  • fairQueue="true" – FIFO-Queue für Fairness
  • SlowQueryReport – Loggt langsame Queries

2. Connection Leak Detection (sehr hilfreich!)

<Resource 
    name="jdbc/ShopDB"
    ...
    
    <!-- Leak Detection -->
    removeAbandonedOnBorrow="true"
    removeAbandonedOnMaintenance="true"
    removeAbandonedTimeout="60"
    logAbandoned="true"
    
    <!-- Erzeugt Stack Trace bei Leak -->
    abandonWhenPercentageFull="50"
/>

Was passiert bei einem Leak?

// ❌ Vergessenes close()
Connection conn = dataSource.getConnection();
// ... vergessen zu schließen

// Nach 60 Sekunden:
// SEVERE: Connection has been abandoned
// Stack Trace zeigt genau, WO die Connection geholt wurde!

Super hilfreich beim Debugging!


3. SlowQueryReport Interceptor

<Resource 
    name="jdbc/ShopDB"
    ...
    
    jdbcInterceptors="org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReport(threshold=5000)"
/>

Loggt alle Queries, die länger als 5 Sekunden dauern:

WARNING: Slow Query Report
Query: SELECT * FROM products WHERE name LIKE '%search%'
Time: 7842 ms

Perfect für Performance-Optimierung!


Vergleichstabelle: Payara vs. Tomcat

FeaturePayara/GlassFishApache Tomcat
KonfigurationAdmin Console (GUI)XML-Dateien (context.xml)
JNDI-Namejdbc/ShopDBjava:comp/env/jdbc/ShopDB
Pool-ImplementierungHikariCPTomcat JDBC Pool / Commons DBCP2
Connection PoolingBuilt-inBuilt-in
EJB-Support✅ Ja❌ Nein
@Stateless, @EJB✅ Funktioniert❌ Nicht verfügbar
@Resource✅ Voll unterstützt⚠️ Nur für Datasources/Mail
JPA/Hibernate✅ Built-in⚠️ Manuell integrieren
MonitoringAdmin ConsoleJMX / Manager App
Hot Reload✅ Ja✅ Ja
Footprint~200 MB~50 MB
Startup-Zeit10-20 Sek2-5 Sek
Use CaseEnterprise AppsWeb Apps, Microservices

Migration: Payara → Tomcat

Wenn du von Payara zu Tomcat wechselst:

1. EJBs entfernen:

// ❌ Funktioniert NICHT in Tomcat
@Stateless
public class ProductDAOImpl implements ProductDAO {
    @EJB
    private SomeOtherService service;
}

// ✅ Tomcat-kompatibel: Normale Klasse
public class ProductDAOImpl implements ProductDAO {
    // Manuelle Instanziierung oder DI-Framework (Spring, etc.)
    private SomeOtherService service = new SomeOtherService();
}

2. Datasource-Zugriff anpassen:

// ❌ EJB-Style (nur Payara)
@Stateless
public class ProductService {
    @Resource(name = "jdbc/ShopDB")
    private DataSource dataSource;
}

// ✅ Servlet/Web-Style (Tomcat + Payara)
@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    @Resource(name = "jdbc/ShopDB")
    private DataSource dataSource;
}

3. context.xml und web.xml hinzufügen (siehe oben)

4. Dependencies anpassen:

<!-- Payara hat Jakarta EE built-in, Tomcat braucht sie explizit -->
<dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <version>6.0.0</version>
    <scope>provided</scope>
</dependency>

Best Practice: Portabler Code

Schreibe Code, der auf BEIDEN läuft:

Nutze nur Servlet API, JSP, JSTL
Vermeide EJBs (wenn Tomcat-Kompatibilität gewünscht)
@Resource für Datasources OK
Plain DAOs ohne @Stateless

Beispiel:

// ✅ Funktioniert auf Payara + Tomcat
@WebServlet("/products")
public class ProductServlet extends HttpServlet {
    
    @Resource(name = "jdbc/ShopDB")
    private DataSource dataSource;
    
    // Plain Java DAO (keine EJBs)
    private ProductDAO productDAO = new ProductDAOImpl(dataSource);
    
    @Override
    protected void doGet(...) {
        List<Product> products = productDAO.findAll();
        // ...
    }
}

// Plain DAO ohne EJB-Annotations
public class ProductDAOImpl implements ProductDAO {
    
    private DataSource dataSource;
    
    public ProductDAOImpl(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    @Override
    public List<Product> findAll() {
        try (Connection conn = dataSource.getConnection()) {
            // ...
        }
    }
}

Dieser Code läuft überall! 🎉


🔵 BONUS: Fortgeschrittene Themen

1. Connection Pool Sizing – Die richtige Größe finden

Faustregel:

Pool Size = (Anzahl CPU-Kerne * 2) + Festplatten-Spindeln

Beispiel:

  • CPU: 4 Kerne
  • HDD: 1 Festplatte
  • Pool Size = (4 * 2) + 1 = 9 Verbindungen

Aber: Das ist nur ein Startpunkt!

In der Praxis:

Min Pool Size:   5-10 (immer bereit)
Max Pool Size:   20-50 (hängt von DB-Server ab)

Zu groß:

  • Verschwendet Ressourcen
  • DB-Server überlastet

Zu klein:

  • Requests warten lange
  • Performance-Probleme

Best Practice: Mit Monitoring optimieren!


2. Multiple Datasources (z.B. Read/Write-Splitting)

Szenario: Separate DB für Reads (Replicas) und Writes (Master)

@Stateless
public class ProductDAOImpl implements ProductDAO {
    
    // Master DB für Writes
    @Resource(name = "jdbc/ShopDB_Master")
    private DataSource masterDataSource;
    
    // Replica DB für Reads
    @Resource(name = "jdbc/ShopDB_Replica")
    private DataSource replicaDataSource;
    
    @Override
    public List<Product> findAll() {
        // ✅ Read von Replica
        try (Connection conn = replicaDataSource.getConnection()) {
            // ...
        }
    }
    
    @Override
    public void save(Product product) {
        // ✅ Write auf Master
        try (Connection conn = masterDataSource.getConnection()) {
            // ...
        }
    }
}

Vorteil: Skalierung durch Read-Replicas!


3. Connection Pool Troubleshooting

Problem 1: „No available connections“ Exception

Caused by: java.sql.SQLException: Error in allocating a connection. 
Cause: In-use connections equal max-pool-size

Ursachen:

  1. Pool zu klein: Erhöhe Max Pool Size
  2. Connection Leaks: Connections werden nicht geschlossen
  3. Slow Queries: Queries dauern zu lang
  4. Zu viel Last: Skalierung nötig

Lösung 1: Pool vergrößern

Admin Console → JDBC Connection Pools → ShopDBPool
→ Max Pool Size: 50 (statt 20)

Lösung 2: Connection Leaks finden

// ✅ Code-Review: Überall Try-with-Resources?
try (Connection conn = dataSource.getConnection()) {
    // ...
}  // ✅ Wird automatisch geschlossen

// ❌ Falsch:
Connection conn = dataSource.getConnection();
// ... vergessen zu schließen!  ← Connection Leak!

Lösung 3: Slow Queries optimieren

-- Indizes hinzufügen
CREATE INDEX idx_products_name ON products(name);

-- Queries mit EXPLAIN analysieren
EXPLAIN SELECT * FROM products WHERE name LIKE '%laptop%';

Problem 2: „Connection timed out“

java.sql.SQLException: Connection is not available, request timed out after 30000ms.

Ursachen:

  1. Pool ist erschöpft (alle Connections in Benutzung)
  2. Slow Queries blockieren Verbindungen
  3. DB-Server antwortet nicht

Lösung:

Admin Console → JDBC Connection Pools → ShopDBPool

Pool Settings:
- Max Wait Time: 60000 (erhöhen auf 60 Sek)
- Max Pool Size: 30 (erhöhen)

Problem 3: „Stale Connection“ (Verbindung ist tot)

Communications link failure
The last packet successfully received from the server was X milliseconds ago.

Ursache: DB hat Connection geschlossen (Idle Timeout), aber Pool weiß es nicht.

Lösung: Connection Validation aktivieren

Admin Console → JDBC Connection Pools → ShopDBPool

Connection Validation:
✓ Connection Validation Required
- Validation Method: table
- Validation Table Name: products (oder dual)
- Validate Atmost Once: 30 (Sekunden)

Was macht das?

Bevor eine Connection aus Pool gegeben wird, prüft Payara:

SELECT 1 FROM products LIMIT 1;

Falls Query fehlschlägt → Connection ist tot → Neue erstellen!

Weiterführende Ressourcen

JavaBeans Spec:

JSP Actions:

Bean Validation:


🎯 Häufig gestellte Fragen (FAQ)

Frage 1: Wann DataSource vs. DriverManager nutzen?

Antwort:

DriverManager (Java SE):

  • ✅ Standalone-Anwendungen
  • ✅ CLI-Tools, Batch-Jobs
  • ✅ Prototyping, kleine Scripts
  • ❌ NIEMALS in Web-Anwendungen!

DataSource (Java Web/EE):

  • ✅ Web-Anwendungen
  • ✅ Application Server Umgebungen
  • ✅ Production-Code
  • ✅ Wenn Connection Pooling gebraucht wird

Faustregel: In Web-Anwendungen IMMER DataSource!


Frage 2: Warum @Resource statt @Inject?

Antwort:

Beide funktionieren, aber:

// ✅ @Resource: Spezifisch für JNDI-Ressourcen
@Resource(name = "jdbc/ShopDB")
private DataSource dataSource;

// ✅ @Inject: CDI (Contexts and Dependency Injection)
@Inject
@Named("shopDBDataSource")
private DataSource dataSource;

Best Practice: Für Datasources nutze @Resource!

  • Expliziter JNDI-Name
  • Standard in Jakarta EE
  • Weniger Dependencies nötig

Frage 3: Kann ich mehrere Datenbanken gleichzeitig nutzen?

Antwort: Ja! Einfach mehrere Datasources konfigurieren:

@Stateless
public class MultiDBService {
    
    // Datenbank 1: Produktdaten
    @Resource(name = "jdbc/ShopDB")
    private DataSource shopDataSource;
    
    // Datenbank 2: User-Daten
    @Resource(name = "jdbc/UserDB")
    private DataSource userDataSource;
    
    // Datenbank 3: Analytics
    @Resource(name = "jdbc/AnalyticsDB")
    private DataSource analyticsDataSource;
    
    public void doComplexOperation() {
        try (Connection shopConn = shopDataSource.getConnection();
             Connection userConn = userDataSource.getConnection()) {
            
            // Nutze beide Verbindungen parallel
        }
    }
}

Jede Datasource hat eigenen Pool!


Frage 4: Wie sichere ich Daten ab? Credentials im Code = Unsicher!

Antwort:

❌ NIEMALS Passwörter im Code:

// ❌ SEHR SCHLECHT!
String password = "secret123";

✅ RICHTIG: In Payara konfigurieren:

Admin Console → JDBC Connection Pools → ShopDBPool

Properties:
- User: ${ENV=DB_USER}
- Password: ${ALIAS=db_password}

Environment Variable setzen:

# domain.xml oder als System Property
export DB_USER=shopuser

Password Alias erstellen:

# Payara asadmin CLI
asadmin create-password-alias db_password
# Enter password: ***

Vorteil: Passwörter NICHT im Code, NICHT in Version Control!


Frage 5: Wie teste ich DAO-Code?

Antwort:

Option 1: Mock-DAO (Unit Tests):

// Mock-Implementierung
public class MockProductDAO implements ProductDAO {
    
    private List<Product> mockData = Arrays.asList(
        new Product(1, "Test Product", "Description", 
                    new BigDecimal("99.99"), 10, LocalDateTime.now())
    );
    
    @Override
    public List<Product> findAll() {
        return mockData;
    }
    
    // ... weitere Mock-Methoden
}

// Im Test:
@Test
public void testProductServlet() {
    ProductServlet servlet = new ProductServlet();
    servlet.setProductDAO(new MockProductDAO());  // Mock injizieren
    
    // Test durchführen...
}

Option 2: In-Memory-DB (Integration Tests):

<!-- pom.xml: H2 für Tests -->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.2.224</version>
    <scope>test</scope>
</dependency>
@Test
public void testProductDAOWithH2() {
    // H2 In-Memory-DB nutzen
    DataSource ds = createH2DataSource();
    ProductDAO dao = new ProductDAOImpl();
    // ... Test durchführen
}

Best Practice: Unit Tests mit Mocks, Integration Tests mit echter DB!


🎉 Tag 9 geschafft!

Legendary! Du rockst! 🔥

Real talk: Datenbankzugriff in Web-Anwendungen ist ein riesiges Thema. Du hast heute die Grundlagen gemeistert!

Das hast du heute gelernt:

  • ✅ Der Unterschied: JDBC in Java SE vs. Java Web
  • ✅ Connection Pools verstehen & konfigurieren
  • ✅ Datasources per JNDI nutzen
  • ✅ DAO-Pattern professionell implementieren
  • ✅ Best Practices für Connection Management
  • ✅ Troubleshooting häufiger Probleme
  • ✅ Production-Ready Datenbank-Code schreiben

Du kannst jetzt:

  • Payara Datasources konfigurieren
  • Connection Pools optimal einrichten
  • DAOs mit Dependency Injection nutzen
  • Datenbankzugriffe skalierbar umsetzen
  • Häufige Fehler vermeiden
  • Echte Daten in JSPs anzeigen

Honestly? Das ist HUGE! Du verstehst jetzt, wie professionelle Anwendungen mit Datenbanken arbeiten.

Und lowkey: Viele Entwickler nutzen immer noch DriverManager in Production. Mit diesem Wissen kannst du echte Performance-Probleme lösen! 💪


🔮 Wie geht’s weiter?

Morgen (Tag 10): Connection Pools & JDBC in Web-Umgebungen – Vertiefung

Was dich erwartet:

  • Advanced Connection Pool Tuning
  • Prepared Statements & Batch-Operations
  • Stored Procedures aufrufen
  • JDBC Best Practices für High-Performance
  • Der finale Kurs-Abschluss! 🎓

Brauchst du eine Pause?
Mach sie! Datenbank-Themen sind komplex.

Tipp für heute Abend:
Erstelle eine einfache CRUD-Anwendung (Create, Read, Update, Delete) mit dem DAO-Pattern. Das festigt das Gelernte!

Learning by doing! 🎨


📧 Troubleshooting

Problem: @Resource findet Datasource nicht

Symptom:

javax.naming.NamingException: Lookup failed for 'jdbc/ShopDB'

Lösung 1: Prüfe JNDI-Name

Admin Console → JDBC → JDBC Resources
→ Ist "jdbc/ShopDB" angelegt?
→ Status: Enabled?

Lösung 2: Prüfe web.xml (falls vorhanden)

<!-- WEB-INF/web.xml: Ressource deklarieren -->
<resource-ref>
    <res-ref-name>jdbc/ShopDB</res-ref-name>
    <res-type>javax.sql.DataSource</res-type>
    <res-auth>Container</res-auth>
</resource-ref>

Bei modernen Projekten: Meist NICHT nötig (Annotations reichen)!


Problem: Connection Pool Ping fehlschlägt

Symptom:

Ping Connection Pool failed for ShopDBPool. 
Connection could not be allocated because: 
Communications link failure

Lösung 1: Datenbank läuft?

# MariaDB Status prüfen
sudo systemctl status mariadb
# ODER in XAMPP Control Panel

# MySQL CLI testen
mysql -u root -p

Lösung 2: Firewall/Port prüfen

# Port 3306 offen?
telnet localhost 3306

# Netstat
netstat -an | grep 3306

Lösung 3: JDBC URL prüfen

jdbc:mariadb://localhost:3306/shopdb
          ↑           ↑      ↑      ↑
      Treiber     Host   Port   DB-Name

Häufige Fehler:

  • jdbc:mysql statt jdbc:mariadb (wenn MariaDB-Treiber verwendet)
  • Falsche Port-Nummer
  • Datenbank existiert nicht
  • User hat keine Rechte

Problem: Connection Leaks – Pool ist immer voll

Symptom:

Nach einigen Requests ist Pool erschöpft, neue Requests scheitern.

Lösung: Code-Review – ALLE Connections geschlossen?

// ✅ Richtig:
try (Connection conn = dataSource.getConnection()) {
    // ...
}  // Automatisch geschlossen

// ❌ Falsch:
Connection conn = dataSource.getConnection();
try {
    // ...
} catch (Exception e) {
    // conn.close() vergessen!  ← Leak!
}

Tipp: Nutze IMMER Try-with-Resources!


Problem: ClassNotFoundException: org.mariadb.jdbc.Driver

Symptom:

java.lang.ClassNotFoundException: org.mariadb.jdbc.Driver

Lösung: JDBC-Treiber fehlt!

<!-- pom.xml: Dependency hinzufügen -->
<dependency>
    <groupId>org.mariadb.jdbc</groupId>
    <artifactId>mariadb-java-client</artifactId>
    <version>3.3.0</version>
    <scope>runtime</scope>
</dependency>

Dann: Clean & Build, neu deployen!


Viel Erfolg beim Lernen! 🚀

Wenn du Fragen hast, schreib uns: support@java-developer.online


„Datenbankzugriff ist wie Kochen – mit den richtigen Werkzeugen (Connection Pool) macht es Spaß, ohne wird’s chaotisch!“ – Elyndra Valen

Autor

  • Elyndra Valen

    28 Jahre alt, wurde kürzlich zur Senior Entwicklerin befördert nach 4 Jahren intensiver Java-Entwicklung. Elyndra kennt die wichtigsten Frameworks und Patterns, beginnt aber gerade erst, die tieferen Zusammenhänge und Architektur-Entscheidungen zu verstehen. Sie ist die Brücke zwischen Junior- und Senior-Welt im Team.