Java Parancssor Maven-nel
v 1.0
Java Parancssor Maven-nel Dátum: 2008-02-11 Szerző: Verhás Péter Verzió: 1.1 Dokumentum azonosító: 1.3.6.1.4.1.13923.0.38494382 Olvasók: programozók, Java fejlesztők Szint: kezdő, közepes
Ez a dokumentum a Verhás & Verhás Szoftver Manufaktúra Kft. által elektronikusan kiadott nyilvános dokumentum. A dokumentumot a http://www.gnu.org/licenses/fdl.txt szabályai szerint szabad felhasználni. A cikk ismeretterjesztő jellegű, marketing célú, de nem “hagyományos marketing” cikk. Két célja van. Az egyik, hogy műszaki ismereteket terjesszen olvasmányos, és könnyen érthető formátumban. A másik, hogy a Verhás & Verhás Szoftver Manufaktúra marketingjét támogassa. A cikkben leírt eset, megoldás nem feltétlenül tartalmaz olyan ismeretet, amely máshol nem érhető el. Nem oldja meg a cikk a világegyenletet, és nem feltétlenül javasol olyan megoldásokat, amit eddig még senki sem talált ki, de lehet, hogy az olvasó számára mégis újat tud mondani. A cikk PDF formátumban elérhető a http://www.verhas.com honlapról a CIKKEK rovat alatt.
1. Bevezető Az első program, amit Java tanulás során megírtunk parancssori volt. Egy egyszerű “Hello World”. Nincs ennél egyszerűbb. Aztán megtanultunk servlet-et programozni, elkezdünk keretrendszereket használni, EJBt. És valamikor eljöhet az a pillanat, amikor újra egy parancssorból indítható programot szeretnénk írni, de most már nem csak egy egyszerű “Szia világ” kiíratása a dolog, hanem sokkal összetettebb. Nem is mi fogjuk futtatni a programot, hanem az ügyfél (jó esetben rendszer menedzser). Ekkor már az eredeti egy darab class, vagy jar fájl már nem egy darabból áll, a futáshoz mindenféle könyvtárak, egyéb jar-ok kellenek, amit mind fel kell installálni az ügyfél gépére, gondoskodni, hogy mind rajta legyenek a classpathon stb. Milyen kellemes lenne egy Windows-on megszokott setup, exe, klikk, klikk installáció! Akár ezt is el lehet érni, erre is vannak eszközök, de ennyire ne legyünk eretnekek. Elég lenne az is, ha az összes szükséges class bekerülne egy jar fájlba. Ekkor különösebb erőlködés nélkül lehetne írni egy shell szkriptet a program köré a program egyszerű futtathatóságáért, ami akár még Windows alatt is elindul Cygwin környezetben. Ebben a cikkben két témáról lesz szó. Az első, hogy hogyan lehet az összepakolt JAR-t elkészíteni Maven1 build eszközzel. A másik, hogy hogyan érdemes olyan bash scriptet írni a Java program futtatására, amelyik mind Linux, mind pedig Windows alatt futtatható.
2. A szituáció A rendszer amit fejlesztettünk egy olyan servlet-eket tartalmazó alkalmazás volt, amelyikben az authentikációt, authorizációt, és a program navigációs struktúráját is a perzisztencia réteg szabályozta. Adatbázisban voltak a jelszavak, és a menü rendszer is. Ez azért lett így kialakítva, hogy könnyen testre szabható legyen, és könnyű legyen a rendszert menteni: csak az adatbázist kellett rendszereszközökkel backup-olni. A fejlesztés során azonban eljutottunk egy olyan pontig, hogy az alkalmazás már el sem indult, ha nem voltak konzisztens adatok az adatbázisban. Például legalább az adminisztrátor username/password meg kellett, hogy legyen, de ennél több is. Ha ezek nem voltak meg, akkor a servlet-ek csak hibajelzéseket adtak. A tesztelés során volt egy minta adatbázis, de mégsem tartottam szerencsésnek, hogy adatbázis backup és restore legyen az installáció első lépése. Úgy döntöttem, hogy készítek néhány egyszerű parancssori Java 1 Maven alatt a maven.apache.org-n található 2.x verziót értjük. Verhás & Verhás Szoftver Manufaktúra Kft.
1/6
Java Parancssor Maven-nel
v 1.0
osztályt, jó kis hagyományos public static void main metódusokkal, amelyek az authentikációs és authorizációs rétegek alá nyúlnak és lehetőséget adnak az üres adatbázis feltöltésére. De nem akartam a perzisztencia réteg API-jait hívni (SQL), hanem az alkalmazás szintű, de az authentikációs és authorizáció alatti réteget akartam használni. Így például megvalósítottam az 'AddUser' osztályt anélkül, hogy a felhasználói neveket és jelszavakat direktbe írtam volna bele az adatbázisba. Ha a későbbi verziókban más perzisztencia lesz a program alatt, vagy csak a tárolási formátum, például a jelszó kódolása változik meg, a parancssori programom akkor is működni fog. Ennek a megközelítésnek viszont az volt a következménye, hogy a parancssori programhoz kellett az összes JAR, amit a program használt. Milyen egyszerű volt a helyzet a servlet esetében! A Maven minden JAR-t belepakolt a WAR fájlba a Tomcat meg szépen mindet kicsomagolta magának, és ami kellett elérte. A parancssori program viszont nem WAR, hanem JAR csomagból fut, és abba (hacsak nem akarok másik osztálybetöltőt használni, lásd az utolsó fejezetben), nem pakolhatom bele a könyvtárak JAR fájljait. (Illetve belepakolhatom, de az osztálybetöltő nem fogja megtalálni.) Ehhez ki kellene bontani az összes könyvtár JAR fájlját, és az összes class fájlt be kell pakolni a megfelelő könyvtárakba a JAR fájlba. Milyen jó, hogy a Maven ezt tudja!
3. Maven a build eszköz A Maven egy olyan build eszköz, amit nagyon nem szeretnek az ant használók. (Ez a tapasztalatom.) Ezért aztán nem is ismerik, és mert nem ismerik, hát nem is szeretik. Mégis terjed. A Maven-ről szóló könyv, a “Better Builds with Maven” (pdf-ben ingyen letölthető legálisan, tessék ráguglizni) is hosszan elmélkedik a Maven build filozófiáról, ahelyett, hogy lapos tanulási görbével azt mondaná: Eddig ANT-tal így csináltad, mostantól Maven-nel így csináld! Most, hogy ezt leírtam, jöttem rá, hogy talán ez a Maven legnagyobb baja: lassan indul a tanulási görbéje, relatíve sokat kell olvasni, mire el lehet kezdeni dolgozni vele. De hát Java programozóknak szól! Értelmes okos embereknek! A Java tanulási görbéje is laposabban indul, mint például a VisualBasic-é! És mennyivel komolyabb, mint a második! Mennyivel messzebbre lehet eljutni vele! Ez általában igaz minden lényegesen új eszközre. Ha meredek lehetne a Maven tanulási görbéje, akkor csak egy új ANT lenne. De nem az. A Maven számára nem írunk le build parancsokat, mert azokat tudja. Minek újra és újra leírni, hogy java fájlokból a javac-cal lesz class fájl. Azt sem feltétlenül kell mindig leírni, hogy a projektben hol vannak a java fájlok. Legyen mindig ugyan ott. Legyen ugyanolyan a struktúra, és akkor csak azokat a részleteket kell definiálni, amik minden egyes projektben eltérőek. Például, hogy milyen külső könyvtárak kellenek, mit kell előállítani (például JAR, EAR), mi a projekt neve, milyen más projektek kimenetét használja stb. Ezt definiálja a 'pom.xml', ami a projekt könyvtárszerkezet gyökerében van. Még a 'pom.xml'-t sem kell magunknak előállítani, a Maven mvn archetype:create -DgroupId=com.mycompany.app -DartifactId=my-app paranccsal létre is hoz egy minta POM-ot, ami így néz ki: <project> <modelVersion>4.0.0
com.mycompany.app <artifactId>my-app <packaging>jar
1.0-SNAPSHOT Maven Quick Start Archetype http://maven.apache.org <dependencies> <dependency>
junit <artifactId>junit
Verhás & Verhás Szoftver Manufaktúra Kft.
2/6
Java Parancssor Maven-nel
v 1.0
3.8.1 <scope>test Ezzel a POM fájllal a Maven egy JAR-t fog a forrásainkból előállítani (ez a leggyakoribb). Ezt a fájlt már meg lehet nyitni Eclipse-szel, vagy éppen más fejlesztői környezet szerkesztő programjával, és mint XML fájlt könnyen lehet szerkeszteni. A függőségek között alaphelyzetben csak a JUNIT megfelelő verziója szerepel, mert tesztelni természetesen minden programot kell, de ide lehet és kell utána a többi függőséget beírni. A Maven egyik nagyon kellemes tulajdonsága (és ez már összefügg a témánkkal), hogy nem kell minden JAR fájlt összeszedni és a projektbe rakni egy lib könyvtárba. Ezeket a fájlokat a Maven magától összeszedi. Persze ehhez az kell, hogy a JAR könyvtárak rendelkezésre álljanak a helyi, vagy a központi Maven lerakatban (repository). Ha viszont ott megvan, például a log4j 1.2.14 verziója, akkor azzal már nem kell törődni, hogy az még milyen más JAR-okat kíván meg: ezeket a Maven automatikusan mind összeszedi, és a build folyamat során a fordításhoz felhasználja. Ha egy másik projektünk ezután erre a JAR-ra hivatkozik, például: <dependency>
com.mycompany.app <artifactId>my-app
1.0-SNAPSHOT sorok szerepelnek a POM fájlban, és a csomagolás WAR, akkor a Maven nem csak a my-app-1.0SNAPSHOT.jar fájlt fogja a WAR fájlba csomagolni, hanem minden olyan JAR-t is, amelyekre ennek a csomagnak szüksége van. Ez mind nagyon szép és dicséretes, de nekünk most nem WAR kell, hanem JAR és nem a JAR állományokat kell belerakni, hanem az összes class fájlt, ami ezekben a JAR fájlokban van.
4. Maven assembly A Maven programhoz nagyon sokféle plugin áll rendelkezésre. Az egyik ilyen plugin az assembly plugin. Ennek használatához jelezni kell a POM fájlban, hogy a build során ezt használni akarjuk, hiszen a Maven nem fogja a világ összes pluginját telepíteni, csak azt amire szükségünk lesz, és amit ilyen módon a POM fájlban jelzünk is.
org.apache.maven.plugins <artifactId>maven-assembly-plugin <descriptorRefs> <descriptorRef>jar-with-dependencies A fenti sorokat kell elhelyezni a POM fájlban, majd ki kell adni a
Verhás & Verhás Szoftver Manufaktúra Kft.
3/6
Java Parancssor Maven-nel
v 1.0
mvn assembly:assembly parancsot. Ez a target könyvtárban a 'my-app-1.0-SNAPSHOT-jar-with-dependencies.jar' fájlt hozza létre, benne az összes szükséges JAR összes class fájljával, mindet összemásolva. Érdemes idáig elolvasva ezt a cikket ki is próbálni ezt az eljárást.
5. Java futtatás Bash-ből Windows-on Ez a következő feladat, amit meg kell oldani. A fejlesztés során tesztelni kellett az alkalmazást, és kényelmetlen volt minden egyes alkalommal beírni a java -cp my-app-1.0-SNAPSHOT-jar-with-dependencies.jar com.verhas.examples.AddUser C:\Program%20Files\Apache\Tomcat%205.5\webapps\ my-app-1.0-SNAPSHOT/WEB-INF/classes/repository.xml “C:\Program Files\Apache\Tomcat 5.5\bin\target\jackrabbit” username password parancsot. Ez a parancssor először bekerült egy BAT fájlba a Windows fejlesztő környezeten. Ez viszont használhatatlan volt Linux-on, arról nem is beszélve, hogy Windows-on sem volt túl kényelmes. Lehetett volna kényelmesebb BAT fájlt készíteni (meg kellene tanulni a szabályokat), de azzal továbbra is semmi esélyünk Linux alatt. Viszont ha bash szkriptet írunk, az futtatható Windows és Linux alatt is, csak Windows-ra telepíteni kell a Cygwin környezetet, ami amúgy sem árt, ha már egyszer valamiért Windows került arra a szerencsétlen gépre. A fenti parancssorból egyébként, aki odafigyelt láthatja, hogy egy kicsit csaltam a cikk eddigi részében: a perzisztencia réteg a konkrét megvalósításban nem adatbázis, hanem Java Content Repository (JCR) alapú volt. Ehhez meg kell adni minden egyes futás számára a JCR-t leíró XML fájt URL-lel, és a repository helyét path-szal. Az első, ami azonnal kényelmessé válik Cygwin alatt, hogy pár paranccsal létre lehet hozni szimbólikus linkeket mindenféle könyvtárakra. Innen kezdve a projekt könyvtárba belépve a hosszú Windows path helyett a /opt/tomcat path-on keresztül érhető el a Tomcat könyvtára, /opt/j5SE a JAVA_HOME. Így könnyű lesz Linux-on futtatni ugyanazt a szkriptet. Ezek után nézzük meg magát a szkriptet! Az első rész aránylag egyszerű, csak különböző bash változók beállítása. Ezeket érdemes a program elejére kiemelni, hogy ha valamelyik installáció során változtatni kell valamelyik paraméteren, akkor ne kelljen szkript közepében keresgélni. #! /bin/bash # set where your java implementation is JAVA_HOME="/opt/j5SE" # the options JAVA_OPTS=" -Xmx1024M" # where all the JAR files are that are needed to run the command LIBDIR=./target # the repository configuration XML file REPOSITORY_XML_FILE=/opt/tomcat/webapps/my-app-1.0-SNAPSHOT/WEBINF/classes/repository.xml # the repository home REPOSITORY_HOME=/opt/tomcat/bin/target/jackrabbit # the transport and address to start Java in debug mode JPDA_TRANSPORT=dt_socket JPDA_ADDRESS=8000 # extra options that are used only in debug mode DEBUG_OPTS= Ezt követően van néhány olyan sor, amelyik a csak Linux-ra tervezett szkriptekben nem található meg. Át
Verhás & Verhás Szoftver Manufaktúra Kft.
4/6
Java Parancssor Maven-nel
v 1.0
kell konvertálni a Cygwin path értékeket a Windows path értékekre. A bash interpreter ugyan kiválóan látja a Cygwin rendszeren keresztül a unix szerű path-szal megadott fájlokat, de az ebből elindított Java (vagy bármilyen más program) sima Windows környezetben fut, és nem látja a /opt/tomcat, /opt/J5SE és hasonló könyvtárakat. Szerencsére erre a Cygwin rendszernek van egy beépített programja, a cygpath. Mivel a Windows-os path nevek előszeretettel tartalmaznak szóközt, ezért az URL konverzió során ezeket ki kell cserélni '%20' karakter sorozatra. Ezt is itt végezzük el. # convert file names to Windows file names if this is cygwin if [ $OSTYPE == "cygwin" ]; then REPOSITORY_XML_FILE=`cygpath -m $REPOSITORY_XML_FILE` REPOSITORY_XML_FILE=` echo $REPOSITORY_XML_FILE | sed 's/ /%20/g' ` REPOSITORY_HOME=`cygpath -m $REPOSITORY_HOME` fi # repository configuration URI REPOSITORY_URI="file:///$REPOSITORY_XML_FILE" Elég sokat szenvedtem a szóközök idézőjelek közé szorításával a parancssori argumentumoknál, végül egy huszárvágással úgy döntöttem, hogy a Java program ezt a két változót inkább kapja meg környezeti változóként. Ehhez két EXPORT utasítás kell, hogy a script által indított program környezeti változói közé is bekerüljön ez a két változó. export REPOSITORY_URI export REPOSITORY_HOME Később az is kiderült, igen szerencsés volt áttérni a környezeti változók használatára ebben az esetben, mert így nem csak a statikus main argumentumaként voltak elérhetőek ezek a paraméterek, hanem más osztályok metódusaiban és konstruktorából is, így egyszerűbb volt ezeket megírni, nem kellett végig átadni mindenkinek a main(args) argumentumát. Jobban belegondolva a repository konfigurációs fájljának az URL-je és a repository URL valóban “környezeti változók” és nem program parancssori paraméterek. A programot fejlesztés során gyakran futtattam debug módban, és mivel a fejlesztő környezetben (Eclipse) nem mindig viselkedett pont úgy, mint parancssorból, ezért hasznos volt a '--debug' opciót kifejleszteni a bash szkriptben. Ezekkel az opciókkal az Eclipse-ből, vagy éppen más fejlesztői környezetből debuggolható remote application-ként az alkalmazás. # if the first argument is --debug than we start in debug mode if [ "$1" == "--debug" ]; then DEBUG_OPTS="$DEBUG_OPTS -Xdebug -Xrunjdwp:transport=\ $JPDA_TRANSPORT,address=$JPDA_ADDRESS,server=y,suspend=y" shift fi Ezt követi a CLASSPATH összerakása. Ugyan az eddigi részben pont arról volt szó, hogy hogyan lehet mindent összerakni egy JAR-ba, de a demonstráció kedvéért most mégis egy rövid script darabot annak szentelünk, hogy a CLASSPATH-ba minden olyan JAR bekerüljön, ami a $LIBDIR könyvtárban van. A CLASSPATH bash változót egy rövid ciklussal építjük fel. CLASSPATH="" SEP="" for i in `find $LIBDIR -name \*.jar` ; do CLASSPATH="$CLASSPATH$SEP$i" SEP=":" done Végül a program indítása az összes összeszedett paraméterrel átadva a maradék parancssori paramétert már igen egyszerű:
Verhás & Verhás Szoftver Manufaktúra Kft.
5/6
Java Parancssor Maven-nel
v 1.0
PROGRAM=$1 shift $JAVA_HOME/bin/java $JAVA_OPTS $DEBUG_OPTS -cp $CLASSPATH $PROGRAM $* A programot többször használtam Windows alatt, Linux-on még nem lett kipróbálva, de valószínűleg nem sok módosítás kell, ha egyáltalán, a Linux alatti futtatáshoz. Összefoglalva: a bash szkripttel, és Windows-on a cigwin a következő előnyökhöz jutottunk: ●
ugyanaz a futtató script Windows alatt és unix-on
●
rövid unix szerű fájlnevek a szimbólikus linkekkel
●
környezeti változók egyszerű állíthatósága (karbantarthatóság) a szkriptben
●
fájl URL konverzió, akár szóközt tartalmazó fájlnevek esetére is
●
indítható a program normál és debug módban
●
ha szükséges CLASSPATH összeállítás egy egyszerű ciklussal.
6. Egy másik megoldás a csomagolásra Nem csak a Maven az egyetlen olyan program amivel megoldható a class-ok összepakolása. Nemrég egy másik probléma során bukkantam rá a one-jar projektre a SourceForge-on. Arra a feladatra (talán majd egy másik cikkben) nem volt alkalmas, viszont az egy JAR-ban való összes szükséges class elhelyezésére egy alternatív lehetőség. Ehhez ez a program egy MANIFEST.MF fájlt vár a JAR fájlban, amiben a 'Main-Class' a one-jar saját indító programjára mutat, és a 'One-Jar-Main-Class' mutat a saját programunk main osztályára, amelyiket a 'main/main.jar'-ban kell elhelyeznünk a jar fájlon belül. A betöltés során, amikor a 'java -jar packedjar.jar' programot elindítjuk a one-jar main indul el, és egy saját class loaderrel tölti be a “valódi” programot. Ez a class loader ezután képes a JAR fájlba pakolt JAR fájlokat betölteni. (Néhány részletet kihagytam, mint classpath definiálás a manifest fájlban stb. Akit érdekel keresse meg a programot és olvassa el a dokumentációt.) Ez a megoldás saját class loader-t használ, saját program betöltőt, és a jelenlegi verzió nem képes másik main-t indítani. Tehát csak azt az egy main-t lehet elindítani, amelyik a manifeszt fájlban le van írva. Ha másik osztályt akarunk indítani, akkor azt a parancssorban megadva nem “indul el” a saját osztálybetöltő, és így nem is találja meg a JAR fájlba pakolt JAR fájlokban levő osztályokat. Ebből a szempontból a Maven megoldás általánosabb, és egyszerűbb. Ugyanakkor nagyon könnyen lehet írni egy olyan osztályt, amelyik a public static main metódusa az args első eleme alapján dönti el, hogy melyik másik osztályt hívja meg, és ezzel ezt a limitációt már át is léptük. Ha pedig olyan JAR fájlokat használunk, amelyeket a készítőik aláírtak, és erre az aláírásra valamiért (pl. hitelesség) szükség is van a futtatás során, akkor a one-jar egyértelműen győzött a Maven összepakolással szemben.
7. Összefoglalás Ebben a cikkben leírtam, hogy egy konkrét feladat kapcsán felmerült problémát hogyan oldottunk meg a Verhás & Verhás Szoftver Manufaktúránál. A Maven build eszköz egy speciális használati módját mutattam be, valamint azt, hogy hogyan kell Windows alatt olyan Java futtató bash szkriptet írni, amelyik megkönnyíti a parancssoros programfuttatást. Megnéztünk egy alternatív módszert is egybecsomagolt JAR készítésére. __END__
Verhás & Verhás Szoftver Manufaktúra Kft.
6/6