Java-projektin hallinta Mavenilla

Keskitymme tästä luvussa Javan perusominaisuuksien sijaan Java-ohjelman toteuttamiseen ja hallintaan saatavailla oleviin apuvälineisiin. Eräs varsin perustavanlaatuinen tarve koskee koodin käännösprosessin ja ulkoisten kirjastojen hallintaa. Tähän tarkoitukseen on olemassa laaja kirjo apuvälineitä. Tässä on valittu tarkasteltavaksi Apache Maven -niminen työkalu, jolla voi määrittää esimerkiksi sen, miten ohjelma käännetään ja mitä ulkoisia kirjastoja ohjelma tarvitsee. Maven lienee yleisin Java-projekteissa käytetty käännöstyökalu, mutta myös esimerkiksi Gradle on nykyisin varsin suosittu. Mavenin roolia Java-projektin hallinnassa voisi verrata npm-pakettienhallintaohjelman rooliin Node.js-viitekehystä käyttävässä JavaScript-projektissa. Käsittelemme Mavenia varsin suppeasti pidättäytyen lähinnä kurssin kannalta oleellisimmissa ominaisuuksissa.

Maven on JDK:n ja Git-versionhallinnan tapaan NetBeansin kaltaisista kehitystyökaluista erillinen kokonaisuus. Netbeans sisältää Mavenin jo valmiiksi Java SE -liitännäisen kautta, jonka avulla Maven-projekteja voi luoda Nebeansista käsin. Jos Mavenia haluaa käyttää komentoriviltä, tarvitaan Mavenin erillinen asennus. Maven on saatavilla kotisivujensa kautta. Jos Maven on kunnolla asennettu, pitäisi se pystyä suorittamaan komentorivillä komennolla mvn.

Maven-projektin rakenne

Maven-projekti koostuu pääasiassa kahdesta asiasta: projektin juurihakemistossa sijaitsevasta Maven-projektin ominaisuudet määrittävästä projektitiedostosta nimeltä pom.xml, ja alihakemistossa src sijaitsevasta projektin lähdekoodista. Java-lähdekoodit sijoitetaan oletuksena alihakemistoon src/main/java. Pakkauksiin liitetyt tiedostot ovat tämän hakemiston alla pakkauksia vastaavissa alihakemistoissa. Esimerkiksi pakkauksen com.example kooditiedoston SomeClass.java polku olisi src/main/java/com/example/SomeClass.java. Jos projekti hakemiston nimeksi on valittu esimerkiksi test, on projektin hakemistorakenne tässä tapauksessa seuraava:

test
│─ pom.xml
└───src
    └───main
        └───java
            └───com
                └───example
                    └───SomeClass.java

Kuten tiedostopääte vihjaa, projektitiedosto pom.xml kuvaa projektin ominaisuudet XML-muodossa. Ellei XML ole vielä tuttu käsite, tutustu sen perusominaisuuksiin esimerkiksi Wikipedian artikkelista.

Alla on esimerkkinä melko, joskaan ei aivan, minimaalinen Mavenin projektitiedosto:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>fi.tuni.prog3</groupId>
  <artifactId>example</artifactId>
  <version>1.0</version>
  <packaging>jar</packaging>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
  </properties>
</project>

pom.xml:n täytyy sisältää ainakin seuraavat osat:

  • project-juurielementti, joka alkaa <project>-alkutagilla ja päättyy </project>-lopputagiin. Alla luetellut elementit sijaitsevat juurielementin sisällä.

  • modelVersion-elementti, jonka arvon tulee nykyisin aina olla 4.0.0. Tämä kuvaa, mitä Mavenin standardia (tai “mallia”) projektitiedosto vastaa. Nykyinen on 4.0.0.

  • groupId-elementti, joka kertoo projektin ryhmän. Maven voi ryhmitellä projekteja tämän perusteella hieman samoin kuin Javan pakkaukset ryhmittelevät lähdekooditiedostoja.

  • artifactId-elementti, joka kertoo projektin nimen.

  • version-elementti, joka kertoo kuvaa projektin version. Tämä voisi siis kasvaa sitä mukaa, kun ohjelma kehittyy. Esimerkiksi version 1.0 jälkeen voisi tulla versio 1.1 tai versio 2.0.

