Build automatizáló eszközök

Egy szoftver fejlődése

Lássuk egy képzeletbeli szoftver fejlődését az alapoktól a valós méretekig, és figyeljük meg, hogy milyen tevékenységeket kell végrehajtani! A példák mindegyike, majd az automatizálás is a Java programozási nyelvet használja példaként, más rendszerekről nincs szó ezen az oldalon.

Egyetlen osztály

Tegyük fel, hogy az első verzió belefér egyetlen osztályba! Ez esetben is két jól elkülönülő műveletre van szükség az indításhoz: le kell fordítani és el kell indítani. Vegyük az alábbi példát:

public class MyMath {
    public static int add(int a, int b) {
        return a + b;
    }
 
    public static void main(String[] args) {
        System.out.println(MyMath.add(3, 2));
    }
}

Fordítás:

javac MyMath.java

Futtatás:

java MyMath

Alapállapotba állítás (Windows operációs rendszeren):

del MyMath.class

Csomagszerkezet több osztállyal

Már a komplexitás minimális bővülésével több osztályra van szükség. A fordítás során tehát meg kell adni az összes osztályt. Az osztályok száma hamarosan robbanásnak indul, és azt tapasztaljuk, hogy logikailag közelebbi és távolabbi osztályok jönnek létre. Legkésőbb ekkor (de célszerűen már az első kapavágástól kezdve) érdemes kialakítani egy csomagszerkezetet. A fordítás során az összes csomag összes forrásfájlját meg kell adni. Ezzel máris elértük a fordítás kézzel történő végrehajtásának a határát. Márpedig a fejlesztés során igen gyakran szükség van erre a műveletre.

A példában most nem hozunk létre túl sok osztályt, de ketté vágjuk: az egyik lesz a főprogram, a másik az összeadó. Kialakítunk egy könyvtárszerkezetet, ami egyébként a Maven szabványa. A források az src/main/java/ alá kerülnek, a csomagszerkezetnek megfelelő osztályba, a lefordított bináris pedig a target/classes/ osztályba.

src/main/java/hu/faragocsaba/math/MyMath.java

package hu.faragocsaba.math;
 
public class MyMath {
    public static int add(int a, int b) {
        return a + b;
    }
}

src/main/java/hu/faragocsaba/main/Main.java

package hu.faragocsaba.main;
 
import hu.faragocsaba.math.MyMath;
 
public class Main {
    public static void main(String[] args) {
        System.out.println(MyMath.add(3, 2));
    }
}

(Ld. a különbséget a két csomagnév között: math és main.)

Itt már nehezebb dolgunk van. A fordítás során egyrészt meg kell adni a gyökér könyvtárat, másrészt fel kell sorolni az összes forrásfájlt:

javac -d target/classes src/main/java/hu/faragocsaba/math/MyMath.java src/main/java/hu/faragocsaba/main/Main.java

A futtatásnál meg kell adni a könyvtárat, ahol a binárisok vannak, valamint az osztályt csomagnévvel együtt:

java -cp target/classes hu.faragocsaba.main.Main

Alapállapotba állítás sem az a parancs, amit álmunkból felébredve is bármikor felidéznénk:

rmdir /s /q target

Egész sok idő elment egyébként azzal, hogy megtaláljam a parancsok pontos szintaxisát, és még mindig egy rendkívül egyszerű programforrásról van szó.

Futtatható jar

Két osztály esetén is kissé határesetnem számít, hogy külön szállítsuk az egyébként egybefüggő osztályokat, de egy valós méretű, akár több ezer osztályt tartalmazó rendszer esetén mindenképpen csomagolni kell. A Java alkalmazásokat alapvetően jar formátumba csomagoljuk. Kivétel a web alkalmazás, melynek formátuma war, valamint a nagyvállalati alkalmazás, ami tartalmazhat tetszőleges számú jar-t és egy war-t, és ennek formátuma ear. Egyébként ezek mindegyik szabványos zip fájl.

Folytassuk a fenti példát! Először hozzuk létre ezt "gyalogos" módon! A manifest fájl írja le a jar meta információit, ami a META-INF könyvtárba kerül. Hozzuk létre a target/classes/META-INF/MANIFEST.MF fájlt az alábbi tartalommal:

Manifest-Version: 1.0
Built-By: fcsaba
Main-Class: hu.faragocsaba.main.Main

Csomagoljuk be a target/classes/ könyvtárat zip formátumba, pl. Total Commander segítségével. A gyökérben a hu ill. a META-INF legyen. Az eredmény neve legyen MyMath.jar.

Ugyanezt a következő paranccsal is elérhetjük:

jar cfe target/MyMath.jar hu.faragocsaba.main.Main -C target/classes/ .

Magyarázat:

  • c: create, azaz létrehozzuk a jart.
  • f: filename, azaz ezzel jelezzük, hogy az első paraméter a fájl neve lesz. Ld. még target/MyMath.jar.
  • e: entry point, azaz ezzel jelezzük, hogy meg szeretnénk adni a program belépési pontját. Ld. még hu.faragocsaba.main.Main.
  • -C target/classes/: ezzel azt adjuk meg, hogy a műveletet úgy hajtsa végre, mintha a target/classes/ könyvtárból adtuk volna ki.
  • .: ez le ne maradjon; ezzel jelezzük azt, hogy a könyvtárban található minden fájl kerüljön bele az eredménybe.

Ha mindent jól csináltunk, az eredmény target/MyMath.jar lesz, melybe egy zip tömörítővel bele is tudunk nézni (tipp: Total Commander segítségével Ctrl + Page Down). Figyeljük meg, hogy a gyökérben található a hu könyvtár, melyben benne van a class, valamint a gyökérben van még egy META-INF könyvtár, azon belül egy MANIFEST.MF fájl. Ez utóbbi tartalmazza többek között a belépési pontot: Main-Class: hu.faragocsaba.main.Main.

A következőképpen tudjuk indítani:

java -jar MyMath.jar

Ez a nem túl bonyolult feladat is rendese megizzasztott!

Modularizálás

Következő lépésben a matematika osztályt szervezzük külön modulba! Hozzunk létre két könyvtárat, pl. math és main néven!

Matematika

A src/main/java/hu/faragocsaba/math/MyMath.java fájl tartalma legyen a fenti! Könyvtár előkészítése:

mkdir target\classes

Fordítás:

javac -d target/classes src/main/java/hu/faragocsaba/math/MyMath.java

A komponens elkészítése:

jar cf target/MyMath.jar -C target/classes/ hu

Eredmény: target/MyMath.jar.

Főprogram

A főprogramhoz tehát szükség van az imént létrehozott jar fájlra. Valójában ez most "rossz" helyen van, egy másik komponens temporális könyvtárában. Innen át kell másolni egy "biztonságos" helyre. Valahol a könyvtárszerkezetben hozzunk létre erre a célra egy könyvtárat. A neve bármi lehet, pl. libs, de fontos, hogy olyan helyre mentsük, ami tartósan megmarad. Az én esetemben ez a d:\prog\libs\. Ide másoljuk be a fenti jar fájlt:

copy ..\math\target\MyMath.jar d:\prog\libs\

Most már "biztonságban" van. Viszont jó, ha az aktuális program könyvtárszerkezetében is megtalálható. A main projekt alatt hozzuk létre a target\libs\ könyvtárat, és a "tartós" helyről másoljuk át ide:

mkdir target\libs
copy d:\prog\libs\MyMath.jar target\libs\

A fordításhoz szükségünk is van az új jar fájlra:

mkdir target\classes
javac -cp target/libs/MyMath.jar -d target/classes src/main/java/hu/faragocsaba/main/Main.java

Ha több függőségünk lenne, azt Windows operációs rendszeren pontosvesszővel (;) kellene felsorolni (pl. -cp MyLib1.jar;MyLib2.jar;MyLib3.jar), Linux alatt kettősponttal (:, pl. -cp MyLib1.jar:MyLib2.jar:MyLib3.jar).

A futtatáshoz is meg kell adni a MyMath.jar könyvtárat, valamint azt a könyvtárat is, ahova a lefordított bináris került:

java -cp target/classes/libs/MyMath.jar;target/classes/ hu.faragocsaba.main.Main

Talán már érezzük a függőségek kezelésének a súlyát: a MyMath is fejlődhet, annak is lehetnek függőségei, ugyanakkor egyrészt biztosítani kell a stabilitást, másrészt azt, hogy verzióváltáskor mindegyik érintett számítógépre a megfelelő kerüljön. A fenti példában kézzel odamásoltuk. De ha a futtató környezet eltér a fordítótól (ami általában igaz), akkor vajon nem felejtjük el odamásolni? És ha több futtató környezet van, akkor mi garantálja azt, hogy mindenhol ott lesz a megfelelő verzió? Ha többen fejlesztik ugyanazt a modult, akkor mi az a mechanizmus, hogy annál a fejlesztőnél is a megfelelő verzió lesz fordításkor és futtatáskor is, aki most jött haza a 3 hetes nyaralásáról? És mindezt nem egyetlen könyvtárral, hanem mondjuk százzal, melyek bonyolult módon hivatkoznak egymásra?

Az alábbiakat kell szem előtt tartanunk:

  • Ahogy láttuk, ha elég nagyra nőtt a programunk, akkor felmerül kiegészítő komponensek írása, melyek evolúciója a fentihez hasonló. Idővel azon kapjuk magunkat, hogy több tucat rendszert tartunk karban. Tipikusan vannak olyan műveletek, amelyeket kettő vagy több komponens is használ. A kód duplikátum elkerülése érdekében ezeket célszerű közös komponensbe, idővel közös komponensekbe kiszervezni, melyekre az érintett komponensek hivatkoznak.
  • Fordítás során a bináris mellett sokszor riportokat is szeretnénk generálni. Ilyen pl. a közös használatra készülő komponensek API dokumentációja, pl. a javadoc alapján, vagy különböző statikus kódelemzők eredményei.
  • A közös komponenseknek is van egy fejlődésük, akár egymásra is hivatkozhatnak, adott, hogy mely verzió melyikkel kompatibilis és melyikkel nem. Ráadásul mindezt flexibilis módon: ha egy függőség újabb verziója úgy van elkészítve, hogy felülről kompatibilis legyen a korábbiakkal, akkor elég megadni egy minimális verziót, nem szeretnénk minden egyes kombinációnál pontos verziót megadni.
  • Ha a szoftvert elkezdik az ügyfelek használni, akkor felmerül a verziózás kérdése. Minimálisan elkülönül az aktuálisan fejlesztett verzió az ügyfelek által használttól, de - különösen akkor, ha több ügyfél van - több verzió támogatását kell egyszerre biztosítani. Márpedig mindegyik verziónak megvannak a maguk függőségei; adott, hogy melyik jar melyikkel kompatibilis és melyikkel nem. Mindezt garantálni kell az összes fejlesztői és üzemeltetői számítógépen.
  • Ha a függőségek eléggé letisztultak, akkor felesleges műveletnek tűnik minden egyes fordításkor az összes függőséget lefordítani, célszerű azokat egy központi helyre gyűjteni, ahonnan az azt használók le tudják tölteni.
  • A fenti példában bemásoltuk az egy szem jar fájlt a könyvtárba, a valóságban viszont nagyon sok könyvtárt kell tárolnunk. Itt egyrészt gondoskodunk kell a párhuzamos verziók kezeléséről, az esetleges névütközésekről (több gyártónak lehet ugyanolyan nevű komponense), és egyébként is, ahelyett, hogy több ezer, akár több tízezer fájlt öntenénk egyetlen könyvtárba, célszerű egy erre alkalmas könyvtárszerkezetet kialakítani.
  • Ugyanez "nagyban": a sokak által használt függőségeket érdemes globálisan is elérhetővé tenni, egy közös helyen, hogy ne az egyes gyártók oldaláról kelljen "levadászni". Egy ilyen rendszertől azt is elvárjuk, hogy az egyes komponensek közötti függőségeket is megfelelően tudja kezelni.
  • Gondoskodni kell arról, hogy futáskor is minden a helyén legyen, pl. az összes függőség egy külön könyvtárba gyűjtve, vagy akár "belefordítva" az eredménybe.

Egységtesztelés

Bővítsük egy kicsit a matematika komponenst: adjunk hozzá egységtesztet, hogy megnézzük, jól működik-e! A fő osztály megmarad a fenti, és az ott leírtak alapján fordítsuk is le. Az egységteszt osztálya a src/test/java/hu/faragocsaba/math/MyMathTest.java forrásfájlba kerül, a következő tartalommal:

package hu.faragocsaba.math;
 
import static org.junit.Assert.assertEquals;
import org.junit.Test;
 
public class MyMathTest {
    @Test
    public void testAdd() {
        assertEquals(5, MyMath.add(3, 2));
    }
}

