Dependency management

Lees ook: Meer uitleg over de Gradle build tool.

Wat is de beste manier om afhankelijkheden te beheren?

Wat is een ‘dependency’?

Een dependency, of afhankelijkheid, is een externe bibliotheek die wordt gebruikt tijdens de ontwikkeling van een toepassing. Tijdens het vak ‘Software ontwerp in Java’ zijn reeds de volgende externe libraries gebruikt:

  1. JavaFX
  2. Google Gson
  3. JUnit

Het vertrouwen op zo’n library houdt in dat een extern bestand, zoals een .jar of .war bestand, download en koppelt wordt aan de applicatie. In Java koppelen we externe libraries door middel van het CLASSPATH: een folder die de compiler gebruikt om te zoeken naar klassen.

Serialisatie met behulp van Gson kan op deze manier:

public class Main {
    public static void main(String[] args) {
        Gson gson = new Gson();
        System.out.println(gson.toJson(1));
    }
}

Bovenstaande Main.java compileren zonder meer geeft de volgende fout:

PS C:\Users\u0158802\OneDrive - KU Leuven\Desktop\java_example_2> javac  Main.java
Main.java:3: error: cannot find symbol
        Gson gson = new Gson();
        ^
  symbol:   class Gson
  location: class Main

De klasse Gson is immers iets dat we niet hebben zelfgemaakt, maar wensen te importeren via het import com.google.gson.*; statement. Er is een manier nodig om de gedownloade library te linken met onze bestaande code: javac -cp gson-2.9.0.jar Main.java. Het programma uitvoeren kan met java -cp "gson-2.9.0.jar;." Main. Er worden dus 2 zaken aan het classpath meegegeven: de Google jar, en de huidige directory (.), om Main.class terug te vinden.

Java classpath separators zijn OS-specifiek! Unix: : in plaats van Windows: ;.

Dit programma kan schematisch worden voorgesteld als volgt:

graph LR; A["Main klasse"] C["Gson 2.9.0"] A -->|dependent on| C

De dependency in bovenstaand voorbeeld is gson-2.9.0.jar. Een gemiddelde Java applicatie heeft echter meer dan 10 dependencies! Het beheren van deze bestanden en de verschillende versies (major, minor, revision) geeft vaak conflicten die beter beheerd kunnen worden door tools dan door de typische vergeetachtigheid van mensen. Dit kluwen aan afhankelijkheden, dat erg snel onhandelbaar kan worden, noemt men een Dependency Hell. Er zijn varianten: DLL Hell sinds 16-bit Windows versies, RPM Hell voor Redhat Linux distributies, en JAR Hell voor Java projecten.

Zie ook xkcd’s Tech Loops rommelboeltje:

Wie beheert dependencies?

De ontwikkelaar (manueel)

De eenvoudigste manier om een library te gebruiken is de volgende procedure te volgen:

  1. Navigeer naar de website van de library en download deze in een bepaalde map, zoals /lib.
  2. Importeer de juiste klasses met het import statement.
  3. Compileer de code door middel van het -cp dependency1.jar argument.

Voor kleine programma’s met enkele libraries is dit meer dan voldoende. Het kost echter redelijk veel moeite om de juiste versie te downloaden: stap 1 kost meestal meer dan 5 minuten werk.

