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

🗺️ Deine Position im Kurs
| Tag | Thema | Status |
|---|---|---|
| 1 | Java EE Überblick & HTTP | ✅ Abgeschlossen |
| 2 | HTTP-Protokoll Vertiefung & Zustandslosigkeit | ✅ Abgeschlossen |
| 3 | Servlets & Servlet API | ✅ Abgeschlossen |
| 4 | Deployment Descriptor & MVC vs Model 2 | ✅ Abgeschlossen |
| 5 | JSP & Expression Languages | ✅ Abgeschlossen |
| 6 | Java Beans, Actions, Scopes & Direktiven | ✅ Abgeschlossen |
| 7 | Include-Action vs Include-Direktive | ✅ Abgeschlossen |
| 8 | JSTL – Java Standard Tag Libraries | ✅ Abgeschlossen |
| → 9 | Java Web und Datenbanken – Datasource | 👉 DU BIST HIER! |
| 10 | Connection 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?
- Performance: Jede DB-Verbindung dauert 50-200ms
- Skalierung: Bei 100 gleichzeitigen Usern = 100 offene Verbindungen
- Ressourcen: Datenbanken haben Connection-Limits
- 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?
- Verbindung öffnen:
DriverManager.getConnection()erstellt eine neue DB-Verbindung - Query ausführen: Statement erstellen und ausführen
- Ergebnisse verarbeiten: ResultSet durchlaufen
- 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:
- Datenbank überlastet: MariaDB/MySQL hat Standard-Limit von ~150 Verbindungen
- Server-Crash: Out of Memory wegen zu vielen Verbindungen
- Langsame Response: Verbindungen müssen warten
- 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()?
- Pool prüft: Ist eine Verbindung frei?
- Falls ja: Gib sie zurück (dauert ~1ms statt 50ms!)
- 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:
- Sicherheit: Passwörter im Code! 😱
- Wartbarkeit: Passwort ändern = Code in 50 Klassen ändern
- Umgebungen: Dev, Test, Production haben unterschiedliche URLs
- 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:
- Sicherheit: Keine Passwörter im Code
- Wartbarkeit: Passwort im Server ändern, nicht im Code
- Flexibilität: Verschiedene Datasources per Umgebung
- 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/ShopDBjdbc/UserDBjdbc/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:
- Payara scannt deine Klasse
- Sieht
@Resourcemit name=“jdbc/ShopDB“ - Macht JNDI-Lookup:
lookup("jdbc/ShopDB") - 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:
- Gehe zu: https://mariadb.com/downloads/connectors/connectors-data-access/java8-connector
- 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:
- Browser öffnen: http://localhost:4848
- 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:
| Feld | Wert |
|---|---|
| Pool Name | ShopDBPool |
| Resource Type | javax.sql.DataSource |
| Database Driver Vendor | MariaDB (oder wähle „MySQL“ wenn MariaDB nicht aufgelistet) |
Klick auf „Next“ →
Erweiterte Eigenschaften:
Scrolle runter zu „Additional Properties“ und setze:
| Property | Value |
|---|---|
| URL | jdbc:mariadb://localhost:3306/shopdb |
| User | root (dein DB-User) |
| Password | deinPasswort (dein DB-Passwort) |
| ServerName | localhost (kann auch leer bleiben) |
| PortNumber | 3306 |
| DatabaseName | shopdb |
Wichtige Pool-Einstellungen:
| Einstellung | Wert | Erklärung |
|---|---|---|
| Initial Pool Size | 5 | Verbindungen beim Start |
| Min Pool Size | 5 | Minimum immer offen |
| Max Pool Size | 20 | Maximum gleichzeitig |
| Idle Timeout | 300 (Sekunden) | Verbindung schließen nach 5 Min Inaktivität |
Klick auf „Finish“ →
Pool testen:
- Gehe zurück zu „JDBC Connection Pools“
- Klick auf „ShopDBPool“
- Klick auf Tab „Advanced“
- 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:
| Feld | Wert |
|---|---|
| JNDI Name | jdbc/ShopDB |
| Pool Name | ShopDBPool (aus Dropdown wählen) |
| Status | Enabled (✓) |
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?
dataSource.getConnection(): Holt Verbindung aus Pool (~1ms!)- Statement erstellen: Wie in Java SE
- Query ausführen: Wie in Java SE
- Ergebnisse verarbeiten: Wie in Java SE
- 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
@Resourcenicht 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:
- Vermischte Verantwortlichkeiten: Servlet macht Controller UND Data Access
- Code-Duplikation: Gleicher Code in jedem Servlet
- Schwer testbar: Kann Servlet nicht ohne DB testen
- 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?
- Suchformular: Ermöglicht Suche nach Produktnamen
- Produktliste: Zeigt alle Produkte in Grid-Layout
- Formatierung: Preis als Währung, Stock-Status farbig
- 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?
- Thread-Safety: Servlets sind NICHT thread-safe für Fields!
- Connection-Leak: Verbindung wird nie zurückgegeben
- 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?
- Auto-Commit aus:
conn.setAutoCommit(false) - Operationen ausführen: Beide SQL-Statements
- Bei Erfolg:
conn.commit()– Alles wird gespeichert - Bei Fehler:
conn.rollback()– Alles wird rückgängig gemacht - 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:
| Aspekt | Payara/GlassFish | Apache Tomcat |
|---|---|---|
| Konfiguration | Admin Console (GUI) | XML-Dateien |
| JNDI-Name | jdbc/ShopDB | java:comp/env/jdbc/ShopDB |
| Pool-Implementierung | HikariCP (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:
| Parameter | Bedeutung | Beispiel |
|---|---|---|
initialSize | Verbindungen beim Start | 5 |
maxTotal | Maximum gleichzeitig | 20 |
maxIdle | Maximum im Leerlauf | 10 |
minIdle | Minimum im Leerlauf | 5 |
maxWaitMillis | Max Wartezeit in ms | 30000 (30 Sek) |
Validierungs-Einstellungen:
| Parameter | Bedeutung |
|---|---|
testOnBorrow | Prüfe Verbindung vor Nutzung |
testOnReturn | Prüfe beim Zurückgeben |
testWhileIdle | Prüfe Leerlauf-Verbindungen |
validationQuery | SQL für Validierung (SELECT 1) |
timeBetweenEvictionRunsMillis | Prüfintervall für Idle-Connections |
Connection Leak Detection:
| Parameter | Bedeutung |
|---|---|
removeAbandonedOnBorrow | Entferne verwaiste Connections beim Holen |
removeAbandonedTimeout | Nach X Sekunden als „abandoned“ markieren |
logAbandoned | Logge 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 Contextjdbc/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 MonitoringfairQueue="true"– FIFO-Queue für FairnessSlowQueryReport– 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
| Feature | Payara/GlassFish | Apache Tomcat |
|---|---|---|
| Konfiguration | Admin Console (GUI) | XML-Dateien (context.xml) |
| JNDI-Name | jdbc/ShopDB | java:comp/env/jdbc/ShopDB |
| Pool-Implementierung | HikariCP | Tomcat JDBC Pool / Commons DBCP2 |
| Connection Pooling | Built-in | Built-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 |
| Monitoring | Admin Console | JMX / Manager App |
| Hot Reload | ✅ Ja | ✅ Ja |
| Footprint | ~200 MB | ~50 MB |
| Startup-Zeit | 10-20 Sek | 2-5 Sek |
| Use Case | Enterprise Apps | Web 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:
- Pool zu klein: Erhöhe Max Pool Size
- Connection Leaks: Connections werden nicht geschlossen
- Slow Queries: Queries dauern zu lang
- 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:
- Pool ist erschöpft (alle Connections in Benutzung)
- Slow Queries blockieren Verbindungen
- 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:mysqlstattjdbc: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

