Platformfüggő fejlesztés Java-ban

Ritka eset, hogy szükségünk van rá, de velem megesett. Szükségem volt a java-t futtató rendszer típusára és architektúrájára. Ráadásul futásidőben. Az ok egyszerű. SWT alkalmazást fejlesztek, és nem akarok minden platformra külön kiadást csinálni, ne a felhasználó találja ki, hogy milyen rendszert futtat. De ez mellékes, a probléma adott, futásidőben kell meghatározni, melyik platformfüggő csomagot töltsem be?

Ritka eset, hogy szükségünk van rá, de velem megesett. Szükségem volt a java-t futtató rendszer típusára és architektúrájára. Ráadásul futásidőben. Az ok egyszerű. SWT alkalmazást fejlesztek, és nem akarok minden platformra külön kiadást csinálni, ne a felhasználó találja ki, hogy milyen rendszert futtat. De ez mellékes, a probléma adott, futásidőben kell meghatározni, melyik platformfüggő csomagot töltsem be?

Egyszerű a megoldás – írják a lelkes és segítőkész, ám nem teljesen tájékozott fórumozók – A System.getProperty() paranccsal le lehet kérni a szabványosított, így minden JVM-ben létező “os.name”, “os.version” és “os.arch” tulajdonságokat, amikkel egyszerű beazonosítani a futtató rendszert. A szabvány valóban rögziti, hogy milyen tulajdonságmezőknek kell lenniük, azonban ezeknek a lehetséges értékeit nem. Most jön csak a szenvedés.

