Java-projektin hallinta Mavenilla

Käytämme tällä kurssilla Java-ohjelmien toteuttamiseen ja hallintaan Apache Maven -nimistä työkalua. 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. Mavenia tullaan käyttämään kaikkien ohjelmointitehtävien toteuttamiseen.

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 NetBeansista käsin. Jos Mavenia haluaa käyttää komentoriviltä, tarvitaan Mavenin erillinen asennus. Maven on saatavilla kotisivujensa kautta. Jos Maven on kunnolla asennettu, pitäisi sitä pystyä käyttämään komentoriviltä 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. Esimerkiksi kooditiedoston SomeClass.java polku olisi src/main/java. Jos projektihakemiston nimeksi on valittu esimerkiksi test, on projektin hakemistorakenne tässä tapauksessa seuraava:

test
│─ pom.xml
└───src
    └───main
        └───java
              └───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.

  • artifactId-elementti, joka kertoo projektin nimen.

  • version-elementti, joka kertoo 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.

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 halutessaan 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 ensimmäisten viikkojen harjoitustehtävissä, joille 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. 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 build-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 pohdintaa 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.

Mavenin pluginit

Mavenin projektitiedostoon on mahdollista (ja melko tavallista) myös lisätä build-elementti, jonka alla tyypillisesti on käännöksessä käytettäviä liitännäisiä määrittävä plugins-elementti. Kurssin kannalta oleellinen on erityisesti onejar-liitännäinen, jota tarvitaan useimmissa tehtävissä. Liitännäisen saa käyttöön esimerkiksi seuraavanlaisella projektitiedostolla:

<?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>
  <build>
    <plugins>
      <plugin>
        <groupId>com.jolira</groupId>
        <artifactId>onejar-maven-plugin</artifactId>
        <version>1.4.4</version>
        <executions>
          <execution>
            <configuration>
              <mainClass>ExampleMain</mainClass>
              <onejarVersion>0.97</onejarVersion>
              <attachToBuild>true</attachToBuild>
            </configuration>
            <goals>
              <goal>one-jar</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Onejar-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 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 pakkaukseen com.example määritetystä luokasta SomeClass.