Mavenilla hallituissa Java-projekteissa on vakiintunut käytäntö se, että groupId- ja artifactId-muodostavat yhdessä projektin Java-pakkauksen nimen. Jos tavoitteena on esimerkiksi sijoittaa projektin tiedostot fi.tuni.prog3.example-nimiseen pakkaukseen, annetaan groupId -elementin arvoksi fi.tuni.prog3 ja artifactId-elementin arvoksi example. Mavenia tuntevat ohjelmoijat olettavat, että tätä käytännettä noudatetaan. Jatka siksi perinnettä itsekin, vaikka pakkaus ja elementti voivat olla teknisellä tasolla eri nimisiä.

Yllä annettiin myös packaging-elementti, jolla voi määrittää millaiseen muotoon Maven paketoi käännetyn ohjelman. Arvo jar tarkoittaa, että ohjelman tavukooditiedostot ja muut resurssit asetetaan yhteen JAR-tiedostoon. Tästä kohta lisää. Yllä määritetään lisäksi lopussa properties-elementin sisällä, että projektin lähdekooditiedostot käyttävät UTF-8 merkistökoodausta, lähdekoodin tulkitaan noudattavan Javan versiota 17, ja kääntäjä tuottaa Javan version 17 mukaisia luokkatiedostoja. Javan versioita koskevat arvot tulee tietenkin asettaa oman ympäristön kannalta toimiviksi. Ne eivät voi olla käännösympäristön tarjoaman Javan versiota korkeampia, mutta alempia on mahdollista käyttää.

NetBeans

Maven-projekti luodaan NetBeansissa valisemalla File | New Project... (tai pienen plusmerkin sisältävä kansiokuvake) ja sitten Java with Maven ja Java Application. NetBeans kysyy tiedot suurimmalle osalle yllä mainituista elementeistä ja luo sitten jokseenkin yllä kuvattua muotoa olevan projektitiedoston, src/main/java-hakemiston ja pakkaushakemiston sekä pakkaushakemistoon main-metodin sisältävän ajoluokan, jonka voi haluessaan poistaa. Projektin sisältö näkyy teemoittain ryhmiteltynä NetBeansin käyttöliittymän vasemmassa yläkulmassa olevalla Projects-välilehdellä. Projektitiedosto pom.xml on Project files -kohdan alla ja Source Packages -kohdan alla on annettu projektin pakkaus ja sen sisältämät tiedostot.

Projektin voi luoda ilman pakkausmääreitä Package:-kentän tyhjentämällä. NetBeans ei tee tällöin pakkaushakemistoa ja sijoittaa valmiin ajoluokan pakkausmääreettä src/main/java-hakemistoon, joka näkyy Project-välilehdellä Project Packages | <default packages> -kohdassa. Myös muiden lähdekooditiedostojen tulee olla tässä tapauksessa src/main/java-hakemistossa. Toimi näin vain, jos harjoitustehtävässä ei ole kiinnitetty lähdekoodille pakkausta tai pakkauksia.

Netbeans lisää versionumeron perään "-SNAPSHOT"-merkkijonon, joka ilmaisee projektin olevan kehitysvaiheessa (epävakaa). Merkkijonon voi poistaa tällä kurssilla, koska projektit ovat sen verran pieniä, että niille voi antaa lopullisen versionumeron heti luotaessa. Näin esimerkiksi versio 1.0-SNAPSHOT voi olla lyhyesti vain 1.0. Jatkossa oletetaan, että projekteille annetaan versionumerot ilman "-SNAPSHOT"-lopuketta.

Maven-projektin kääntäminen

Maven-projektin voi kääntää joko Mavenia tukevan IDE:n komennolla tai komentoriviltä. IDE vaatii usein lisäosan asentamisen, jotta Mavenia voidaan käyttää IDE:stä käsin.

Maven käsittelee käännöstä oletusarvoisesti monivaiheisena projektin koostamisprosessina (build), johon kuuluu muun muassa seuraavat vaiheet:

  • compile: Kääntää projektin alihakemistossa src olevat lähdekoodit.

  • test: Suorittaa käännetylle ohjelmalle automaattiset testit, jos sellaisia on määritetty.

  • package: Paketoi käännetyn ohjelman helpommin jaettavaan muotoon yhdeksi tiedostoksi. Paketti sisältää ohjelman tavukoodin ja on usein JAR-muotoinen.

  • install: Tallentaa paketin Mavenin paikalliseen tietovarastoon, joka sijoitetaan käyttäjän kotihakemiston alihakemistoon .m2/repository. Maven tulostaa oletusarvoisesti tiedon siitä, mihin ja millä nimellä paketti tarkkaan ottaen kopioitiin.

