Subsections of Bauen von Programmen, Automatisierung, Continuous Integration
Build-Systeme: Gradle
Um beim Übersetzen und Testen von Software von den spezifischen Gegebenheiten auf einem Entwicklerrechner unabhängig zu werden, werden häufig sogenannte Build-Tools eingesetzt. Mit diesen konfiguriert man sein Projekt abseits einer IDE und übersetzt, testet und baut seine Applikation damit entsprechend unabhängig. In der Java-Welt sind aktuell die Build-Tools Ant, Maven und Gradle weit verbreitet.
In Gradle ist ein Java-Entwicklungsmodell quasi eingebaut. Über die Konfigurationsskripte
müssen nur noch bestimmte Details wie benötigte externe Bibliotheken oder die Hauptklasse
und sonstige Projektbesonderheiten konfiguriert werden. Über "Tasks" wie build
, test
oder run
können Java-Projekte übersetzt, getestet und ausgeführt werden. Dabei werden die
externen Abhängigkeiten (Bibliotheken) aufgelöst (soweit konfiguriert) und auch abhängige
Tasks mit erledigt, etwa muss zum Testen vorher der Source-Code übersetzt werden.
Gradle bietet eine Fülle an Plugins für bestimmte Aufgaben an, die jeweils mit neuen Tasks
einher kommen. Beispiele sind das Plugin java
, welches weitere Java-spezifische Tasks
wie classes
mitbringt, oder das Plugin checkstyle
zum Überprüfen von Coding-Style-Richtlinien.
- (K3) Schreiben und Verstehen einfacher Gradle-Skripte
Automatisieren von Arbeitsabläufen
Works on my machine ...
Einen häufigen Ausspruch, den man bei der Zusammenarbeit in Teams zu hören bekommt, ist "Also, bei mir läuft der Code." ...
Das Problem dabei ist, dass jeder Entwickler eine andere Maschine hat, oft ein anderes Betriebssystem oder eine andere OS-Version. Dazu kommen noch eine andere IDE und/oder andere Einstellungen und so weiter.
Wie bekommt man es hin, dass Code zuverlässig auch auf anderen Rechnern baut? Ein wichtiger Baustein dafür sind sogenannte "Build-Systeme", also Tools, die unabhängig von der IDE (und den IDE-Einstellungen) für das Übersetzen der Software eingesetzt werden und deren Konfiguration dann mit im Repo eingecheckt wird. Damit kann die Software dann auf allen Rechnern und insbesondere dann auch auf dem Server (Stichwort "Continuous Integration") unabhängig von der IDE o.ä. automatisiert gebaut und getestet werden.
- Build-Tools:
- Apache Ant
- Apache Maven
- Gradle
Das sind die drei am häufigsten anzutreffenden Build-Tools in der Java-Welt.
Ant ist von den drei genannten Tools das älteste und setzt wie Maven auf XML als Beschreibungssprache. In Ant müssen dabei alle Regeln stets explizit formuliert werden, die man benutzen möchte.
In Maven wird dagegen von einem bestimmten Entwicklungsmodell ausgegangen, hier müssen nur noch die Abweichungen zu diesem Modell konfiguriert werden.
In Gradle wird eine DSL basierend auf der Skriptsprache Groovy (läuft auf der JVM) eingesetzt, und es gibt hier wie in Maven ein bestimmtes eingebautes Entwicklungsmodell. Gradle bringt zusätzlich noch einen Wrapper mit, d.h. es wird eine Art Gradle-Starter im Repo konfiguriert, der sich quasi genauso verhält wie ein fest installiertes Gradle (s.u.).
Achtung: Während Ant und Maven relativ stabil in der API sind, verändert sich Gradle teilweise deutlich zwischen den Versionen. Zusätzlich sind bestimmte Gradle-Versionen oft noch von bestimmten JDK-Versionen abhängig. In der Praxis bedeutet dies, dass man Gradle-Skripte im Laufe der Zeit relativ oft überarbeiten muss (einfach nur, damit das Skript wieder läuft - ohne dass man dabei irgendwelche neuen Features oder sonstige Vorteile erzielen würde). Ein großer Vorteil ist aber der Gradle-Wrapper (s.u.).
Gradle: Eine DSL in Groovy
DSL: Domain Specific Language
// build.gradle
plugins {
id 'java'
id 'application'
}
repositories {
mavenCentral()
}
dependencies {
testImplementation 'junit:junit:4.13.2'
}
application {
mainClass = 'fluppie.App'
}
Dies ist mit die einfachste Build-Datei für Gradle.
Über Plugins wird die Unterstützung für Java und das Bauen von Applikationen aktiviert, d.h. es stehen darüber entsprechende spezifische Tasks zur Verfügung.
Abhängigkeiten sollen hier aus dem Maven-Repository MavenCentral
geladen werden. Zusätzlich wird hier als Abhängigkeit für den Test (testImplementation
)
die JUnit-Bibliothek in einer Maven-artigen Notation angegeben (vgl.
mvnrepository.com). (Für nur zur Übersetzung der Applikation
benötigte Bibliotheken verwendet man stattdessen das Schlüsselwort implementation
.)
Bei der Initialisierung wurde als Package fluppie
angegeben. Gradle legt darunter per
Default die Klasse App
mit einer main()
-Methode an. Entsprechend kann man über den
Eintrag application
den Einsprungpunkt in die Applikation konfigurieren.
Gradle-DSL
Ein Gradle-Skript ist letztlich ein in Groovy geschriebenes Skript. Groovy ist eine auf Java basierende und auf der JVM ausgeführte Skriptsprache. Seit einigen Versionen kann man die Gradle-Build-Skripte auch in der Sprache Kotlin schreiben.
Dateien
Für das Bauen mit Gradle benötigt man drei Dateien im Projektordner:
-
build.gradle
: Die auf der Gradle-DSL beruhende Definition des Builds mit den Tasks (und ggf. Abhängigkeiten) eines Projekts.Ein Multiprojekt hat pro Projekt eine solche Build-Datei. Dabei können die Unterprojekte Eigenschaften der Eltern-Buildskripte "erben" und so relativ kurz ausfallen.
-
settings.gradle
: Eine optionale Datei, in der man beispielsweise den Projektnamen oder bei einem Multiprojekt die relevanten Unterprojekte festlegt. -
gradle.properties
: Eine weitere optionale Datei, in der projektspezifische Properties für den Gradle-Build spezifizieren kann.
Gradle Init
Um eine neue Gradle-Konfiguration anlegen zu lassen, geht man in einen Ordner
und führt darin gradle init
aus. Gradle fragt der Reihe nach einige Einstellungen
ab:
$ gradle init
Select type of project to generate:
1: basic
2: application
3: library
4: Gradle plugin
Enter selection (default: basic) [1..4] 2
Select implementation language:
1: C++
2: Groovy
3: Java
4: Kotlin
5: Scala
6: Swift
Enter selection (default: Java) [1..6] 3
Split functionality across multiple subprojects?:
1: no - only one application project
2: yes - application and library projects
Enter selection (default: no - only one application project) [1..2] 1
Select build script DSL:
1: Groovy
2: Kotlin
Enter selection (default: Groovy) [1..2] 1
Select test framework:
1: JUnit 4
2: TestNG
3: Spock
4: JUnit Jupiter
Enter selection (default: JUnit Jupiter) [1..4] 1
Project name (default: tmp): wuppie
Source package (default: tmp): fluppie
Typischerweise möchte man eine Applikation bauen (Auswahl 2 bei der ersten Frage). Als nächstes wird nach der Sprache des Projekts gefragt sowie nach der Sprache für das Gradle-Build-Skript (Default ist Groovy) sowie nach dem Testframework, welches verwendet werden soll.
Damit wird die eingangs gezeigte Konfiguration angelegt.
Ordner
Durch gradle init
wird ein neuer Ordner wuppie/
mit folgender Ordnerstruktur
angelegt:
drwxr-xr-x 4 cagix cagix 4096 Apr 8 11:43 ./
drwxrwxrwt 1 cagix cagix 4096 Apr 8 11:43 ../
-rw-r--r-- 1 cagix cagix 154 Apr 8 11:43 .gitattributes
-rw-r--r-- 1 cagix cagix 103 Apr 8 11:43 .gitignore
drwxr-xr-x 3 cagix cagix 4096 Apr 8 11:43 app/
drwxr-xr-x 3 cagix cagix 4096 Apr 8 11:42 gradle/
-rwxr-xr-x 1 cagix cagix 8070 Apr 8 11:42 gradlew*
-rw-r--r-- 1 cagix cagix 2763 Apr 8 11:42 gradlew.bat
-rw-r--r-- 1 cagix cagix 370 Apr 8 11:43 settings.gradle
Es werden Einstellungen für Git erzeugt (.gitattributes
und
.gitignore
).
Im Ordner gradle/
wird der Gradle-Wrapper abgelegt (s.u.). Dieser
Ordner wird normalerweise mit ins Repo eingecheckt. Die Skripte
gradlew
und gradlew.bat
sind die Startskripte für den Gradle-Wrapper
(s.u.) und werden normalerweise ebenfalls ins Repo mit eingecheckt.
Der Ordner .gradle/
(erscheint ggf. nach dem ersten Lauf von Gradle
auf dem neuen Projekt) ist nur ein Hilfsordner ("Cache") von Gradle.
Hier werden heruntergeladene Dateien etc. abgelegt. Dieser Order
sollte nicht ins Repo eingecheckt werden und ist deshalb auch
per Default im generierten .gitignore
enthalten. (Zusätzlich gibt es
im User-Verzeichnis auch noch einen Ordner .gradle/
mit einem globalen
Cache.)
In settings.gradle
finden sich weitere Einstellungen. Die eigentliche
Gradle-Konfiguration befindet sich zusammen mit dem eigentlichen Projekt
im Unterordner app/
:
drwxr-xr-x 4 root root 4096 Apr 8 11:50 ./
drwxr-xr-x 5 root root 4096 Apr 8 11:49 ../
drwxr-xr-x 5 root root 4096 Apr 8 11:50 build/
-rw-r--r-- 1 root root 852 Apr 8 11:43 build.gradle
drwxr-xr-x 4 root root 4096 Apr 8 11:43 src/
Die Datei build.gradle
ist die durch gradle init
erzeugte (und
eingangs gezeigte) Konfigurationsdatei, vergleichbar mit build.xml
für Ant oder pom.xml
für Maven. Im Unterordner build/
werden die
generierten .class
-Dateien etc. beim Build-Prozess abgelegt.
Unter src/
findet sich dann eine Maven-typische Ordnerstruktur
für die Sourcen:
$ tree src/
src/
|-- main
| |-- java
| | `-- fluppie
| | `-- App.java
| `-- resources
`-- test
|-- java
| `-- fluppie
| `-- AppTest.java
`-- resources
Unterhalb von src/
ist ein Ordner main/
für die Quellen der Applikation (Sourcen
und Ressourcen). Für jede Sprache gibt es einen eigenen Unterordner, hier entsprechend
java/
. Unterhalb diesem folgt dann die bei der Initialisierung angelegte Package-Struktur
(hier fluppie
mit der Default-Main-Klasse App
mit einer main()
-Methode). Diese
Strukturen wiederholen sich für die Tests unterhalb von src/test/
.
Wer die herkömmlichen, deutlich flacheren Strukturen bevorzugt, also unterhalb von src/
direkt die Java-Package-Strukturen für die Sourcen der Applikation und unterhalb von test/
entsprechend die Strukturen für die JUnit-Test, der kann dies im Build-Skript einstellen:
sourceSets {
main {
java {
srcDirs = ['src']
}
resources {
srcDirs = ['res']
}
test {
java {
srcDirs = ['test']
}
}
}
Ablauf eines Gradle-Builds
Ein Gradle-Build hat zwei Hauptphasen: Konfiguration und Ausführung.
Während der Konfiguration wird das gesamte Skript durchlaufen (vgl. Ausführung der direkten Anweisungen eines Tasks). Dabei wird ein Graph erzeugt: welche Tasks hängen von welchen anderen ab etc.
Anschließend wird der gewünschte Task ausgeführt. Dabei werden zuerst alle Tasks ausgeführt, die im Graphen auf dem Weg zu dem gewünschten Task liegen.
Mit gradle tasks
kann man sich die zur Verfügung stehenden Tasks ansehen.
Diese sind der Übersicht halber noch nach "Themen" sortiert.
Für eine Java-Applikation sind die typischen Tasks gradle build
zum Bauen der
Applikation (inkl. Ausführen der Tests) sowie gradle run
zum Starten der Anwendung.
Wer nur die Java-Sourcen compilieren will, würde den Task gradle compileJava
nutzen.
Mit gradle check
würde man compilieren und die Tests ausführen sowie weitere Checks
durchführen (gradle test
würde nur compilieren und die Tests ausführen), mit gradle jar
die Anwendung in ein .jar
-File packen und mit gradle javadoc
die Javadoc-Dokumentation
erzeugen und mit gradle clean
die generierten Hilfsdateien aufräumen (löschen).
Plugin-Architektur
Für bestimmte Projekttypen gibt es immer wieder die gleichen Aufgaben. Um hier Schreibaufwand zu sparen, existieren verschiedene Plugins für verschiedene Projekttypen. In diesen Plugins sind die entsprechenden Tasks bereits mit den jeweiligen Abhängigkeiten formuliert. Diese Idee stammt aus Maven, wo dies für Java-basierte Projekte umgesetzt ist.
Beispielsweise erhält man über das Plugin java
den Task clean
zum Löschen
aller generierten Build-Artefakte, den Task classes
, der die Sourcen zu
.class
-Dateien kompiliert oder den Task test
, der die JUnit-Tests
ausführt ...
Sie können sich Plugins und weitere Tasks relativ leicht auch selbst definieren.
Auflösen von Abhängigkeiten
Analog zu Maven kann man Abhängigkeiten (etwa in einer bestimmten Version
benötigte Bibliotheken) im Gradle-Skript angeben. Diese werden (transparent für
den User) von einer ebenfalls angegeben Quelle, etwa einem Maven-Repository,
heruntergeladen und für den Build genutzt. Man muss also nicht mehr die
benötigten .jar
-Dateien der Bibliotheken mit ins Projekt einchecken.
Analog zu Maven können erzeugte Artefakte automatisch publiziert werden, etwa
in einem Maven-Repository.
Für das Projekt benötigte Abhängigkeiten kann man über den Eintrag dependencies
spezifizieren. Dabei unterscheidet man u.a. zwischen Applikation und Tests:
implementation
und testImplementation
für das Compilieren und Ausführen von
Applikation bzw. Tests. Diese Abhängigkeiten werden durch Gradle über die im
Abschnitt repositories
konfigurierten Repositories aufgelöst und die entsprechenden
.jar
-Files geladen (in den .gradle/
-Ordner).
Typische Repos sind das Maven-Repo selbst (mavenCentral()
) oder das Google-Maven-Repo
(google()
).
Die Einträge in dependencies
erfolgen dabei in einer Maven-Notation, die Sie
auch im Maven-Repo mvnrepository.com finden.
Beispiel mit weiteren Konfigurationen (u.a. Checkstyle und Javadoc)
plugins {
id 'java'
id 'application'
id 'checkstyle'
}
repositories {
mavenCentral()
}
application {
mainClass = 'hangman.Main'
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
run {
standardInput = System.in
}
sourceSets {
main {
java {
srcDirs = ['src']
}
resources {
srcDirs = ['res']
}
}
}
checkstyle {
configFile = file(“${rootDir}/google_checks.xml”)
toolVersion = '8.32'
}
dependencies {
implementation group: 'org.apache.poi', name: 'poi', version: '4.1.2'
}
javadoc {
options.showAll()
}
Hier sehen Sie übrigens noch eine weitere mögliche Schreibweise für das Notieren
von Abhängigkeiten: implementation group: 'org.apache.poi', name: 'poi', version: '4.1.2'
und implementation 'org.apache.poi:poi:4.1.2'
sind gleichwertig, wobei die letztere
Schreibweise sowohl in den generierten Builds-Skripten und in der offiziellen Dokumentation
bevorzugt wird.
Gradle und Ant (und Maven)
Vorhandene Ant-Buildskripte kann man nach Gradle importieren und ausführen lassen. Über die DSL kann man auch direkt Ant-Tasks aufrufen. Siehe auch "Using Ant from Gradle".
Gradle-Wrapper
project
|-- app/
|-- build.gradle
|-- gradlew
|-- gradlew.bat
`-- gradle/
`-- wrapper/
|-- gradle-wrapper.jar
`-- gradle-wrapper.properties
Zur Ausführung von Gradle-Skripten benötigt man eine lokale Gradle-Installation. Diese sollte für i.d.R. alle User, die das Projekt bauen wollen, identisch sein. Leider ist dies oft nicht gegeben bzw. nicht einfach lösbar.
Zur Vereinfachung gibt es den Gradle-Wrapper gradlew
(bzw. gradlew.bat
für Windows).
Dies ist ein kleines Shellskript, welches zusammen mit einigen kleinen .jar
-Dateien im
Unterordner gradle/
mit ins Repo eingecheckt wird und welches direkt die Rolle des
gradle
-Befehls einer Gradle-Installation übernehmen kann. Man kann also in Konfigurationskripten,
beispielsweise für Gitlab CI, alle Aufrufe von gradle
durch Aufrufe von gradlew
ersetzen.
Beim ersten Aufruf lädt gradlew
dann die spezifizierte Gradle-Version herunter und speichert
diese in einem lokalen Ordner .gradle/
. Ab dann greift gradlew
auf diese lokale (nicht
"installierte") gradle
-Version zurück.
gradle init
erzeugt den Wrapper automatisch in der verwendeten Gradle-Version mit.
Alternativ kann man den Wrapper nachträglich über gradle wrapper --gradle-version 6.5
in einer bestimmten (gewünschten) Version anlegen lassen.
Da der Gradle-Wrapper im Repository eingecheckt ist, benutzen alle Entwickler damit automatisch die selbe Version, ohne diese auf ihrem System zuvor installieren zu müssen. Deshalb ist der Einsatz des Wrappers einem fest installierten Gradle vorzuziehen!
Wrap-Up
-
Automatisieren von Arbeitsabläufen mit Build-Tools/-Skripten
-
Einstieg in Gradle (DSL zur Konfiguration)
- Typisches Java-Entwicklungsmodell eingebaut
- Konfiguration der Abweichungen (Abhängigkeiten, Namen, ...)
- Gradle-Wrapper: Ersetzt eine feste Installation
Link-Sammlung Gradle
- "Getting Started"
- "Building Java Applications Sample"
- "Building Java Applications with libraries Sample"
- "Building Java Libraries Sample"
- "Building Java & JVM projects"
Betrachten Sie das Buildskript gradle.build
aus Dungeon-CampusMinden/Dungeon.
Erklären Sie, in welche Abschnitte das Buildskript unterteilt ist und welche Aufgaben diese
Abschnitte jeweils erfüllen. Gehen Sie dabei im Detail auf das Plugin java
und die dort
bereitgestellten Tasks und deren Abhängigkeiten untereinander ein.
- [Gradle] Gradle Build Tool
Gradle Inc., 2022. - [Inden2013] Der Weg zum Java-Profi
Inden, M., dpunkt.verlag, 2013. ISBN 978-3-8649-1245-0.
Continuous Integration (CI)
In größeren Projekten mit mehreren Teams werden die Beteiligten i.d.R. nur noch "ihre" Codestellen compilieren und testen. Dennoch ist es wichtig, das gesamte Projekt regelmäßig zu "bauen" und auch umfangreichere Testsuiten regelmäßig laufen zu lassen. Außerdem ist es wichtig, das in einer definierten Umgebung zu tun und nicht auf einem oder mehreren Entwicklerrechnern, die i.d.R. (leicht) unterschiedlich konfiguriert sind, um zuverlässige und nachvollziehbare Ergebnisse zu bekommen. Weiterhin möchte man auf bestimmte Ereignisse reagieren, wie etwa neue Commits im Git-Server, oder bei Pull-Requests möchte man vor dem Merge automatisiert sicherstellen, dass damit die vorhandenen Tests alle "grün" sind und auch die Formatierung etc. stimmt.
Dafür hat sich "Continuous Integration" etabliert. Hier werden die angesprochenen Prozesse regelmäßig auf einem dafür eingerichteten System durchgeführt. Aktivitäten wie Übersetzen, Testen, Style-Checks etc. werden in sogenannten "Pipelines" oder "Workflows" zusammengefasst und automatisiert durch Commits, Pull-Requests oder Merges auf dem Git-Server ausgelöst. Die Aktionen können dabei je nach Trigger und Branch unterschiedlich sein, d.h. man könnte etwa bei PR gegen den Master umfangreichere Tests laufen lassen als bei einem PR gegen einen Develop-Branch. In einem Workflow oder einer Pipeline können einzelne Aktionen wiederum von anderen Aktionen abhängen. Das Ergebnis kann man dann auf dem Server einsehen oder bekommt man komfortabel als Report per Mail zugeschickt.
Wir schauen uns hier exemplarisch GitHub Actions und GitLab CI/CD an. Um CI sinnvoll einsetzen zu können, benötigt man Kenntnisse über Build-Tools. "CI" tritt üblicherweise zusammen mit "CD" (Continuous Delivery) auf, also als "CI/CD". Der "CD"-Teil ist nicht Gegenstand der Betrachtung in dieser Lehrveranstaltung.
- (K2) Arbeitsweise von/mit CI
Motivation: Zusammenarbeit in Teams
Szenario
- Projekt besteht aus diversen Teilprojekten
- Verschiedene Entwicklungs-Teams arbeiten (getrennt) an verschiedenen Projekten
- Tester entwickeln Testsuiten für die Teilprojekte
- Tester entwickeln Testsuiten für das Gesamtprojekt
Manuelle Ausführung der Testsuiten reicht nicht
- Belastet den Entwicklungsprozess
- Keine (einheitliche) Veröffentlichung der Ergebnisse
- Keine (einheitliche) Eskalation bei Fehlern
- Keine regelmäßige Integration in Gesamtprojekt
Continuous Integration
- Regelmäßige, automatische Ausführung: Build und Tests
- Reporting
- Weiterführung der Idee: Regelmäßiges Deployment (Continuous Deployment)
Continuous Integration (CI)
Vorgehen
- Entwickler und Tester committen ihre Änderungen regelmäßig (Git, SVN, ...)
- CI-Server arbeitet Build-Skripte ab, getriggert durch Events: Push-Events, Zeit/Datum, ...
- Typischerweise wird dabei:
- Das Gesamtprojekt übersetzt ("gebaut")
- Die Unit- und die Integrationstests abgearbeitet
- Zu festen Zeiten werden zusätzlich Systemtests gefahren
- Typische weitere Builds: "Nightly Build", Release-Build, ...
- Ergebnisse jeweils auf der Weboberfläche einsehbar (und per E-Mail)
- Typischerweise wird dabei:
Einige Vorteile
- Tests werden regelmäßig durchgeführt (auch wenn sie lange dauern oder die Maschine stark belasten)
- Es wird regelmäßig ein Gesamt-Build durchgeführt
- Alle Teilnehmer sind über aktuellen Projekt(-zu-)stand informiert
Beispiele für verbreitete CI-Umgebungen
GitLab CI/CD
Siehe auch "Get started with Gitlab CI/CD". (Für den Zugriff wird VPN benötigt!)
Übersicht über Pipelines
- In Spalte "Status" sieht man das Ergebnis der einzelnen Pipelines: "pending" (die Pipeline läuft gerade), "cancelled" (Pipeline wurde manuell abgebrochen), "passed" (alle Jobs der Pipeline sind sauber durchgelaufen), "failed" (ein Job ist fehlgeschlagen, Pipeline wurde deshalb abgebrochen)
- In Spalte "Pipeline" sind die Pipelines eindeutig benannt aufgeführt, inkl. Trigger (Commit und Branch)
- In Spalte "Stages" sieht man den Zustand der einzelnen Stages
Wenn man mit der Maus auf den Status oder die Stages geht, erfährt man mehr bzw. kann auf eine Seite mit mehr Informationen kommen.
Detailansicht einer Pipeline
Wenn man in eine Pipeline in der Übersicht klickt, werden die einzelnen Stages dieser Pipeline genauer dargestellt.
Detailansicht eines Jobs
Wenn man in einen Job einer Stage klickt, bekommt man quasi die Konsolenausgabe dieses Jobs. Hier kann man ggf. Fehler beim Ausführen der einzelnen Skripte oder die Ergebnisse beispielsweise der JUnit-Läufe anschauen.
GitLab CI/CD: Konfiguration mit YAML-Datei
Datei .gitlab-ci.yml
im Projekt-Ordner:
stages:
- my.compile
- my.test
job1:
script:
- echo "Hello"
- ./gradlew compileJava
- echo "wuppie!"
stage: my.compile
only:
- wuppie
job2:
script: "./gradlew test"
stage: my.test
job3:
script:
- echo "Job 3"
stage: my.compile
Stages
Unter stages
werden die einzelnen Stages einer Pipeline definiert. Diese werden
in der hier spezifizierten Reihenfolge durchgeführt, d.h. zuerst würde my.compile
ausgeführt, und erst wenn alle Jobs in my.compile
erfolgreich ausgeführt wurden,
würde anschließend my.test
ausgeführt.
Dabei gilt: Die Jobs einer Stage werden (potentiell) parallel zueinander ausgeführt, und die Jobs der nächsten Stage werden erst dann gestartet, wenn alle Jobs der aktuellen Stage erfolgreich beendet wurden.
Wenn keine eigenen stages
definiert werden, kann man
(lt. Doku)
auf die Default-Stages build
, test
und deploy
zurückgreifen. Achtung: Sobald
man eigene Stages definiert, stehen diese Default-Stages nicht mehr zur Verfügung!
Jobs
job1
, job2
und job3
definieren jeweils einen Job.
-
job1
besteht aus mehreren Befehlen (unterscript
). Alternativ kann man die beijob2
gezeigte Syntax nutzen, wenn nur ein Befehl zu bearbeiten ist.Die Befehle werden von GitLab CI/CD in einer Shell ausgeführt.
-
Die Jobs
job1
undjob2
sind der Stagemy.compile
zugeordnet (Abschnittstage
). Einer Stage können mehrere Jobs zugeordnet sein, die dann parallel ausgeführt werden.Wenn ein Job nicht explizit einer Stage zugeordnet ist, wird er (lt. Doku) zur Default-Stage
test
zugewiesen. (Das geht nur, wenn es diese Stage auch gibt!) -
Mit
only
undexcept
kann man u.a. Branches oder Tags angeben, für die dieser Job ausgeführt (bzw. nicht ausgeführt) werden soll.
Durch die Kombination von Jobs mit der Zuordnung zu Stages und Events lassen sich unterschiedliche Pipelines für verschiedene Zwecke definieren.
Hinweise zur Konfiguration von GitLab CI/CD
Im Browser in den Repo-Einstellungen arbeiten:
- Unter
Settings > General > Visibility, project features, permissions
dasCI/CD
aktivieren - Prüfen unter
Settings > CI/CD > Runners
, dass unterAvailable shared Runners
mind. ein shared Runner verfügbar ist (mit grün markiert ist) - Unter
Settings > CI/CD > General pipelines
einstellen:Git strategy
:git clone
Timeout
:10m
Public pipelines
:false
(nicht angehakt)
- YAML-File (
.gitlab-ci.yml
) in Projektwurzel anlegen, Aufbau siehe oben - Build-Skript erstellen, lokal lauffähig bekommen, dann in Jobs nutzen
- Im
.gitlab-ci.yml
die relevanten Branches einstellen (s.o.) - Pushen, und unter
CI/CD > Pipelines
das Builden beobachten- in Status reinklicken und schauen, ob und wo es hakt
README.md
anlegen in Projektwurzel (neben.gitlab-ci.yml
), Markdown-Schnipsel ausSettings > CI/CD > General pipelines > Pipeline status
auswählen und einfügen .…
Optional:
- Ggf. Schedules unter
CI/CD > Schedules
anlegen - Ggf. extra Mails einrichten:
Settings > Integrations > Pipeline status emails
GitHub Actions
Siehe "GitHub Actions: Automate your workflow from idea to production" und auch "GitHub: CI/CD explained".
Übersicht über Workflows
Hier sieht man das Ergebnis der letzten Workflows. Dazu sieht man den Commit und den Branch, auf dem der Workflow gelaufen ist sowie wann er gelaufen ist. Über die Spalten kann man beispielsweise nach Status oder Event filtern.
In der Abbildung ist ein Workflow mit dem Namen "GitHub CI" zu sehen, der aktuell noch läuft.
Detailansicht eines Workflows
Wenn man in einen Workflow in der Übersicht anklickt, werden die einzelnen Jobs dieses Workflows genauer dargestellt. "job3" ist erfolgreich gelaufen, "job1" läuft gerade, und "job2" hängt von "job1" ab, d.h. kann erst nach dem erfolgreichen Lauf von "job2" starten.
Detailansicht eines Jobs
Wenn man in einen Job anklickt, bekommt man quasi die Konsolenausgabe dieses Jobs. Hier kann man ggf. Fehler beim Ausführen der einzelnen Skripte oder die Ergebnisse beispielsweise der JUnit-Läufe anschauen.
GitHub Actions: Konfiguration mit YAML-Datei
Workflows werden als YAML-Dateien im Ordner .github/workflows/
angelegt.
name: GitHub CI
on:
# push on master branch
push:
branches: [master]
# manually triggered
workflow_dispatch:
jobs:
job1:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- uses: gradle/wrapper-validation-action@v1
- run: echo "Hello"
- run: ./gradlew compileJava
- run: echo "wuppie!"
job2:
needs: job1
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- uses: gradle/wrapper-validation-action@v1
- run: ./gradlew test
job3:
runs-on: ubuntu-latest
steps:
- run: echo "Job 3"
Workflowname und Trigger-Events
Der Name des Workflows wird mit dem Eintrag name
spezifiziert und sollte sich im
Dateinamen widerspiegeln, also im Beispiel .github/workflows/github_ci.yml
.
Im Eintrag on
können die Events definiert werden, die den Workflow triggern. Im
Beispiel ist ein Push-Event auf dem master
-Branch definiert sowie mit workflow_dispatch:
das manuelle Triggern (auf einem beliebigen Branch) freigeschaltet.
Jobs
Die Jobs werden unter dem Eintrag jobs
definiert: job1
, job2
und job3
definieren
jeweils einen Job.
-
job1
besteht aus mehreren Befehlen (untersteps
), die auf einem aktuellen virtualisierten Ubuntu-Runner ausgeführt werden.Es wird zunächst das Repo mit Hilfe der Checkout-Action ausgecheckt (
uses: actions/checkout@v4
), das JDK eingerichtet/installiert (uses: actions/setup-java@v3
) und der im Repo enthaltene Gradle-Wrapper auf Unversehrtheit geprüft (uses: gradle/wrapper-validation-action@v1
).Die Actions sind vordefinierte Actions und im Github unter
github.com/
+ Action zu finden, d.h.actions/checkout
oderactions/setup-java
. Actions können von jedermann definiert und bereitgestellt werden, in diesem Fall handelt es sich um von GitHub selbst im Namespace "actions" bereit gestellte direkt nutzbare Actions. Man kann Actions auch selbst im Ordner.github/actions/
für das Repo definieren (Beispiel: plfa.github.io).Mit
run
werden Befehle in der Shell auf dem genutzten Runner (hier Ubuntu) ausgeführt. -
Die Jobs
job2
ist vonjob1
abhängig und wird erst gestartet, wennjob1
erfolgreich abgearbeitet ist.Ansonsten können die Jobs prinzipiell parallel ausgeführt werden.
Durch die Kombination von Workflows mit verschiedenen Jobs und Abhängigkeiten zwischen Jobs lassen sich unterschiedliche Pipelines ("Workflows") für verschiedene Zwecke definieren.
Es lassen sich auch andere Runner benutzen, etwa ein virtualisiertes Windows oder macOS. Man kann auch über einen "Matrix-Build" den Workflow auf mehreren Betriebssystemen gleichzeitig laufen lassen.
Man kann auch einen Docker-Container benutzen. Dabei muss man beachten, dass dieser am besten aus einer Registry (etwa von Docker-Hub oder aus der GitHub-Registry) "gezogen" wird, weil das Bauen des Docker-Containers aus einem Docker-File in der Action u.U. relativ lange dauert.
Hinweise zur Konfiguration von GitHub Actions
Im Browser in den Repo-Einstellungen arbeiten:
-
Unter
Settings > Actions > General > Actions permissions
die Actions aktivieren (Auswahl, welche Actions erlaubt sind) -
Unter
Settings > Actions > General > Workflow permissions
ggf. bestimmen, ob die Actions das Repo nur lesen dürfen oder auch zusätzlich schreiben dürfen -
Unter
Actions > <WORKFLOW>
den Workflow ggf. deaktivieren:
Wrap-Up
Überblick über Continuous Integration:
- Konfigurierbare Aktionen, die auf dem Gitlab-/GitHub-Server ausgeführt werden
- Unterschiedliche Trigger: Commit, Merge, ...
- Aktionen können Branch-spezifisch sein
- Aktionen können von anderen Aktionen abhängen
- [GitHubCI] Documentation GitHub CI
GitHub Inc., 2022. - [GitlabCI] Documentation Gitlab CI
Einführung in Docker
Container sind im Gegensatz zu herkömmlichen VMs eine schlanke Virtualisierungslösung.
Dabei laufen die Prozesse direkt im Kernel des Host-Betriebssystems, aber abgeschottet
von den anderen Prozessen durch Linux-Techniken wie cgroups
und namespaces
(unter
Windows kommt dafür der WSL2 zum Einsatz, unter macOS wird eine kleine Virtualisierung
genutzt).
Container sind sehr nützlich, wenn man an mehreren Stellen eine identische Arbeitsumgebung benötigt. Man kann dabei entweder die Images (fertige Dateien) oder die Dockerfiles (Anweisungen zum Erzeugen eines Images) im Projekt verteilen. Tatsächlich ist es nicht unüblich, ein Dockerfile in das Projekt-Repo mit einzuchecken.
Durch Container hat man allerdings im Gegensatz zu herkömmlichen VMs keinen Sicherheitsgewinn, da die im Container laufende Software ja direkt auf dem Host-Betriebssystem ausgeführt wird.
Es gibt auf DockerHub fertige Images, die man sich ziehen und starten kann. Ein solches gestartetes Image nennt sich dann Container und enthält beispielsweise Dateien, die in den Container gemountet oder kopiert werden. Man kann auch eigene Images bauen, indem man eine entsprechende Konfiguration (Dockerfile) schreibt. Jeder Befehl bei der Erstellung eines Images erzeugt einen neuen Layer, die sich dadurch mehrere Images teilen können.
In der Konfiguration einer Gitlab-CI-Pipeline kann man mit image
ein Docker-Image
angeben, welches dann in der Pipeline genutzt wird.
VSCode kann über das Remote-Plugin sich (u.a.) mit Containern verbinden und dann im Container arbeiten (editieren, compilieren, debuggen, testen, ...).
In dieser kurzen Einheit kann ich Ihnen nur einen ersten Einstieg in das Thema geben. Wir haben uns beispielsweise nicht Docker Compose oder Kubernetes angeschaut, und auch die Themen Netzwerk (zwischen Containern oder zwischen Containern und anderen Rechnern) und Volumnes habe ich außen vor gelassen. Dennoch kommt man in der Praxis bereits mit den hier vermittelten Basiskenntnissen erstaunlich weit ...
- (K2) Unterschied zwischen Containern und VMs
- (K2) Einsatzgebiete für Container
- (K2) Container laufen als abgeschottete Prozesse auf dem Host - kein Sandbox-Effekt
- (K3) Container von DockerHub ziehen
- (K3) Container starten
- (K3) Eigene Container definieren und bauen
- (K3) Einsatz von Containern in GitLab CI/CD und GitHub Actions
- (K3) Einsatz von VSCode und Containern
Motivation CI/CD: WFM (Works For Me)
Auf dem CI-Server muss man eine Arbeitsumgebung konfigurieren und bereitstellen, für Java-basierte Projekte muss beispielsweise ein JDK existieren und man benötigt Tools wie Maven oder Gradle, um die Buildskripte auszuführen. Je nach Projekt braucht man dann noch weitere Tools und Bibliotheken. Diese Konfigurationen sind unabhängig vom CI-Server und werden üblicherweise nicht direkt installiert, sondern über eine Virtualisierung bereitgestellt.
Selbst wenn man keine CI-Pipelines einsetzt, hat man in Projekten mit mehreren beteiligten Personen häufig das Problem "WFM" ("works for me"). Jeder Entwickler hat sich auf ihrem Rechner eine Entwicklungsumgebung aufgesetzt und nutzt in der Regel seine bevorzugte IDE oder sogar unterschiedliche JDK-Versionen ... Dadurch kann es schnell passieren, dass Probleme oder Fehler auftreten, die sich nicht von allen Beteiligten immer nachvollziehen lassen. Hier wäre eine einheitliche Entwicklungsumgebung sinnvoll, die in einer "schlanken" Virtualisierung bereitgestellt wird.
Als Entwickler kann man zeitgleich in verschiedenen Projekten beteiligt sein, die unterschiedliche Anforderungen an die Entwicklungstools mit sich bringen. Es könnte beispielsweise passieren, dass man zeitgleich drei bestimmte Python-Versionen benötigt. In den meisten Fällen schafft man es (mit ein wenig Aufwand), diese Tools nebeneinander zu installieren. Oft ist das in der Praxis aber schwierig und fehleranfällig.
In diesen Fällen kann eine Virtualisierung helfen.
Virtualisierung: Container vs. VM
Wenn man über Virtualisierung auf dem Desktop spricht, kann man grob zwei Varianten unterscheiden. In beiden Fällen ist die Basis die Hardware (Laptop, Desktop-Rechner) und das darauf laufende (Host-) Betriebssystem (Linux, FreeBSD, macOS, Windows, ...). Darauf läuft dann wiederum die Virtualisierung.
Im rechten Bild wird eine herkömmliche Virtualisierung mit virtuellen Maschinen (VM) dargestellt. Dabei wird in der VM ein komplettes Betriebssystem (das "Gast-Betriebssystem") installiert und darin läuft dann die gewünschte Anwendung. Die Virtualisierung (VirtualBox, VMware, ...) läuft dabei als Anwendung auf dem Host-Betriebssystem und stellt dem Gast-Betriebssystem in der VM einen Rechner mit CPU, RAM, ... zur Verfügung und übersetzt die Systemaufrufe in der VM in die entsprechenden Aufrufe im Host-Betriebssystem. Dies benötigt in der Regel entsprechende Ressourcen: Durch das komplette Betriebssystem in der VM ist eine VM (die als Datei im Filesystem des Host-Betriebssystems liegt) oft mehrere 10GB groß. Für die Übersetzung werden zusätzlich Hardwareressourcen benötigt, d.h. hier gehen CPU-Zyklen und RAM "verloren" ... Das Starten einer VM dauert entsprechend lange, da hier ein komplettes Betriebssystem hochgefahren werden muss. Dafür sind die Prozesse in einer VM relativ stark vom Host-Betriebssystem abgekapselt, so dass man hier von einer "Sandbox" sprechen kann: Viren o.ä. können nicht so leicht aus einer VM "ausbrechen" und auf das Host-Betriebssystem zugreifen (quasi nur über Lücken im Gast-Betriebssystem kombiniert mit Lücken in der Virtualisierungssoftware).
Im linken Bild ist eine schlanke Virtualisierung auf Containerbasis dargestellt. Die Anwendungen
laufen direkt als Prozesse im Host-Betriebssystem, ein Gast-Betriebssystem ist nicht notwendig.
Durch den geschickten Einsatz von namespaces
und cgroups
und anderen in Linux und FreeBSD
verfügbaren Techniken werden die Prozesse abgeschottet, d.h. der im Container laufende Prozess
"sieht" die anderen Prozesse des Hosts nicht. Die Erstellung und Steuerung der Container übernimmt
hier beispielsweise Docker. Die Container sind dabei auch wieder Dateien im Host-Filesystem.
Dadurch benötigen Container wesentlich weniger Platz als herkömmliche VMs, der Start einer Anwendung
geht deutlich schneller und die Hardwareressourcen (CPU, RAM, ...) werden effizient genutzt.
Nachteilig ist, dass hier in der Regel ein Linux-Host benötigt wird (für Windows wird mittlerweile
der Linux-Layer (WSL) genutzt; für macOS wurde bisher eine Linux-VM im Hintergrund hochgefahren,
mittlerweile wird aber eine eigene schlanke Virtualisierung eingesetzt). Außerdem steht im Container
üblicherweise kein graphisches Benutzerinterface zur Verfügung. Da die Prozesse direkt im
Host-Betriebssystem laufen, stellen Container keine Sicherheitsschicht ("Sandboxen") dar!
In allen Fällen muss die Hardwarearchitektur beachtet werden: Auf einer Intel-Maschine können normalerweise keine VMs/Container basierend auf ARM-Architektur ausgeführt werden und umgekehrt.
Getting started
-
DockerHub: fertige Images => hub.docker.com/search
-
Image downloaden:
docker pull <IMAGE>
-
Image starten:
docker run <IMAGE>
Begriffe
- Docker-File: Beschreibungsdatei, wie Docker ein Image erzeugen soll.
- Image: Enthält die Dinge, die lt. dem Docker-File in das Image gepackt werden sollen. Kann gestartet werden und erzeugt damit einen Container.
- Container: Ein laufendes Images (genauer: eine laufende Instanz eines Images). Kann dann auch zusätzliche Daten enthalten.
Beispiele
docker pull debian:stable-slim
docker run --rm -it debian:stable-slim /bin/sh
debian
ist ein fertiges Images, welches über DockerHub bereit gestellt wird. Mit dem
Postfix stable-slim
wird eine bestimmte Version angesprochen.
Mit docker run debian:stable-slim
startet man das Image, es wird ein Container
erzeugt. Dieser enthält den aktuellen Datenstand, d.h. wenn man im Image eine Datei
anlegt, wäre diese dann im Container enthalten.
Mit der Option --rm
wird der Container nach Beendigung automatisch wieder gelöscht. Da
jeder Aufruf von docker run <IMAGE>
einen neuen Container erzeugt, würden sich sonst
recht schnell viele Container auf dem Dateisystem des Hosts ansammeln, die man dann manuell
aufräumen müsste. Man kann aber einen beendeten Container auch erneut laufen lassen ...
(vgl. Dokumentation von docker
). Mit der Option --rm
sind aber auch im Container angelegte
Daten wieder weg! Mit der Option -it
wird der Container interaktiv gestartet und man landet
in einer Shell.
Bei der Definition eines Images kann ein "Entry Point" definiert werden, d.h. ein Programm,
welches automatisch beim Start des Container ausgeführt wird. Häufig erlauben Images aber auch,
beim Start ein bestimmtes auszuführendes Programm anzugeben. Im obigen Beispiel ist das /bin/sh
,
also eine Shell ...
docker pull openjdk:latest
docker run --rm -v "$PWD":/data -w /data openjdk:latest javac Hello.java
docker run --rm -v "$PWD":/data -w /data openjdk:latest java Hello
Auch für Java gibt es vordefinierte Images mit einem JDK. Das Tag "latest
" zeigt dabei auf die
letzte stabile Version des openjdk
-Images. Üblicherweise wird "latest
" von den Entwicklern immer
wieder weiter geschoben, d.h. auch bei anderen Images gibt es ein "latest
"-Tag. Gleichzeitig ist
es die Default-Einstellung für die Docker-Befehle, d.h. es kann auch weggelassen werden:
docker run openjdk:latest
und docker run openjdk
sind gleichwertig. Alternativ kann man hier auch
hier wieder eine konkrete Version angeben.
Über die Option -v
wird ein Ordner auf dem Host (hier durch "$PWD"
dynamisch ermittelt) in den
Container eingebunden ("gemountet"), hier auf den Ordner /data
. Dort sind dann die Dateien sichtbar,
die im Ordner "$PWD"
enthalten sind. Über die Option -w
kann ein Arbeitsverzeichnis definiert
werden.
Mit javac Hello.java
wird javac
im Container aufgerufen auf der Datei /data/Hello.java
im Container, d.h. die Datei Hello.java
, die im aktuellen Ordner des Hosts liegt (und in den
Container gemountet wurde). Das Ergebnis (Hello.class
) wird ebenfalls in den Ordner /data/
im Container geschrieben und erscheint dann im Arbeitsverzeichnis auf dem Host ... Analog kann
dann mit java Hello
die Klasse ausgeführt werden.
Images selbst definieren
FROM debian:stable-slim
ARG USERNAME=pandoc
ARG USER_UID=1000
ARG USER_GID=1000
RUN apt-get update && apt-get install -y --no-install-recommends \
apt-utils bash wget make graphviz biber \
texlive-base texlive-plain-generic texlive-latex-base \
#
&& groupadd --gid $USER_GID $USERNAME \
&& useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME \
#
&& apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/*
WORKDIR /pandoc
USER $USERNAME
docker build -t <NAME> -f <DOCKERFILE> .
FROM
gibt die Basis an, d.h. hier ein Image von Debian in der Variante stable-slim
,
d.h. das ist der Basis-Layer für das zu bauende Docker-Image.
Über ARG
werden hier Variablen gesetzt.
RUN
ist der Befehl, der im Image (hier Debian) ausgeführt wird und einen neuen Layer
hinzufügt. In diesen Layer werden alle Dateien eingefügt, die bei der Ausführung des
Befehls erzeugt oder angelegt werden. Hier im Beispiel wird das Debian-Tool apt-get
gestartet und weitere Debian-Pakete installiert.
Da jeder RUN
-Befehl einen neuen Layer anlegt, werden die restlichen Konfigurationen
ebenfalls in diesem Lauf durchgeführt. Insbesondere wird ein nicht-Root-User angelegt,
der von der UID und GID dem Default-User in Linux entspricht. Die gemounteten Dateien
haben die selben Rechte wie auf dem Host, und durch die Übereinstimmung von UID/GID
sind die Dateien problemlos zugreifbar und man muss nicht mit dem Root-User arbeiten
(dies wird aus offensichtlichen Gründen als Anti-Pattern angesehen). Bevor der RUN
-Lauf
abgeschlossen wird, werden alle temporären und später nicht benötigten Dateien von
apt-get
entfernt, damit diese nicht Bestandteil des Layers werden.
Mit WORKDIR
und USER
wird das Arbeitsverzeichnis gesetzt und auf den angegebenen User
umgeschaltet. Damit muss der User nicht mehr beim Aufruf von außen gesetzt werden.
Über docker build -t <NAME> -f <DOCKERFILE> .
wird aus dem angegebenen Dockerfile und
dem Inhalt des aktuellen Ordners (".
") ein neues Image erzeugt und mit dem angegebenen
Namen benannt.
Hinweis zum Umgang mit Containern und Updates: Bei der Erstellung eines Images sind bestimmte Softwareversionen Teil des Images geworden. Man kann prinzipiell in einem Container die Software aktualisieren, aber dies geht in dem Moment wieder verloren, wo der Container beendet und gelöscht wird. Außerdem widerspricht dies dem Gedanken, dass mehrere Personen mit dem selben Image/Container arbeiten und damit auch die selben Versionsstände haben. In der Praxis löscht man deshalb das alte Image einfach und erstellt ein neues, welches dann die aktualisierte Software enthält.
CI-Pipeline (GitLab)
default:
image: openjdk:17
job1:
stage: build
script:
- java -version
- javac Hello.java
- java Hello
- ls -lags
In den Gitlab-CI-Pipelines (analog wie in den GitHub-Actions) kann man Docker-Container für die Ausführung der Pipeline nutzen.
Mit image: openjdk:17
wird das Docker-Image openjdk:17
vom DockerHub geladen und durch
den Runner für die Stages als Container ausgeführt. Die Aktionen im script
-Teil, wie
beispielsweise javac Hello.java
werden vom Runner an die Standard-Eingabe der Shell des
Containers gesendet. Im Prinzip entspricht das dem Aufruf auf dem lokalen Rechner:
docker run openjdk:17 javac Hello.java
.
CI-Pipeline (GitHub)
name: demo
on:
push:
branches: [master]
workflow_dispatch:
jobs:
job1:
runs-on: ubuntu-latest
container: docker://openjdk:17
steps:
- uses: actions/checkout@v4
- run: java -version
- run: javac Hello.java
- run: java Hello
- run: ls -lags
https://stackoverflow.com/questions/71283311/run-github-workflow-on-docker-image-with-a-dockerfile https://docs.github.com/en/actions/using-jobs/running-jobs-in-a-container
In den GitHub-Actions kann man Docker-Container für die Ausführung der Pipeline nutzen.
Mit docker://openjdk:17
wird das Docker-Image openjdk:17
vom DockerHub geladen und auf dem
Ubuntu-Runner als Container ausgeführt. Die Aktionen im steps
-Teil, wie beispielsweise
javac Hello.java
werden vom Runner an die Standard-Eingabe der Shell des Containers gesendet.
Im Prinzip entspricht das dem Aufruf auf dem lokalen Rechner: docker run openjdk:17 javac Hello.java
.
VSCode und das Plugin "Remote - Containers"
- VSCode (Host): Plugin "Remote - Containers" installieren
- Docker (Host): Container starten mit Workspace gemountet
- VSCode (Host): Attach to Container => neues Fenster (Container)
- VSCode (Container): Plugin "Java Extension Pack" installieren
- VSCode (Container): Dateien editieren, kompilieren, debuggen, ...
Mit Visual Studio Code (VSC) kann man über SSH oder in einem Container arbeiten. Dazu installiert man sich VSC lokal auf dem Host und installiert dort das Plugin "Remote - Containers". VSC kann darüber vordefinierte Docker-Images herunterladen und darin arbeiten oder man kann alternativ einen Container selbst starten und diesen mit VSC verbinden ("attachen").
Beim Verbinden öffnet VSC ein neues Fenster, welches mit dem Container verbunden ist. Nun kann man in diesem neuen Fenster ganz normal arbeiten, allerdings werden alle Dinge in dem Container erledigt. Man öffnet also Dateien in diesem Container, editiert sie im Container, übersetzt und testet im Container und nutzt dabei die im Container installierten Tools. Sogar die entsprechenden VSC-Plugins kann man im Container installieren.
Damit benötigt man auf einem Host eigentlich nur noch VSC und Docker, aber keine Java-Tools o.ä. und kann diese über einen im Projekt definierten Container (über ein mit versioniertes Dockerfile) nutzen.
Anmerkung: IntelliJ kann remote nur debuggen, d.h. das Editieren, Übersetzen, Testen läuft lokal auf dem Host (und benötigt dort den entsprechenden Tool-Stack). Für das Debuggen kann Idea das übersetzte Projekt auf ein Remote (SSH, Docker) schieben und dort debuggen.
Noch einen Schritt weiter geht das Projekt code-server: Dieses stellt u.a. ein Docker-Image codercom/code-server bereit, welches einen Webserver startet und über diesen kann man ein im Container laufendes (angepasstes) VSC erreichen. Man braucht also nur noch Docker und das Image und kann dann über den Webbrowser programmieren. Der Projektordner wird dabei in den Container gemountet, so dass die Dateien entsprechend zur Verfügung stehen:
docker run -it --name code-server -p 127.0.0.1:8080:8080 -v "$HOME/.config:/home/coder/.config" -v "$PWD:/home/coder/project" codercom/code-server:latest
Auf diesem Konzept setzt auch der kommerzielle Service GitHub Codespaces von GitHub auf.
Link-Sammlung
- Wikipedia: Docker
- Wikipedia: Virtuelle Maschinen
- Docker: Überblick, Container
- Docker: HowTo
- DockerHub: Suche nach fertigen Images
- Docker und Java
- Dockerfiles: Best Practices
- Gitlab, Docker
- VSCode: Entwickeln in Docker-Containern
- [DockerInAction] und [DockerInPractice]
Wrap-Up
-
Schlanke Virtualisierung mit Containern (kein eigenes OS)
-
Kein Sandbox-Effekt
-
Begriffe: Docker-File vs. Image vs. Container
-
Ziehen von vordefinierten Images
-
Definition eines eigenen Images
-
Arbeiten mit Containern: lokal, CI/CD, VSCode ...
- [DockerInAction] Docker in Action
Nickoloff, D., Manning Publications, 2019. ISBN 978-1-6172-9476-1. - [DockerInPractice] Docker in Practice
Miell, I. und Sayers, A. H., Manning Publications, 2019. ISBN 978-1-6172-9480-8. - [Inden2013] Der Weg zum Java-Profi
Inden, M., dpunkt.verlag, 2013. ISBN 978-3-8649-1245-0.
Build-Systeme: Apache Ant
Zum Automatisieren von Arbeitsabläufen (Kompilieren, Testen, ...) stehen in der Java-Welt verschiedene Tools zur Verfügung: Apache Ant, Apache Maven und Gradle sind sicher die am bekanntesten darunter.
In Apache Ant werden die Build-Skripte in XML definiert. Die äußere Klammer ist dabei das
<project>
. In einem Projekt kann es ein oder mehrere Teilziele (Targets) geben, die
untereinander abhängig sein können. Die Targets können quasi "aufgerufen" werden bzw. in
der IDE selektiert und gestartet werden.
In einem Target kann man schließlich mit Tasks Aufgaben wie Kompilieren, Testen, Aufräumen, ... erledigen lassen. Dazu gibt es eine breite Palette an vordefinierten Tasks. Zusätzlich sind umfangreiche Operationen auf dem Filesystem möglich (Ordner erstellen, löschen, Dinge kopieren, ...).
Über Properties können Werte und Namen definiert werden, etwa für bestimmte Ordner. Die Properties sind unveränderliche Variablen (auch wenn man sie im Skript scheinbar neu setzen kann).
Über Apache Ivy können analog zu Maven und Gradle definierte Abhängigkeiten aus Maven-Central aufgelöst werden.
Im Unterschied zu Maven und Gradle ist in Ant kein Java-Entwicklungsmodell eingebaut. Man muss sämtliche Targets selbst definieren.
- (K3) Schreiben einfacher Ant-Skripte mit Abhängigkeiten zwischen den Targets
- (K3) Nutzung von Ant-Filesets (Dateisystemoperationen, Classpath)
- (K3) Nutzung von Ant-Properties
- (K3) Ausführen von Ant-Targets aus der IDE heraus
Automatisieren von Arbeitsabläufen
Works on my machine ...
-
Build-Tools:
- Apache Ant
- Apache Maven
- Gradle
-
Aufgaben:
- Übersetzen des Quellcodes
- Ausführen der Unit-Tests
- Generieren der Dokumentation
- Packen der Distribution
- Aufräumen temporärer Dateien
- ...
=> Automatisieren mit Apache Ant: ant.apache.org
Aufbau von Ant-Skripten: Projekte und Targets
<project name="Vorlesung" default="clean" basedir=".">
<target name="init" />
<target name="compile" depends="init" />
<target name="test" depends="compile" />
<target name="dist" depends="compile,test" />
<target name="clean" />
</project>
- Ein Hauptziel:
project
- Ein oder mehrere Teilziele:
target
- Abhängigkeiten der Teilziele untereinander möglich:
depends
- Vorbedingung für Targets mit
if
oderunless
- Abhängigkeiten der Teilziele untereinander möglich:
Aufgaben erledigen: Tasks
- Tasks: Aufgaben bzw. Befehle ("Methodenaufruf"), in Targets auszuführen
- Struktur:
<taskname attr1="value1" attr2="value2" />
<target name="simplecompile" depends="clean,init">
<javac srcdir="src" destdir="build" classpath="." />
</target>
- Beispiele:
echo
,javac
,jar
,javadoc
,junit
, ... - Je Target mehrere Tasks möglich
- Quellen:
- Eingebaute Tasks
- Optionale Task-Bibliotheken
- Selbst definierte Tasks
=> Überblick: ant.apache.org/manual/tasksoverview.html
Properties: Name-Wert-Paare
<property name="app" value="MyProject" />
<property name="build.dir" location="build" />
<target name="init">
<mkdir dir="${build.dir}" />
</target>
-
Setzen von Eigenschaften:
<property name="wuppie" value="fluppie" />
- Properties lassen sich nur einmal setzen ("immutable")
- Erneute Definition ist wirkungslos, erzeugt aber leider keinen Fehler
- Properties lassen sich nur einmal setzen ("immutable")
-
Nutzung von Properties:
<property name="db" value="${wuppie}.db" />
-
Pfade: "
location
" statt "value
" nutzen:<property name="ziel" location="${p}/bla/blub" />
-
Properties beim Aufruf setzen mit Option "
-D
":ant -Dwuppie=fluppie
Tasks zum Umgang mit Dateien und Ordnern
<target name="demo">
<mkdir dir="${build.dir}/lib" />
<delete dir="${build.dir}" />
<delete file="${dist.dir}/wuppie.jar" />
<copy file="myfile.txt" tofile="../bak/mycopy.txt" />
<move file="src/file.orig" tofile="bak/file.moved" />
</target>
<mkdir dir="${dist}/lib" />
- Legt auch direkte Unterverzeichnisse an
- Keine Aktion, falls Verzeichnis existiert
<delete dir="${builddir}" />
- Löscht eine Datei ("
file
") oder ein Verzeichnis ("dir
") (rekursiv!)
- Löscht eine Datei ("
<copy file="myfile.txt" tofile="../bak/mycopy.txt" />
<move file="src/file.orig" tofile="bak/file.moved" />
Nutzung von Filesets in Tasks
<copy todir="archive">
<fileset dir="src">
<include name="**/*.java" />
<exclude name="**/*.ba?" />
</fileset>
</copy>
<delete>
<fileset dir="." includes="**/*.ba?" />
</delete>
- "
*
" für beliebig viele Zeichen - "
?
" für genau ein Zeichen - "
**
" alle Unterverzeichnisse
Es gibt auch die Variante <dirset dir="...">
, um Verzeichnisse zu
gruppieren.
Pfade und externe Bibliotheken
-
Als Element direkt im Task:
<classpath> <pathelement location="${lib}/helper.jar" /> <pathelement path="${project.classpath}" /> </classpath>
D.h. die Einbettung in den
javac
-Task würde etwa so erfolgen:<target ... > <javac ...> <classpath> <pathelement location="${lib}/helper.jar" /> <pathelement path="${project.classpath}" /> </classpath> </javac> </target>
Anmerkung: Neben dem
pathelement
können Sie hier auch (wie im nächsten Beispiel gezeigt) ein oder mehrerefileset
nutzen. -
Wiederverwendbar durch ID und "
refid
":<path id="java.class.path"> <fileset dir="${lib}"> <include name="**/*.jar" /> </fileset> </path> <classpath refid="java.class.path" />
Die Einbettung in den
javac
-Task würde hier etwa so erfolgen:<path id="java.class.path"> <fileset dir="${lib}"> <include name="**/*.jar" /> </fileset> </path> <target ... > <javac ...> <classpath refid="java.class.path" /> </javac> </target>
Anmerkung: Neben dem
fileset
können Sie hier auch (wie oben gezeigt) ein oder mehrerepathelement
nutzen.
Anmerkung: Laut ant.apache.org/manual/Tasks/junit.html
benötigt man neben der aktuellen junit.jar
noch die ant-junit.jar
im Classpath, um mit dem junit
-Ant-Task
entsprechende JUnit4-Testfälle ausführen zu können.
Für JUnit5 gibt es einen neuen Task JUnitLauncher
(vgl.
ant.apache.org/manual/Tasks/junitlauncher.html).
Beispiele
Beispiel-Task: Kompilieren
<path id="project.classpath">
<fileset dir="${lib.dir}" includes="**/*.jar" />
</path>
<target name="compile" depends="init" description="compile the source " >
<javac srcdir="${src.dir}" destdir="${build.dir}">
<classpath refid="project.classpath" />
</javac>
</target>
Beispiel-Task: Packen
<target name="dist" depends="compile" description="generate the distribution" >
<mkdir dir="${dist.dir}" />
<jar jarfile="${dist.dir}/${app}.jar" basedir="${build.dir}">
<manifest>
<attribute name="Main-Class" value="${app}" />
</manifest>
</jar>
</target>
Beispiel-Task: Testen
-
Tests einer Testklasse ausführen:
<junit> <test name="my.test.TestCase" /> </junit>
-
Test ausführen und XML mit Ergebnissen erzeugen:
<junit printsummary="yes" fork="yes" haltonfailure="yes"> <formatter type="xml" /> <test name="my.test.TestCase" /> </junit>
-
Verschiedene Tests als Batch ausführen:
<junit printsummary="yes" haltonfailure="yes"> <classpath> <pathelement location="${build.tests}" /> <pathelement path="${java.class.path}" /> </classpath> <formatter type="plain"/> <test name="my.test.TestCase" haltonfailure="no" outfile="result"> <formatter type="xml" /> </test> <batchtest fork="yes" todir="${reports.tests}"> <fileset dir="${src.tests}"> <include name="**/*Test*.java" /> <exclude name="**/AllTests.java" /> </fileset> </batchtest> </junit>
-
Aus Testergebnis (XML) einen Report generieren:
<junitreport todir="${reportdir}"> <fileset dir="..."> <include name="TEST-*.xml" /> </fileset> <report format="frames" todir="..." /> </junitreport>
-
Abbruch bei Fehler:
<junit ... failureproperty="tests.failed" ... > <fail if="tests.failed" /> </junit>
=> junit.jar
und ant-junit.jar
(JUnit4.x) im Pfad!
Programme ausführen
<target name="run">
<java jar="build/jar/HelloWorld.jar" fork="true" classname ="test.Main">
<arg value ="-h" />
<classpath>
<pathelement location="./lib/test.jar" />
</ classpath>
</ java>
</target>
Ausblick: Laden von Abhängigkeiten mit Apache Ivy
Apache Ivy: Dependency Manager für Ant
<!-- build.xml -->
<project xmlns:ivy="antlib:org.apache.ivy.ant">
<target name="resolve">
<ivy:retrieve/>
</target>
</project>
Wenn Ivy installiert ist, kann man durch den Eintrag xmlns:ivy="antlib:org.apache.ivy.ant"
in der Projekt-Deklaration im Ant-Skript die Ivy-Tasks laden. Der wichtigste Task ist dabei
ivy:retrieve
, mit dem externe Projektabhängigkeiten heruntergeladen werden können.
<!-- ivy.xml -->
<ivy-module version="2.0">
<dependencies>
<dependency org="commons-cli" name="commons-cli" rev="1.5.0" />
<dependency org="junit" name="junit" rev="4.13.2" />
</dependencies>
</ivy-module>
Zur Steuerung von Ivy legt man eine weitere Datei ivy.xml
an. Das Wurzelelement ist ivy-module
,
wobei die version
die niedrigste kompatible Ivy-Version angibt.
Der dependencies
-Abschnitt definiert dann die Abhängigkeiten, die Ivy auflösen muss. Die Schreibweise
ist dabei wie im Maven2 Repository (mvnrepository.com) angelegt. Dort
findet man beispielsweise für Apache Commons CLI den Eintrag für Maven ("POM"-Datei):
<!-- https://mvnrepository.com/artifact/commons-cli/commons-cli -->
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.4</version>
</dependency>
Für die Ivy-Konfiguration übernimmt man die groupId
als org
, die artifactId
als name
und die
version
als rev
im Eintrag dependency
.
Damit kann Ivy diese Bibliothek über den Ant-Task ivy:retrieve
vor dem Bauen herunterladen, sofern
die Bibliothek noch nicht lokal vorhanden ist. Eventuelle Abhängigkeiten werden dabei ebenfalls aufgelöst.
Im Detail: Der Ant-Task ivy:retrieve
löst zunächst die Abhängigkeiten auf und lädt die Dateien (sofern
sie noch nicht vorhanden oder veraltet sind) in den Ivy-Cache (per Default: ~/.ivy2/cache/
). Danach
werden die Dateien in den Default-Library-Order im Projekt kopiert (per Defaul: ./lib/
). Die Ordner kann
man über Optionen im ivy:retrieve
-Task einstellen.
Ausblick: Weitere Build-Systeme
-
- War als Nachfolger von Ant gedacht
- Statt wie bei Ant explizit Targets zu formulieren, geht Maven von einem Standardprojekt aus - nur noch Abweichungen müssen formuliert werden
- Zieht Abhängigkeiten in zentralen
.maven
-Ordner
-
- Eine Art Mischung aus Ant und Maven unter Nutzung der Sprache Groovy
-
- DER Klassiker, stammt aus der C-Welt. Kann aber natürlich auch Java.
- Analog zu Ant: Aktionen und Ziele müssen explizit definiert werden
Wrap-Up
Apache Ant: ant.apache.org
- Automatisieren von Arbeitsabläufen
- Apache Ant: Targets, Tasks, Properties
- Targets sind auswählbare Teilziele
- Abhängigkeiten zwischen Targets möglich
- Tasks erledigen Aufgaben (innerhalb Targets)
- Properties sind nicht änderbare Variablen
- Umfangreiche Operationen auf Filesystem möglich
- [Inden2013] Der Weg zum Java-Profi
Inden, M., dpunkt.verlag, 2013. ISBN 978-3-8649-1245-0.
Abschnitt 2.5.2: Ant
Build-Systeme: Apache Maven
Zum Automatisieren von Arbeitsabläufen (Kompilieren, Testen, ...) stehen in der Java-Welt verschiedene Tools zur Verfügung: Apache Ant, Apache Maven und Gradle sind sicher die am bekanntesten darunter.
In Apache Maven ist bereits der typische Java-Standard-Lebenszyklus eingebaut und es müssen nur noch Abweichungen davon und Festlegung von Versionen und Dependencies in XML formuliert werden. Dies nennt man auch "Convention over Configuration".
Die Maven-Goals sind auswählbare Ziele und werden durch Plugins bereitgestellt. Zwischen den Goals sind Abhängigkeiten möglich (und bereits eingebaut). Über Properties kann man noch Namen und Versionsnummern o.ä. definieren.
Abhängigkeiten zu externen Bibliotheken werden als Dependencies formuliert: Am besten den Abschnitt von Maven-Central kopieren.
- (K3) Schreiben einfacher Maven-Skripte zu Übersetzen des Projekts, zum Testen und zum Erzeugen von Jar-Files
- (K3) Nutzung von Maven-Properties
- (K3) Einbinden externer Bibliotheken als Dependencies
- (K3) Ausführen von Maven-Goals aus IDE heraus und Einbindung als Builder
Build-Tool Maven: Alternative zu Ant oder Gradle
mvn archetype:generate -DgroupId=de.hsbi.pm -DartifactId=my-project
-DarchetypeArtifactId=maven-archetype-quickstart
Von der zeitlichen Entstehung her kommt Maven nach Ant, aber vor Gradle. Wie in Ant sind auch die Maven-Buildskripte XML-basierte Textdateien (Gradle nutzt eine Groovy-basierte DSL).
Allerdings hat Maven im Gegensatz zu Ant bereits ein Modell des Java-Entwicklungsprozess "eingebaut": Im Ant-Skript muss alles, was man tun möchte, explizit als Target formuliert werden, d.h. auch ein Kompilieren der Sourcen oder Ausführen der Tests muss extra als Target ins Ant-Skript geschrieben werden, um benutzbar zu sein. In Maven ist dieses Modell bereits implementiert, d.h. hier muss man lediglich zusätzliche oder abweichende Dinge im XML-File konfigurieren. Das nennt man auch "convention over configuration".
Der Maven-Aufruf
mvn archetype:generate -DgroupId=de.hsbi.pm -DartifactId=my-project -DarchetypeArtifactId=maven-archetype-quickstart
erzeugt mit Hilfe des Plugins archetype
, welches das Ziel (engl.: "Maven goal") generate
bereitstellt, ein neues Projekt mit dem Namen my-project
und der initialen Package-Struktur
de.hsbi.pm
. Das von Maven für die Projekterstellung genutzte Plugin ist unter der ID
maven-archetype-quickstart
in den Maven-Repositories (etwa Maven-Central)
verfügbar, hier kann man mit der zusätzlichen Option -DarchetypeVersion=1.4
auf die letzte
Version schalten.
Die erzeugte Ordnerstruktur entspricht der Standardstruktur von Gradle (Gradle hat diese
quasi von Maven übernommen). Die Konfigurationsdatei für Maven hat den Namen pom.xml
.
Hinweis: Die groupId
und artifactId
werden auch für eine Veröffentlichung des Jar-Files
des Projekts auf dem zentralen Maven-Repository Maven-Central
genutzt. Von hier würde Maven auch als Abhängigkeit konfigurierte Bibliotheken herunterladen.
Lebenszyklus (eingebaut in Maven)
In Maven ist das typische Java-Entwicklungsmodell als "Lebenszyklus" implementiert.
Entsprechende Plugins stellen die jeweiligen "Goals" (Ziele) bereit. Dabei sind
auch die Abhängigkeiten berücksichtigt, d.h. das Ziel test
erfordert ein compile
...
Project Object Model: pom.xml
<project>
<!-- aktuelle Version für Maven 2.x-->
<modelVersion>4.0.0</modelVersion>
<!-- Basisinformationen -->
<groupId>de.hsbi.pm</groupId>
<artifactId>my-project</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- Eigenschaften, vergleichbar zu den Properties in Ant -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<!-- Abhängigkeiten zu externen Bibliotheken -->
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Die Konfigurationsdatei pom.xml
stellt die Konfiguration für das Maven-Projekt bereit
("Project Object Model").
Es werden mindestens der Name des Projekts sowie die Abhängigkeiten definiert.
Die groupId
ist ein eindeutiger Bezeichner für die Organisation oder den Autor des Projekts. Oft
wird hier einfach wie im obigen Beispiel eine Package-Struktur genutzt, aber wie im Fall von JUnit
kann dies auch ein einfacher String (dort "junit
") sein.
Die artifactId
ist der eindeutige Name für das Projekt, d.h. unter diesem Namen wird das generierte
Jar-File im Maven-Repository zu finden sein (sofern es denn veröffentlicht wird).
Über dependencies
kann man benötigte Abhängigkeiten definieren, hier als Beispiel JUnit in der
4.x Variante ... Diese werden bei Bedarf von Maven vom Maven-Repository heruntergeladen. Die Einträge
für die Dependencies findet man ebenfalls auf MavenCentral.
Project Object Model: Plugins
<project>
...
<!-- Plugins: Stellen eigene "Goals" zur Verfügung -->
<build>
<plugins>
<plugin>
<!-- https://mvnrepository.com/artifact/org.apache.maven.plugins/maven-compiler-plugin -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
</plugin>
<plugin>
<!-- https://mvnrepository.com/artifact/org.apache.maven.plugins/maven-surefire-plugin -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
</build>
</project>
Zusätzlich können die Phasen des Build-Prozesses konfiguriert werden, d.h. für die
entsprechenden Plugins finden sich Abschnitte unter <build><plugins>
in der pom.xml
.
Auf maven.apache.org/plugins/index.html finden Sie eine Übersicht über häufig benutzte Plugins sowie die von den Plugins bereitgestellten Goals sowie Konfigurationsmöglichkeiten.
Die entsprechenden POM-Einträge finden Sie analog zu den Dependencies ebenfalls auf MavenCentral (Tag "plugin" statt "dependency").
Plugins können aber auch selbst erstellt werden und in das Projekt eingebunden werden, ein erster Einstieg ist die Plugin-API.
Und wie lasse ich jetzt eine Anwendung mal laufen?
mvn clean
: Lösche alle generierten Artefakte, beispielsweise.class
-Dateien.mvn compile
=>mvn compiler:compile
: Übersetze die Sourcen und schiebe die generierten.class
-Dateien in den Ordnertarget/classes/
(Default). Dazu werden alle Abhängigkeiten aufgelöst und bei Bedarf (neu) heruntergeladen (Default: Userverzeichnis, Ordner.m2/
).mvn test
=>mvn surefire:test
: Lasse die Tests laufen. Hängt voncompile
ab. Namenskonvention: Alle Klassen mit*Test.java
undTest*.java
im Standard-Testordnersrc/test/java/
werden betrachtet (und weitere, vgl. maven.apache.org/surefire/maven-surefire-plugin/examples/junit-platform.html).mvn package
: Hängt voncompile
ab und erzeugt ein Jar-File mit dem Namen "artifactId-version.jar" im Ordnertarget/
. Mitmvn install
kann man dieses Jar-File dann auch dem lokalen Repository im Home-Verzeichnis des Users (.m2/
) hinzufügen.mvn exec:java -Dexec.mainClass="de.hsbi.pm.Main"
: Hängt voncompile
ab und führt die Klassede.hsbi.pm.Main
aus.
Wrap-Up
Apache Maven: maven.apache.org, Maven Getting Started Guide
- Automatisieren von Arbeitabläufen
- Apache Maven: Goals, Properties, Dependencies => "Convention over Configuration",
Java-Standard-Lebenszyklus eingebaut
- Goals sind auswählbare Ziele, bereitgestellt durch Plugins
- Abhängigkeiten zwischen Goals möglich
- Properties agieren wie Variablen, etwa für Versionsnummern
- Abhängigkeiten zu externen Bibliotheken werden als Dependencies formuliert: Abschnitt von Maven-Central kopieren
- [Inden2013] Der Weg zum Java-Profi
Inden, M., dpunkt.verlag, 2013. ISBN 978-3-8649-1245-0.