Ennek a lefordításához szükség van az org.junit külső könyvtárra. Egy-egy ilyen könyvtárat vagy a gyártó honlapjáról tudjuk letölteni, vagy egy központi repository-ból. Azt majd később, a Maven Repository résznél láthatjuk, hogy hogyan tudjuk megkeresni, most megadom a közvetlen linket: https://repo1.maven.org/maven2/junit/junit/4.12/junit-4.12.jar. Töltsük le a már létrehozott könyvtárba, ahova a külső függőségeket gyűjtjük (az én esetemben ez a d:\prog\libs\). Most hozzunk létre egy könyvtárat a saját könyvtárszerkezetben target\libs néven, és másoljuk oda a jar fájlt.

mkdir target\libs
copy d:\prog\libs\junit-4.12.jar target\libs\

Folytassuk a teszt osztály fordításával! Ennek is hozzunk létre egy külön célkönyvtárat!

mkdir target\test-classes

Magához a fordításhoz a classpath-ban egyrészt meg kell adnunk az imént letöltött könyvtárat, másrészt a tesztelendő osztály elérését is:

javac -cp target/libs/junit-4.12.jar;target/classes/ -d target/test-classes src/test/java/hu/faragocsaba/math/MyMathTest.java

Ha idáig mindent jól csináltunk, akkor a target/classes/ könyvtár alatt találjuk a tesztelendő osztály binárisát, míg a target/test-classes/ alatt magáét a teszt osztályét.

A futtatáshoz szükség van még egy osztályra, ami a junit-4.12 függősége. Ezt innen tudjuk letölteni: https://repo1.maven.org/maven2/org/hamcrest/hamcrest-all/1.3/hamcrest-all-1.3.jar. Ezt is mentsük le a központi könyvtárba (d:\prog\libs\), majd onnan a target\libs\ alá:

copy d:\prog\libs\hamcrest-all-1.3.jar target\libs\

Futtatás:

java -cp target/libs/junit-4.12.jar;target/libs/hamcrest-all-1.3.jar;target/classes;target/test-classes org.junit.runner.JUnitCore hu.faragocsaba.math.MyMathTest

Ha mindent jól csináltunk, akkor egy szűkszavú információt kapunk arról, hogy egy egységteszt sikeresen lefutott.

Kézzel végrehajtva kifejezetten összetettnek éreztem ezt a feladatot! Ráadásul a felhasznált külső függőségek is különlegesek:

  • a fordításhoz csak a junit-4.12.jar kellett,
  • az egységteszt futtatásához a hamcrest-all-1.3.jar is (a junit-4.12.jar-on felül),
  • magának a tesztelt programnak viszont nincs függősége.

Egyre jobban hiányzik egy automata rendszer, ami mindezt megoldja…

Külső függőségek

Az előző lépés valójában egy kis kitérő volt, hogy lássuk, build kezelő nélkül hogyan tudunk egységtesztelni. Következő lépésben elvetjük a frissen felokosított matematikai komponensünket, és helyette egy már létező megoldást használunk. Az Apache Guava könyvtár tartalmaz olyan aritmetikai műveleteket, amelyek túlcsordulás esetén nem helytelen eredményt adnak, hanem ArithmeticException kivételt dobnak.

A példában a főprogramunk (src/main/java/hu/faragocsaba/main/Main.java) a következő:

package hu.faragocsaba.main;
 
import com.google.common.math.IntMath;
 
public class Main {
    public static void main(String[] args) {
        System.out.println(IntMath.checkedAdd(3, 2));
    }
}

A fordításhoz hozzuk létre itt is a target/libs és a target/classes könyvtárat:

mkdir target\libs
mkdir target\classes