Merk op dat jar files in een submap steken de syntax van de -cp parameter lichtjes wijzigt: bij compileren wordt het javac -cp lib/* bla.java en bij uitvoeren wordt het java -cp "lib/*:." bla. Zonder de toegevoegde punt (.) bij het java commando wordt de main methode in bla zelf niet gevonden. Wildcards zijn toegestaan. Zie ook Understanding the Java Classpath. In de praktijk worden build tools als Gradle gebruikt om projecten automatisch te builden, inclusief het doorgeven van de juiste parameters/dependencies.

De tools (automatisch)

Apache Maven

Maven is een build tool van de Apache Foundation die zowel de manier waarop de software wordt gecompileerd als zijn afhankelijkheden beheert. Maven is de voorloper van Gradle en bestaat reeds 15 jaar.

Een Maven project heeft een pom.xml bestand (Project Object Model), waarin in XML formaat wordt beschreven hoe de structuur er uit ziet, welke libraries men gebruikt, en zo voort:

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.mycompany.app</groupId>
  <artifactId>my-app</artifactId>
  <version>1.0</version>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Maven is erg populair in de Java wereld, waardoor er verschillende servers zijn die deze pom bestanden samen met hun libraries beheren, zoals de Central Maven Repository en de Google Maven Repository mirrors. De syntax van het configuratiebestand is echter erg onoverzichtelijk, en er zijn ondertussen betere alternatieven beschikbaar, zoals Gradle.

Gradle

Belangrijk: neem dit eerst door - Meer informatie over Gradle.

Gradle is net zoals Maven een automatisatie tool voor de Java wereld (en daarbuiten), die verder bouwt op de populariteit van Maven door bijvoorbeeld compatibel te zijn met de Repository servers, maar de grootste pijnpunten wegneemt: een slorig configuratiebestand in XML, en complexe command-line scripts.

De volgende procedure volg je als je Gradle dependencies laat beheren:

  1. Zoek op de Maven Repository website naar de gewenste library.
  2. Voeg één regel toe in je gradle.build bestand, in het dependencies stuk:
dependencies {
    implementation 'com.google.code.gson:gson:2.9.0'
}

Bij het uitvoeren van gradlew download Gradle automatisch de juiste opgegeven versie. Gradle bewaart lokale kopies van libraries in een submap van je home folder: ~/.gradle. Dit kan je controleren door in IntelliJ naar File -> Project Structure te gaan en te klikken op “Libraries”:

Het lokale path naar de auto-cached libraries.

Voordelen van het gebruik van deze methode:

  1. Het zoeken van libraries beperkt zich tot één centrale (Maven Repository) website, waar alle verschillende versie revisies duidelijk worden vermeld.
  2. Het downloaden van libraries beperkt zich tot één centrale locatie op je harde schijf: 10 verschillende Java projecten die gebruik maken van Gson vereisen linken naar dezelfde gradle bestanden.
  3. Het beheren van dependencies en versies beperkt zich tot één centraal configuratiebestand: build.gradle. Dit is (terecht) een integraal deel van het project!

Lees ook: Declaring dependencies in de Gradle docs.

Custom Repository URLs voorzien

Veelgebruikte libraries zijn eenvoudig te vinden via de Central Maven Repository. Wanneer echter een eigen library werd gecompileerd, die dan in andere projecten worden gebruikt, schiet deze methode tekort: interne libraries zijn uiteraard niet op een publieke server gepubliceerd.

Gradle voorziet gelukkig genoeg een eenvoudige manier om repository websites toe te voegen, met de volgende eenvoudige syntax:

repositories {
  mavenCentral()
}

mavenCentral(), jcenter(), en google() zijn ingebouwde repositories. Eigen Maven folders en URLs toevoegen kan ook, evenals een lokale folder:

repositories {
    maven {
        // dit kan zowel een folder als HTTP(s) URL zijn
        url "C:\\Users\\u0158802\\development\\java\\maven-repo"
    }
    flatDir {
        dirs 'lib'
    }
}

Transitieve dependencies

Er zijn twee types van dependencies: directe (1) en transitieve (2). Een directe dependency is een afhankelijkheid die het project nodig heeft, zoals het gebruik van Gson, waarbij dit in de dependencies {} config zit. Een transitieve of indirecte dependency is een dependency van een dependency. In de oefening hieronder maken we een project (1) aan, dat een project (2) gebruikt, dat Gson gebruikt. In project 1 is project 2 een directe dependency, en Gson een transitieve. In Project 2 is Gson een directe dependency (en komt project 1 niet voor):

graph LR; A[Project een] B[Project twee] C[Gson] A --> B B --> C A -.-> C

Het is geen goed idee om bij fouten in uitvoering de zachte link (stippellijn) te veranderen in een harde, door dit als directe dependency toe te voegen. Gradle biedt hier alternatieven voor. Het voor de hand liggende alternatief is van de library ook een Maven module te maken en deze te uploaden naar een (lokale) repository.

Publiceren naar een Maven Repository

Klik op ‘View All’ bij de Gson module op de MVN Central Repo om te inspecteren welke bestanden typisch worden aangeleverd in een Maven repository:

  1. De library zelf, in een bepaalde versie.
  2. Eventueel de javadoc en/of sources als aparte jars.
  3. Een .pom XML bestand.
  4. metadata als md5 checksums.

Het XML bestand beschrijft welke afhankelijkheden deze module op zich heeft. Zo kan een hele dependency tree worden opgebouwd! Het beheren van alle afhankelijkheden is complexer dan op het eerste zicht lijkt, en laat je dus beter over aan deze gespecialiseerde tools. Google heeft voor Gson enkel Junit als test dependency aangeduid:

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Grote projecten kunnen makkelijk afhankelijk zijn van tientallen libraries, die op hun beurt weer afhankelijk zijn van libraries. Een typische grote webapplicatie geschreven in java heeft de volgende dependency tree, die opgevraagd kan worden via Gradle of Maven:

Gebruik hiervoor de task dependencies: ./gradlew dependencies. Detailinformatie voor specifieke dependencies kunnen worden opgevraagd met de dependencyInsight task. Zie ook: Viewing and debugging dependencies in de Gradle docs.

Gradle voorziet een plugin genaamd ‘maven-publish’ die deze bestanden automatisch aanmaakt.

plugins {
    id 'java'
    id 'maven-publish' // toevoegen!
}

publishing {
    publications {
        maven(MavenPublication) {
            groupId = project.group.toString()
            version = version
            artifactId = 'projectnaam'

            from components.java
        }
    }
    repositories {
        maven {
            url = "C:\\Users\\u0158802\\development\\java\\maven-repo"
        }
    }
}

Windows gebruikers dienen in de url value te werken met dubbele backslashes (\\) in plaats van forward slashes (/) om naar het juiste pad te navigeren.

Deze uitbreiding voegt de target publish toe aan Gradle. Dus: ./gradlew publish publiceert de nodige bestanden in de aangegeven folder. Een Gradle project die daar gebruik van wenst te maken dient enkel een tweede Maven Repository plaats te definiëren:

repositories {
    mavenCentral()
    maven {
        url = "C:\\Users\\u0158802\\development\\java\\maven-repo"
    }
}

Opgaven

Neem dit eerst door: Meer informatie over Gradle.

Opgave 1

Ontwerp een eenvoudige library genaamd ‘scorebord’ die scores kan bijhouden voor bordspelletjes. Deze library kan dan gebruikt worden door toekomstige digitale Java bordspellen. In een Scorebord kan je spelers toevoegen door middel van een naam en een score. Er is een mogelijkheid om de huidige score van een speler op te vragen, en de winnende speler. Deze gegevens worden met behulp van Gson in een JSON bestand bewaard, zodat bij het heropstarten van een spel de scores behouden blijven.
De API (publieke methodes) van de library ziet er zo uit:

public class Speler {
    public String getNaam() { }
    public int getScore() { }
}
public class Scorebord {
    public void voegToe(String x, int huidigeScore) { }
    public int getTotaleScore(String x) { }
    public String getWinnaar() { }
}

De klasse Speler is een intern hulpmiddel om te serialiseren.
Extra methodes toevoegen mag altijd. De constructor van het scorebord leest automatisch de score van de vorige keer in, als dat bestand bestaat. Denk bij de implementatie aan een collectie om spelers en hun scores bij te houden. Maak via IntelliJ een nieuw Gradle - Java project. Groupid: be.kuleuven. Arifactid: scorebord. Vergeet niet op ‘refresh’ te drukken wanneer je een dependency toevoegt (linksboven op onderstaande screenshot):

Met het commando gradlew jar creëer je het bestand scorebord-1.0-SNAPSHOT.jar in de build/libs folder.

Denk na over het bijhouden van Spelers in Scorebord. Een simpele ArrayList zal volstaan. Gebruik Gson in een methode als save() om gewoon de lijst (of het object zelf) naar de HDD te serialiseren. Tip: java.nio.files.write.

Tip: indien de Gralde wrapper een oudere versie aanmaakt (< v6), update met gradlew wrapper --gradle-version 6.0.1. Gradle versie 6 of groter is vereist voor JDK 13 of groter.

Opgave 2

Maak een nieuw Gradle project aan genaamd ‘scorebord-darts’, dat bovenstaand scorebord project als een library gaat gebruiken. Bewaar de jar file lokaal in een ’lib’ folder en instrueer Gradle zo dat dit als flatDir repository wordt opgenomen (zie boven). Het tweede project heeft als Artifactid scorebord-darts. De klasse DartsGame ziet er zo uit:

public class DartsGame {
    private String player = "jos";
    public void throwDart() {}
}

Als de dependencies goed liggen, kan je een nieuw Scorebord aanmaken, en herkent IntelliJ dit met CTRL+Space:

Maak een Main klasse met een public static void main(String[] args) methode, waarin een darts spel wordt opgezet, en een aantal keer ter test wordt ‘gegooid’. Druk de totale score en de winnaar af, dat opgevraagd kan worden via het spelbord. Krijg je deze klasse opgestart?

> Task :Main.main() FAILED
Exception in thread "main" java.lang.NoClassDefFoundError: com/google/gson/Gson
    at be.kuleuven.scorebord.Scorebord.(Scorebord.java:24)
    at be.kuleuven.DartsGame.(DartsGame.java:11)
    at be.kuleuven.Main.main(Main.java:6)
Caused by: java.lang.ClassNotFoundException: com.google.gson.Gson
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:583)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
    ... 3 more

Caused by: java.lang.ClassNotFoundException: com.google.gson.Gson

Execution failed for task ':Main.main()'.
> Process 'command '/Library/Java/JavaVirtualMachines/jdk-11.0.2.jdk/Contents/Home/bin/java'' finished with non-zero exit value 1

Dit werkt niet omdat we een library gebruiken (ScoreBord), die op zijn beurt een library gebruikt (Gson), die niet in onze huidige Gradle file is gedefiniëerd. Om dit op te lossen dienen we over te schakelen naar een lokale Maven repository, die ook transitieve dependencies automatisch inlaadt. Verwijder de flatDir en voeg een lokale maven URL toe. Publiceer in het scorebord project naar diezelfde URL volgens de instructies van de maven-publish plugin.