A kisebb problémát az “os.name” mező jelenti, ami többé-kevésbé konzisztens értékeket ad vissza a Sun és az alternatívEzen a ponton megjegyezném, hogy csak abból az egyszerű tényből kifolyólag hívom a nem a Sun által készített JVM-eket alternatívoknak, mert a Java nyelv szabványát a Sun saját szabványügyi hivatala adja ki. Az igazsághoz hozzátartozik, hogy az említett hivatal úgy jött létre, hogy az ISO visszadobta a Java szabványt, és a Sun ezt a megoldást találta ki a nyelv szabványosítására. JVM-eknél is. Ezek közül egyetlen kakukktojás a Windows (mi más), aminek minden verziójához tartozik egy saját “os.name” érték, és ezen felül az “os.version” is tartalmazza a rendszer ritkábban látott verziószámát. Hogy ezzel mi a gond? Csupán az, hogy az 1.5-ös JVM a Vista-t “Windows NT (Unknown)”-nak hívja, míg az 1.6-os rendesen “Windows Vista”-nak. Értelmesebb lett volna, ha a “Mac OS” / “Mac OS X”-hez hasonlóan külön jelzik a rendszer két fő ágát, és a verziószám ad bővebb információt a rendszer mibenlétéről. Más rendszerek (pl. Linux) esetén a helyzet jobb, ott tényleg nincs eltérés a verziók között. Íme egy rövid felsorolás az “os.name” lehetséges értékeiről: [[http://mindprod.com/jgloss/properties.html#OSNAME]]

Ennél sokkal komolyabb fejtörést okoz a rendszer architektúrájának meghatározása. Ennél rendkívül változatos értékeket tapasztaltam különböző gépeken, rendszertől függetlenül. Még a Sun JVM-ek se konzisztensek ebben. Az még nem túlzottan meglepő, hogy a 64 bites, intel-alapú rendszeremet “amd64”-nek hívja, hiszen a disztróm hivatalosan amd64-re van fordítva. Más gépeken viszont szinte véletlenszerűen kerül elő a fent említett megnevezés mellett “x86_64”, illetve “x64” is. További érdekesség, hogy mivel “Mac OS X”-en még nincs 64 bites 1.6os Java, így azt “i386”-osnak jelzi. Ebből látszik, hogy a kérdéses mező nem a rendszer, hanem a JVM tulajdonságát adja vissza. Ennél a pontnál elérkeztünk a régebbi, 32 bites rendszerekhez, ami lehet “x86”, “i386”, “i686”, és hasonszőrű társai bármelyike. Utolsóként megemlíteném a Power PC architektúrát, ami lehet “ppc”, vagy “PowerPC” elnevezésű is.

A fenti mezők értékeire vonatkozólag láthatóan a Sun berkein belül sincs megegyezés, különböző rendszereken, vagy akár csak különböző verziók más-más értéket adhatnak. A legnagyobb probléma mégis az, hogy ez nincs sehol dokumentálva! A néhány kicsit használható gyűjteményt lelkes felhasználók hozták össze, de ezek hiányosak, és/vagy régen frissültek. Íme az általam talált legjobb, ami határozottan kevés: [[http://lopica.sourceforge.net/os.html]].

Végső megoldás lehet kitaposott úton járni, azaz felhasználni egy már létező megoldást, ami talán megmondja, mégis milyen rendszeren vagyok. Hasonló megoldásokban szinte kizátólagosan az [[http://lopica.sourceforge.net/os.html|Ant]] projekt kerül szóba, ami tartalmaz egy osztályt, mely egy szimpla enumerált értéket állít elő a létező operációs rendszereknek megfelelően. Az “os.arch” változót pedig csak nemes egyszerűséggel kivezeti a felhasználó számára az összes JVM tulajdonsággal egyetemben. Továbbá az Ant csak fordítás-idejű megoldást kínál, ezzel én nem lettem okosabb.

Végső elkeseredésemben azon kezdtem agyalni, hogy a program első futásakor végigpróbálgatom az összes rendelkezésre álló SWT lib-et, és amelyik nem dob kivételt, az működik. Ha ehhez hozzávesszük, hogy felismerhetünk pár “os.name”, “os.arch” értéket, akkor nem is olyan elvetemült ötlet.

(Update) A megoldás

[[http://cubussapiens.hu/user/30|Happy]] [[http://cubussapiens.hu/node/593#comment-117|hozzászólása]] alapján a zseniálisan egyszerű megoldás:


package proba;

import org.eclipse.core.runtime.internal.adaptor.EclipseEnvironmentInfo;

public class EnvInfo {

/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
EclipseEnvironmentInfo eei = EclipseEnvironmentInfo.getDefault();
System.out.println(eei.getWS()+"."+eei.getOS()+"."+eei.getOSArch());
System.out.println(eei.getNL());
}

}

, ami nálam a következő kimenetet adja:


linux.gtk.x86_64
hu_HU

Stampie jól gondolta, ezt a problémát már megoldották az Eclipse-ben, csak akkor én ezt nem találtam meg. Az [[http://mobius.inria.fr/eclipse-doc/org/eclipse/core/runtime/internal/adaptor/EclipseEnvironmentInfo.html|EclipseEnvironmentInfo]] osztály a org.eclipse.osgi_3.4.0.v20080605-1900.jar csomagban található meg, és ez az swt csomagok elnevezésével kompatiblis jelölésekkel adja meg a rendszer jellemzőit.

Java rendezés – avagy hogyan ne tegyük

A Java Collection könyvtára remek munka – könnyen használható, mégis sokoldalú API-t biztosít. Megfelelő listákhoz, gráfreprezentációkhoz és feltételezem, hogy még sok minden egyébhez.

Ehhez képest nem (sokkal) bonyolultabb kezelni, mint egy tömböt. Sőt, a C nyelv tömbkezeléséhez képest sokkal egyszerűbb is… 🙂

Ami nekem különösen tetszik, az a beépített rendezési lehetőségek: a Collections.sort statikus metódus használatával saját Comparator segítségével lehet rendezéseket definiálni.

A Java Collection könyvtára remek munka – könnyen használható, mégis sokoldalú API-t biztosít. Megfelelő listákhoz, gráfreprezentációkhoz és feltételezem, hogy még sok minden egyébhez.

Ehhez képest nem (sokkal) bonyolultabb kezelni, mint egy tömböt. Sőt, a C nyelv tömbkezeléséhez képest sokkal egyszerűbb is… 🙂

Ami nekem különösen tetszik, az a beépített rendezési lehetőségek: a Collections.sort statikus metódus használatával saját Comparator segítségével lehet rendezéseket definiálni.

Ezután szinte magától adódott már, hogy amikor nekem egy objektumhierarchia reprezentálására van szükségem, akkor ahhoz ezt a beépített könyvtári osztályokat használjam. Az általam használt algoritmus (most ezt nem részletezném a szükséges tömörség miatt) feltételezte, hogy a hierarchia elemeit felülről lefelé adom be (azaz mindig előbb az őst adom be, és csak utána a leszármazottakat). Miután ezt a bemenetemről nem tudtam feltételezni, ezért adódott a dolog, írjunk rá egy rendezést, saját komparátorral.

public int compare(IEntity i1, IEntity i2){
if (i1.isSubtypeOf(i2)) return -1;
else if (i2.isSubtypeOf(i1)) return 1;
else return i1.getFullyQualifiedName().compareTo(i2.getFullyQualifiedName());
}

Szép, olvasható kód, és pontosan azt írja le, amit szeretnék. Igaz? Hisz itt látszik, hogy szépen az öröklési hierarchia szerint rendezi a mezőket. Amikor ezt a kódot úgy nagyjából egy hónapja megírtam, akkor működött is – igaz, csak erőteljesen korlátozott tesztelés volt, mert ez csak egy nagyon parányi része volt a dolognak, amit csináltam, és utána pedig egy kicsit ezt a részt jegeltem.

Ehhez képest most jött a feketeleves, hogy amikor egy kicsit változott a környezet, amiben az új funkciót teszteltem, kaptam egy hatalmas nullpointer exception-t. Remek…

Némi játék után (ugyanis a futási környezet lenyelte a kivételt, és csak az üzetenet jelenítette meg, ami nullpointer exception-nél mindig “”, azaz nem látszik), kiderítettem, hogy a gondot az okozza, hogy egy objektum kapcsán próbálok hivatkozni az ősére egy saját adatszerkezetben, de ebbe az adatszerkezetbe nem került bele. Ez pedig csak úgy lehetséges, ha az eredeti rendezésnél valami miatt a leszármazott előbbre kerül, mint az őse (általában nem, de az én programomban igen).

Kíváncsi vagyok, hogy hány ember látja, hogy hol is van a probléma a fenti komparátorral, hogyan jöhet össze, hogy a beépített Java sort algoritmus is ezt a rossz eredményt adja ki…

Tökéletes magyarázatom nincs a problémára, de a hiba oka nagyjából két dolog lehet. Egyfelől a definiált komparátor csak egy részben rendezéshez ad megfelelő támpontot, másrészt a Java feltehetőleg a qsort algoritmust használja, ami nem végzi el az összehasonlítást az összes párra, így tipikusan nem hasonlít össze minden egyes elemet minden másik elemmel.

Van még egy tényező, ami a rendezési eljárást még kevésbé egyértelművé teszi, ez pedig az egymástól akár teljesen független ágak a hierarchiában. Az eredeti példáimban ezekből kevés volt, míg a példa, amin kiakadt a program, viszonylag sok, egymástól független ágat tartalmazott, ezzel jócskán megnehezítve az algoritmus munkáját.

Ötletem nem volt, hogyan kellene működővé varázsolni a rendezést egy átírt komparátorral, ezért más, korábbi tanulmányokhoz nyúltam vissza: a bsz-en/algoritmuselméleten bemutatott topologikus rendezéshez nyúltam vissza. Ugyan az jóval erősebb rendezési feltétel, mint amire nekem konkrétan szükségem volna, de ezért cserébe viszonylag kevés munkával lekódolható, és nem is túlságosan lassú (jó, ez persze relatív).

Persze ennek a kódolásakor is segítségemre volt a Java Collections API, még így is kevesebb, mint 20 sor volt a teljes rendezési eljárás, amit még zárásnak beszúrok az írás végé elé.

/**
* Creates a topological ordering on the modelelements
*/
private List orderModelElements(List elementsToOrdered){

List orderedElements = new ArrayList();
while (!elementsToOrdered.isEmpty()){
//The currentLevel is needed in order to remove all the elements that
//do not have any ancestors, not only the first one
//If we delete the elements straight from the elementsToOrdered list,
//the iterator in the for cycle will not work.
List currentLevel = new ArrayList();
for (IEntity element : elementsToOrdered){
Collection ancestors = new ArrayList();
ancestors.addAll(element.getSupertypes());
ancestors.retainAll(entities);//remove all non-entities
if (ancestors!=null && orderedElements.containsAll(ancestors)){
currentLevel.add(element);
}
}
orderedElements.addAll(currentLevel);
elementsToOrdered.removeAll(currentLevel);
}
return orderedElements;
}

És hogy miért is írtam ezt így ide meg? Ennek két oka is van: egyrészt egy nagyon önző, magamat szem előtt tartó indok: ha valaki el tudja esetleg mondani nekem, hogyan kellene a komparátort kijavítani, esetleg részletesebben elmagyarázza csak, hogy miért is hasal el az a komparátor a quick sorton (ha quick sort), nagy örömmel veszem akármilyen formában a reakcióját. Másfelől pedig okulásnak is akart menni mások számára, hogy esetleg nekik ne kelljen órákat bogarászniuk egy pár ezer soros kódbázisban, hogy pontosan hogyan is adódhat az a nullpointerexception.

Java osztályok tárolása MySQL adatbázisban

A cím talán megtévesztő lehet, most nem arról van szó, hogy hogyan lehet szérializált osztálypéldányokat tárolni adatbázisban. Ehelyett magukat a Java osztályokat (típusokat) tárolom ilyen módon. Ez megtehető, hiszen a Java futásidőben tölti be az osztályokat, egészen pontosan az osztály első aktív használata elött. A továbbiakban a cikk feltételezi a Java és a MySQL alapszintű ismeretét, és a [[http://java.sun.com/javase/technologies/database/|JDBC]] használatát se fogom részletezni. Kezdetnek essen pár szó a Java osztálybetöltő mechanizmusáról.

A cím talán megtévesztő lehet, most nem arról van szó, hogy hogyan lehet szérializált osztálypéldányokat tárolni adatbázisban. Ehelyett magukat a Java osztályokat (típusokat) tárolom ilyen módon. Ez megtehető, hiszen a Java futásidőben tölti be az osztályokat, egészen pontosan az osztály első aktív használata elött. A továbbiakban a cikk feltételezi a Java és a MySQL alapszintű ismeretét, és a [[http://java.sun.com/javase/technologies/database/|JDBC]] használatát se fogom részletezni. Kezdetnek essen pár szó a Java osztálybetöltő mechanizmusáról.

A Java ClassLoader osztály használható arra, hogy egy keresett osztályt betöltsön a memóriába. Minden JVM indulásakor létrejön egy alapértelmezett rendszer ClassLoader, és ha nem teszünk semmit ez marad végig az egyetlen ilyen jellegű objektum a JVM-ben. A ClassLoader-ek működésének megértéséhez meg kell ismernünk néhány metódusát:

  • A konstruktor: ClassLoader(ClassLoader parent) : Minden ClassLoader-nek létezik pontosan egy szűlője (kivéve az alapértelmezett ClassLoader-t), ugyanis mielött az adott ClassLoader megpróbálná betölteni a keresett osztályt, megkérdezi a szülőt, hogy ő be tudja-e tölteni, és csak akkor próbálkozik meg, ha a még nincs betöltve, és a szülő sem tudja betölteni.
  • Class loadClass(String name) :ez hívódik meg, amikor felmerül az igény egy osztály betöltésére. Ez a metódus elöször meghívja a szülő ClassLoader hasonló metódusát, és ha az ClassNotFoundException-t dob, akkor meghívja ezen osztály findClass metódusát.
  • Class findClass(String name) :a fentiekből látszik, hogy ez a metódus hívódik meg akkor, ha már ránk hárult az osztály betöltésének a feladata. Ha új ClassLoader-t írunk, rendszerint ezt a függvényt akarjuk felülírni.
  • Class defineClass(String name, byte[] b, int off, int len) :ez a lényeg. Ez a függvény hozza létre az osztályt a memóriában a beolvasott bytecode-ból, aminek meg kell egyeznie egy “.class” fájl tartalmával.

Már látszik a turpisság: a fentiek alapján lehet írni egy ClassLoader-t, ami nem fájlból, hanem adatbázisból olvassa be a megfelelő bytesorozatot. Első lépésként írjuk meg az adatbázis réteget, ami a kérdéses bytecode-ot tudja menteni/kiolvasni az adatbázisból (az adatbázisba a bytecode-ot Base64 kódolásban mentem, amit egy segédosztály végez. Lásd: lent. Ugyanúgy nem térek ki a File beolvasására, annak a megoldásában is a Google segített):


package mysqlclasses;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;

public class ClassSaver {

public static byte[] getBytesFromFile(File file) throws IOException {
.....
}

public static void saveToDB(Connection con, String name, byte[] data){
try{
Statement s = con.createStatement();
s.execute("CREATE TABLE IF NOT EXISTS classes (name VARCHAR(120) NOT NULL,bytecode TEXT)");

String bytecode = new String(Base64Coder.encode(data));

ResultSet rs = s.executeQuery("SELECT * FROM classes WHERE name = '"+name+"'");
if (rs.next()){
//update
s.execute("UPDATE classes SET bytecode = '"+bytecode+"' WHERE name = '"+name+"'");
}else{
//insert
s.execute("INSERT INTO classes SET name = '"+name+"', bytecode = '"+bytecode+"'");
}
}catch(Exception e){
System.err.println(e.getMessage());
}
}

public static byte[] loadFromDB(Connection con, String name){
try{
Statement s = con.createStatement();
ResultSet rs = s.executeQuery("SELECT * FROM classes WHERE name = '"+name+"'");
if (rs.next()){
return Base64Coder.decode(rs.getString("bytecode"));
}else{
return null;
}
}catch(Exception e){
System.err.println(e.getMessage());
return null;
}

}
}

Ezek után következzék a MySQLClassLoader:


package mysqlclasses;

import java.sql.Connection;

/**
* @author balage
*
*/
public class MySQLClassLoader extends ClassLoader {

Connection con;

public MySQLClassLoader(Connection sqlcon) {
con = sqlcon;
}

public MySQLClassLoader(Connection sqlcon,ClassLoader arg0) {
super(arg0);
con = sqlcon;
}

@Override
protected Class< ?> findClass(String name) throws ClassNotFoundException{
System.out.println("Searching for class: "+name);
byte[] data = ClassSaver.loadFromDB(con, name);
if (data != null){
return defineClass(name, data, 0, data.length);
}else{
throw new ClassNotFoundException();
}
}

}

Meglepően egyszerű, nemde? A kipróbálásához létrhozunk pár egyszerű osztályt:

package test;

public interface Test {
public void some();
}


package test;

public class TestA implements Test {

@Override
public void some() {
System.out.println("I'm an A instance.");
}

}


package test;

public class TestB implements Test {

@Override
public void some() {
System.out.println("I'm a B instance.");
}

}

Az egyszerűbb tesztelés érdekében hoztam létre egy Interface-t is, ami fordítás időben ismert, így egyszerűen meghívható a some() metódus. A létrehozott osztályokkat most le kell fordítani, és be lehet írni az adatbázisba:


Class.forName("com.mysql.jdbc.Driver");

Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "balage", "");

Test a = new TestA();
a.some();
Test b = new TestB();
b.some();

ClassSaver.saveToDB(con, a.getClass().getName(), ClassSaver.getBytesFromFile(new File("bin/test/TestA.class")));
ClassSaver.saveToDB(con, b.getClass().getName(), ClassSaver.getBytesFromFile(new File("bin/test/TestB.class")));

Ezen a ponton érdemes ellenőrizni az adatbázis tartalmát, ahol jól láthatóan szerepel egy “classes” tábla, benne a két osztály bytecode-jával. Ezek után lehet kiovasni onnan őket a következő kóddal (Arra vigyázzunk, hogy a kiolvasandó osztályok NE legyenek elérhetőek a classpath-ban, különben az adatbázis helyett az alapértelmezett ClassLoader fogja őket betölteni!):


Class.forName("com.mysql.jdbc.Driver");

Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "balage", "");
MySQLClassLoader loader = new MySQLClassLoader(con);

Class< ?> clsa = loader.loadClass("test.TestA");
Test a = (Test)clsa.newInstance();

a.some();

Ha mindent jól csinálunk, a kimeneten a TestA osztály üdvözlőüzenete elött látni fogjuk a MySQLClassLoader-ben elrejtett kiírást is, ami egyértelműen jelzi, hogy valóban ő olvasta be az osztályt. Siker. Mielött még kikiáltanám a cikk végét, had tegyek pár megjegyzést, ami szükséges lehet a módszer komolyabb használatához:

Jól láthatóan szövegként hivatkozunk a betölteni kívánt test.TestA osztályra. Ekkor felmerül a kérdés, mi van azokkal az osztályokkal, amikre a betöltött test.TestA osztály hivatkozik? Például létrehoz egy test.TestB osztályt, ami ugyancsak az adatbázisból érhető el, és nem szerepel a megadott classpath-ban. A válasz egyszerű. Azokat akkor próbálja betölteni, amikor aktívan használni próbáljuk. Akkor az osztály nevét átadja az alapértelmezett ClassLoader-nek, ami megkeresi a classpath-ban és.. ClassNotFoundException. A probléma ott van, hogy arra már nem a mi MySQLClassLoader-ünket használja. A teendő tehát: alapértelmezetté kell tenni a létrehozott MySQLClassLoader-t, amit megtehetünk az aktuális Thread ismeretében:


MySQLClassLoader loader = new MySQLClassLoader(con);
Thread.currentThread().setContextClassLoader(loader);

Ezen sor után a futásidőben felmerült osztályhivatkozásokat már az általunk megadott ClassLoader-rel fogja betölteni. Kiemelném, hogy ekkor is létezik a MySQLClassLoader szűlője, ami prioritást élvez, azaz mindig az próbálja meg betölteni a keresett osztályt elsőként (persze csak akkor, ha nincs még betöltve).

Még egy probléma: az általunk létrehozot ClassLoader-t csak futásidőben tudjuk használni, a javac fordításkor csak a beépített ClassLoader-t tudja használni, azaz fordításkor minden hivatkozott osztálynak kéznél kell lennie, csak fordítás után lehet az egyes osztályokat bepakolni az adatbázisba. Ami után viszont az adatbázisból hivatkozott .class fájlok törölhetőek a classpath-ból. A programunkat indító osztálynak, és a MySQLClassLoader-nek viszont minden esetben elérhetőnek kell lennie a classpath-ban, ugyanis a java indulásakor csak az alapértelmezett ClassLoader áll rendelkezésünkre.

Végezetül pár hasznos link:

  • [[http://java.sun.com/docs/books/tutorial/ext/basics/load.html]] Egy részletesebb leírás a Java ClassLoader-ek működéséről
  • [[http://java.sun.com/j2se/1.4.2/docs/api/java/lang/Class.html]] A Class metaosztály javadoc-ja
  • [[http://java.sun.com/j2se/1.4.2/docs/api/java/lang/ClassLoader.html]] A ClassLoader osztály javadoc-ja
  • [[http://www.source-code.biz/snippets/java/2.htm]] A Base64 kódolást végző osztály
  • [[http://www.java-tips.org/java-se-tips/java.io/reading-a-file-into-a-byte-array.html]] Egy függvény, ami egy fájlból beolvassa a byte-okat tömbbe

J2EE és JUnit – avagy a platformfüggő Java

Végre, beadtuk. Az UML tárgyból esedékes házi feladatot (lásd még [[J2EE – a nagy aknamező|a múltkori írást]]). Ez J2EE egyszerűen valami félelmetes. Ez a technológia szép. De tényleg…

Az implementáció után mindössze teszteseteket kellett írni az utolsó beadásra. Mindössze. Persze ez akkor sem olyan egyszerű, hiszen azt JUnitban kellett megírni, ami alapvetően maga szeretné futtatni a teszteket. Semmi gond, akkor átverjük, feltöltjük a JUnitot is a konténerbe, és ott majd látni fogja a keresett dolgokat.

Végre, beadtuk. Az UML tárgyból esedékes házi feladatot (lásd még [[J2EE – a nagy aknamező|a múltkori írást]]). Ez J2EE egyszerűen valami félelmetes. Ez a technológia szép. De tényleg…

Az implementáció után mindössze teszteseteket kellett írni az utolsó beadásra. Mindössze. Persze ez akkor sem olyan egyszerű, hiszen azt JUnitban kellett megírni, ami alapvetően maga szeretné futtatni a teszteket. Semmi gond, akkor átverjük, feltöltjük a JUnitot is a konténerbe, és ott majd látni fogja a keresett dolgokat.

Hehe, már megint optimista vagyok ezekkel a cuccokkal kapcsolatban. Ez, hogy feltöltsük a JUnitot, ez körülbelül két óra szívást jelentett a classpath, build path, referenced projects, requirements és hasonló témában projektek és pluginek viszonylatában. Ezek tulajdonképpen mind-mind egyfajta függőséget jelentenek a projektek és más projektek, esetleg libek vagy hasonlók között, de az a poén az egészben, hogy a szerepük között jelentős átfedések vannak. És ami igazán kellemetlen az az, hogy ha a helyes megoldás mellett egy nem szükséges helyen megadod a függőséget, akkor már nem jó. Lehet vele szépen játszadozni.

Jó, túl vagyunk az alapozáson, megvan a teszt keretrendszer, kezdünk teszteseteket írni. Egyszer csak egyik kolléga megkeres, hogy nagyon eszement hibaüzenetet ad egy importra: nem azt, hogy nem találja, hanem azt, hogy nem engedélyezett a használata. Remek, újabb szép körnek néz ki. Ezúttal sajnos nem tévedtem. Némi játszadozás után értettem meg, hogy tényleg látja, csak valami miatt nem akarja használni. Közben szétvertem a tesztkörnyezetet (utána újból órákba kerül, amíg mindenkinél összeáll, de erről később). Na, ekkor némi Google, meg váltogatás a különböző Eclipse környezeteim között (bizony, nekem több is van, be-be-be 🙂 ), és kiderül, hogy a probléma egészen csúnya: az Eclipse támogat egy package-korlátozást (access rule-nak hívja) a különböző pluginekre. Ok, ez önmagában nem csúnya, hisz így az Eclipse-plugin írója rákényszerítheti a felhasználó plugint, hogy az api-n keresztül érje el. De ami gáz, hogy ezt alkalmazta a beépített Java típuskönyvtárra.

Ok, módosítsuk a dolgot. Némi segítség után rájövök, hogy melyik jarban van a keresett osztály. Adjunk hozzá egy engedélyező bejegyzést a jarhoz tartozó access rule-ok közé. Újabb pofára esés, és kiderül, hogy nem írható. Remek. További leírásböngészés, és ekkor kiderül, hogy az ősét, ha tudom szerkeszteni, az is jó. Na, azt tudom, mert a globálisat engedi szerkeszteni, csak a system jarokét nem, meg a workspace-ben definiált pluginekét sem (azokat a plugin.xml szerkesztőjén lehet hakkolni). Remek, beírom az osztálynevet com.sun…. formában, örülnék, hogy megy, de nem megy. Ok, nézzünk mintát, igaza van, /-rel kell írni, és ha *-gal zárom, akkor egy package-re lebontva tudom szerepeltetni. Ok, egy részét átírom /-esre (nem kéne azért a teljes com. tartományt engedélyezni), majd csillaggal lezárom. Még mindig nem jó. Aztán kiderült. Ahhoz, hogy rekurzívan elinduljon lefelé a csomaghierarchiában, ahhoz nem árt **-gal lezárni… Na, ezután már megy.

Illetve csak az a hivatkozás. Csak a libekkel szórakozás miatt elment a futtatókörnyezet. Sebaj, rutinos róka vagyok már, nekilátok és összerakom a megfelelő fájlt (jó, most csaltam, összeklikkeltem a run dialogban). Végre megy nálam. Ekkor töltsük fel svn-re, hogy a többieknek is menjen.

Eltelik pár perc, és kiderült, hogy nem ment át. Mint utóbb kiderült, verzió- és oprendszerkülönbségek miatt. Az OSX->Windows irányban nem működik. És fordítva sem. Az lett a vége, hogy a Windows-osok megcsinálták a környezetet, majd feltöltötték svn-re, ezután én is megcsináltam az enyémet, de én nem töltöttem fel. Erre ezután minden commitnál vigyázni kellett remek. Még szerencse, hogy a Java platformfüggetlen.

De ez semmi ahhoz képest, ami ma hajnali 5 magasságában derült ki, 6 órával a beadás előtt (ameddig persze be is kellett érni az egyetemre). Megírtam már 40 tesztesetet – ez majdnem ezer sor kód, ezek segítségével több hibát is kijavítottam a tesztelt kódrészben. De tényleg. Futott minden. Amikor a kolléga közli velem, hogy a teszteseteim egy része meghal, és ez hazavágja az egész adatbázist. Nálam persze semmi ilyesmi. Némi kísérletezés után kiderül, a gond az, hogy a Windows nem eszi meg a megoldást. Pont. Akkor mutassuk be Macen. De Macen meg egyes Windowson megírt tesztesetek nem futnak. 😕

A hibát nem sikerült megoldani… Mindenesetre kezdem azt hinni, hogy olyan, hogy platformfüggetlen nem létezik. A Java legalábbis biztosan nem az. Például az AbevJava máshogy megy Windows-on, mint Linuxon vagy OSX-en. És ez a mostani hiba is megerősíti ezt az elképzelést. Azt, hogy mi lehet a hibás, csak tippelni tudom. Van egy elszúrt (értsd: feleslegesen túlbonyolított) entitáshalmazunk, van egy toplinkünk mysql adatbázishoz csatlakoztatva, van alatta oprendszer, OSGi konténer, stb. stb. stb. Erre már csak Murphyt idézném zárógondolatként:

  1. A bonyolult rendszerek hajlamosak a bonyolult meghibásodásokra.
  2. Ezzel szemben az egyszerű rendszerek is hajlamosak a bonyolult meghibásodásokra.

J2EE – a nagy aknamező

Hát, megvan egy ideje, hogy utoljára írtam. Pedig magamban már többször megfogadtam, hogy rendszeresebben írok. Úgy volna értelme csinálni az egészet, hogy hetente legalább egy bejegyzés születik. Persze ez így hiú ábránd ennyi házi feladat mellett.

Ez az UML házi kész volt. 80 órát egy hét alatt beletolni abba, hogyan kell leimplementálni egy hello world-nél egy szinttel bonyolultabb feladatot J2EE alapon, na ez teljesítmény. És örülök, hogy ennyi idő alatt összeállt…

Hát, megvan egy ideje, hogy utoljára írtam. Pedig magamban már többször megfogadtam, hogy rendszeresebben írok. Úgy volna értelme csinálni az egészet, hogy hetente legalább egy bejegyzés születik. Persze ez így hiú ábránd ennyi házi feladat mellett.

Ez az UML házi kész volt. 80 órát egy hét alatt beletolni abba, hogyan kell leimplementálni egy hello world-nél egy szinttel bonyolultabb feladatot J2EE alapon, na ez teljesítmény. És örülök, hogy ennyi idő alatt összeállt…

Nem véletlen tartott eddig. Van benne egy gyönyörű implementált objektumrelációs leképezés, néhány tag elhelyezése, egy xml-fájl és még néhány sor kód megírása után az objektumokat szépen kimenti az adatbázisba, illetve visszaolvassa onnan igény szerint. Tényleg szép. Csak az ember el ne szúrja a tagelést, mert olyan exception-trace-t kap futáskor, hogy öröm nézni. A legnagyobb, amit sikerült kapni, 40 kB méretű volt. Iszonyatosan redundáns, gyakorlatilag az egész háromszor szerepel, az értelmes tartalom másfél-két sor. Szűrd ki…

A fejlesztés során mi RAP-t használtunk GUI-készítéshez. Nem gyenge technológia: Eclipse plugineket AJAX-szal támogatott weblapokra fordít. De azért ezt több projektből összefejleszteni szép teljesítmény. Például nem mindegy, hogy hol kapcsoljuk össze a projekteket. Ha az Eclipse projekt tulajdonságainál fogjuk, és beikszeljük a Referenced projects résznél (hogy szerepeljen a build path-ban), az nem jó. A plugin.xml fájlban kell a depencencies blokkban bejelölni.

Remek, most már lefordul, elindul, és gyönyörű exception trace megint. Most az a baja, hogy nem töltötte be a futás közben a hivatkozott projekteket. Némi szórakozás után kiderült, hogy a megoldás az, hogy még a futtatási konfigurációnál is fel kell venni a hivatkozott projekteket, mint bundle-t. Eredetileg ezt nyersen az xml-be hakkoltam bele, később hívta fel valaki a figyelmemet, hogy a futtatási konfigurációnál is fel lehet venni.

Hasonló szépségek vannak az APIban is. A RAP1.1M1 és az 1.1M3 között megváltozott az alkalmazás belépési pontjának szintaktikája: az M1-ben Display típusú, míg az M3-ban int típusú visszatérési értéket vár. Érdemes megnézni a sorrendet, Display-ből int.

Beletelt némi időbe, amíg rájöttem, hogyan kell megváltoztatni a mintakódot, hogy lefusson. Gyakorlatilag a Display egy azonosítóját kell visszaadni a metódus végén. Szóval minden érdekes.

Hasonlóan jópofa volt az, hogy amikor beraktuk a szükséges webservice meghívásához szükséges fájlokat, akkor kb 25-30 MB méretben kellett jar-fájlokat a lib könyvtárba tenni. Ami problémássá teszi a dolgot, hogy ez csoportmunkában készült házi feladat, azaz fel kellett tölteni egy svn szerverre, és a többieknek le kellett szedni. Hab a tortán, hogy az svn csak fájlok között frissíti a status bart, így a 10 megás jarnál úgy néz ki a dolog, mintha meghalt volna menet közben. A csúcs az volt, amikor ezt upload közben sikerült valakinek fagyásnak értelmeznie, és kilőtte az Eclipse-et. Ezután egy órába került, amíg sikerült rendbe tennie a rendszert…

Érdekes ez az Enterprise Java technológia. Ekkora aknamezőt még nem láttam… Elég valami apró hiba, és hihetetlen mennyiségű hiba bukkan fel, és a hibaüzenetekből is alig lehet kikövetkeztetni, hogy mi lehetett az ok. Elég bonyolult architektúra, rengeteg 3rd party lib, és nagyon korlátozott tapasztalat: ez elég ahhoz, hogy a nem túl bonyolult feladat megoldását nagyon megnehezítse.