Vaiheita on enemmänkin, mutta emme käsittele niitä tässä tarkemmin.

Maven-projektin voi kääntää komentorivillä muotoa mvn phase olevalla komennolla, missä phase kertoo, mihin käännösvaiheeseen asti haluamme suorittaa käännöksen. Vaiheet seuraavat toinen toistaan järjestyksessä, ja myöhemmän vaiheen suorittaminen tarkoittaa, että suoritetaan myös kaikki sitä edeltävät vaiheet. Esimerkiksi mvn compile kääntää projektin, mvn test sekä kääntää että testaa ja mvn package sekä kääntää, testaa että paketoi.

Maven asettaa käännetyt luokkatiedostot oletusarvoisesti projektihakemiston juuren alihakemistoon target/classes. Kuten tavallisestikin, luokkatiedostot sijoittuvat niille määritettyjen pakkausten mukaiseen hakemistorakenteeseen. Jos projekti paketoidaan esimerkiksi JAR-tiedostoon, asettaa Maven lopputuloksen alihakemiston target juureen. Tuloksena syntyvän tiedoston nimi johdetaan projektitiedostossa määritetyistä tiedoista: se on yleensä muotoa artifactId-version.jar.

Mavenin oletusprosessin lisäksi huomion arvoinen prosessi on clean, joka poistaa target -alihakemiston sisältöineen. Komento mvn clean ei etene käännösvaiheeseen, vaan vain “siivoaa” projektin poistamalla build-prosessin tuottamat tiedostot.

NetBeans

Maven-projektin kääntäminen Netbeansin Run | Build project -komennolla tai kääntäminen ja ajaminen Run | Run Project -komennolla vastaa komentoa mvn install. Koska install on myöhäisempi vaihe kuin package, sekin siten jo esimerkiksi paketoi ohjelman alihakemistoon target.

NetBeansin Run | Clean and Build Project -valinta suorittaa clean-prosessin ja aloittaa sitten build-prosessin. Tämä on hyödyllistä, jos projekti on tarpeen kääntää uudestaan aivan alusta alkaen.

Projektin kääntämiselle ja ajamiselle on omat kuvakkeensa työkalupakissa.

Maven-projektin suorittaminen

Mavenin built-käännösprosessiin ei kuulu omana vaiheenaan projektin suorittaminen. IDE:eissä on toki omat keinonsa suorittaa projekti ja Maveniin on olemassa projektin suorittamiseen tarkoitettu Exec Maven-liintännäinen, jota NetBeans hyödyntää, kun projekti ajetaan. Tämä liitännäinen otetaan käyttöön automaattisesti, jos projekti halutaan suorittaa Mavenilla komentoriviltä (tästä tietoa alempana NetBeans-otsikon alla).

Ehkä yksinkertaisempi tapa suorittaa projekti komentoriviltä on kuitenkin käyttää Mavenin käännösprosessinsa package-vaiheessa luomia JAR-tiedostoja suoraan Java-tulkilla ilman Mavenin apua. Ennen asian tarkempaa pohdintaan on syytä tutustua lähemmin JAR-tiedostoihin. JAR-tiedostot ovat eräs Java-kääntäjän tukema mahdollisuus paketoida ohjelma tai luokkakirjasto yhteen tiedostoon, jotta ohjelma olisi helpompi jakaa esimerkiksi loppukäyttäjille. JAR-tiedosto sisältää kaikki ohjelman luokkatiedostot ja mahdollisesti muitakin sen tarvitsemia resursseja. JAR-tiedostot ovat oikeastaan vain erilaista tiedostopäätettä käyttäviä ZIP-tiedostoja, ja niiden sisältöä voi siten sinänsä halutessaan käsitellä tavallisilla ZIP-tiedostojen käsittelyohjelmilla.

Java-kääntäjän ohessa tulee apuohjelma jar, jolla voi halutessaan itse paketoida komentorivillä jo valmiiksi käännettyjä luokkatiedostoja ja muita tiedostoja JAR-tiedostoksi. Emme käsittele tätä kuitenkaan tarkemmin, vaan keskitymme Mavenin luomien JAR-tiedostojen käyttöön.

