- COMP.CS.140
- 7. Pakkaukset
- 7.2 Java-projektin hallinta Mavenilla
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 alihakemistossasrc
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)