Der Spickzettel zum Seminar Java SE in der Praxis — für den Moment, in dem eine POM nicht baut, ein Scope falsch gesetzt ist oder die Frage aufkommt, wie ein PreparedStatement noch mal aussah. Er zeigt die Formen, nicht den Weg dorthin: Wie man ein Projekt Schritt für Schritt aufzieht, ist die Leistung des Seminars. Maßgeblich bleiben die Dokumentationen von Maven, dem JDK und den eingesetzten Bibliotheken. Jeder Abschnitt verweist auf sein Modul.
Werkzeuge und Projektlayout
| Werkzeug | Version | Anmerkung |
|---|---|---|
| JDK | 25 | LTS, ältere Stände fallen an Sprachfeatures aus |
| Maven | 3.9.x | Genaue Patch-Version egal |
| IntelliJ IDEA | aktuell | Community-Umfang genügt |
| SDKMAN! | optional | Mehrere Java- und Maven-Stände parallel |
| Pfad | Inhalt |
|---|---|
| src/main/java | Produktionscode |
| src/main/resources | Konfiguration und Dateien zur Laufzeit |
| src/test/java | Testcode |
| target | Erzeugnisse des Builds, nie eingecheckt |
maven.compiler.release ersetzt source und target — eine Angabe statt
zweier.
Modul: Projekt aufsetzen
Abhängigkeiten: Scope und Ort
| Scope | Bedeutung |
|---|---|
| compile | Gegen diese API wird programmiert (Vorgabe) |
| runtime | Wird zur Laufzeit gebraucht, nie im Code referenziert |
| test | Nur im Testbaum verfügbar |
| Ort | Inhalt |
|---|---|
Eltern-POM, properties | Versionsnummern der geteilten Bibliotheken |
Eltern-POM, dependencyManagement | Version und Scope, auch für Mitgezogenes |
Untermodul, dependencies | Nur groupId und artifactId |
| Untermodul, Einzelfall | Version nur, wenn die Bibliothek nur dort vorkommt |
dependencyManagement bindet nichts ein — es legt nur fest, wie eingebunden
wird, falls ein Modul die Abhängigkeit deklariert. Die Angabe im Untermodul
gewinnt immer.
Module: Projekt aufsetzen · Richtung Produktion
Logging mit SLF4J
| Artefakt | Scope | Rolle |
|---|---|---|
| slf4j-api | compile | Gegen diese API wird programmiert |
| slf4j-simple | runtime | Schreibt zur Laufzeit, nie im Code referenziert |
| jul-to-slf4j | runtime | Brücke für fremdes java.util.logging |
private static final Logger LOG =
LoggerFactory.getLogger(KursAbruf.class);
LOG.info("Rufe Kurse für '{}' ab", autorId);
LOG.error("Unerwarteter Fehler", e);
Platzhalter {} statt Verkettung — die Nachricht entsteht nur, wenn die Stufe
aktiv ist. Die Ausnahme gehört als zweites Argument übergeben, nicht in den
Text. Fehlt die Implementierung, meldet SLF4J beim Start
„NOP logger implementation”.
Module: Projekt aufsetzen · Richtung Produktion
HTTP-Aufrufe mit dem JDK-Client
private static final HttpClient CLIENT =
HttpClient.newBuilder()
.followRedirects(Redirect.NORMAL)
.build();
HttpRequest anfrage = HttpRequest.newBuilder()
.uri(URI.create(URL.formatted(autorId)))
.GET()
.build();
CLIENT.send(anfrage, ofString());
Ein HttpClient ist threadsicher — einmal anlegen und wiederverwenden. send
wirft IOException und InterruptedException; beide lassen sich in einem
Catch-Block fangen. Den Statuscode als switch-Ausdruck auszuwerten hält die
Fälle beieinander; der default-Zweig ist dort Pflicht.
Modul: Externe Web-API
JSON binden mit Jackson
| Erzeugt ein Record | Bedeutung |
|---|---|
| Kanonischer Konstruktor | Ein Parameter je Komponente, in Reihenfolge |
| Zugriffsmethoden | titel() statt getTitel() |
| equals und hashCode | Vergleich über alle Komponenten |
| toString | Lesbare Ausgabe im Log ohne Zusatzcode |
@JsonIgnoreProperties(ignoreUnknown = true)
record KatalogKurs(String id, String titel, String dauer) { }
var typ = MAPPER.getTypeFactory()
.constructCollectionType(List.class, KatalogKurs.class);
List<KatalogKurs> kurse = MAPPER.readValue(json, typ);
Ohne ignoreUnknown bricht ein zusätzliches Feld in der Antwort den Lauf ab
(„Unrecognized field”). Für Listen genügt List.class nicht — sonst bekommen
Sie eine Liste, nur nicht von KatalogKurs. Den ObjectMapper einmal anlegen,
nicht je Aufruf.
Modul: Externe Web-API
Datenbankzugriff mit JDBC
| Baustein | Bedeutung |
|---|---|
| H2 | Eingebettete SQL-Datenbank, keine Installation nötig |
| JDBC | Standard-API im JDK für den Zugriff auf SQL-Datenbanken |
| DataSource | Beschreibt die Datenbank, liefert Verbindungen |
| AUTO_SERVER | Erlaubt weiteren Prozessen den Zugriff auf dieselbe Datei |
try (Connection c = quelle.getConnection();
var s = c.prepareStatement(MERGE)) {
s.setString(1, kurs.id());
s.setLong(3, kurs.minuten());
s.execute();
} catch (SQLException e) {
throw new RepositoryException("Speichern", e);
}
Platzhalter statt Verkettung: Werte werden als Werte übergeben und können die
Struktur der Anfrage nicht mehr verändern. try-with-resources schließt
Verbindung und Statement auch im Fehlerfall. MERGE statt INSERT legt
denselben Datensatz beim zweiten Lauf nicht doppelt an.
Die Repository-Schnittstelle bleibt frei von Technikbegriffen — keine
SQLException in der Signatur, sonst steht die Technik in der Abstraktion.
Modul: Datenbankzugriff
REST mit JAX-RS und Jersey
| Annotation | Wirkung |
|---|---|
@Path | Pfad an Klasse oder Methode, relativ zur Klasse |
@GET @POST | HTTP-Verfahren der Methode |
@Produces | Medientyp der Antwort |
@Consumes | Erwarteter Medientyp der Anfrage |
@PathParam | Bindet einen Pfadabschnitt an einen Parameter |
| Artefakt | Scope | Rolle |
|---|---|---|
| jakarta.ws.rs-api | compile | Die Annotationen der Spezifikation |
| jersey-server | compile | Kern der Umsetzung |
| jersey-container-grizzly2-http | compile | Der HTTP-Server |
| jersey-hk2 | runtime | Wird zur Laufzeit vorausgesetzt |
| jersey-media-json-jackson | runtime | Brücke von Jersey zu Jackson |
@Path("/kurse")
public class KursRessource {
@GET @Produces(MediaType.APPLICATION_JSON)
public List<Kurs> kurse() { … }
@POST @Path("/{id}/notizen")
@Consumes(MediaType.TEXT_PLAIN)
public void notiz(@PathParam("id") String id, String notiz) { … }
}
Annotationen allein führen nichts aus — die Ressource muss registriert werden, sonst antwortet der Server mit 404. Eigene Ausnahmen gehören in Statuscodes übersetzt, sonst wird jede Störung zu einem 500.
Modul: REST-API
Ausliefern
mvn clean verify
java -jar kursradar-server/target/kursradar-server-1.0-SNAPSHOT.jar
Das Shade-Plugin erzeugt ein JAR samt Bibliotheken; daneben liegt
original-…jar, das schlanke ohne. Zwei Transformer sind dabei wesentlich:
einer trägt die Hauptklasse ins Manifest ein, der andere führt gleichnamige
Dienst-Dateien aus mehreren JARs zusammen, statt sie einander überschreiben zu
lassen. Konfiguration gehört in eine Eigenschaftsdatei unter
src/main/resources; getProperty liefert bei fehlendem Schlüssel still
null.
Modul: Richtung Produktion
Typische Fallen
- Zwei JDKs installiert und der Pfad zeigt auf das ältere. Vor dem ersten
Build
java -versionundmvn -versiongegeneinander halten — das JDK der IDE ist nicht zwingend das der Kommandozeile. target/oder die Datenbankdatei eingecheckt. Beides ist Ergebnis des Builds, nicht Quelle.- Nur in der IDE gebaut. Der erste Lauf auf der Kommandozeile oder im CI fördert dann Überraschungen zutage.
- Implementierung mit
compilestattruntimeeingebunden — und anschließend gegen sie programmiert. Damit ist die Austauschbarkeit weg. - Für jeden Aufruf einen neuen
HttpClientoderObjectMappererzeugt, statt einen zu teilen. - Geprüfte Ausnahmen geschluckt, statt sie mit Kontext weiterzureichen.
e.printStackTrace()als Dauerlösung verdeckt den Fehler doppelt. - SQL aus Zeichenketten zusammengesetzt und Werte hineingeklebt — genau das verhindern Platzhalter.
- Verbindungen geöffnet und nicht geschlossen. Die Ressourcen gehen still aus; auffallen wird es unter Last.
- Die veränderliche Liste zurückgegeben, statt eine Kopie zu liefern.
- Unsortiert ausgeliefert und die Reihenfolge für stabil gehalten.
- Eine schreibende Aktion über GET angeboten, weil es im Browser bequemer ist.
- Zugangsdaten in die Eigenschaftsdatei gelegt, die mit ins JAR und ins Repository wandert.