Kun ohjelma on paketoitu JAR-tiedostoon, sen voi suorittaa antamalla Java-virtuaalikoneelle lisävalitsimen -jar ja sen perässä suoritettavan JAR-tiedoston nimen: esimerkiksi komennolla java -jar program.jar, jos ohjelma on paketoitu JAR-tiedostoon program.jar.

JAR-tiedostoja käytetään yleisesti luokkakirjastojen jakamiseen. Jos haluamme suorittaa ohjelman, joka tarvitsee vaikkapa JAR-tiedoston library.jar tarjoamia luokkia tai rajapintoja, tulee library.jar asettaa johonkin sellaiseen paikkaan, mistä Java löytää sen ja tarvittaessa antaa tiedostoa vastaava luokkapolkumääritys. Esimerkiksi java -classpath /some/directory/library.jar MyClass suorittaisi luokan MyClass niin, että Java etsii suorituksen aikana tarvittavia tiedostoja tavanomaisten Javan luokkakirjastojen lisäksi myös hakemistossa /some/directory olevasta JAR-tiedostosta library.jar. Luokkapolkumääritys antaa useita polkuja ja JAR-tiedostoja, jolloin ne vain erotellaan toisistaan kaksoispisteellä (Linux, Mac) tai puolipisteellä (Windows). Esimerkiksi Linuxissa suoritus java -classpath /another/directory/:.:/some/directory/library.jar MyClass ohjaisi Java-virtuaalikoneen etsimään tarvittavia luokkatiedostoja hakemistosta /another/directory, nykyhakemistosta . ja hakemistossa /some/directory olevasta JAR-tiedostosta library.jar.

Ulkoisten luokkakirjastojen nouto Mavenilla

Eräs merkittävä motiivi Mavenin käyttöön on sen kyky noutaa ulkoisia kirjastoja projektin käyttöön internetistä. Javan oma luokkakirjasto on laaja, mutta ei luonnollisestikaan sisällä kaikkia mahdollisesti tarvitsemiamme apuvälineitä. Jos on olemassa jokin ulkoinen (muu kuin Javan omaan luokkakirjastoon sisältyvä) kirjasto, jonka toimintoja haluamme käyttää, on yleensä varsin todennäköistä, että kyseinen kirjasto löytyy Mavenin jopa miljoonia kirjastoja sisältävästä kirjastohakemistosta. Tällöin kirjasto voidaan ottaa projektissa käyttöön yksinkertaisesti lisäämällä pom.xml-tiedostoon kyseistä kirjastoa vastaava riippuvuusmääritys. Kun tämän jälkeen projektin kääntää, noutaa Maven kyseisen kirjaston automaattisesti internetistä ja voimme alkaa käyttämään kyseisen kirjaston luokkia ja rajapintoja koodissamme hyvin samaan tapaan kuin olemme toistaiseksi käyttäneet Javan oman luokkakirjaston luokkia ja rajapintoja.

Alempana on esimerkki siitä, miten riippuvuusmääritys tehdään: projektitiedostoon on lisätty dependencies-elementti, jonka sisällä olevat dependency-elementit luettelevat projektin riippuvuudet. Alla on vain yksi riippuvuus: jdom2-kirjasto, joka tarjoaa apuvälineitä XML-datan käsittelyyn. Riippuvuuden määrittelyssä annetaan groupId, artifactId ja version. Nämä riittävät kuvaamaan Mavenille, mistä luokkakirjastosta ja sen versiosta on tarkkaan ottaen kyse. Tämä samalla ehkä antaa esimerkin siitä, miksi Mavenin projektitiedostoon vaaditaan kyseiset tiedot: jos oma projektimme julkaistaisiin Mavenin keskushakemistossa, jotta muut Mavenin käyttäjät voisivat helposti käyttää sitä ulkoisena luokkakirjastona, toimisivat meidän omalle projektillemme määritetyt groupId, artifactId ja version luokkakirjastomme yksilöivinä riippuvuuden kuvaavina arvoina.

Mavenin keskushakemisto