töltsük le a Guava könyvtárat (https://repo1.maven.org/maven2/com/google/guava/guava/29.0-jre/guava-29.0-jre.jar) a központi helyre (d:\prog\libs\), majd onnan a target\libs könyvtárba:

copy d:\prog\libs\guava-29.0-jre.jar target\libs\

Fordításkor adjuk meg ezt classpath paraméterként:

javac -cp target/libs/guava-29.0-jre.jar -d target/classes src/main/java/hu/faragocsaba/main/Main.java

Itt már futtatáskor is szükség van erre a könyvtárra:

java -cp target/libs/guava-29.0-jre.jar;target/classes hu.faragocsaba.main.Main

Nem tűnik a probléma jelentősnek azzal, hogy ugyanazon a környezeten fordítottuk és futtattuk is a komponenst. A valóság nem ennyire vegytiszta.

  • Idővel a függőségek száma "robban", valamint megjelennek a függőségek függőségei. Egyre kényelmetlenebb felsorolni fordításkor és futtatáskor az összes függőséget. És gondoljunk itt arra is, hogy egy tipikus projektet többen fejlesztenek; gondoskodni kell arról is, hogy minden egyes fejlesztőnél minden egyes függőségből pont ugyanaz a verzió legyen használatban, és ez igaz legyen az összes futó környezetre is. Mert hamarosan elérkezik az a komplexitási szint, hogy nem elég csak a fejleszti gépeken kipróbálni, szükség van különböző dedikált környezetre, melyek tipikusan TEST, INT, PROD.
  • Ha egy függőség esetén verziót váltunk, garantálnunk kell, hogy az összes érintett rendszeren egyidőben bekövetkezik: a fejlesztői gépeken, a központi fordító rendszereken (mert elég gyorsan ilyenre is szükség van, ld. Integrációs eszközök) és az összes futtató környezeten. Itt tipikusan nem egyetlen könyvtárról beszélünk, hanem azok függőségeiről is.
  • A rekurzív függőségnek is idővel "határt kell szabni". A külső függőségek mérete, a rekurzív függőségek miatt, elég gyorsan exponenciálisan akár több tíz megabájtra nő, még nem túl jelentős komplexitású kód esetén is, pont a rekurzív függőségek miatt. Ezek jó részét nem is használjuk. Tegyük fel például, hogy egy külső könyvtárnak van 10 szolgáltatása, de mi ebből csak egyet használunk; miért cipeljük magunkkal azokat a függőségeket, amelyek csak a másik 9 miatt van szükség?
  • Nem kell túl komplex programot írnunk ahhoz, hogy finomhangoljuk a könyvtárak használatát. A tipikus persze az, amikor egy könyvtárra fordítás és futás során is szükség van, de itt is több lehetőség létezik. Amint láttuk az előző példában, vannak könyvtárak, amelyeket csak egységteszteléskor használunk; vannak, amelyeket egy futtató környezet - ha használunk - már tartalmaz, így csak fordításkor van rá szükség; a rekurzív függőségekre fordításkor nincs szükség, futtatáskor viszont van stb.
  • Az is elég gyakori, hogy különböző környezetekhez eltérő módon szeretnénk fordítani. Például az éles környezethez szükség lehet a kétlépcsős azonosításra belépéskor, míg egy teszt környezetben ez csak hátráltat (persze kivéve akkor, ha pont ezt a funkciót szeretnénk tesztelni). A fejekben esetleg kialakulhatott idáig egy fordító és egy futtató script: ezen a ponton már elágazásokra is szükség van, vagy esetleg külön scriptekre a különböző környezetekhez. Ami felveti azt a problémát, hogy hogyan garantáljuk azt, hogy az egyik scriptben végrehajtott változás kihat egy másikra is.

Futtatható jar függőségekkel

Lépjünk ismét egy szintet: folytassuk az előző példát úgy, hogy futtatható jar-t készítünk a főprogramból. Ha a korábban bemutatott módon próbálkozunk, akkor nem fog sikerülni: futtatáskor nem fogja megtalálni a Guava függőséget. Itt derült ki számomra egy fontos dolog (amivel elég sok időm elment): ha jart futtatunk, akkor a classpath-ot a jar metainformációja tartalmazza, azt kívülről befolyásolni nem tudjuk.

Először oldjuk meg "gyalogosan" a dolgot! A fent megadott módon készítsük elő a terepet, fordítsuk és csomagoljuk:

mkdir target\libs
mkdir target\classes
copy d:\prog\libs\guava-29.0-jre.jar target\libs\
javac -cp target/libs/guava-29.0-jre.jar -d target/classes src/main/java/hu/faragocsaba/main/Main.java
jar cfe target/MyMath.jar hu.faragocsaba.main.Main -C target/classes/ .

Megpróbálhatjuk elindítani; nem fog menni. Csomagoljuk ki a MEAT-INF/MANIFEST.MF fájlt, és adjuk hozzá a következő sort (a többi változatlanul hagyásával): Class-Path: libs/guava-29.0-jre.jar. (Tipp ismét: Total Commander, Ctrl + Page Down.) Az eredmény kb. így néz ki:

Manifest-Version: 1.0
Class-Path: libs/guava-29.0-jre.jar
Created-By: 1.8.0_251 (Oracle Corporation)
Main-Class: hu.faragocsaba.main.Main

Most már tudnunk kell futtatni a következő módon:

java -jar target/GuavaTest.jar

Most láttuk meg értelmét annak, hogy miért a target könyvtárba kerültek a külső könyvtárak, jelen esetben a Guava: hogy könnyebb legyen rájuk hivatkozni. Most ugyanis ha nem a fejlesztő környezetben szeretnénk futtatni, akkor elég átmásolni a GuavaTest.jar és a libs/guava-29.0-jre.jar-t a target könyvtárból, ezt a struktúrát megtartva (a target nem kell, csak ami belül van), és a fenti módon tudjuk futtatni.

Következő lépésben valósítsuk meg ugyanezt parancssorból! Ehhez létre kell hoznunk kézzel egy Manifest fájlt, amit hozzá tudunk adni a jar paranccsal az eredményhez. Tkp. mindegy, hogy hol van, meg a neve is mindegy, de célszerű az alábbi helyre tenni, a megadott tartalommal:

src/main/resources/META-INF/MANIFEST.MF

Class-Path: libs/guava-29.0-jre.jar

Egyébként tetszőleges számú kulcs-érték párt megadhatunk. Az alábbi parancs a megadott adatokkal kiegészíti az egyébként is legenerálandó Manifest fájlt:

jar cfem target/GuavaTest.jar hu.faragocsaba.main.Main src/main/resources/META-INF/MANIFEST.MF -C target/classes/ .

Itt a m paraméterrel jelezzük, hogy Manifest fájlt is megadunk, ami a src/main/resources/META-INF/MANIFEST.MF paraméter. A többit már láttuk fent. A belépési pontot is megadhattuk volna a saját Manifest fájlunkban, akkor annyival rövidebb lenne a parancs.

Ezt is tudnunk kell futtatni ezzel a paranccsal:

java -jar target/GuavaTest.jar

Ennek a szakasznak az elkészítésében sokat segített a https://dzone.com/articles/java-8-how-to-create-executable-fatjar-without-ide blogbejegyzés.

Überjar

Az überjar (angolosan uberjar) pont onnan kapta a nevét, ahonnan gondoljuk: a német über (jelentése: felett) szóból származik: egy jar mindenek felett. Hívják még fat jarnak, azaz kövér jarnak is. Azt jelenti, hogy egyetlen fájl tartalmazza az összes függőséget. Nem kell tehát a futó környezetben gondoskodnunk a függőségekről; az általunk készített jarban minden benne van, a megfelelő verzió.

Az ötlet a következő: a függőségek teljes tartalmát másoljuk bele a mi jar fájlunkba. Ehhez ki kell azt csomagolni, a megfelelő helyre mozgatni, majd így elkészíteni az eredményt:

jar xf target/libs/guava-29.0-jre.jar com
move com target\classes
jar cfe target/GuavaTest.jar hu.faragocsaba.main.Main -C target/classes/ .

Legyünk türelmese, eltart egy darabig!

A szokásos módon tudjuk indítani az eredményt:

java -jar target/GuavaTest.jar

Ennek a megoldásnak is vannak persze hátrányai:

  • Ha több program hivatkozik ugyanarra a könyvtárra, akkor nem tudjuk kihasználni azt, hogy csak egyszer másoljuk oda, így tárterületet veszítünk. Persze a mai világban, amikor már egy megabájt tárterület is szó szerint filléres tétel…
  • Ha a mi programunkat függőségként használja egy másik, akkor ütközés léphet fel. Tegyük fel, hogy a mi komponensünkben benne van a Guava, de a minket használó program egy másik függősége is hivatkozik rá, esetleg tartalmazza, de adott esetben egy másik verzióját. Ennek kezelésére találták ki az árnyékolás (shading) technikát: becsomagolás előtt hozzáadunk a csomagnévhez egy prefixet, pl. shaded, átírjuk a bennük levő hivatkozásokat, és a saját hivatkozásainkat is. Ezt nem fogjuk megtenni most kézzel mind a kb. kétezer osztály esetén (beleértve azok belső hivatkozásait); vannak erre megfelelő eszközök.
  • Elvben van még egy lehetőség: magukat a jarokat is bele lehet csomagolni a nagy jarba. Ezzel viszont az a probléma, hogy önmagában nem látja a jarban levő főprogram, ahhoz olyan programra van szükség, ami ezt lehetővé teszi. Kézzel ezt szintén nem hozzuk létre, de a Mavennél látni fogunk erre is lehetőséget.

A build rendszerek

Ebben a szakaszban a build rendszerek szolgáltatásait nézzük meg. Itt tehát még indig nem lesz szó konkrét technológiáról, hanem elvekről, amelyeket a technológiák megvalósítanak.

Fordítás

A fordítással kapcsolatos alapműveletek, a tipikus megnevezéseivel:

  • Clean: a korábbi műveletek eredményeinek törlése.
  • Compile: a források lefordítása.
  • Test: az egységtesztek futtatása.
  • Package: a lefordított binárisok becsomagolása pl. jar-ba.
  • Install: az eredmény felmásolása a helyi repository-ba.
  • Deploy: az eredmény bemásolása a távoli repository-ba.

Rendszertől függően már számos egyéb művelet létezik.

A build kezelő rendszerek lehetővé teszik a műveletek paraméterekkel történő finomhangolását. Ezen kívül a jó build rendszerek kiterjeszthetőek: beépülők segítségével új műveleteket is létrehozhatunk, vagy egy-egy műveletet a szokásostól eltérő módon hajthatunk végre. Ez utóbbira jó példa a futtatható jar készítése függőségekkel: a futtatható jar készítése és a függőségek kezelése is általában alapművelet, de a kettő együtt már nem az, és pl. a Maven esetében ezt beépülővel tudjuk megoldani.

Egy jó build rendszer támogatja a hierarchiákat. Egy tipikus nagyvállalati környezetben sok komponenst fejlesztenek, melyeknek tipikusan elég nagy a közös metszete, mivel általában nagyon hasonló technológia halmazzal dolgoznak. Ilyenkor célszerű a közös részeket kiemelni külön komponensbe, amelyre az egyes komponensek hivatkoznak, és a komponensek leírójába csak a komponens specifikus dolgok kerülnek. Ezt persze csak akkor tudjuk megtenni, ha az adott build rendszer támogatja a hierarchiákat. Valamilyen megoldás többnyire van.

Függőségek kezelése

Egy-egy komponens egyértelmű azonosítása a következő elem hármassal történik:

  • A komponens "kiadójának", azaz annak a szervezetnek az azonosítója, amely a komponenst elkészítette, karban tartja. Szokásos hivatkozás: groupId vagy org. Ez elvileg bármi lehet, a konvenció szerint viszont kisbetűkből és pontokbül áll, és általában az adott szervezet weboldalának a fordítottjából adódik, pl. org.apache, com.google, com.microsoft stb. A gyakorlatban ez általában ennél némiképp specifikusabb, pl. a bevezetőben említett log4j példában ez konkrétan com.google.guava.
  • Magának a komponensnek az azonosítója, azaz a jar neve, verziószám és kiterjesztés nélkül, pl. guava. Ennek a szokásos hivatkozása: artifactId vagy name.
  • A komponens verziója, pl. 29.0-jre Szokásos hivatkozás: version vagy rev.

Elsősorban az általunk fejlesztett komponens azonosítóját kell megadnunk a fenti hármassal. Valamint ezzel az elem hármassal hivatkozunk a függőségekre is. Függőség esetén viszont a fentieken kívül szükség van még annak a megadására is, hogy mi módon szeretnénk használni. Néhány lehetőség0, a szokásos megnevezéssel:

  • Compile: a fordításhoz és futtatáshoz is szükséges függőség. Általában ez az alapértelmezett, és tranzitív módon működik, azaz a függőségek függőségeit is automatikusan letölti.
  • Provided: ezzel azt jelezzük, hogy csak a fordításhoz kell, a futtató környezetben jelen van (a provided szó jelentése adott), pl. ha az alkalmazásunk valamilyen keretrendszerben, pl. webszerveren fut. Ehhez hasonló a system; ez utóbbi esetben meg kell adnunk az elérési útvonalát. Tehát a provided azt jelenti, hogy tudjuk, a futó rendszeren rajta lesz a CLASSPATH-on, míg a system esetén azt tudjuk, hogy ott van, és tudjuk is, hogy hol. Ez utóbbira akkor lehet szükség, ha az adott rendszeren több alkalmazás fut ugyanazokkal a függőségekkel, és a függőségek nagyok, viszont nincs egy közös alkalmazás szerver.
  • Runtime: csak futás során van rá szükség, fordításkor nincs. Tipikus példa erre a JDBC meghajtó.
  • Test: csak az egységteszteléshez szükséges. Tipikus példája a junit.

Ha egy könyvtár mérete elég nagyra nőtt, ami sok, egymástól független osztályt tartalmaz, akkor felmerül az a kérdés, hogy egy (vagy kevés) nagy, monolitikus könyvtár legyen, vagy sok kicsi. Az előbbinek az előnye az, hogy nem kell sok függőséget felsorolni, a hátárnya viszont az, hogy azok a részek is bekerülnek az eredménybe, amit nem használunk. Így előfordulhat az, hogy egyetlen függvény miatt megabájtokkal nő az alkalmazásunk mérete. A modularizálás lehetőség komolyabb finomhangolást tesz lehetővé, viszont ott nagyon sok lehet a függőség. A "kecske is jóllakik és a káposzta is megmarad" megoldás lehet az, hogy a rendszer alapvetően modularizált (tehát ha csak egyetlen függvényt szeretnénk használni, akkor elég azt az egy kicsi függőséget betölteni), viszont a függőségeket valahogy közösen is tudjuk kezelni. Ez utóbbi azt jelenti, hogy definiálunk egy halmazt, pl. "webalkalmazás készítéséhez szükséges tipikus komponensek", a kliensnek elég csak erre az egy "metafüggőségre" hivatkozni, és a rendszer a háttérben feloldja a tényleges, sok kicsi függőségre. Ezt a módszert nem mindegyik build rendszer támogatja; a Maven esetén a pom kulcsszó használandó erre a célra.

Amint arról már volt szó, a függőségeknek lehetnek további függőségei. Ez több problémát is felvet:

  • Elképzelhető, hogy a függőség függősége felesleges számunkra. Tegyük fel, hogy egy osztály nyújt 10 műveletet, ebből mi csak egyet használunk, melynek nincs függősége, a másik 9 miatt viszont több megabájt függőséget hoz be a komponens tranzitívan. A függőség kezelő rendszerek általában lehetőséget biztosítanak arra, hogy kizárjuk a felesleges függőségeket.
  • Verzió konfliktus léphet fel, ha ugyanannak a könyvtárnak más verzióját használják a függőségeink tranzitívan, vagy akár a mi általunk fejlesztett komponens közvetlenül. A függőség kezelő rendszerek általában lehetővé teszik a lazább verziókezelést, pl. meg lehet adni minimális verziószámot. (Korábban támogatták a latest, azaz a legfrissebb verzió lehetőségét, de ez újabban kikerült.)

Repository-k

Repository-nak nevezzük azt a szervert, ami a közös komponenseket tárolja. Alapvetően itt 3 dolgot különböztethetünk meg:

  • Magának a repository szervernek a telepítése, beállítása, adminisztrálása.
  • Kliens oldalon, a build konfigurációban a hozzáférés biztosítása. Tehát meg kell tudnunk adni a repository elérését, szükség esetén a proxy-t be kell tudnunk állítani stb.
  • Vannak központi repository szerverek, pl. a mvnrepository.com. A kliensektől elvárt működés az, hogy ezeket a szervereket automatikusan elérjék, azokat ne kelljen explicit megadni.

Integráció

Külön csoportba kerültek a különböző integrációs eszközök. Ez alatt azt értem, hogy jó, ha ezek a rendszerek jól együttműködnek egymással ill. a többi fejlesztőeszközzel. Konkrétabban az alábbiakat sorolhatjuk ide:

  • Beépülés az integrált fejlesztőeszközökbe. Az a jó, ha a népszerű IDE-k alapból ismerik a build rendszereket, tehát ha gond nélkül be tudjuk importálni a projekteket, automatikusan letölteni a függőségeket stb.
  • Együttműködés a CI/CD eszközökkel.

Ant

Áttekintés

Az Ant egy régi, kiforrott build automatizáló rendszer. Már kikezdte az idő vasfoga, az írás pillanatában már nem ez a legnépszerűbb, de még mindig elég sok rendszer használja, ezért érdemes megismerkedni vele.

Nem tartalmaz függőség kezelést. Ez utóbbihoz az Ivy beépülőt tudjuk használni.

Az Ant telepítése:

  • Töltsük le a programot a https://ant.apache.org/ oldalról. Bal oldalon Downloads → Binary Distributions, majd kicsit legörgetve töltsük le a legfrissebb Ant verziót zip formában.
  • Csomagoljuk ki egy tetszőleges könyvtárba. Pl. én a c:\programs\apache-ant-1.10.8 könyvtárba csomagoltam.
  • Hozzunk létre egy ANT_HOME környezeti változót, melynek az értéke az imént kicsomagolt könyvtár legyen, jelen esetben a c:\programs\apache-ant-1.10.8.
  • A PATH környezeti változóhoz adjuk hozzá a bin könyvtárat, jelen esetben ezt: c:\programs\apache-ant-1.10.8\bin.

Linux esetén is hasonló a telepítés, az értelemszerű eltérésekkel.

Az Ant központi eleme a projekt gyökerében elhelyezkedő build.xml leíró fájl. Itt adjuk meg a feladatokat, ill. azt, hogy melyik feladatot pontosan hogyan hajtsa végre. Az XML egy <project> tag-ből áll, ami tetszőleges számú feladatot, azaz <target> tag-et tartalmazhat. Ez utóbbiakat tudjuk felkonfigurálni attribútumokkal vagy belső tag-ekkel. A project tag attribútumaként ((depends)) egymás között függőségeket adhatunk meg (pl. a futtatás függ a sikeres fordítástól), ill. a project tag-ben (default) megadhatjuk az alapértelmezett célt.

Ha jól felkonfiguráltuk, akkor elég kiadni az ant parancsot, és végrehajtja a teljes folyamatot. Paraméterként megadhatjuk, hogy pontosan mit szeretnénk. Tipikus parancs pl. az ant clean, mellyel töröljük a korábbi fordítások eredményeit.

Az alábbi oldalak segítettel elkészíteni ezt a szakaszt:

Egyetlen osztály

Lássuk először az egyetlen osztályból álló programunkat! A fent leírtak alapján hozzuk létre a fájlt! A fordítást és a futtatást is az Ant segítségével fogjuk végrehajtani. Hozzuk létre a build.xml fájlt az alábbi tartalommal:

<project default="compile">
    <target name="clean">
        <delete file="MyMath.class"/>
    </target>
 
    <target name="compile" depends="clean">
        <javac srcdir="." includeantruntime="false"/>
    </target>
 
    <target name="run">
        <java classname="MyMath" classpath="."/>
    </target>
</project>

A példában láthatjuk, hogy 3 célt definiáltunk:

  • clean: törli a korábbi fordítás eredményét.
  • compile: lefordítja az osztályt. Ez függ a törléstől, azaz előtte letörli a korábbi fordítás eredményét.
  • run: elindítja a programot. Ez nem függ semmitől. Ha beállítanánk, hogy függjön a fordítástól, akkor mindig lefordítaná. Persze be lehetne állítani azt, hogy megnézze, le van-e fordítva, és ha nincs, akkor lefordítja, de belépő példaként ez túl bonyolult lenne.

A továbbiakban is ezt a mintát fogjuk követni. Amennyiben értelmezett, az alább 3 parancsot fogjuk tudni használni:

  • ant: törlés + a teljes fordítási folyamat (majd látni fogjuk, hogy ez nem feltétlenül egy lépést jelent majd).
  • ant clean: csak törlés.
  • ant run: a program indítása.

Csomagszerkezet több osztállyal

Ha több osztályunk, sőt, több csomagunk van, a build.xml változik csak, a kiadandó parancs megmarad. Lássuk a fenti második verzióhoz tartozó build.xml fájlt!

<project default="compile">
    <property name="src.main.dir" location="src/main/java" />
    <property name="target.dir" location="target" />
    <property name="target.classes.dir" location="${target.dir}/classes" />
 
    <target name="clean">
        <delete dir="${target.dir}"/>
    </target>
 
    <target name="init">
        <mkdir dir="${target.classes.dir}"/>
    </target>
 
    <target name="compile" depends="init">
        <javac srcdir="${src.main.dir}" destdir="${target.classes.dir}" includeantruntime="false"/>
    </target>
 
    <target name="run">
        <java classname="hu.faragocsaba.main.Main" classpath="${target.classes.dir}"/>
    </target>
</project>

Nem sokkal bonyolultabb mint az előző, de a figyelmes szemlélőnek pár dolog feltűnhet:

  • Bevezettük a property-ket. A többször előforduló, esetleg megváltoztatható dolgokat érdemes property-kként eltárolni, és azokat használni. Másik lehetőség: külön property fájlba szervezhetjük a ki a property-ket, a következő szintaxissal: <property file = "build.properties"/>, és a build.properties fájlban egyenlőség jellel megadott kulcs-érték párokkal tudjuk megadni az értékeket, egyet egy sorban, pl. src.main.dir = src/main/java.
  • A delete művelet (task) paramétere itt nem file, hanem dir. A taszkokról és azok lehetséges paramétereiről a https://ant.apache.org/manual/ → Ant Tasks oldalon tájékozódhatunk. Jelen esetben List of Tasks → Delete, és táblázatban láthatjuk az lehetséges paramétereket.
  • Bevezettük az init targetet, amely létrehozza a szükséges könyvtárat. A clean ezt törli.

Futtatható jar

Következő lépésben is csak a build.xml változik, a kiadandó parancsok (ant, ant clean, ant run) nem:

<project default="jar">
    <property name="src.main.dir" location="src/main/java" />
    <property name="target.dir" location="target" />
    <property name="target.classes.dir" location="${target.dir}/classes" />
 
    <target name="clean">
        <delete dir="${target.dir}"/>
    </target>
 
    <target name="init">
        <mkdir dir="${target.dir}"/>
        <mkdir dir="${target.classes.dir}"/>
    </target>
 
    <target name="compile" depends="init">
        <javac srcdir="${src.main.dir}" destdir="${target.classes.dir}" includeantruntime="false"/>
    </target>
 
    <target name="jar" depends="compile">
        <jar destfile="${target.dir}/mymath.jar" basedir="${target.classes.dir}">
            <manifest>
                <attribute name="Main-Class" value="hu.faragocsaba.main.Main"/>
            </manifest>
        </jar>
    </target>    
 
    <target name="run">
        <java jar="${target.dir}/mymath.jar" fork="true"/>
    </target>
</project>

Néhány megjegyzés:

  • A property-k egymásra is hivatkozhatnak.
  • A jar készítésnél megadtuk a főosztályt is.
  • A java taszk paramétere nem class, hanem jar.
  • A fork=true azt jelenti, hogy a program külön JVM-ben induljon, ne az ant JVM-ében.

Több komponens

Lássuk, hogyan tudunk kezelni több komponenst az Ant segítségével! A példában ketté osztottuk a programot. A math az egyszerűbb, ott a következő build.xml fájlra van szükség:

<project default="jar">
    <property name="src.dir" location="src/main/java" />
    <property name="target.dir" location="target" />
    <property name="target.classes.dir" location="${target.dir}/classes" />
 
    <target name="clean">
        <delete dir="${target.dir}"/>
    </target>
 
    <target name="init">
        <mkdir dir="${target.dir}"/>
        <mkdir dir="${target.classes.dir}"/>
    </target>
 
    <target name="compile" depends="init">
        <javac srcdir="${src.dir}" destdir="${target.classes.dir}" includeantruntime="false"/>
    </target>
 
    <target name="jar" depends="compile">
        <jar destfile="${target.dir}/mymath.jar" basedir="${target.classes.dir}"/>
    </target>    
</project>

Semmi különös, elkészíti a target/mymath.jar fájlt. Futtatni ezt nem lehet, így itt nincs run target.

Mivel az Ant nem függőség kezelő rendszer, ezt a részt kézzel kell nekünk megoldani. Majd később látni fogjuk, hogy hogyan tudjuk automatizálni az Ivy segítéségével. Most viszont tegyük a következőt:

  • A main komponensnél hozzunk létre egy lib nevű könyvtárat. (Annak az oka, hogy pont lib és nem valami más, az, hogy ugyanúgy működjön, mint az Ivy, amint azt később látni fogjuk.)
  • Kézzel másoljuk be oda a mymath.jar fájlt.
  • Megtehetjük, hogy egy külső helyre másoljuk, pl. ide: d:\prog\libs.

A main komponensből el kell tudnunk érni fordításkor és futtatáskor is a MyMath.jar fájlt:

<project default="compile">
    <property name="src.dir" location="src/main/java" />
    <property name="target.dir" location="target" />
    <property name="target.classes.dir" location="${target.dir}/classes" />
 
    <path id="dependency">
        <pathelement location="lib/MyMath.jar"/>
    </path>
 
    <target name="clean">
        <delete dir="${target.dir}"/>
    </target>
 
    <target name="init">
        <mkdir dir="${target.dir}"/>
        <mkdir dir="${target.classes.dir}"/>
    </target>
 
    <target name="compile" depends="init">
        <javac srcdir="${src.dir}" destdir="${target.classes.dir}" classpathref="dependency" includeantruntime="false"/>
    </target>
 
    <target name="run">
        <java classname="hu.faragocsaba.main.Main">
            <classpath>
                <path refid="dependency"/>
                <pathelement location="${target.classes.dir}"/>
            </classpath>
        </java>
    </target>
</project>

Itt megismerkedünk a path elemmel, amivel elérési útvonalakat, jelen esetben osztály elérési útvonalat (classpath) tudunk megadni. Ebben a példában a neve dependency (ez bármi lehet). A javac és a java is hivatkozik rá, két külön formában: az egyik paraméterként (classpathref), a másik pedig külön <classpath> tag-en belül, ugyanis ez utóbbi kiterjeszti a classpath-ot magával a class osztályokat tartalmazó könyvtárral is. A lehetőségekről a specifikáció megfelelő oldalain (https://ant.apache.org/manual/Tasks/javac.html, https://ant.apache.org/manual/Tasks/java.html, https://ant.apache.org/manual/using.html#path) tájákozódhatunk.

Ezzel a két build fájllal automatizálni tudjuk az Ant segítségével a több komponens problémáját is, de magát a függőséget egyelőre még kézzel kellett másolnunk.

Egységtesztelés

Az egységtesztelés az Ant segítségével már feszegeti a komfortzónát. Először -az előzőhöz hasonlóan hozzunk létre a math komponensen belül is egy lib könyvtárt, és oda másoljuk be kézzel a kézzel letöltött junit-4.12.jar és hamcrest-all-1.3.jar fájlokat. A build.xml fájl kezd túlbonyolódni:

<project default="jar">
    <property name="src.main.dir" location="src/main/java" />
    <property name="src.test.dir" location="src/test/java" />
    <property name="target.dir" location="target" />
    <property name="target.classes.dir" location="${target.dir}/classes" />
    <property name="target.test-classes.dir" location="${target.dir}/test-classes" />
    <property name="target.surefire-reports.dir" location="${target.dir}/surefire-reports" />
 
    <path id="classpath.test">
        <pathelement location="lib/junit-4.12.jar"/>
        <pathelement location="lib/hamcrest-all-1.3.jar"/>
        <pathelement location="${target.classes.dir}"/>
    </path>
 
    <target name="clean">
        <delete dir="${target.dir}"/>
    </target>
 
    <target name="init">
        <mkdir dir="${target.dir}"/>
        <mkdir dir="${target.classes.dir}"/>
        <mkdir dir="${target.test-classes.dir}"/>
        <mkdir dir="${target.surefire-reports.dir}"/>
    </target>
 
    <target name="compile" depends="init">
        <javac srcdir="${src.main.dir}" destdir="${target.classes.dir}" includeantruntime="false"/>
    </target>
 
    <target name="test-compile" depends="compile">
        <javac srcdir="${src.test.dir}" destdir="${target.test-classes.dir}" includeantruntime="false">
            <classpath refid="classpath.test"/>
        </javac>
    </target>
 
    <target name="test" depends="test-compile">
        <junit printsummary="yes" showoutput="on" haltonfailure="yes" fork="true">
            <classpath>
                <path refid="classpath.test"/>
                <pathelement location="${target.test-classes.dir}"/>
            </classpath>
            <formatter type="plain"/>
            <formatter type="xml"/>
            <batchtest todir="${target.surefire-reports.dir}">
                <fileset dir="${src.test.dir}" includes="**/*Test.java"/>
            </batchtest>
        </junit>
    </target>
 
    <target name="jar" depends="test">
        <jar destfile="${target.dir}/mymath.jar" basedir="${target.classes.dir}">
            <manifest>
                <attribute name="Main-Class" value="hu.faragocsaba.main.Main"/>
            </manifest>
        </jar>
    </target>    
</project>

Itt láthatunk példát arra, hogy a classpath-ban hogyan tudunk megadni több dolgot is. Valójában három komponensre van szükség:

  • magára a junit-4.12.jar fájlra,
  • annak függőségére, a hamcrest-all-1.3.jar,
  • valamint arra az osztályra, amit tesztelni szeretnénk.

Külön le kell fordítani az egységteszt osztályokat, és futtatáskor is külön targetre van szükség. Felhívom a figyelmet a /*Test.java részre: ez igen gyakori módszer a könyvtárak megadásánál, és azt jelenti, amire gondoljunk: az ö0sszes olyan osztályt jelenti, ami Test-re végződik, és bárhol van (). Azt nem tudom megmondani, hogy futtatáskor miért van szükség a .java kiterjesztésre, de így működik.

Sajnos még így sem az igazi: hiba esetén sok támpontot nem ad, a target/surefire-reports könyvtárban generálódó riportok tartalmaznak csak némi részleteket.

A példa elkészítése nem volt egyszerű; az alábbi oldalak adtak valamennyi támpontot:

Külső függőség

A külső függőség ezen a szinten semmiben sem tér el a belső függőséges megoldástól, az eltérés nyilvánvalóan csak annyi, hogy a külső jar fájlt kell a lib könyvtárba másolni, és ezt kell megadni a build.xml fájlban:

<project default="compile">
    <property name="src.dir" location="src/main/java"/>
    <property name="target.dir" location="target"/>
    <property name="target.classes.dir" location="${target.dir}/classes"/>
 
    <path id="dependency">
        <fileset dir="lib" includes="*.jar"/>
    </path>
 
    <target name="clean">
        <delete dir="${target.dir}"/>
    </target>
 
    <target name="init">
        <mkdir dir="${target.dir}"/>
        <mkdir dir="${target.classes.dir}"/>
    </target>
 
    <target name="compile" depends="init">
        <javac srcdir="${src.dir}" destdir="${target.classes.dir}" classpathref="dependency" includeantruntime="false"/>
    </target>
 
    <target name="run">
        <java classname="hu.faragocsaba.main.Main">
            <classpath>
                <path refid="dependency"/>
                <pathelement location="${target.classes.dir}"/>
            </classpath>
        </java>
    </target>
</project>

Futtatható jar függőségekkel

A futtatható jar készítése külső függőségekkel szintén kívül esik a "komfortzónán", nem egyszerű. A https://mkyong.com/ant/ant-how-to-create-a-jar-file-with-external-libraries/ leírás segített elkészíteni az alábbi fájlt:

<project default="jar">
    <property name="src.dir" location="src/main/java"/>
    <property name="target.dir" location="target"/>
    <property name="target.classes.dir" location="${target.dir}/classes"/>
    <property name="lib.dir" location="lib"/>
    <property name="target.libs.dir" location="target/libs"/>
    <property name="jar.name" location="${target.dir}/guava-test.jar"/>
 
    <path id="dependency">
        <fileset dir="${lib.dir}" includes="*.jar"/>
    </path>
 
    <target name="clean">
        <delete dir="${target.dir}"/>
    </target>
 
    <target name="init" depends="clean">
        <mkdir dir="${target.dir}"/>
        <mkdir dir="${target.classes.dir}"/>
        <mkdir dir="${target.libs.dir}"/>
    </target>
 
    <target name="compile" depends="init">
        <javac srcdir="${src.dir}" destdir="${target.classes.dir}" classpathref="dependency" includeantruntime="false"/>
    </target>
 
    <target name="copy-dependencies">
        <copy todir="${target.libs.dir}">
            <fileset dir="${lib.dir}" includes="**/*.jar" excludes="**/*sources.jar, **/*javadoc.jar" />
        </copy>
    </target>
 
    <target name="jar" depends="compile, copy-dependencies">
        <pathconvert property="classpath.name" pathsep=" ">
            <path refid="dependency"/>
            <mapper>
                <chainedmapper>
                    <flattenmapper/>
                    <globmapper from="*.jar" to="libs/*.jar"/>
                </chainedmapper>
            </mapper>
        </pathconvert>
        <jar destfile="${jar.name}" basedir="${target.classes.dir}">
            <manifest>
                <attribute name="Main-Class" value="hu.faragocsaba.main.Main"/>
                <attribute name="Class-Path" value="${classpath.name}" />
            </manifest>
        </jar>
    </target>    
 
    <target name="run">
        <java jar="${jar.name}" fork="true"/>
    </target>
</project>

Itt két dolgot kell megadnunk:

  • Egyrészt a függőségeket olyan helyre kell másolni, ami elérhető az eredmény jar fájlból, pl. jelen esetben ez lehet a target/libs.
  • Meg kell adni a függőségeket a Manifest fájlban, a Class-Path attribútumaként. Jelen esetben csak egy függőség van, a példa viszont elő van készítve arra, hogy több ilyen függőség is legyen. A probléma abból fakad, hogy az elválasztó karakternek szóköznek kell lennie. Emiatt használjuk a pathconvert taszkot, amely a korábban megadott függőségeket alakítja át úgy, hog egyrészt tartalmazza a libs/ prefixet, másrészt szóköz legyen az elválasztó.

Überjar

Überjar készítése szintén bőven komfortzónán kívüli dolog az Antban; itt a leírás segített https://mkyong.com/ant/ant-create-a-fat-jar-file/. A build.xml így néz ki:

<project default="jar">
    <property name="src.dir" location="src/main/java"/>
    <property name="target.dir" location="target"/>
    <property name="target.classes.dir" location="${target.dir}/classes"/>
    <property name="lib.dir" location="lib"/>
    <property name="target.libs.dir" location="target/libs"/>
 
    <path id="dependency">
        <fileset dir="${lib.dir}" includes="*.jar"/>
    </path>
 
    <target name="clean">
        <delete dir="${target.dir}"/>
    </target>
 
    <target name="init" depends="clean">
        <mkdir dir="${target.dir}"/>
        <mkdir dir="${target.classes.dir}"/>
        <mkdir dir="${target.libs.dir}"/>
    </target>
 
    <target name="compile" depends="init">
        <javac srcdir="${src.dir}" destdir="${target.classes.dir}" classpathref="dependency" includeantruntime="false"/>
    </target>
 
    <pathconvert property="classpath.name" pathsep=" ">
        <path refid="dependency"/>
        <mapper>
            <chainedmapper>
                <flattenmapper />
                <globmapper from="*.jar" to="libs/*.jar"/>
            </chainedmapper>
        </mapper>
    </pathconvert>
 
    <target name="copy-dependencies">
        <jar jarfile="${target.libs.dir}/dependencies-all.jar">
            <zipgroupfileset dir="${lib.dir}">
                <include name="**/*.jar" />
            </zipgroupfileset>
        </jar>
    </target>
 
    <target name="jar" depends="compile, copy-dependencies">
        <jar destfile="${target.dir}/GuavaTest.jar" basedir="${target.classes.dir}">
            <manifest>
                <attribute name="Main-Class" value="hu.faragocsaba.main.Main"/>
            </manifest>
            <zipfileset src="${target.libs.dir}/dependencies-all.jar"/>
        </jar>
    </target>    
 
    <target name="run">
        <java jar="${target.dir}/GuavaTest.jar" fork="true"/>
    </target>
</project>

A két új lépés a következő:

  • Az összes külső függőség összefűzése egyetlenné, target/libs/dependencies-all.jar fájlnéven.
  • Az imént létrejött jar fájl tartalmának beleírása az eredménybe.
  • A Main-Class attribútumot továbbra is meg kell adni, a Class-Path attribútumot viszont már nem, mert minden benne van a végeredményben.

Ivy

Áttekintés

Az Ivy az Ant függőségkezelő kiegészítője. Az alábbi oldalak nyújtottak számomra segítséget a technológia megértésében és a fejezet elkészítésében:

Az Ivy beépülő telepítése:

  • Töltsük le a beépülőt a https://ant.apache.org/ivy/ oldalról. Bal oldalon Download, majd válasszuk ki a binárist. Én a 2.5.0-ás verziót töltöttem le.
  • A letöltött zip fájlból valójában csak a benne található ivy-2.5.0.jar fájlra van szükségünk. Ezt másoljuk az Ant lib könyvtárába (pl. c:\programs\apache-ant-1.10.8\lib\)

A Ivy működése

Au Ivy használatához a build.xml fejlécébe, a project tag attribútumaként az alábbi névtér hivatkozást kell megadni:

<project [...] xmlns:ivy="antlib:org.apache.ivy.ant">

Ennek eredményeképpen elérhetőek az Ivy taszkok, amelyek ivy: előtaggal érhetőek el, pl.:

    <target name="retrieve">
        <ivy:retrieve/>
    </target>

Az Ivy leíró fájl neve ivy.xml, a struktúrája pedig az alábbi:

<ivy-module version="2.0">
    <info organisation="[myorg]" module="[mymodule]"/>
    <dependencies>
        <dependency org="[groupId]" name="[artifactId]" rev="[version]"/>
        <dependency"/>
    </dependencies>
</ivy-module>

A legfontosabb négy taszk az alábbi, ami a működését is jól illusztrálja:

  • install: a függőségek letöltése a központi repository-ból a céges repository-ba. Az Ant is a Maven repository-t használja. Lokálisan ez a home könyvtár gyökerében található, a .ivy2/local/ könyvtárban.
  • resolve: a céges repository-ból átmásolja a függőségeket a lokális cache-be. A cache könyvtár a home könyvtár gyökerében a .ivy2/cache/, tehát ez a művelet lokálisan gyakorlatilag átmásolja a .ivy2/local/ könyvtárból a .ivy2/cache/ könyvtárba a dolgokat.
  • retrieve: a lokális cache-ből átmásolja a jar fájlokat az adott projekt lib könyvtárába. Ha nincs a lokális cache-ben, akkor megpróbálja a céges repository-ból letölteni, és ha ott sincs, akkor a központi repository-ból. Első futáskor tehát először letölti a netről a .ivy2/local/ könyvtárba (mivel nemcsak a jarokat tölti le, ez hosszú ideig is eltarthat), majd bemásolja a .ivy2/cache/-be, végül onnan a projekt lib// könyvtárába. A fenti példában tehát ha lefuttatjuk a retrieve nevű targetet, az az ugyanolyan nevű Ivy taszkot futtatja le, melynek következtében letöltődnek és az említett könyvtárakba másolódnak az ivy.xml fájlban megadott függőségek.
  • publish: a saját komponens bemásolása a céges repository-ba. A szervezet és a komponens neve az ivy.xml-ben, az info tag-ben adott, míg a verzió az ivy:publish taszk pubrevision attribútuma definiálja.

Mág számos egyéb taszk létezik, melynek teljes listáját a https://ant.apache.org/ivy/history/2.5.0/ant.html oldalon találjuk. Az Ivy működésének megértéséhez elég a fenti négyet ismernünk, a gyakorlatban pedig ritkán van szükségünk a retrieve és publish taszkokon kívül másra.

A Ivy leíró struktúrája is döntőrészt a fenti, hiszen csak fel szeretnénk sorolni a függőségeket. De van lehetőségünk további finomhangolásra. A leggyakrabban használt lehetőség az exclude, amellyel a tranzitív függőségekből tudunk kizárni komponenseket. Ezt a dependency tag altageként tudjuk megadni, a következőképpen:

<dependency org="[groupId]" name="[artifactId]" rev="[version]">
    <exclude module="[otherArtifactId]"/>
</dependency>

Az alábbiakban azokat az Ant projekteket bővítjük ki, ahol van függőség. A program korai fázisait átlépjük. A parancsok megmaradnak az eredetiek (ant clean, ant és ant run).

Több komponens

Lássuk, hogyan tudunk több saját készítésű komponenst kezelni Ivy segítségével!

Math

A matematika komponenst publikálni szeretnénk az Iv repository-ba. Ez volt az a komponens, amit idáig kézzel másoltunk. A build.xml fájlt az alábbi módon kell megadnunk:

<project name="mymath" default="publish" xmlns:ivy="antlib:org.apache.ivy.ant">
    <property name="src.dir" location="src/main/java" />
    <property name="target.dir" location="target" />
    <property name="target.classes.dir" location="${target.dir}/classes" />
 
    <target name="clean">
        <delete dir="${target.dir}"/>
    </target>
 
    <target name="init">
        <mkdir dir="${target.dir}"/>
        <mkdir dir="${target.classes.dir}"/>
    </target>
 
    <target name="compile" depends="init">
        <javac srcdir="${src.dir}" destdir="${target.classes.dir}" includeantruntime="false"/>
    </target>
 
    <target name="jar" depends="compile">
        <jar destfile="${target.dir}/mymath.jar" basedir="${target.classes.dir}"/>
    </target>
 
    <target name="publish" depends="jar">
        <ivy:resolve/>
        <ivy:publish resolver="local" pubrevision="1.0" overwrite="true">
            <artifacts pattern="${target.dir}/[artifact].[ext]" />
        </ivy:publish>
   </target>
</project>

Felhívom a figyelmet a megváltozott első sorra, valamit az utolsó targetre. Itt két Ivy taszkot adtunk meg:

  • ivy:resolve: ezt itt nem értem, ugyanis semmit sem szeretnénk letölteni. Mindenesetre enélkül nem működik, ezzel működik, úgyhogy itt marad.
  • ivy:publish: ezzel adjuk meg azt, hogy az elkészült komponenst publikálni szeretnénk a céges repositoryba.

Az ivy.xml tartalmaz az alábbi:

<ivy-module version="2.0">
    <info organisation="hu.faragocsaba" module="mymath"/>
</ivy-module>

Itt tehát megadjuk a szervezetünknek valamint komponensünknek a nevét. A verziószám az ivy:publish tag paramétere.

Ha először adjuk ki a parancsot, akkor létrejön a következő fájlt: c:\Users\Csaba\.ivy2\local\hu.faragocsaba\mymath\1.0\jars\mymath.jar.

Main

Ebben fogjuk felhasználni az imént publikált komponenst:

<project name="mymain" default="compile" xmlns:ivy="antlib:org.apache.ivy.ant">
    <property name="src.dir" location="src/main/java" />
    <property name="target.dir" location="target" />
    <property name="target.classes.dir" location="${target.dir}/classes" />
 
    <path id="dependency">
        <fileset dir="lib" includes="*.jar"/>
    </path>
 
    <target name="clean">
        <delete dir="${target.dir}"/>
    </target>
 
    <target name="init">
        <mkdir dir="${target.dir}"/>
        <mkdir dir="${target.classes.dir}"/>
    </target>
 
    <target name="retrieve">
        <ivy:retrieve/>
    </target>
 
    <target name="compile" depends="init, retrieve">
        <javac srcdir="${src.dir}" destdir="${target.classes.dir}" classpathref="dependency" includeantruntime="false"/>
    </target>
 
    <target name="run">
        <java classname="hu.faragocsaba.main.Main">
            <classpath>
                <path refid="dependency"/>
                <pathelement location="${target.classes.dir}"/>
            </classpath>
        </java>
    </target>
</project>

Valójában csak az ivy:retrieve taszkot kell meghívni, valamint rendesen felkonfigurálni az ivy.xml fájlt:

<ivy-module version="2.0">
    <info organisation="hu.faragocsaba" module="mymain"/>
    <dependencies>
        <dependency org="hu.faragocsaba" name="mymath" rev="1.0"/>
    </dependencies>
</ivy-module>

Ez utóbbiban a lényeges rész a dependency, ahol megadjuk a szervezünk azonosítóját, a szükséges komponens nevét és verzióját, és az Ivi teszi a dolgát: bemásolja a lib könyvtárba, ahonnan a classpath hivatkozás feloldja.

Egységtesztelés

Az egységteszteléshez meg kell adnunk a junit külső függőséget. A build.xml-t két ponton kell csak módosítani: egyrészt a fejlécet, másrészt a teszt osztályok fordításánál szükség van a ivy:retrieve taszkra a függőségek letöltéséhez.

<project name="mymath" default="jar" xmlns:ivy="antlib:org.apache.ivy.ant">
    <property name="src.main.dir" location="src/main/java" />
    <property name="src.test.dir" location="src/test/java" />
    <property name="target.dir" location="target" />
    <property name="target.classes.dir" location="${target.dir}/classes" />
    <property name="target.test-classes.dir" location="${target.dir}/test-classes" />
    <property name="target.surefire-reports.dir" location="${target.dir}/surefire-reports" />
 
    <path id="classpath.test">
        <fileset dir="lib" includes="*.jar"/>
        <pathelement location="${target.classes.dir}"/>
    </path>
 
    <target name="clean">
        <delete dir="lib"/>
        <delete dir="${target.dir}"/>
    </target>
 
    <target name="init">
        <mkdir dir="${target.dir}"/>
        <mkdir dir="${target.classes.dir}"/>
        <mkdir dir="${target.test-classes.dir}"/>
        <mkdir dir="${target.surefire-reports.dir}"/>
    </target>
 
    <target name="compile" depends="init">
        <javac srcdir="${src.main.dir}" destdir="${target.classes.dir}" includeantruntime="false"/>
    </target>
 
    <target name="test-compile" depends="compile">
        <ivy:retrieve/>
        <javac srcdir="${src.test.dir}" destdir="${target.test-classes.dir}" includeantruntime="false">
            <classpath refid="classpath.test"/>
        </javac>
    </target>
 
    <target name="test" depends="test-compile">
        <junit printsummary="yes" showoutput="on" haltonfailure="yes" fork="true">
            <classpath>
                <path refid="classpath.test"/>
                <pathelement location="${target.test-classes.dir}"/>
            </classpath>
            <formatter type="plain"/>
            <formatter type="xml"/>
            <batchtest todir="${target.surefire-reports.dir}">
                <fileset dir="${src.test.dir}" includes="**/*Test.java"/>
            </batchtest>
        </junit>
    </target>
 
    <target name="jar" depends="test">
        <jar destfile="${target.dir}/mymath.jar" basedir="${target.classes.dir}">
            <manifest>
                <attribute name="Main-Class" value="hu.faragocsaba.main.Main"/>
            </manifest>
        </jar>
    </target>    
</project>

Az ivy.xml-ben elég megadnunk a junit függőséget, a letöltéskor automatikusan letöltésre kerül a hamcrest is:

<ivy-module version="2.0">
    <info organisation="hu.faragocsaba" module="mymath"/>
    <dependencies>
        <dependency org="junit" name="junit" rev="4.12"/>
    </dependencies>
</ivy-module>

Külső függőség

A fentiek után a Guava függőség letöltése nem okoz meglepetést:

<project name="guava-test" default="compile" xmlns:ivy="antlib:org.apache.ivy.ant">
    <property name="src.dir" location="src/main/java"/>
    <property name="target.dir" location="target"/>
    <property name="target.classes.dir" location="${target.dir}/classes"/>
 
    <path id="dependency">
        <fileset dir="lib" includes="*.jar"/>
    </path>
 
    <target name="clean">
        <delete dir="${target.dir}"/>
    </target>
 
    <target name="init">
        <mkdir dir="${target.dir}"/>
        <mkdir dir="${target.classes.dir}"/>
    </target>
 
    <target name="retrieve">
        <ivy:retrieve/>
    </target>
 
    <target name="compile" depends="init, retrieve">
        <javac srcdir="${src.dir}" destdir="${target.classes.dir}" classpathref="dependency" includeantruntime="false"/>
    </target>
 
    <target name="run">
        <java classname="hu.faragocsaba.main.Main">
            <classpath>
                <path refid="dependency"/>
                <pathelement location="${target.classes.dir}"/>
            </classpath>
        </java>
    </target>
</project>

Az ivy.xml-ben adjuk meg a Guava függőséget:

<ivy-module version="2.0">
    <info organisation="hu.faragocsaba" module="guava-example"/>
    <dependencies>
        <dependency org="com.google.guava" name="guava" rev="29.0-jre"/>
    </dependencies>
</ivy-module>

Ha lefuttatjuk, akkor azt tapasztaljuk, hogy a lib könyvtárban nemcsak a Guava szerepel, hanem számos másik komponens is, amelyek tranzitív módon töltődtek le.

A futtatható jar külső függőségekkel, valamint az überjar készítése is ugyanígy történik, tehát elég megadni az ivy:retrieve taszkot, így helyspórolás miatt összecsukott állapotban mutatom be.

Futtatható jar külső függőségekkel

Überjar

Maven

Áttekintés

Az írás pillanatában a Maven a legnépszerűbb build automatizáló rendszer. Néhány ponton tovább gondolta az Ant működését.

Az Ant struktúráját nézve az embernek kicsit az az érzése, mintha ugyanazokat a parancsokat adnánk ki, mint amit közvetlenül is kiadunk, csak kicsit más szintaxissal. Ha megnézzük a taszkok listáját, egész sok mindent meg lehet vele csinálni. De egy Java fejlesztő nem mást szeretne végrehajtani, mint Java programot fordítani. A Maven leírója inkább valóban leíró, és nem parancsok egymás utánjai, más szintaxissal.

Az Ant leírójában szinte minden apróságot meg kell adni, valahogy semminek sincs egy épkézláb alapértelmezett értéke. Ezzel szemben a Maven a maximális konfigurálhatóság mellett számos konvenciót vezetett be: van alapértelmezett helye

  • a Java forrásoknak (src/main/java/),
  • az egységteszteknek (src/test/java/),
  • az erőforrás fájloknak (src/main/resources/ ill. src/test/resources/),
  • a lefordított binárisoknak (target/classes/, ill. minden eredmény a target/ alá kerül) stb.

Mivel nincs különösebb oka annak, hogy ne oda kerüljenek a dolgok, különböző szoftvercégek ugyanolyan felépítésű szoftvereket készítenek. Ez megkönnyíti a fejlesztők dolgát is, ha rákerülnek más projektre.

Az Antban létre kell hozni targeteket. Ezzel két probléma van:

  • Létre kell hozni a nyilvánvalót. Nyilván fordítani szeretnénk, szükség van tehát egy olyan targetre, ami törli az előző fordítás eredményét, egy olyanra, ami végrehajtja a fordítást, egy olyanra, ami létrehozza a jar fájlt, egy olyanra, ami végrehajtja az egységteszteket stb. Ezek között természetes függőségek vannak, amit szintén meg szoktunk adni: nyilván előbb fordítjuk le a főprogramot, mint becsomagoljuk jar fájlba.
  • Megadja azt a szabadságot, hogy úgy nevezzük el, ahogy szeretnénk. A jar fájl elkészítése így a projektek egy jelentős hányadában egy jar nevű target fogja elvégezni, máshol meg package lesz a neve. De olyan is lesz, ahol ennek a lépésnek nem is hoznak létre nevesített targetet. Vagy olyan is, ahol sok targetet hoznak létre.

Mivel úgyis ugyanazt az életciklust járja be mindegyik fordítás, a Maven eleve létrehozta ezeket, és életciklus fázisoknak (lifecycle phase) nevezte el. A fázisok száma már egész sok, és ennek a leírásnak nem célja mindegyiknek a tüzetes bemutatása; a legfontosabbak az alábbiak:

  • clean: a korábbi fordítás törlése. Az Anttal ellentétben itt természetesen nem kell megadnunk, hogy mit szeretnénk törölni, természetesen a target könyvtárat.
  • compile: a Java források lefordítása.
  • test: az egységtesztek futtatása. Azt sem kell megmondani, hogy előtte le kell fordítani magát a főprogram forrását, majd az egységtesztek forrását is, ezt a nyilvánvalót magától értetődőnek veszi.
  • package: a jar fájl elkészítése.
  • install: az elkészült jar fájl felmásolása a lokális repository-ba. Ez a felhasználó home könyvtárában található, .m2 néven.
  • deploy: az eredmény véglegesítése: bemásolása a távoli repository-ba.

Két módon lehet mégis belenyúlni a rendszerbe:

  • Beépülők segítségével. A leíróba megfelelő szintaxissal felül tudjuk definiálni azt, hogy egy-egy fázist pontosan mi hajtsa végre, és azokat megfelelően fel tudjuk paraméterezni. Vannak olyan beépülők is, amelyek "tudják maguk", hogy milyen fázisban érintettek, és ott nem kell azt megadni. Elvileg mi magunk is tudunk ilyen beépülőt készíteni, de nem gondolnám, hogy létezik olyan művelet, amire szükség van egy build folyamat során, és még más nem valósította meg. Az alapértelmezett megoldás viszont sajnos igen sok esetben nem megfelelő (ez egyébként részemről komoly kritika a Mavennek szemben), így a szabványos beépülők használata szinte elkerülhetetlen. Ráadásul ezek igen jelentősen elbonyolítják a leírót. Egy példa: annak a leírósnak a mérete majdnem ötször (!!!) akkora, amelyben futtathatóvá szeretnénk tenni az eredményt, és ezáltal meg szeretnénk adni a főosztály nevét, ahhoz képest, hogy csak a jart készítjük el, ami nem futtatható.
  • Saját életciklus fázis létrehozásával. Persze ezt sem úgy kell elképzelni a gyakorlatban, hogy mi magunk létrehozunk ilyen fázisokat (bár megtehetnénk), hanem - ritkán - szükség lehet arra, hogy már egy létezőt használjunk. Habár a fenti felsorolás nem teljes, igen látványosan hiányzik belőle a futtatás, és ha lenne ilyen, akkor bele került volna. De futtatás fázis nincs a Mavenben. Szerintem nincs is rá szükség. Viszont a lustábbak számára hasznos lehet az egyes webszerverek vagy alkalmazás szerverek által biztosított beépülők ill. életciklus fázisok, melynek során Maven parancs segítségével tudjuk a deploy műveletet végrehajtani.

Az Ant gondosan elkülönítette a build automatizálást és a függőség kezelést. A valóságban a kettő kéz a kézben jár: nyilván minden valamirevaló programnak van függősége (ha már felmerül a build automatizálás, akkor egészen biztos van függőség), és azt is kezelni kell. A Maven a build automatizálást és a függőség kezelést egyben kezeli.

A Maven leírója a pom.xml. Tehát XML, ami eléggé szószátyár, és ez - sok más egyéb mellett - szinte előrevetítette az újabb build rendszerek, mindenekelőtt a Gradle megjelenését, amely megtartotta a Maven jó tulajdonságait, ugyanakkor kijavította a bosszantó hibáit. Pl. azt is, hogy végre-valahára nem XML a leíró.

Lássuk most a Maven telepítését! A Maven - az Anthoz hasonlóan - zip fájlt formájában érkezik, amit ingyenesen le tudunk tölteni a https://maven.apache.org/download.cgi oldalról. Tömörítsük ki valahova, a bin könyvtárát adjuk hozzá a PATH-hoz. Győződjünk meg arról, hogy a Java fel van telepítve, és valójában készen is vagyunk. Opcionálisan megfelelőképpen beállíthatjuk a MAVEN_HOME könyvtárat.

A Maven indítása: mvn. Próbáljuk ki pl. ezt:

mvn -version

Van még egy hiányossága a Mavennek: nincs alapértelmezett végrehajtandó fázisa, azt mindenképpen meg kell adni. Ebben a tananyagban alapvetően az alábbit fogjuk használni:

mvn clean install

Ez tehát törli a korábbi fordítás eredményeit, majd fordít, lefordítja és lefuttatja az egységteszteket, elkészíti a jart, és felmásolja a helyi repository-ba.

Hasznos olvasmény kezdők számára et: http://maven.apache.org/guides/getting-started/maven-in-five-minutes.html.

Ennyi elméleti bevezető után gyakorlati példákon keresztül mutatom be ezt a rendszert.

Csomagszerkezet több osztállyal

Mivel a Maven konvenciókra épül, és az egyik konvenció az, hogy a forrás az src/main/java/ könyvtárban van, az egy szem Java forrásfájl a gyökérben esetet Maven segítségével nem tudjuk kezelni, így azt átugorjuk. (Ennyire egyszerű feladatokra egyébként az Ant jobb. Összetettebbekre a Maven, ahogy majd azt látni fogjuk.) Rögtön lépjünk tehát a "csomagszerkezet több osztállyal" esetre. A gyökében hozzuk létre a pom.xml fájlt az alábbi tartalommal:

<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>hu.faragocsaba</groupId>
    <artifactId>mymath</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>
</project>

Ez egy minimalista Maven leíró. Az alábbiakat adtuk meg:

  • Fejléc információ, egy tonna névtér információval. Tapasztalatom szerint az újabb verziókban ez végre elhagyható.
  • modelVersion: egy verziószám, ami kivétel nélkül mindig 4.0.0, kivétel nélkül mindig kötelező megadni. (ÚŐjabb fekete pont a Maven számláján.)
  • groupId: annak a szervezetnek neve, amelyik létrehozza a komponenst. Kötelező megadni.
  • artifactId: annak a komponensnek a neve, amit létrehozunk. Szintén kötelező (és ennek van legnagyobb értelme).
  • version: verziószám; kötelező.
  • packaging: a csomagolás típusa. Nem kötelező kiírni, az alapértelmezett a jar. Lehetne még war vagy ear.

A fentiek figyelembe vételével az abszolút minimalista pom.xml az alábbi:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>mymath</artifactId>
    <version>1.0</version>
</project>

Azért ez is sok szerintem; majd látjuk, hogy a Gradle hogyan tömörített ezt is.

Kiadhatjuk a fenti mvn clean install parancsot is, bár a csomagolásnak most még sok értelme nincs, mert nem lesz futtatható. Adjuk ki ezt:

mvn clean compile

Ez lefordítja a Java forrásokat. A bevezetőben megadott módon tudjuk indítani.

Futtatható jar

Mit jelent az, hogy egy jar futtatható? Az eredményben levő META-INF/MANIFEST-MF fájlban meg van adva a fő osztály neve (Main-Class). Ez a fájl automatikusan létrejön, de alapból nem tartalmazza ezt a kulcs-érték párt. A fenti elírtak alapján a következőt kell tenni:

  • Keresni kell egy olyan beépülőt, amely a package fázist hajtja végre.
  • Megfelelően fel kell paraméterezni, melyben meg tudjuk adni a főosztályt.

Ez a beépülő történetesen a maven-jar-plugin, a pontos szintaxis pedig az alábbi:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>mymath</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>
                                hu.faragocsaba.main.Main
                            </mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Szerintem ez túl komplikált erre a célra (fekete pont). Mindenesetre csak egyszer kell megadni, és a mvn clean install teszi a dolgát.

Több komponens

Math

A build.xml ennyi:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>mymath</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>
</project>

Ha lefuttatjuk a szokásos mvn clean install parancsot, akkor a home könyvtárban, a .m2-n belül megtaláljuk az eredményt, pl. c:\Users\Csaba\.m2\repository\hu\faragocsaba\mymath\1.0\mymath-1.0.jar.

Main

A főprogramban megadjuk a függőséget az alábbi módon:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>mymain</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>
 
    <dependencies>
        <dependency>
            <groupId>hu.faragocsaba</groupId>
            <artifactId>mymath</artifactId>
            <version>1.0</version>
        </dependency>
    </dependencies>
</project>

Ez tudja, hogy fordításkor a fent megadott helyen kell keresni a jar fájlt, így ha mindent jól csináltunk, a mvn clean install sikeresen lefordítja ezt a komponenst is. Futtatása viszont a projektnek ebben a fázisában kicsit trükkös: sajnos nem másolja be a külső függőséget a főprogram fájlstruktúrájába, és a Mafinest alapból nem is tartalmazza sajnos függőséget automatikusan (fekete pont!), így legegyszerűbben talán a következőképpen tudjuk elindítani:

java -cp target\mymain-1.0.jar;c:\Users\Csaba\.m2\repository\hu\faragocsaba\mymath\1.0\mymath-1.0.jar hu.faragocsaba.main.Main

Természetesen lehet ennél egyszerűbben is, de ennek az ára az összetettebb pom.xml, amit azt kéősbb látni fogjuk.

Egységtesztelés

A konvencióknak hála az egységtesztek lefuttatása Maven segítségével automatikusan történik. Mindössze a megfelelő junit függőséget kell megadni:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>math</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>
 
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

A mvn clean install lefuttatja az egységteszteket. Figyeljük meg a függőség scope paraméterét: a test azt jelenti, hogy csak az egységtesztek végrehajtásához kellenek. Érdemes megfigyelni egyébként azt is, hogy ez le is futtatja az egységteszeket anélkül, hog a projekt struktúrába másolta volna a junit függőséget. (Itt egyébként egy valamit nem értek: a dokumentáció azt állítja, hogy a test scope-ú függőségek nem tranzitívak. Csakhogy a hamcrest függőséget nem kellett megadni, azt automatikusan feloldotta.)

Külső függőség

A külső függőséget pontosan úgy kell megadni, mint a belsőt:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>guava-example</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>
 
    <dependencies>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>29.0-jre</version>
        </dependency>
    </dependencies>
</project>

A mvn clean install első futáskor, amikor még nincs letöltve a jar fájl, automatikusan letölti a Maven repository-ból, Windows alatt a következő helyre (a saját home könyvtárba): c:\Users\Csaba\.m2\repository\com\google\guava\guava\29.0-jre\guava-29.0-jre.jar. Az indításhoz most még a belső függőségnél megismert trükköt alkalmazzuk:

java -cp target\guava-example-1.0.jar;c:\Users\Csaba\.m2\repository\com\google\guava\guava\29.0-jre\guava-29.0-jre.jar hu.faragocsaba.main.Main

Futtatható jar függőségekkel

A futtatható jar készítése, ha függőségeket is tartalmaz, akkor ismét bonyolódik egy nagyot (fekete pont!). A probléma az, hogy - amint tapasztaltuk - nem másolja be automatikusan a függőségeket a projekt saját könyvtárszerkezetébe, az nekünk kell külön megadnunk. (Pedig ez lehetne az alapértelmezett!) A pom.xml így néz ki:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>guava-example</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>
 
    <dependencies>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>29.0-jre</version>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>
                                ${project.build.directory}/libs
                            </outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
 
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <classpathPrefix>libs/</classpathPrefix>
                            <mainClass>
                                hu.faragocsaba.main.Main
                            </mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Tehát a prepare-package fázisban végrehajtjuk a megfelelő (valójában saját) beépülő (maven-dependency-plugin) copy-dependencies goal-ját, ahol azt adjuk meg, hogy a target/libs/ könyvtárba másolja be az összes függőséget. A ${project.build.directory} egy beépített változó, aminek az alapértelmezett értéke a target.

A következő beépülő a jar csomagolást szabályozza. A már bemutatott módon megadjuk a főosztály nevét, valamint itt adjuk meg azt is, hogy adja hozzá a classpath-t is (<addClasspath>true</addClasspath>), ill. azt, hogy mely könyvtárban található elemeket vegye fel a listára (<classpathPrefix>libs/</classpathPrefix>). Annyiban intelligensebb ez mint az Ant, hogy nem kell külön megadni azt is, hogy a jar fájlokat keresse meg, és szóközzel válassza el azokat, a Maven automatikusan így csinálja.

Itt is fontos, hogy a külső függőségek a futtatható jar mellett legyen a libs/ könyvtárban, mivel a classpath adott a Manifest fájlban, azt kívülről nem lehet módosítani. Nem egyszerű tehát Mavenben sem külső függőségekkel rendelkező futtatható jart készíteni, pedig ez a legtipikusabb formája egy valós Java programnak, és lehetne itt is alapértelmezettét tenni sok mindent.

Viszont hogy ne csak a Maven hátrányairól legyen szó: itt még van 3 lépés, ami nem visz jelentős plusz komplexitást a rendszerbe, az eredményt viszont javítja.

Überjar

Az Überjart még láttuk az Ant-ban is. A Maven leírója még kisebb méretű is, mint a sima futtatható jar külső függőségeké, mindössze egyetlen jól felparaméterezett beépülőre (maven-assembly-plugin) van szükségünk:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>guava-example</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>
 
    <dependencies>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>29.0-jre</version>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                        <configuration>
                            <archive>
                            <manifest>
                                <mainClass>
                                    hu.faragocsaba.main.Main
                                </mainClass>
                            </manifest>
                            </archive>
                            <descriptorRefs>
                                <descriptorRef>jar-with-dependencies</descriptorRef>
                            </descriptorRefs>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

A mvn clean install elkészíti a végeredményt guava-example-1.0-jar-with-dependencies.jar néven. Ez tehát önmagában tartalmaz mindent, ami az indításhoz kell.

Shading

Az Überjar gyorsan népszerűvé vált, és kiderült egy probléma vele kapcsolatban: az ütközés. Ha veszünk egy izmosabb Java projektet több tucat közvetlen és ki tujda menni tranzitív függőséggel, akkor előfordul, hogy lesz két olyan osztály, aminek az elérése csomagnévvel együtt pontosan megegyezik. Ennek kezelésére találták ki az árnyékolás (angolul shading) technikát: ennek segítéségével át tudjuk nevezni a csomag struktúrát, pl. egy prefixet tudunk elé tenni, és így el tudjuk kerülni az ütközést.

Ehhez is van egy beépülő maven-shade-plugin néven, és ezt megfelelően felkonfigurálva tudunk futtatható árnyékolt überjart készíteni:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>guava-example</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>
 
    <dependencies>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>29.0-jre</version>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <relocations>
                                <relocation>
                                    <pattern>com</pattern>
                                    <shadedPattern>shaded.com</shadedPattern>
                                </relocation>
                                <relocation>
                                    <pattern>org</pattern>
                                    <shadedPattern>shaded.org</shadedPattern>
                                </relocation>
                                <relocation>
                                    <pattern>javax</pattern>
                                    <shadedPattern>shaded.javax</shadedPattern>
                                </relocation>
                            </relocations>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>hu.faragocsaba.main.Main</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

A könyvtárak közvetlen jar-ba helyezése

Van még egy lehetőség, ami jó lenne, ha alapértelmezett lenne: a legelegánsabb megoldás valójában az lenne, ha magába a jarba tudnánk a jarokat másolni (ugyanúgy, mintha egy zip-be kerülnének további zipek). Ezt közvetlenül két okból sem lehet végrehajtani: egyrészt maga a jar formátum nem támogatja, másrészt - ettől valószínűleg nem függetlenül - a Maven beépülők sem. Ám a Spring Boot készített egy beépülőt - elsősorban a saját céljára, de alkalmas attól független használatra is - amely egy kis beleforduló program segítségével mégis tudja kezelni a jar-ba helyezett jar fájlokat, maga a beépülő pedig belehelyezi a jarokat a jarba:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>guava-example</artifactId>
    <version>1.0</version>
 
    <dependencies>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>29.0-jre</version>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                        <configuration>
                            <classifier>spring-boot</classifier>
                            <mainClass>hu.faragocsaba.main.Main</mainClass>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Igazából méretre ez a legkisebb. Az eredményben a BBOT-INF/lib/ könyvtárba kerülnek a külső függőségek.

További beállítási lehetőségek

Ezzel még mindig csak a felszínét karcoltuk a Maven lehetőségeinek, és nem is célom annak részletes bemutatása. Ám van néhány olyan dolog, amit érdemes megemlíteni.

Alapértelmezésben a Maven Java 1.5-ös szintaxis szerint fordítja a forrást (fekete pont), így bizonyos nyelvi struktúrák esetén fordítási hibát eredményez. Ennek elkerülésére az alábbi müdon tudjuk megadni a Java verziót, amit célszerűen a szokásos első 4-5 sor után érdemes megadni:

<project>
    […]
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    […]
</project>

Hasonló módon tudunk saját változókat is létrehozni, amit fel tudunk használni, pl.:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>mymain</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>
 
    <properties>
        <mymath.version>1.0</mymath.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>hu.faragocsaba</groupId>
            <artifactId>mymath</artifactId>
            <version>${mymath.version}</version>
        </dependency>
    </dependencies>
</project>

Ennek nyilván leginkább akkor van értelme, ha sok komponensnek ugyanaz a verziója, és verzióváltáskor elég legyen egyszer megadni.

A rendszerszintű Maven beállítások a .m2/settings.xml fájlban találhatóak. Ez a fájl alapból nem is létezik, és ez esetben az alapértelmezett beállítások lépének életbe. Itt tudunk olyan dolgokat beállítani, mint pl. a rendszer proxy. Ennek a részletei szintén túlmutatnak ennek a leírásnak a keretein.

Gradle

Áttekintés

A Maven tovább gondolta az Antot, és számos hiányosságán javított, ám - amint azt láttuk - számos rossz döntéssel valójában a Maven maga is kissé nehézkes. Mindenekelőtt a pom.xml mérete maradt jóval nagyobb mint feltétlenül muszáj. A Gradle az Ant és a Maven tapasztalatait figyelembe véve tovább lépett az úton, és alkotott egy újabb rendszer, ami valóban előre lépés, és majd látni fogjuk, hogy még innen is van hová fejlődni.

A legszembetűnőbb eredmény a sokkal kisebb build leíró. Ezt kétféle szintaxissal tudjuk megadni: Groovy van Kotlin. (És ezzel el is értünk az első kritikához: személy szerint nem nagyon szeretem a programozásban a demokráciát: igenis csak egyetlen módszer legyen, amit mindenki használ!) Az alábbiakban a Groovy szintaxist fogjuk használni. Mivel a Groovy egy önálló programozási nyelv, a Gradle messze többre képes mint Java projektek fordítása (ami szerintem szintén probléma: egy eszköz egy dologhoz értsen csak, de ahhoz nagyon), mi most csak e tulajdonságát nézzük meg.

A Gradle leírójának a neve build.gradle. Ide kerülnek bele a szükséges dolgok. Szerencsére a Gradle is megtartotta azt a "maveni" megközelítést, hogy ne kelljen mindent megadni, hanem legyenek lerögzített alapértelmezések, és részben sikerült ugyanazt választani, részben egészen mást (fekete pont). Valamint - amint arról már volt szó - végre-valahára megszabadult az XML formátumtól, viszont a Groovy ill. Kotlin azért továbbra is eredményez némi boilerplate, helyenként olvashatatlan kódot (újabb fekete pont). A fejezet végén majd látni fogunk egy példát arra, hogy hogyan lehetne ezt igazán letisztultan csinálni.

Amint azt láttuk, az Ant targetekre, a Maven pedig fázisokra osztotta az elvégzendő feladatokat. Miért ne adna egy új nevet a Gradle ugyanannak? Itt taszknak hívjuk (ami nem mellesleg létező fogalom az Antban, de más tartalommal - újabb fekete pont). A Gradle alapértelmezésben nem sok mindent csinál, a beépülőket kell használni. A lenti példákban egy kivétellel csak a Java beépülőt fogjuk használni. Itt elég sok taszkot különböztetünk meg, melyek közül számunkra kettő igazán érdekes:

  • clean: letörli a korábbi fordítást. Nem függ semmitől, és nem függ tőle semmi.
  • build: kiváltja a teljes fordítási folyamatot, beleérve a fordítást, az egységtesztelést, a jar készítést stb.

Mi alapvetően az alábbi módon fogjuk használni a Gradle-t:

gradle clean build

Van még egy lehetőség: a wrapper módszer, ami azt jelenti, hogy bizonyos parancsokkal (pl. gradle init) le lehet generálni scripteket, és nem a gradle parancsot kell hívni megfelelő paraméterekkel, hanem azt a wrappert, melynek tipikus neve gradlew.bat vagy gradlew.sh. A példákban mi ezt nem használjuk. (Egyébként személy szerint ezt sem tartom jó ötletnek: ez csak azt eredményezi, hogy egyesek használják, mások nem, és ez is csökkenti az egységes képet.)

Csomagszerkezet több osztállyal

A build.gradle az alábbi:

apply plugin: 'java'

Igazából elég ennyit megadni, a gradle clean build elkészíti az eredményt. Az alábbiakat tapasztalhatjuk:

  • A projekt szerkezetben létrejön egy .gradle könyvtár, amit a clean nem töröl le. (Én azt várnám el, hogy a clean művelet minden ideiglenes fájlt kitöröl. Fekete pont.)
  • A home könyvtárunkban is létrejön egy .gradle könyvtár. Ennek a tartalma nem a megszokott jar cache. Kézzel törölni nem tudjuk. (Fekete pont.)
  • A fordítás eredményénél nem követi a Maven konvenciókat: a build könyvtárba kerülnek az eredmények, pl. a jar fájl a kicsit félrevezető build/libs/ könyvtárba. (Fekete pont.)
  • Az első futásnál láthatjuk, hogy megpróbál egy daemon szálhoz kapcsolódni, és mivel nem sikerül neki, ezért indít egyet. A háttrében tehát folyamatosan fut egy Gradle daemon. Ez akadályozza egyébként a home-ban levő .gradle törlését. (Fekete pont, ugyanis azt várom egy egy build rendszertől, hogy csak akkor fusson, amikor mondom neki.)
  • A leírója szemmel láthatólag idáig a legkisebb.
  • Az eredmén ynevét sem atduk meg; automatikusan annak a könyvtárnak a nevét veszi alapértelmezettként, amelyben van. (Piros pont!)
  • Viszont tegyük fel, hogy egy univerzális eszközt szeretnének létrehozni, ami nemcsak Java fordításra alkalmas, így azt meg kelljen adni. De az apply plugin: rész számomra még itt is biolerplate kód.

Futtatható jar

A build.gradle a következő:

apply plugin: 'java'
 
jar {
    archiveBaseName = 'mymath'
    manifest {
        attributes 'Main-Class': 'hu.faragocsaba.main.Main'
    }
}

Nőtt, de nem "robbant" a mérete; tényleg csak azzal kellett kiegészíteni, ami ehhez szükséges, mégpedig letisztult, logikus formában. A példában megadtuk az archiveBaseName értékét is; ennek következtében nem a könyvtárnév lesz az eredmény neve, hanem a megadott.

Futtatás:

java -jar build/libs/mymath.jar

Több komponens

Több komponens készítése meglepően nehézkes Gradle-ban, a Mavenhez viszonyítva.

Math

A build.gradle az alábbi:

apply plugin: 'java'
apply plugin: 'maven-publish'
 
publishing {
    publications {
        maven(MavenPublication) {
            groupId = 'hu.faragocsaba'
            artifactId = 'mymath'
            version = '1.0'
            from components.java
        }
    }
}

Tehát:

  • Ehhez külön beépülőt kell használni, azt nevesíteni is kell. (Univerzális eszköz lévén máshogy nem is lehet, de azért egy pici fekete pontot odateszek.)
  • A leírója elég komplikált. Én azt várnám el, hogy valamilyen kiemelt helyem megadjuk a szervezet nevét, a komponens nevét és a verziót, és automatikusan teszi a dolgát. Ebben a formában szerintem túlkomplikált, és engem különösen a from components.java zavart meg. Most létre kell hozni egy ilyet? De miért kisbetűs? (Nagy fekete pont.)
  • A leíróból nem adódik, viszont jó tudunk, hogy ez a home könyvtárunkban található .m2/repository/-t használja. (Piros pont.)

A használata nem nyilvánvaló (fekete pont):

gradle clean build publishToMavenLocal

Main

A főosztályban az alábbi módon hivatkozunk a fenti komponensre:

apply plugin: 'java'
 
repositories {
    mavenLocal()
}
 
dependencies {
    implementation 'hu.faragocsaba:mymath:1.0'
}

Meg kell tehát adnunk azt, hogy a lokális Maven repository-t szeretnénk használni (fekete pont), majd a függőségeknél letisztult formában magát a függőséget (piros pont).

Fordítás:

gradle clean build

Indítás (mivel még nem futtatható):

java -cp c:\Users\Csaba\.m2\repository\hu\faragocsaba\mymath\1.0\mymath-1.0.jar;build\libs\main.jar hu.faragocsaba.main.Main

Egységtesztelés

Az egységtesztelés szintén mélyen beágyazott:

apply plugin: 'java'
 
repositories {
    mavenCentral()
}
 
dependencies {
    testImplementation 'junit:junit:4.12'
}

A gradle clean build hatásár lefutnak az egységtesztek is. Létható, hogy a függőségeknél itt testImplementation-t adtunk meg, fent implementation-t, és a rendszer tudja, hogy mikor van ezekre szükség. Itt meg kell adnunk azt, hogy a függőséget a központi Maven repository-ból szeretnénk letölteni (fekete pont).

Külső függőség

Külső függőséget ugyanúgy adunk meg, mint belsőt, viszont meg kell adnunk, hogy a központi Maven repository-t szeretnénk használni:

apply plugin: 'java'
 
jar {
    archiveBaseName = 'guava-example'
    manifest {
        attributes 'Main-Class': 'hu.faragocsaba.main.Main'
    }
}
 
repositories {
    mavenCentral()
}
 
dependencies {
    implementation 'com.google.guava:guava:29.0-jre'
}

Az indítás viszont trükkös. Nem másolja be automatikusan a függőséget a projekt fájlstruktúrájába (fekete pont), és még csak nem is a .m2/repository/ könyvtárba (fekete pont), hanem a .gradle-ba, és oda se teljesen logikus helyre, hanem pl. nálam ide: c:\Users\Csaba\.gradle\caches\modules-2\files-2.1\com.google.guava\guava\29.0-jre\801142b4c3d0f0770dd29abea50906cacfddd447\guava-29.0-jre.jar;build\libs\guava-example (fekete pont). Az a hosszú hexadecimális számokból álló karaktersorozat fogalmam sincs, hogy mi, ahogyan azt sem, hogy a modules-2\files-2.1\ mennyire lesz stabil, vagy másoknál is ugyanez. Mindenesetre ez alapján az indítás:

java -cp c:\Users\Csaba\.gradle\caches\modules-2\files-2.1\com.google.guava\guava\29.0-jre\801142b4c3d0f0770dd29abea50906cacfddd447\guava-29.0-jre.jar;build\libs\guava-example.jar hu.faragocsaba.main.Main

Überjar

A "futtatható jar külső függőségekkel" témában csak Überjar megoldást találtam, az viszont viszonylag egyszerű. A build.gradle a következőképpen néz ki:

apply plugin: 'java'
 
jar {
    archiveBaseName = 'guava-example'
    manifest {
        attributes 'Main-Class': 'hu.faragocsaba.main.Main'
    }
    from {
        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
}
 
repositories {
    mavenCentral()
}
 
dependencies {
    implementation 'com.google.guava:guava:29.0-jre'
}

A from részt kellett beleírni, ami személy szerint nekem nagyon nem tetszik (fekete pont), de legalább rövid.

Összegzés

A build rendszerek összehasonlítása

Az Ant a legegyszerűbb feladattól a közepesig használható kényelmesen, a függőségek megjelenésével kissé nehézkessé válik, az olyan "extra" dolgok pedig, mint pl. az egységtesztek futtatása, vagy futtatható jar készítése függőségekkel, már-már szinte áttekinthetetlenül bonyolulttá teszi a konfigurációt. Ráadásul mivel nem függőségkezelő rendszer, külön be kell állítani hozzá az Ivy-t.

A Maven a túl egyszerű feladatokra (pl. egyetlen osztályból álló, gyökérben levő forrás fordítása) nem alkalmas, a közepes feladatokra optimális (tetszőleges számú csomagot és osztályt tartalmazó rendszer fordítása, egységtesztek fordítása és futtatása, függőségek kezelése), és a komfortzónát az olyan extra feladatokkal hagyjuk el, mint pl. az überjar készítése. Ugyanakkor van még pár érthetetlen megoldás is, amit nem javítanak ki:

  • Az alapértelmezett Java verzió az 1.5. Az aktuális Java verziónak kellene lennie.
  • Mindenképpen meg kell adni paramétert. Pedig feltételezhetné, hogy a felhasználó lefordítani szeretné a kódot, tehát pl. egy clean install lehetne az alapértelmezett.
  • Az XML túl bőbeszédű, és sok esetben elvész a sok bába közt a gyerek.

A Gradle kijavította a Maven bőbeszédűségét. Ugyanakkor - ahogy fent láthattuk - itt is van egy csomó logikátlanság és kiforratlanság benne, melynek egyik része abból következik, hogy egy univerzális eszközt szerettek volna létrehozni, ami nagyon sok mindenre jó; a kevesebb viszont néha több.

Az írás pillanatában a Maven a piacvezető, a Gradle feltörekvő, az Antnak pedig nagyon lassan, de leáldozóban van a csillaga. A Gradle-t nem tartom annyival jobbnak a Mavenhez képest, hogy kívánatos fejleménynek tartanám azt, hogy az legyen az egyedüli build rendszer, sőt, a Maven bogarai nekem kevésbé zavaróak, mint a Gradle-é.

És hogy lehetne-e még ennél is jobban? Szerintem igen! Vegyük pl. az Scala Build Tool (sbt) leíróját (build.sbt); a fentiek kb. a következőképpen néznének ki:

name := "MyProject"
version := "1.0"
organization := "hu.faragocsaba"
scalaVersion := "2.12.7"
libraryDependencies += "junit" % "junit" % "4.12" % "test"
libraryDependencies += "com.google.guava" % "guava" % "29.0-jre"

Tényleg csak az van benne, amit meg kell adni, az viszont benne van. Reméljük, egyszer Főnixmadárként megjelenik a Java szoftverfejlesztés egén egy olyan build rendszer, ami egyesíti a fentiek összes előnyét, és nem hoz be újabb hátrányt, majd mindenki felfedezi ennek nagyszerűségét, és ezt fogják használni.

Könyvtárszerkezet konvenciók összehasonlítása

Az alábbi táblázat összehasonlítja az egyes rendszerek könyvtárstruktúráit:

Alap Java Ant + Ivy Maven Gradle Eclipse NetBeans IDEA
Forrás aktuális könyvtár aktuális könyvtár src/main/java/ src/main/java/ src/ nincs * src/
Class aktuális könyvtár aktuális könyvtár target/classes/ build/classes/java/main/ bin/ nincs out/production/[projectname]/
Teszt forrás aktuális könyvtár aktuális könyvtár src/test/java/ src/test/java/ test/ nincs nincs
Teszt class aktuális könyvtár aktuális könyvtár target/test-classes/ build/classes/java/test/ bin/ nincs nincs
Külső lib nincs lib/ nincs nincs nincs (ld. .classpath) nincs nincs (ld. iml fájl)
Eredmény jar aktuális könyvtár aktuális könyvtár target/[név]-[verzió].jar build/libs/ nincs nincs nincs
Ext lib cache nincs $HOME/.ivy2/cache/ $HOME/.m2/repository $HOME/.gradle/caches nincs nincs nincs

A NetBeans IDE-nek - tapasztalatom szerint - nincs saját formátuma; ki kell választani a megfelelő build rendszert, és annak megfelelően alakítja ki a könyvtárszerkezetet.

Repository rendszerek

Maven repository

A Maven repository-ban szinte minden külső könyvtár megtalálható, amire szükségünk lehet. Használata a következő:

  • Nyissuk meg a https://mvnrepository.com oldalt.
  • Írjuk be a keresőbe a keresett komponenst, pl. junit.
  • Kattintsunk a megfelelő találatra, ami jelen esetben a következő: https://mvnrepository.com/artifact/junit/junit.
  • A Maven jelez, hogy ennek a komponensnek van egy újabb verziója, az 5-ös sorozat, Jupiter néven. De mi most ragaszkodjunk a 4-eshez, és azon belül is ne a legfrissebbet, hanem a 4.12-est töltsük le. Láthatjuk azt is, hogy ez a legnépszerűbb verzió, kattintsunk a verziószámra.
  • Láthatjuk, hogy különböző build rendszerek esetén hogyan kell rá hivatkozni; mi most a fenti jar linkre kattintva töltsük le.

Ha végképp elvesztünk volna, itt a közvetlen link: https://repo1.maven.org/maven2/junit/junit/4.12/junit-4.12.jar.

Artifactory

TODO

Nexus

TODO

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License