Herää ehkä kysymys, että kuinka osaamme luoda oikeanlaisen riippuvuusmäärityksen? Kirjaston (jonka olemme löytäneet esim. internet-hakukoneen avulla tms.) dokumentaatiossa on toisinaan kuvattu sitä vastaava Mavenin riippuvuusmääritys. Jos kirjasto ylipäänsä on Mavenin saatavilla, sen tietojen pitäisi myös löytyä Mavenin keskushakemistosta, johon on tarjolla hakusivu https://search.maven.org/. Keskushakemisto esimerkiksi luettelee saatavilla olevien kirjastojen eri versiot (joista yleensä kannattanee valita uusin) sekä näyttää valitsemaame kirjaston versiota vastaavan riippuvuusmäärityksen.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>fi.tuni.prog3</groupId>
  <artifactId>example</artifactId>
  <version>1.0</version>
  <packaging>jar</packaging>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.jdom</groupId>
      <artifactId>jdom2</artifactId>
      <version>2.0.6.1</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>com.jolira</groupId>
        <artifactId>onejar-maven-plugin</artifactId>
        <version>1.4.4</version>
        <executions>
          <execution>
            <configuration>
              <mainClass>fi.tuni.prog3.example.ExampleMain</mainClass>
              <onejarVersion>0.97</onejarVersion>
              <attachToBuild>true</attachToBuild>
            </configuration>
            <goals>
              <goal>one-jar</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Edellä on jdom2-kirjaston käyttöönoton lisäksi yksi toinenkin muutos aiempaan projektitiedostoon: loppuun on lisätty build-elementti, jonka alla on käännöksessä käytettäviä liitännäisiä määrittävä plugins-elementti. Tässä on tarkemmin ottaen yksi liitännäinen (“plugin”): yksi plugin-elementti, joka ottaa käyttöön onejar-liitännäisen. Kyseinen liitännäinen tarjoaa sellaisen mukavan ominaisuuden, että sen myötä Maven paketoi JAR-tiedostoon projektin omien luokkatiedostojen yms. resurssien lisäksi myös sen tarvitsemat ulkoiset luokkakirjastot. Nimittäin Mavenin luomat JAR-tiedostot eivät tavallisesti sisältäisi niitä, ja tällöin ulkoisia kirjastoja käyttävää ohjelmaa ei voisikaan suorittaa suoraan pelkän yhden JAR-tiedoston varassa: sen oheen tarvittaisiin erikseen myös ohjelman tarvitsemat kirjastot. Sen sijaan onejar-liitännäistä käyttämällä saadaan luotua itseriittoinen JAR-tiedosto. Onejaria käyttäessä tuloksena on kaksi JAR-tiedostoa: tavallinen sekä riippuvuudet sisältävä versio. Jälkimmäisen nimeen on lisätty osa “one-jar”. Esimerkiksi edellä annetun projektitiedoston tapauksessa komento mvn package loisi JAR-tiedostot target/example-1.0.jar ja target/example-1.0.one-jar.jar.

Onejar-liitännäisen määrityksessä on yksi kohta, joka pitää yleensä määrittää projektikohtaisesti eri tavalla: mainClass-elementin sisältö. Kyseinen arvo määrittää projektin pääluokan eli sen luokan, jonka määrittämästä main-funktiosta ohjelman suoritus alkaa. Yllä määritettiin, että projektin pääluokka olisi fi.tuni.prog3.example.ExampleMain. Tällainen määritys tarvitaan, jotta JAR-tiedoston sisältämän ohjelman voisi suorittaa tapaan java -jar program.jar ilman erillistä parametria, joka kertoisi mistä luokasta suoritus aloitetaan. Projektin ei toki ole pakko sisältää main-funktiota, jos se on tarkoitettu muiden ohjelmien käyttämäksi luokkakirjastoksi.

Jos käytät onejar-liitännäistä määrittämättä pääluokkaa, tai haluat aloittaa suorituksen jostain muusta saman projektin luokasta, suoritettavan luokan voi määrittää ohjelmaa ajettaessa erillisellä valitsimella -Done-jar.main.class=MainClass, missä MainClass ilmaisee luokan (tarvittaessa pakkauksineen päivineen). Esimerkiksi java -Done-jar.main.class=com.example.SomeClass -jar program.jar aloittaisi suorituksen JAR-tiedoston program.jar sisältämästä pakkaukseen com.example määritetystä luokasta SomeClass.

Ohjelmointidemo (kesto 43:49)