2016. szeptember 30., péntek
Üzenet hitelesítése Java és Go szervizek között
Kezdjük a fogadó féllel. A Go nyelv dokumentációját olvasva hamar ráakadhatunk, hogy létezik beépített crypto/rsa csomag. Nem bővelkedik a lehetőségekben, ugyanis csak PKCS#1-et támogat. Remélem nem spoiler, de a Go lesz a szűk keresztmetszet választható sztenderdek közül. Létezik persze külső csomag pl. PKCS#8 támogatással, de mi a biztonsági kockázatát kisebbnek ítéltük a beépített bár gyengébb eljárásnak, mint a külső kevesek által auditált megoldásnak. A crypto/rsa csomagnál maradva az egyetlen lehetőségünk, hogy PSS (Probabilistic signature scheme) aláírásokat hitelesítsünk a VerifyPSS metódussal. Szóval nincs más dolgunk mint az RSA kulcspár publikus részét eljuttatni a virtuális gépre, és már mehet is a hitelesítés.
Küldés során a kérés teljes törzsét írtuk alá, így nincs más dolgunk mint a kérésből kibányászni a törzset és ellenőrizni a hitelességét.
Valamint implementálni és regisztrálni a kérés feldolgozót.
Természetesen tesztet is írtam az aláírás ellenőrzésére.
Miután megvagyunk a hitelesítéssel jöhet az aláírás Java oldalon. Kutattam egy darabig hogyan lehet PSS aláírást Java SE-vel generálni, de mivel a projektünknek már része volt a Bouncy Castle Crypto API, így kézenfekvő volt, hogy azt használjam fel.
A Java oldali kulcspár generálással tele van az internet, azzal nem untatnák senkit.
2015. június 5., péntek
Konténerezett Hadoop és Cassandra cluster konfigurálása - harmadik rész
Előkészítés
Első dolgunk, hogy meglévő projektünket frissítjük a megfelelő verzióra, és építsük újra a konténereket:cd docker-cassandra && git checkout 2.6.0-cassandra && git pull origin 2.6.0-cassandra cd ../hadoop-docker && git checkout 2.6.0-cassandra && git pull origin 2.6.0-cassandra cd .. && git checkout 2.6.0-cassandra && git pull origin 2.6.0-cassandraÉlesszük fel a gépeket:
vagrant halt && vagrant upMajd a futó Vagrantos környezetben építsük újra a konténereket:
vagrant provision master slave1 slave2 slave3Lépjünk be a virtuális gépekre, és töröljünk minden futó konténert:
docker rm -f $(docker ps -qa)Ha mindezzel végeztünk, hozzuk létre ismét a Weave hálózatot és a Swarm clustert, a Hadoop konténereket egyelőre hagyjuk parlagon.
Változások
Csökkentettem a master nevű gép memória igényét, mivel a továbbiakban csak mesterként fog szolgálni a benne futó Hadoop konténer, és ezzel egy időben növeltem a slaveX nevű gépek memóriáját, mert az eddigi beállítás ki volt hegyezve a Hadoopra, mostantól viszont a Cassandrának is kell helyet szorítani. A fejlesztés előrehaladtával a gépemben lévő 8 Gb RAM már sokszor kevésnek bizonyult, elkezdte a Vagrant aktívan használni a swapet, ami igen rossz hatással van a jobok futtatására, lépten nyomon elhasaltak. Én átmenetileg a 3-s számú slave gépet kikapcsoltam. Összességében 4 virtuális gépből és 15 konténerből áll a cluster, szóval személy szerint nem is csodálkozom, hogy ilyen mértékben megnövekedett a gép igény.bootstrap.sh
if [ -n "$MASTER_IS_SLAVE_TOO" ]; then echo $HOST_NAME > $HADOOP_PREFIX/etc/hadoop/slaves else echo "" > $HADOOP_PREFIX/etc/hadoop/slaves fiBevezettem egy környezeti változót (MASTER_IS_SLAVE_TOO) melynek hatására a mester konténer szolga is lesz egyben, a változó nélkül csak mesteri teendőit látja el.
Dockerfile
RUN sed -i "s|^# Extra Java CLASSPATH.*|&\nexport HADOOP_CLASSPATH=/usr/share/cassandra/*:/usr/share/cassandra/lib/*:\$HADOOP_CLASSPATH|" $HADOOP_PREFIX/etc/hadoop/hadoop-env.shJavítottam a HADOOP_CLASSPATHon, hiányzott egy Cassandrás függőség.
cassandra-clusternode.sh
if [ -n "$PUBLIC_INTERFACE" ]; then IP=$(ifconfig $PUBLIC_INTERFACE | awk '/inet addr/{print substr($2,6)}') PUBLIC_IP=$IP fi if [ -n "$PUBLIC_IP" ]; then sed -i -e "s/^# broadcast_address: 1.2.3.4/broadcast_address: $PUBLIC_IP/" $CASSANDRA_CONFIG/cassandra.yaml fiA Cassandrás konténerben egy új környezeti változóval, név szerint PUBLIC_INTERFACE, megoldottam, hogy a Cassandra a megfelelő IP címet használja minden nemű kommunikációhoz.
if [ -n "$CASSANDRA_SEEDS" ]; then for a in $(echo $CASSANDRA_SEEDS | sed 's/,/ /g'); do CASSANDRA_SEEDS=$(echo $CASSANDRA_SEEDS | sed "s/$a/$(ping -c1 $a | grep PING | awk '{ print $3 }' | sed "s/(//;s/)//")/"); done fiMivel a konténerek dinamikusan kapnak IP címet, a Cassandra viszont csak IP alapján tud kapcsolódni a seed szerverekhez, ezért meg kellett trükköznöm a CASSANDRA_SEEDS változót, domain neveket és IP címeket is elfogad egyaránt, majd a Cassandra indítása előtt feloldja a domain neveket IP címekre.
Futtatás
slave1
nohup docker -H tcp://192.168.50.15:1234 run --name cassandra-slave1 --dns 192.168.50.15 -h cassandra1.lo -e "PUBLIC_INTERFACE=eth0" -e "CASSANDRA_CLUSTERNAME=HadoopTest" -e "CASSANDRA_TOKEN=-9223372036854775808" -t mhmxs/cassandra-cluster > cassandra.log 2>&1 & docker -H tcp://192.168.50.15:1234 run --name hadoop-slave1 --dns 192.168.50.15 -h slave1.lo -e "MASTER=master.lo" -e "SLAVES=slave1.lo,slave2.lo,slave3.lo" -it mhmxs/hadoop-docker:2.6.0 /etc/bootstrap.sh -bashA Cassadnrás konténer logját a cassandra.log fájlban találjuk, érdemes a Hadoop cluster elindítása előtt összeállítani a Cassandra clustert (vagy külön terminálban nohup nélkül indítani), mert ha valami időzítési vagy hálózati probléma miatt nem találták meg egymást a nodeok, akkor elég kényelmetlen a Swarm clusterből törölgetni, majd újraindítgatni a megfelelő konténereket. Sokszor kellett a fejlesztés alatt hasonlót csinálnom, és a rendszer sem túl hiba tűrő, úgyhogy rászoktam, hogy minden lépés előtt ellenőrzöm, hogy az elemek a helyükre kerültek-e.
slave2
nohup docker -H tcp://192.168.50.15:1234 run --name cassandra-slave2 --dns 192.168.50.15 -h cassandra2.lo -e "PUBLIC_INTERFACE=eth0" -e "CASSANDRA_CLUSTERNAME=HadoopTest" -e "CASSANDRA_SEEDS=cassandra1.lo" -e "CASSANDRA_TOKEN=-3074457345618258603" -t mhmxs/cassandra-cluster > cassandra.log 2>&1 & docker -H tcp://192.168.50.15:1234 run --name hadoop-slave2 --dns 192.168.50.15 -h slave2.lo -e "MASTER=master.lo" -e "SLAVES=slave1.lo,slave2.lo,slave3.lo" -it mhmxs/hadoop-docker:2.6.0 /etc/bootstrap.sh -bashslave3
nohup docker -H tcp://192.168.50.15:1234 run --name cassandra-slave3 --dns 192.168.50.15 -h cassandra3.lo -e "PUBLIC_INTERFACE=eth0" -e "CASSANDRA_CLUSTERNAME=HadoopTest" -e "CASSANDRA_SEEDS=cassandra1.lo" -e "CASSANDRA_TOKEN=3074457345618258602" -t mhmxs/cassandra-cluster > cassandra.log 2>&1 & docker -H tcp://192.168.50.15:1234 run --name hadoop-slave3 --dns 192.168.50.15 -h slave3.lo -e "MASTER=master.lo" -e "SLAVES=slave1.lo,slave2.lo,slave3.lo" -it mhmxs/hadoop-docker:2.6.0 /etc/bootstrap.sh -bashmaster
docker -H tcp://192.168.50.15:1234 run --name hadoop-master --dns 192.168.50.15 -h master.lo -e "SLAVES=slave1.lo,slave2.lo,slave3.lo" -v /vagrant:/vagrant -it mhmxs/hadoop-docker:2.6.0 /etc/bootstrap.sh -bash
Job
A hozzuk létre a projekt könyvtárban a KeyCollector.java fájlt az alábbi tartalommal:
import java.io.IOException; import java.util.*; import java.nio.ByteBuffer; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.*; import org.apache.hadoop.mapred.*; import org.apache.cassandra.thrift.SlicePredicate; import org.apache.cassandra.hadoop.*; import org.apache.cassandra.db.*; import org.apache.cassandra.utils.ByteBufferUtil; public class KeyCollector { public static void main(String[] args) throws IOException { if (args.length != 1) { System.err.println("Usage: KeyCollector <output path>"); System.exit(-1); } JobConf conf = new JobConf(KeyCollector.class); conf.setJobName("KeyCollector"); ConfigHelper.setInputInitialAddress(conf, "cassandra1.lo"); ConfigHelper.setInputColumnFamily(conf, "HadoopTest", "content"); ConfigHelper.setInputPartitioner(conf, "org.apache.cassandra.dht.Murmur3Partitioner"); SlicePredicate predicate = new SlicePredicate().setColumn_names(Arrays.asList(ByteBufferUtil.bytes("text"))); ConfigHelper.setInputSlicePredicate(conf, predicate); conf.setInputFormat(ColumnFamilyInputFormat.class); conf.setMapperClass(KeyCollectorMapper.class); FileOutputFormat.setOutputPath(conf, new Path(args[0])); conf.setOutputKeyClass(Text.class); conf.setOutputValueClass(IntWritable.class); conf.setReducerClass(KeyCollectorReducer.class); JobClient.runJob(conf); } public static class KeyCollectorMapper extends MapReduceBase implements Mapper<ByteBuffer, Map<ByteBuffer, BufferCell>, Text, IntWritable> { public void map(ByteBuffer key, Map<ByteBuffer, BufferCell> columns, OutputCollector<Text, IntWritable> output, Reporter reporter) throws IOException { String textKey = ByteBufferUtil.string(key); output.collect(new Text(textKey), new IntWritable(1)); } } public static class KeyCollectorReducer extends MapReduceBase implements Reducer<Text, IntWritable, Text, IntWritable> { public void reduce(Text key, Iterator<IntWritable> values, OutputCollector<Text,IntWritable> output, Reporter reporter) throws IOException { int sum = 0; while (values.hasNext()) { sum += values.next().get(); } output.collect(key, new IntWritable(sum)); } } }Valószínűleg ez a világ legértelmetlenebb map/reduce jobja, összegyűjti a column familybe lévő kulcsokat, de ez tűnt a legegyszerűbb implementációnak. Természetesen lett volna lehetőség a job eredményét a Cassandrába tárolni, de a letisztultság jegyében, én a fájl rendszert preferáltam. Fordítsuk le az osztályt és csomagoljuk be egy jar-ba a mester Hadoop konténerben.
yum install -y java-1.8.0-openjdk-devel cd /vagrant mkdir build classpath=. for jar in /usr/share/cassandra/*.jar; do classpath=$classpath:$jar; done for jar in /usr/share/cassandra/lib/*.jar; do classpath=$classpath:$jar; done for jar in `find /usr/local/hadoop/share/hadoop/ *.jar`; do classpath=$classpath:$jar; done javac -classpath $classpath -d build KeyCollector.java jar -cvf KeyCollector.jar -C build/ .Következő lépés, hogy ellenőrizzük a cluster működését, és teszt adattal töltjük fel az adatbázist, szintén a mester konténerből.
nodetool -h cassandra1.lo status cassandra-cli -h casandra1.lo create keyspace HadoopTest with strategy_options = {replication_factor:2} and placement_strategy = 'org.apache.cassandra.locator.SimpleStrategy'; use HadoopTest; create column family content with comparator = UTF8Type and key_validation_class = UTF8Type and default_validation_class = UTF8Type and column_metadata = [ {column_name: text, validation_class:UTF8Type} ]; set content['apple']['text'] = 'apple apple red apple bumm'; set content['pear']['text'] = 'pear pear yellow pear bumm';Elérkezett a várv várt pillanat, futtathatjuk a jobot.
$HADOOP_PREFIX/bin/hadoop jar KeyCollector.jar KeyCollector output $HADOOP_PREFIX/bin/hdfs dfs -cat output/*A kimenetben láthatjuk, hogy egy darab apple és egy darab pear kulcs van az adatbázisban.
SLF4J: Class path contains multiple SLF4J bindings. SLF4J: Found binding in [jar:file:/usr/local/hadoop-2.6.0/share/hadoop/common/lib/slf4j-log4j12-1.7.5.jar!/org/slf4j/impl/StaticLoggerBinder.class] SLF4J: Found binding in [jar:file:/usr/share/cassandra/lib/logback-classic-1.1.2.jar!/org/slf4j/impl/StaticLoggerBinder.class] SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation. SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory] apple: 1 pear: 1
Teljesítmény
Befejezésül ejtsünk pár szót a rendszer teljesítmény optimalizálásáról.
A Cassandra elég korai verziójában bevezetésre kerültek az un. vNodeok, amik azt biztosítják, hogy egy Cassandra valós node a token tartomány több szeletét is birtokolhassa egyszerre. A Cassandra oldaláról számos előnye van ennek a megoldásnak, viszont a Hadoop felöl érkezve kifejezetten káros hatása van. A vNodeok száma kihatással van a szeletek (split) számára, ami annyit tesz, hogy annyi darab szelet egészen biztos lesz, ahány vNode van engedélyezve.
A következő paraméter, amire érdemes figyelni, az a szeletek mérete. Az alapértelmezett szelet mérete 64K, ha mondjuk van 5,000,000,000 sor az adatbázisban, akkor 64,000-el számolva 78,125 szeletben fogja elvégezni a műveletet a Hadoop, ami szeletenkénti 10 másodperccel szorozva száz óra körüli futás időt eredményez. Az alábbi sorral konfigurálhatjuk a szeletek méretét.
ConfigHelper.setInputSplitSize(job.getConfiguration(), 10000000);Mivel a feldolgozó egységekben korlátozott mennyiségű memória áll rendelkezésre, javasolt finom-hangolni az egyszerre feldolgozott adatok számát, a példában 100-as kötegekben fogja a CQL driver a sorokat prezentálni, az alapértelmezett 1000 darab helyett.
CqlConfigHelper.setInputCQLPageRowSize(job.getConfiguration(), Integer.toString(100));A negyedik megkerülhetetlen téma az un. data locality. A jó teljesítmény eléréséhez elengedhetetlen, hogy az adatokat a legkisebb mértékben kelljen mozgatni. A Cassandra fejlesztői az InputFormat implementálása során szerencsére gondoltak erre a probléma forrásra, annyi a dolgunk, hogy ugyanarra a hostra telepítjük a Cassandrát, és a Hadoop JobTrackert. Sajnos Docker konténereket használva ezt bukjuk, mert a két konténer mindig két különálló host lesz, gyakorlatilag mintha külön gépen futnának a szolgáltatások. Megtehetnénk, hogy összeházasítjuk a két konténert, de akkor a Docker szemlélettel mennénk szembe, miszerint minden szolgáltatás egy önálló konténer legyen. Ha ezt el szeretnénk kerülni, akkor bizony be kell koszolnunk a kezünket. Én első lépésben megnyitottam a legfrissebb dokumentációt. Láthatjuk, hogy semmi nem található benne a témával kapcsolatban, de ne keseredjünk el, ugyanis van egy őse ennek a dokumentumnak, nyissuk meg a AbstractColumnFamilyInputFormat fájlt is. A public List<InputSplit> getSplits(JobContext context) metódusban történik a csoda, itt állítja össze a Hadoop a szeleteket, minden egyes szeletet egy InputSplit objektum reprezentál, és tartalmazza azt/azokat a host neveket, ahol az adat megtalálható.
A jelen cluster egy szoftveres hálózaton kommunikál, így ebben az esetben nincs jelentősége az adatok tényleges helyének, nem is bonyolítanám tovább a rendszert saját InputFormat írásával és/vagy különböző DNS trükkök bevezetésével. A jó megoldást mindenféleképpen az adott hálózati architektúrának megfelelően kell kialakítani. Akit érdekelnek további a teljesítménnyel kapcsolatos kérdések itt talál pár választ.
2015. február 17., kedd
JVM futásidejű monitorozása
Egy ideje a JVM valós idejű monitorozásának lehetőségeit kutattam, és csak hosszas keresés után találtam meg a projektnek, és a költség tervnek megfelelőt.
Szerencsére megoldás akad bőven a fizetőstől az ingyenesen át a szabadig megoldásokig. A teljesség igénye nélkül szeretnék bemutatni néhányat közülük.
- Dynatrace kétségtelen, hogy egyike a legprofibb megoldásoknak. Volt szerencsém egy hosszabb lélegzet vételű prezentációt végigülni, ahol ebből a csodás eszközből kaptunk ízelítőt. Gyakorlatilag a monitorozás, hiba feltárás, és reprodukálás mekkája a Dynatrace. Központilag telepített szerver gyűjti az információkat a különböző ügynököktől (Java, .NET, böngésző), majd ezeket az információkat un. PurePathokba szervezi, ahol nyomon tudjuk követni egy kérés teljes útvonalát az infrastruktúrában. Pontos képet kaphatunk, hogy mely rétegben mennyi időt töltött a kiszolgálás, és a PurePathon belül minden irányban teljes átjárást biztosít a rendszer, ami azt jelenti, hogy pár kattintással el lehet érni a végrehajtó kód sort, a futtatott lekérdezéseket, a felhasználót, és annak a többi kérését, és még sorolhatnám. Profizmusához mérten van az árazása, természetesen létezik ingyenes próba időszak, cserébe viszont képzett kollégák segítenek a rendszert beüzemelni.
- Következő alternatíva a New Relic. Mi aktívan használjuk több projekten is, és alapvetően meg vagyunk vele elégedve. Ára sokkal pénztárca barátabb, mint a Dynatracé, de tudása is ezzel arányosan kevesebb. A New Relic képes monitorozni a szervert (mi Linuxot használunk, nincs tapasztalatom egyéb operációs rendszerekkel), az adatbázist, a JVMet, kéréseket, hibákat, majd ezekből tetszetős grafikonokat rajzol. Létezik hozzá mobil alkalmazás és böngésző monitorozó eszköz is. Hátránya, hogy a New Relic szerverei felé jelentenek az ügynökök, így egyfelől van egy minimális késleltetése, másfelől a weboldaluk sebessége is hagy némi kívánnivalót maga után. Kevésbé ár érzékeny projektek esetén kiváló választás lehet.
- Az AppDynamicsról olvastam még eléggé pozitív cikkeket, sajnos saját tapasztalatom nincs velük kapcsolatban.
- Utolsó fizetős megoldás az előzőekhez képest még szerényebb a nyújtott szolgáltatások terén, de olyan kedvező a fizetési modelljük, hogy mindenféleképpen érdemes őket megemlíteni. A SemaText szinte valósidejű monitorozást végez fáék egyszerűséggel, de a támogatott platformok igen széles palettán mozognak: AWS, Apache, Cassandra, Elasticsearch, HAProxy, HBase, Hadoop-MRv1, Hadoop-YARN, JVM, Kafka, Memcached, MySQL, Nginx, Redis, Sensei, Solr, SolrCloud, Spark, Storm, ZooKeeper. Az ingyenes verzióban 30 percig vissza menőleg örzik meg az adatokat, és még van pár limitáció, de alkalmazásonként választhatunk csomagot, és akármikor felfüggeszthetjük egy alkalmazás/cluster monitorozását (órában van megadva a legkisebb fizetési egység). Amit mindenféleképpen, mint előnyt meg szeretnék említeni, hogy APIjukon keresztül saját metrikékat is viszonylag kényelmesen megjeleníthetünk. Hátránya, hogy a New Relichez hasonlóan az ő szervereik tárolják az adatokat, és az ügynöknek, amit telepíteni kell, rengeteg a csomag-függősége, legalábbis Linuxon.
- Első delikvens a JavaMelody, aminek a telepítése igen egyszerű, a letöltött war fájlt deployoljuk az alkalmazás-szerveren. Hátránya, hogy csak lokális monitorozást végez, ami több mint a semmi, de csak egy hajszállal.
- A stagemonitor igen ígéretes projektnek tűnik, kár, hogy csak a JVM helyi megfigyelésére alkalmas, és számunkra csak központosított megoldások jöhetnek számításba. A weboldalt böngészve láthatjuk, hogy igen széles spektrumon követi nyomon az alkalmazás működését, és gyönyörű grafikonokon ábrázolja az adatokat.
- Legtöbben az interneten a JAMont ajánlották, ami egy Java monitorozásra alkalmas API. A dokumentációból első olvasásra kiderült számomra, hogy telepítése nem triviális, és a metrikák pontos megtervezése után az alkalmazásban implementálni is kell azokat. Őszinte leszek nem ugrottam fejest a JAMon világába. Biztos nagyon szép és nagyon jó, de a csapat "produktivitását" nem növeli, az ügyfélnek nem eladható hogy x hétig metrikákat reszelgetünk, meg grafikonokat rajzolgatunk.
- Nyílt forrású megoldások közül nekem a MoSKito tűnik a legkiemelkedőbbnek, sajnálom, hogy későn akadtam rá, és addigra már belaktunk egyéb szolgáltatásokat. A MoSKitó kifejezetten Java fürtök valós idejű megfigyelését célozza meg.
2012. december 30., vasárnap
Reflection - a bajkeverő csodagyerek
RequestParams rp = new RequestParams(); final Field[] fields = rp.getClass().getDeclaredFields(); for (final Field field : fields) { firstChar = String.valueOf(field.getName().charAt(0)); methodName = "set" + field.getName().replaceFirst(firstChar, firstChar.toUpperCase()); try { method = rp.getClass().getMethod(methodName, String.class); method.invoke(rp, request.getParameter(field.getName())); } catch (final SecurityException e) { } catch (final NoSuchMethodException e) { } catch (final IllegalArgumentException e) { } catch (final IllegalAccessException e) { } catch (final InvocationTargetException e) { } }A példa jól szemlélteti, hogy pofon egyszerű feltölteni a kapott paraméterekkel egy objektumot, újabb paraméterek létrehozásakor nem kell setterekkel bajlódni, ráadásul a kód teljesen hordozható, így bármely osztályra dinamikusan alkalmazható (a példa csak String-eket kezel az egyszerűség kedvéért).
Itt merül fel a kérdés, hogy a Reflection nem egy kétélű fegyver? Mivel mondhatni teljes hozzáférést biztosít objektumjaink belső működéséhez és felépítéséhez, nem lehet ezt rosszra is használni? A válasz egyértelmű IGEN! Daily WTF-en találtam ezt a kedves kis szösszenetet:
class ValueMunger extends Thread { public void run() { while(true) { munge(); try { sleep(1000); } catch (Throwable t) { } } } public void munge() { try { Field field = Integer.class.getDeclaredField( "value" ); field.setAccessible( true ); for(int i = -127; i <= 128; i++) field.setInt( Integer.valueOf(i), // either the same (90%), +1 (10%), or 42 (1%) Math.random() < 0.9 ? i : Math.random() < 0.1 ? 42 : i+1 ); } catch (Throwable t) { ; } } }A fenti kódrészlet az Integer wrapper cachet-t zagyválja egy kicsit össze, időnként nem a valós értéket kapjuk vissza, hanem az élet értelmét, ami üzleti kritikus alkalmazásokban igen nagy probléma, nem szeretnénk egy pészmékerben ilyesmi kóddal találkozni, de azt sem szeretnénk, ha a banki alkalmazás viccelné meg a bankszámlánkat időnként. A final mezőink, a private tagjaink sincsenek biztonságban, ahogy az Enum-jaink sem, és ezen a ponton a végtelenségi lehetne sorolni az ártalmas és vicces példákat, ugyanis kijelenthető, hogy (Sun/Oracle) Java verziótól függően szinte mindenhez hozzá lehet férni Reflection API segítségével, nincs menekvés.
Ártalmas kódot hagyhat hátra kilépő munkatárs, tölthetünk le ártatlanul egy külső osztálykönyvtárral, szóval több forrásból is beszerezhetőek, de vajon mit lehet tenni ellenük? Az egyetlen gyógyszer a tudatos és körültekintő felhasználás.
- Használjunk statikus kódelemzőt, és szigorúan tartsuk számon azokat a részeket, ahol az alkalmazásunk Reflection API-t használ.
- Használjunk nyílt forrású könyvtárakat, és fordítsuk magunk (kódelemzés után).
- A letöltött osztálykönyvtárak hitelességét minden lehetséges módon ellenőrizzük, ugyanis koránt sem biztos, hogy senki nem törte fel kedvenc keretrendszerünk weboldalát, és cserélte ott le a letölthető jar-okat saját módosított verziójára (jó lenne a Linux repository-khoz hasonló központosított megoldás).
- Csak a készítő által aláírt osztályokat, és jar-okat használjunk, ha külső forrásra kell támaszkodnunk.
- Minimalizáljuk a külső forrásokat, ne használjunk két külön osztálykönyvtárat közel ugyanarra a feladatra.
2012. november 4., vasárnap
Eclipselink beizzítása Jboss 7.1 környezetben
Az élesítés óta megjelent a Liferayből 2 újabb verzió, és szerettem volna frissen tartani a rendszert, de természetesen az új Liferayhez új Jboss is dukál. A frissítendő architektúra hibridsége miatt nem volt könnyű szülés a művelet, ennek lépéseit szeretném megosztani.
Az új Jboss architektúra teljesen modularizált, ezért egy 3rd party lib használata nem annyi, hogy bemásoljuk a megfelelő jar-t a classpathra, hanem modulként definiálnunk kell a rendszerben. Nincs ez másként az Eclipselinkkel sem. Modul létrehozásához annyit kell tennünk, hogy létrehozzuk a modules/org/eclipse/persistence/main könyvtárakat az alkalmazásszerverünkben, és a main könyvtárba bemásoljuk az eclipselink.jart, a konfigurációhoz pedig létre kell hoznunk ugyanitt egy module.xml-t, amiben a modult magát definiáljuk.
Az előző lépéshez hasonlóan egy Ant, és egy adatbázis modult is létre kell hozni (estemben egy PostgreSQLt).
Ezután nincs más dolgunk az alkalmazásszerveren, mint a standalone/configuration/standalone.xml-t szerkeszteni. Először is definiáljuk a szükséges DataSource-ot.
Lehet, hogy ez Liferay specifikus, de nálam volt Hibernate konfiguráció is ebben az xmlben, ami miatt timeoutolt a perzisztens réteg inicializálása, így azt egy kecses mozdulattal kitöröltem (egyelőre nem jelentkezett a hiányából fakadó probléma).jdbc:postgresql://[hostname]:[port]/[database] postgres 14 20 postgres org.postgresql.xa.PGXADataSource
Miután az alkalmzásszerver konfigurációjával végeztünk, már csak az alkalmazásunkat kell egy kicsit reszelgetni. Ahol JNDI név szerint hivatkoztunk EJB Session Beanekre, ott a hivatkozott nevet célszerű átírni. A Jboss az EJB deployment során kiírja, hogy milyen neveken regisztrálta a beaneket, nekem nem mindegyik működött ??, de ez a forma bevált: global/[appname]/[serviceimplclassname].
A persistence.xml-ben is be kell állítanunk pár beállítást.
org.eclipse.persistence.jpa.PersistenceProvider java:/jboss/datasources/LiferayPool
Utolsó lépésként az alkalmazásunkban be kell állítani, hogy milyen Jboss modulok a függőségei, ezt két féle módon is megtehetjük, vagy a META-INF/MANIFEST.MF állományban vesszük fel az alábbi sort:
Dependencies: org.eclipse.persistencevagy létrehozunk ugyanitt egy jboss-deployment-structure.xml nevű konfigurációs állományt, az alábbi tartalommal:
Mindenkinek javaslom, hogy tegyen egy próbát az Eclipselinkkel, számos területen jobb, mint a Hibernate, nem csak teljesítményben és szabványkövetésben. Kedvenc tulajdonságom pl, hogy Hibernattel ellentétben a Session bezárása után is eléri még a DataSourcot (limitáltan), így a lustán inicializált relációkat nem kell kézzel betöltögetni még az üzleti rétegben, hanem a helyükre tett proxyn keresztül később is eléri azokat.
2012. március 23., péntek
Könyvajánló: Java fejtörők
Lássuk először a szerzőket. Joshua Block neve ismerősen csenghet, a Carnegie Mellon University-n doktorált, a Java 5 nyelvi bővítésein dolgozott és a Java Collections Framework tervezését és fejlesztését is irányította többek közt. Dolgozott a Sun Microsystemsnél és jelenleg a Google egyik főmérnöke. Szerzője a díjat nyert Hatékony Java című könyvnek. Ez utóbbi műve szerintem kötelező olvasmány minden Java fejlesztőnek! Neal Gafter a University of Rochesteren szerzett doktorátust, majd az Sun Microsystemsnél irányította a Java fordító fejlesztését rangidős mérnökként. Jelenleg ő is a Googlenél - kinél másnál - dolgozik szoftvermérnökként.
A könyv a Java nyelvben rejlő csapdákkal, buktatókkal, és olyan szélsőséges esetekkel foglalkozik, amelyekbe nap mint nap belefuthatunk munkánk/hobbink során, és kellő ismeret nélkül csak a fejünket vakargathatjuk a nem várt eredmény miatt. A könyv 9 gyakorlati fejezetbe kategorizálja az összesen 95 esetet, és az egyszerűbbtől a bonyolultabb példák felé halad, a leg harcedzettebbeknek is sokszor feladva a leckét. Nem spoilerezek többet, tessék megvenni a könyvet, ha még nincs meg :).
2012. január 21., szombat
Weak és Soft referenciák a Javaban
Több éves Java tapasztalattal a hátam mögött hallottam először a gyenge (weak) referenciák létezéséről a nyelvben, és úgy gondoltam érdemel pár szót a téma kör. Javaslom, akinek nem világos a JVM szemétgyűjtőjének (továbbiakban GC) működése, ezen a ponton olvassa el Viczián István idevágó bejegyzését. Tehát mint tudjuk a GC kidob a memóriából minden olyan objektumot, melyre nem mutat már egyetlen referencia sem. Amikor létrehozunk egy objektumot, Integer foo = new Integer(0), akkor a foo egy strong reference lesz rá, és amíg az objektum "strongly reachable", addig a GC-nek tabu. Mivel ezeket az objektumokat nem tudja a GC felszabadítani, a memória beteltével jön a fránya OutOfMemotyError, és az alkalmazás kilép. Egy darabig persze lehet növelni a memóriát, majd a fizikai határok elérésével lehet elosztani az alkalmazást, de előbb álljunk meg egy szóra! Vajon minden objektumra szükségünk van a memóriában? Kézenfekvő megoldás, hogy bizonyos objektumokat azonnal megszüntessünk, amint nincs rájuk szükség, viszont az objektumok újbóli létrehozása is nagy költség, ebben az esetben pedig értékes processzoridőt fecsérelünk az állandó memóriaallokációra, példányosításra, stb. Arany középútként az 1.2-es, igen az 1.2-es Java verzióban bevezették a weak referenciákat, melynek implementációja a WeakReference osztály. A WeakReference referenciát tárol az adott objektumra, annyi különbséggel, hogy ezt az objektumot a GC első futáskor, szó nélkül eltávolítja a memóriából, felszabadítva ezzel a helyet mások számára.
WeakReference<StringBuilder> weakString = new WeakReference<StringBuilder>(new StringBuilder()); int i = 0; while (weakString != null && weakString.get() != null) { weakString.get().append(i++); System.out.println(i); } System.out.println("Finish");
Kezdetként futási paraméterként állítsuk be a JVM-nek a maximális memóriát mondjuk 2 Mb-ra (-Xmx2m), majd az értékkel játszva láthatjuk, hogy mennyit változik programunk kimenete. Ez a technika kiválóan alkalmas memória gyorsítótárak készítésére, ám amennyiben kulcs-érték-pár alapú gyorsítótárat készítünk a new WeakReference(myMapInstance) helyett használjuk, az erre a célra készített WeakHashMap implementációt. A WeakHashMap a kulcsokat őrzi weak referenciával, és a MapEntry automatikusan eltávolításra kerül, amikor a kulcs már nincs rendszeres használat alatt.
A WeakReference mellett létezik a SoftReference osztály is, amely azt garantálja, hogy az OutOfMemoryError előtt felszámolásra kerülnek az objektumok, helyet biztosítva az újonan létrejövőknek.
SoftReference softObj = new SoftReference(new Serializable() { @Override public void finalize() throws Throwable { System.out.print("Finalize runned"); super.finalize(); } }); StringBuilder sb = new StringBuilder("foobar"); while (true) { sb.append(sb.toString()); }A kimenetben láthatjuk, hogy miközben az alkalmazás akkut memóriahiányban elhalálozott, még utolsó leheletével felszámolta osztályunkat.
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space Finalize runned at java.util.Arrays.copyOf(Arrays.java:2882) at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:100) at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:390) at java.lang.StringBuilder.append(StringBuilder.java:119) at com.blogspot.jpattern.Main.run(Main.java:36) at com.blogspot.jpattern.Main.main(Main.java:22) Java Result: 1Természetesen ennél complexebb program esetében az alkalmazás tovább tud dolgozni a felszabadult memóriával.A Java specifikáció szerint semmi garancia nincs a finalize() metódus futására!
Evezzünk egy kicsit sötétebb vizekre. Ha jól megnézzük a Reference API dokumentációját, láthatjuk, hogy van még egy ismert implementáció, a PhantomReference. Az első különbség társaihoz képest, hogy a bele helyezett objektumra soha nem tudunk referenciát kérni, ugyanis konstans null értékkel válaszol a get() hívására. Másik nagy eltérés lényege tömören, hogy az ilyen objektumokat a GC csak azelőtt rendezi sorba, mielőtt a fizikai memóriából kitakarítaná. Mivel úgy tudom Darth Vader nagyurat is ennek használata állította át az erő sötét oldalára, én magam nem merészkedtem ennél tovább (esetleg írhatnátok valami konkrét használati esetet).
Előfordulhat, hogy az alkalmazásunkban szükséges tudni, hogy mely objektumokat dobta már ki a GC, és melyeket nem. Ilyen esetben egy ReferenceQueue osztályt kell példányosítanunk, és a queue példányt átadni a WeakReference vagy SoftReference konstruktorának. A bejegyzésben említett referenciákkal óvatosan bánjunk, és csak alapos tervezés és tesztelést követően alkalmazzuk éles bevetésben őket.
2011. december 12., hétfő
Liferay Service Builder, a sötét oldal
- Az entitásokat leíró XML-ben csak primitív, String, vagy Date típust jelölhetünk meg az adatok tárolására, viszont osztály szinten a primitív adattagok inicializálódnak, tehát nem tudunk olyan mezőt felvenni, amelynek az értéke üres, vagyis NULL. Képzeljük el a User entitásunkat, egy opcionális életkor mezővel. Mivel az életkor alapértelmezetten 0 értéket vesz fel, nem tudjuk megkülönböztetni a csecsemő felhasználóinkat azoktól, akik nem töltötték ki az életkorukat. A példa légből kapott, de jól szemlélteti a primitív típusok hiányosságát. A Liferay készített egy workaroundot a probléma orvoslására, és használhatunk wrapper osztályokat is a primitívek helyett, viszont ebben az esetben szembesülnünk kell két dologgal, mégpedig, hogy a DTD miatt kedvenc IDE-nk figyelmeztetni fog, hogy nem megfelelő adatot írtunk be, másrészt hivatalosan a Liferay nem támogatja ezt a megoldást.
- Rengetegszer futottam bele, hogy a String mezők hossza alapértelmezetten 75 karakter széles. Ezen a ponton lehet vitatkozni, hogy valahol meg kellett húzni a határt az adatbázis méretének és a programozói munkának az optimalizációja közben, de véleményem szerint a 75 karakter a legtöbb esetben nem elég. Lehetett volna egy kicsit a felesleges adatbázis helyfoglalás felé billenteni a mérleget. A programozók feledékenyek, a tesztelők meg hanyagok, és ebből majdnem egyenesen következik, hogy 100 String mezőből legalább 1-nél csak túl későn derül ki, hogy nem elegendő a 75 karakter. Természetesen van megoldás, mégpedig az src/META-INF könyvtárban lévő portlet-model-hints.xml fájlban kell a mezőkre "tippeket" adni a Liferaynek. Persze itt nem csak a mező szélességére adhatunk hasznos tanácsokat, egyéb dolgokat is beállíthatunk, ezeket most nem részletezném.
A megoldás árny oldala, hogy EE verzió esetén megcsinálja az adatbázis módosítását a rendszer, azonban az általam próbált CE verziók nem módosították, az adatbázist, és a módosítást végrehajtó SQL-t is magamnak kellett megírnom. Ezen a ponton bukott meg az adatbázis-függetlenség. Nem ennyire elkeserítő a helyzet, mert írhatunk olyan általános SQL-t, amit a Liferay is használ a Service Builder tábláinak legenerálásához, és létezik egy osztály a Liferayben, ami az általános SQL-ből adatbázisnak megfelelő SQL-t generál, amit egy startup hookba ágyazva futtathatunk, de azt hiszem ez túl nagy ár a függetlenségért. - A tranzakciók kezelése sem túl kifinomult szerintem a Liferayben. A Liferay filozófiája, hogy a tranzakciók kezelését teljesen a Spring AOP-re bízza, amely a Liferay szívében van konfigurálva. Az alapértelmezett működés, hogy az add*, check*, clear*, delete*, set*, és update* karakter-lánccal kezdődő metódusok egy tranzakciót indítanak. Ettől eltérően csak annyit tehetünk, hogy a service.xml-ben az entitásnál a tx-required mezővel megadhatjuk, hogy ezen felül milyen metódusok indítsanak tranzakciót. Nekem nagyon kényelmes és kézenfekvő, hogy szabadon válogathatok a 6 féle tranzakció-kezelés közül, és bármikor eldönthetem, hogy becsatlakozok-e a meglévő tranzakcióba, hogy indítok-e újat, stb. Megint csak egy légből kapott példa, banki tranzakciót indítok a rendszerben, és a folyamat közben szeretném loggolni, hogy pontosan mi történt a tranzakcióval, de sajnos a log szerver timeoutol/betelik a lemez/stb. Ebben az esetben a loggolást végző kódrészlet hibája miatt gördül vissza az egész tranzakció, holott csak egy harmadlagos funkció nem teljesült. Létezik a Liferayben EXT plugin, amivel felül lehet írni a Spring konfigot, de a 7-es verziótól ezek egyáltalán nem lesznek támogatottak, így én sem javaslom senkinek, hogy ezt az utat válassza.
- A service réteg deleteAll metódusai a perzisztes rétegen keresztül egyesével törlik az entitásokat, ami egy 10M+ rekordszámú táblánál órákba is telhet. Ezt különösen azért nem értem, mert a Liferay nem kezel entitás-kapcsolatokat, tehát feleslegesnek érzem az adatbázisból egyesével kikérni a rekordokat, és törlést végrehajtani rajtuk. Szerencsére erre is van egy kiskapu, közvetlenül el tudunk kérni egy JDBC kapcsolatot, ahol szabadon garázdálkodhatunk:
DataAccess.getConnection.createStatement().execute("delete from MY_Entity");
Természetesen a kapcsolat élet ciklusáról magunknak kell gondoskodnunk. - Mint fentebb is írtam a Liferay nem igazán kezeli az entitás relációkat. Meg lehet ugyan adni kapcsolatokat, de a OneToOne kapcsolaton kívül, amit kezel a rendszer, minden kapcsolat OneToMany esetén csak egy long típusú mező, ManyToOne esetén pedig egy üres Collection lesz. a OTO kapcsolatot az service.xml-ben a reference tag segítségével adhatjuk meg. Ennek célja egyébként a minél szélesebb adatbázis paletta támogatása, de szerény véleményem szerint ez akkor plusz terhet ró a fejlesztőkre, és annyi hiba lehetőséget, és adatbázis-szemetet eredményez, ami nem biztos, hogy megéri ezt az árat. További hátránya a szemléletnek, hogy így az entitások közötti kapcsolatot View oldalon kell lekezelni, onnan kell az adatbázishoz fordulni minden kapcsolat esetén. A Liferay egyébként MVC patternt követi, de vallja azt a nézetet, hogy lekérdezés esetén lehet közvetlenül a Model-hez fordulni, egyszerűsítve a logikát, így viszont egy standard Liferay portlet plugin jsp-je tele van scriptletekkel, olvashatatlanná téve a kódot. Ízlések és pofonok, én személy szerint szeretem, ha jól el vannak szeparálva a dolgok egymástól, a jsp szerkesztésekor az IDE tud hasznos tanácsokat adni, és nincs tele a Problems fül felesleges figyelmeztetésekkel, ha a Model önállóan végzi a dolgát, és nem a View fejlesztőnek kell gondolkodnia a különböző service hívásokról.
2011. október 30., vasárnap
OWASP AntiSamy Javaban
A webes biztonság kérdése egyidős magával az internettel, hiszen a hálózaton elérhető adatokat felhasználók milliói tekintik meg napról-napra. Mivel a weboldalak végeredményben a kliensek gépein futnak, hamar beláthatjuk, hogy a biztonsági kérdések nem elhanyagolhatóak, és mind a klienseknek, mind a weboldalak üzemeltetőinek fontos, hogy a kiszolgált tartalom hiteles és biztonságos legyen. Tipikus támadási forma az un. XSS, amikor pl. egy az oldalba ágyazott külső Javascript próbál szenzitív információkhoz hozzájutni a böngésző valamely biztonsági hibáját kihasználva. Az ilyen és ehhez hasonló problémák megoldására jött létre 2001-ben OWASP Antisamy Project, melynek célja nyílt forrású alternatívát, és szabványt kínálni az alkalmazások megvédésére.
Mint minden 3rd party fejlesztés, ez is úgy kezdődik, hogy letöltjük a programkönyvtár lehetőleg legfrissebb verzióját. AntiSamy jelenleg az 1.4.4-es verziónál tart. A dokumentációval ellentétbe a csomag függőségeit nem lehet letölteni, rákeresve a antisamy-required-libs.zip kulcsszavakra találtam egy oldalt. A készítők szerint deprecated ami a zip-be van, de a próba erejéig megteszi, éles használatra össze kell vadászni a függőségeket. A szoftver konfigurációja eléggé bonyolult, hosszas dokumentáció olvasással és tanulással egész biztos össze lehet dobni egy normális konfig XML-t, de szerencsére több nagyobb felhasználó is rendelkezésre bocsátotta saját összeállítását, így nyugodtan mazsolázgathatunk az eBay, MySpace, vagy akár Slashdot beállításaiból.
Az általam készített tesztprogram az alábbi (belecsempészve kis Java 7-et):
import java.io.IOException; import java.net.URL; import java.nio.charset.Charset; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import org.owasp.validator.html.*; public class AntiSamyTest { public static void main(String[] args) { try { URL dirtyHtmlUrl = AntiSamyTest.class.getResource("dirtyHtml.html"); Path dirtyHtmlPath = FileSystems.getDefault().getPath(dirtyHtmlUrl.getFile()); List<String> lines = Files.readAllLines(dirtyHtmlPath, Charset.defaultCharset()); URL configXmlUrl = AntiSamyTest.class.getResource("antisamy-ebay-1.4.4.xml"); Policy policy = Policy.getInstance(configXmlUrl.getFile()); AntiSamy as = new AntiSamy(); CleanResults cleanResult = as.scan(concatString(lines), policy); System.out.println(cleanResult.getCleanHTML()); } catch (IOException | PolicyException | ScanException ex) { throw new RuntimeException(ex); } } private static String concatString(List<String> input) { StringBuilder output = new StringBuilder(); for(String line : input) output.append(line); return output.toString(); } }A dirtyHtml.html fájl tartalma szabvány HTML (HTML, HEAD, BODY). A programot futtatva láthatjuk, hogy a body tartalmán kívül minden HTML taget kidobott az AntiSamy. Azért mondom, hogy minden HTML taget, mert ha van pl. title a headbe, a tartalma bizony ottmarad, tehát csak a sallang kerül ki. Szerintem ez utóbbi működés konfigurálható (fixme).
Az elméleti ismerkedés után ideje valami komolyabb megbizatást adni Samykének. Személy szerint Liferay fejlesztő vagyok, így szinte evidens, hogy erre esett a választásom. A Liferay 6-os verziója óta létezik egy Sanitizers-nek nevezett funkcionalitás, amely, bár még nem teljeskörű, mégis segít az igényes programozónak, a kritikus user inputokat szűrni. A funkcionalitás mint írtam nem teljeskörű, ugyanis egyelőre csak a Blog bejegyzéseket tudjuk kontrollálni out-of-the-box. A dolgunk egyszerű, a portal.properties-ben van egy sanitizer.impl paraméter, amit a portal-ext.properties-ben felül tudunk definiálni. Az alapbeállítás a com.liferay.portal.sanitizer.DummySanitizerImpl osztályra mutat, ami jóformán semmit nem csinál, viszont jó kiindulási pont lehet saját Saniterünk elkészítéséhez. Létezik a Liferaynek beépített osztálya com.liferay.portal.kernel.sanitizer.SanitizerUtil képében, választhatjuk ezt is, de saját megoldást is minden további nélkül.
Miután Blog bejegyzéseinket megvédtük a sokszor óvatlan bloggerektől, sajnos nem dőlhetünk hátra nyugodtan, mivel a Liferayben is, mint minden CMS-ben, nem csak Blogot szerkesztenek a felhasználók, hanem számtalan egyéb módon is lehetőségük van HTML szöveget a rendszerbe juttatni. Mivel "gyári" támogatás még nincs ezen bejegyzésekre, nincs más lehetőségünk, mint hook-ot írni. A Plugin tárolóban van egy "antisamy hook", amely alapján könnyedén megírhatjuk saját kiterjesztésünket. A legjobb módszer un. Model Wrapper Hook készítése. Hozzunk létre egy hook-ot, majd a liferay-hook.xml fájlba írjuk be a felülírandó szervíz definícióját
<hook> <service> <service-type>com.liferay.portlet.wiki.service.WikiPageLocalService</service-type> <service-impl>com.test.hooks.SaniterWikiPageLocalService</service-impl> </service> </hook>Majd írjuk meg saját osztályjunkat
import com.liferay.portal.kernel.exception.PortalException; import com.liferay.portal.kernel.exception.SystemException; import com.liferay.portal.kernel.sanitizer.SanitizerUtil; import com.liferay.portal.kernel.util.ContentTypes; import com.liferay.portal.service.ServiceContext; import com.liferay.portlet.wiki.model.WikiPage; import com.liferay.portlet.wiki.service.WikiPageLocalService; import com.liferay.portlet.wiki.service.WikiPageLocalServiceWrapper; public class SaniterWikiPageLocalService extends WikiPageLocalServiceWrapper { public SaniterWikiPageLocalService(WikiPageLocalService wikiPageLocalService) { super(wikiPageLocalService); } public WikiPage addPage( long userId, long nodeId, String title, double version, String content, String summary, boolean minorEdit, String format, boolean head, String parentTitle, String redirectTitle, ServiceContext serviceContext) throws PortalException, SystemException { String sanitizedContent = SanitizerUtil.sanitize( serviceContext.getCompanyId(), serviceContext.getScopeGroupId(), userId, WikiPage.class.getName(), 0, ContentTypes.TEXT_HTML, content); return super.addPage(userId, nodeId, title, version, sanitizedContent, summary, minorEdit, format, head, parentTitle, redirectTitle, serviceContext); } }Bár én a Wiki-t választottam példának, ez alapján bármely más szervízre megírható a szűrés.
2011. szeptember 24., szombat
Liferay Portál OSGi bundle támogatással
Liferay és OSGi témák már többször is előkerültek, de most egy olyan egyedülálló kombináció bemutatását tűztem ki célul, mely egyesíti a két platform minden előnyét. A bemutatott módszer aktív fejlesztés alatt áll, éles használatát nem ajánlom senkinek. A fejlesztés Raymond Augé keze alatt történik, jelenleg a teljes kódbázis branch repóból érhető el. A cikk megírását az érdeklődés ihlette, és a remény, hogy másoknak is megtetszik a kezdeményezés, és programozók tucatjai állnak be Ray mögé, hogy minél előbb core feature legyen a dolog.
Első lépésként szükségünk lesz a módosított Liferay forrására, amit itt tudunk beszerezni. A letöltött forrást egyszerűen tömörítsük ki valahová. A fordításhoz egy Apache Antot is be kell üzemelnünk, ennek lépéseit nem részletezném. A kibontott Liferay gyökérében találunk egy app.server.properties állományt, melyben tudjuk konfigurálni, hogy milyen konténert szeretnénk használni. Amennyiben valami "különleges" igény nem szól közbe, én az Apache Tomcat verziót preferálom, ez egyébként az alapértelmezett. A properties fájlban 7.0.21-es verzió van beállítva, letöltés után a Liferay forrásunkkal párhuzamosan lévő bundles könyvtárba csomagoljuk is ki. Vagy az app.server.properties-ben írjuk át az útvonalat, vagy az apache-tomcat-7.0.21 könyvtárat nevezzük át tomcat-7.0.21-re.
A fordításhoz Antunkat is meg kell kicsit piszkálni. Először is az ecj.jar-t be kell másolni az Ant libjei közé (vagy szimlinkelni, vagy egyéb úton a classpath-ra tenni). Az ecj.jar beszerezhető a Plugin SDK lib könyvtárából. Be kell állítanunk az Ant memóriahasználatát is az alábbi módon.
ANT_OPTS="-Xmx1024m -XX:MaxPermSize=256m" export ANT_OPTSNincs más hátra, mint elindítani a fordítást a Liferay gyökérkönyvtárában.
ant all
A fordítás befejeztével a ../bundles/tomcat-7.0.21/bin/startup.sh parancs futtatásával kelthetjük életre friss Liferayünket. Miután elindult, a Control Panel Server szekcióban találunk egy OSGi Admin menüpontot. Belépve láthatjuk, ha minden jól ment, hogy van egy OSGi System Bundle nevű batyu, ami aktív, tehát az OSGi konténer fut rendben.Az általam próbált verzióba Eclipse Equinox 3.7 volt integrálva, Ray elmondása szerint minden >=4.1-es implementációval változtatás nélkül működik.
Amit a rendszer tud jelenlegi állás szerint:
- Az adminisztrátor portleten keresztül lehetséges OSGi batyuk hozzáadása, eltávolítása, frissítése, elindítása, újraindítása, és leállítása.
- Egy saját deploy listenerrel a deploy könyvtárba másolt batyukat a Liferay megpróbálja telepíteni és elindítani.
Most, hogy működik a portálunk, nincs más dolgunk, mint nekiállni első OSGi Servletünk megalkotásához. A batyu készítésről itt írtam részletesen, így az alapokba nem is mennék bele. Hozzunk létre egy OSGi Plugin Projectet. A MANIFEST.MF állomány tartalma legyen a következő:
Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: OSGiOnLiferay Bundle-SymbolicName: OSGiOnLiferay;singleton:=true Bundle-Version: 1.0.0.qualifier Bundle-ActivationPolicy: lazy Bundle-RequiredExecutionEnvironment: JavaSE-1.6 Import-Package: javax.servlet;version="2.5.0", javax.servlet.http;version="2.5.0", org.eclipse.equinox.http;resolution:=optional, org.osgi.framework;version="1.6.0"Hozzunk létre a gyökér-könyvtárban egy plugin.xml-t, amelybe regisztráljuk a Servletünket.
Végezetül írjunk egy egyszerű Servletet.
public class TestServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { PrintWriter out = resp.getWriter(); out.print("Java'nother blog"); out.close(); } }A plugin buildelése után egyszerűen másoljuk be a kapott JAR-t a deploy könyvtárba. A Liferay logot nézve az alábbi sor jelöli a sikeres telepítést.
12:14:29,798 INFO [AutoDeployDir:167] Processing sample.jarAz admin felületre látogatva remélhetőleg meg is találjuk batyunkat az OSGi Admin felületen, nincs más dolgunk, mint aktiválni.
És itt jön a szomorú, ám reményteli vég. Mint említettem eléggé fejlesztési fázisban van a projekt, ami sajnálatosan azt is jelenti, hogy ezen a ponton nem tudunk túljutni. Ray a közeljövőben publikál egy birdge Servletet, aminek a segítségével a Liferay a HTTP kéréseket az OSGi HttpService rétegén keresztül bármely, a keretrendszerbe regisztrált Servletnek vagy JSPnek továbbítani tudja. Tervezi a Plugin SDK bővítését is, hogy OSGi batyukat lehessen vele kényelmesen fejleszteni. Véleményem szerint ígéretes a kezdeményezés, hiszen a Liferay egy jól gyúrható platform, az OSGi, pedig égy végtelenül rugalmas keretrendszer.
2011. augusztus 27., szombat
Validálás, ahogy én csinálom
Egy programozó életében rendkívül gyakran előfordul, hogy entitásokat kell validálnia. Lényegében az üzleti logika mindig úgy kezdődik, hogy a bemeneti paramétereket, melyek legtöbbször felhasználói interakció által kerülnek be, validálni kell, hogy a hibás entitások a későbbiekben ne okozzanak hibát a program működésébe. A validálás az a dolog, amit egy kicsit mindenki másként csinál, mindenkinek megvan a saját módszere. Ebben az írásban bemutatnám, én hogyan is szoktam ezt a problémát orvosolni. A bemutatott megoldás nem tökéletes, de remélem gondolat-ébresztőnek megteszi.
A validálás alapproblémája szerintem a következő: az entitások validitását az üzleti réteg végzi el, amelyhez különböző kliensek kapcsolódnak. A különböző kliensek különböző nyelveken, és módon kívánják megjeleníteni a hibaüzeneteket, tehát az üzleti rétegbe írt return "Hibás a név!" és hasonlók megengedhetetlenek (később úgyis kiderül, hogy többnyelvű lesz a megjelenítés). A következő probléma, amivel szembesülünk, hogy egyszerre több hibát is tudni kell kezelni, hiszen egy bonyolult form kitöltésekor nem szerencsés egyesével untatni a felhasználót a hibák közlésével, meg egyébként sem tudjuk klienseink miként működnek. A nyelvi problémát orvosolni viszonylag egyszerű, vezessünk be jól dokumentált hibakódokat, klienseink, meg feldolgozzák saját belátásuk szerint. Az egyidejű több hibajelzés kérdésköre már egy kicsit rafináltabb dolog. A legegyszerűbb megoldás, egy hibakódokkal feltöltött tömb, vagy Collection visszaadása. Ezzel a megoldással a legnagyobb problémám, hogy rengeteg favágó programozással jár. Ha mindenhová, mind kliensben mind az üzleti rétegben kézzel írogatjuk a hibakódokat, akkor a hibaszázalékunk igencsak megnő, mert óhatatlanul elgépeljük valahol a kódot. Ha bevezetünk konstansokat, vagy Enumokat, akkor legalább a hibákat kiküszöböljük, de a ThisIsMyEntity.ERROR_NAME_REQ gépelésébe fogunk megőszülni.
Nekem a következő megoldás vált be: mivel 2db kettő hatványa sosem lesz egy harmadik kettő hatványával egyenlő, a hibakódot adjuk vissza kettő hatványaival. Tehát, ha hiányzik a név adjunk vissza 2-t, amennyiben a lakcím hiányzik adjunk vissza 4-et, ha pedig mindkettő hiányzik adjunk vissza 6-ot. A metódus implementációját személy szerint magába az Entitást megvalósító osztályba szoktam tenni, de ez ízlés dolga. Van aki szerint az Entitásnak csak egy DTO-nak kell lennie, és a validitás eldöntése az üzleti logika feladata. Véleményem szerint egy entitás tudja leginkább magáról eldönteni, hogy megfelelőek-e a benne foglalt adatok.
public int validate() { int valid = 0; if (foo == null || foo.isEmpty()) valid |= 1 << 1; if (bar == null) valid |= 1 << 2; if (param == null || param.isEmpty()) valid |= 1 << 3; return valid; }Mivel későbbiekben is bitműveletekkel fogunk operálni, elegánsabbnak tartottam bitműveletekkel megoldani az összeadást, természetesen dolgozhatunk "hagyományos" számokkal is.
Kliens oldalon a hibakód lekezelése pofon egyszerű. Kiválasztunk egy tetszőleges 2 hatványt (érdemes a dokumentációban is említettet választani :)), mondjuk a 4-et, és ellenőrizzük, hogy adott bit, 2^2 az 100, tehát a harmadik, egyezik-e a visszakapott érték harmadik bitjével.
boolean somethingIsValid = (4 & entity.validate()) == 4;Ez a módszer sem tökéletes, hiszen a hibákat csak bővíteni lehet, egyéb változtatás esetén már meglévő klienseink fognak hibásan működni. A teljes példaprogram az alábbi:
public class ValidatorTest { private static final Map<Integer, String> ERRORS = new HashMap<Integer, String>(); static { ERRORS.put(2, "Foo is required!"); ERRORS.put(4, "Bar is required!"); ERRORS.put(8, "Param is required!"); } public static void main(String[] args) { List<Entity> entities = Arrays.asList(new Entity[] { new Entity(null, null, null), new Entity("foo", null, null), new Entity("", null, "param") }); for(Entity entity : entities) { System.out.println(entity); int valid = entity.validate(); for(Integer code : ERRORS.keySet()) { if ((code & valid) == code) { System.out.println(ERRORS.get(code)); } } } } static class Entity { private String foo; private Integer bar; private String param; public Entity(String foo, Integer bar, String param) { this.foo = foo; this.bar = bar; this.param = param; } public int validate() { int valid = 0; if (foo == null || foo.isEmpty()) valid |= 1 << 1; if (bar == null) valid |= 1 << 2; if (param == null || param.isEmpty()) valid |= 1 << 3; return valid; } } }
Nagyon kíváncsi vagyok a véleményetekre, illetve, hogy nektek milyen módszer vált be ebben a témakörbe.
2011. augusztus 15., hétfő
Collections.emptyList() és a generikus típuskényszerítés
A Collections osztály, mint nevéből is következik, a Collection példányok manipulálására, és előre definiált Collection-ök hozzáféréséhez nyújt kényelmes eszközt. Az osztály részleteibe nem szeretnék belemenni, ezt az olvasóra bízom. Kiszemelt áldozatom az emptyList metódus. A metódus forrását megnézve láthatjuk, hogy nem csinál egyebet, mint az osztályban statikus paraméterként deklarált nyers típusú EMPTY_LIST-et adja vissza a kívánt típusra alakítva. Természetesen ez a lista egy változtathatatlan lista, tehát elemeket ebbe nem tudunk tenni. Használata végtelenül egyszerű.
List<String> stringList = Collections.emptyList();A fordító megpróbálja kitalálni, hogy milyen típusra kell alakítani az EMPTY_LIST példányt, teszi ezt úgy, hogy megnézi milyen típusba szeretnénk belepasszírozni a listát. Az esetek többségében sikerül is ez a művelet, ám mit tehetünk a következő esetben:
public static void bar() { foo(Collections.emptyList()); } public static void foo(List<String> stringList) {}Már az IDE is jelzi, hogy ez így nem lesz jó, nem tudja megállapítani milyen típusra kell alakítani a listát. Netbeans fel is ajánlja, hogy létrehoz egy foo(List<Object> stringList) metódust, de mi ezt nem szeretnénk. Egyik lehetőségünk, hogy a paramétert egy lokális változóba kimozgatjuk, és a helyes típussal adjuk át a metódusnak. Ez a megoldás célra vezető, viszont feleslegesen növeli kódunk méretét. A következő eshetőség, hogy a nyers EMPTY_LIST-et adjuk át paraméterként, viszont így hazavágjuk a típusbiztonságot. Nyers típusoknál a JVM egyszerűen levágja a típusellenőrzést. Ennek oka a visszafelé kompatibilitás, a generikusok megjelenése óta nem javallott a nyers típusok használata. Az elegáns megoldás egyszerű, segítsük a fordítót, és mondjuk meg neki milyen típusra alakítsa a listát.
foo(Collections.<String>emptyList());Az itt említett módszer nem a Collections osztály sajátossága, alkalmazható minden generikus metódusra. Választásom azért esett erre az osztályra, mert ezt eléggé gyakran használja mindenki , aki Java-ban programozik, és része a Java SE-nek.
2011. április 28., csütörtök
JDK7 újdonság: java.util.Objects
- compare(T a, T b, Comparator c):int - Mint a neve is mutatja objektumokat tudunk vele összehasonlítani, az egyetlen érdekessége, hogy Objects.compare(null, null) visszatérési értéke 0
- equals(Object a, Object b):boolean - Szintén zenész Objects.equals(null, null) igaz értékkel tér vissza
- deepEquals(Object a, Object b):boolean - Igazzal tér vissza, ha a két objektum "mélységében" egyenlő, minden más esetben hamis. Szintén elmondható, hogy Objects.deepEquals(null, null) igaz. Amennyiben mindkét átadott paraméter tömb, a metódus meghívja az Arrays.deepEquals-t ellenkező esetben az első objektum equals metódusának adja paraméterül a második paramétert
- hashCode(Object o):int - Visszatér az átadott objektum hashCode-jával, null paraméter esetén 0-val
- hash(Object... values):int - Az Arrays.hashCode(Object[]) mintájára az átadott paramétereknek kiszámolja a hashCode-ját. Gyakorlati haszna leginkább a DTO osztályoknál mutatkozik meg, ahol több objektum összessége alkot egy egységet.
public class Student { private Integer id; private Mother mother; private Father father; ... @Override public int hashCode() { return Objects.hash(id, mother, father); } }
- requireNonNull(T obj):T - Ez a metódus egy egyszerű validáció, amely NullPointerException-t dob, amennyiben az átadott referencia értéke null. A metódusnak átadhatunk még egy String paramétert, ekkor ez lesz a kivétel szövege
- toString(Object o): String - Az átadott objektum toString metódusával tér vissza, null érték esetén "null" Stringgel. Második paraméternek egy String is átadható, ez a paraméter lesz a null érték toString-je
logger.info(Objects.toString(bar, “Bar is null”));
2011. április 1., péntek
Liferay "gyári" Servicek és a DynamicQuery használata
DynamicQuery query = DynamicQueryFactoryUtil .forClass(User.class, PortalClassLoaderUtil.getClassLoader()); query.add(PropertyFactoryUtil.forName("email").eq("foo@bar.hu")); query.addOrder(OrderFactoryUtil.asc("name")); List fullResult = UserLocalServiceUtil.dynamicQuery(query); List someResult = UserLocalServiceUtil.dynamicQuery(query, start, end); long resultsNo = UserLocalServiceUtil.dynamicQueryCount(query);Fontos tudnunk, hogy LR osztályok 2 fő csoportba sorolhatók. Az egyik a portal-impl, amely kívülről nem érhető el, ezek a LR saját belső osztályai, és a portal-service, amik pedig a kifelé ajánlott szolgáltatások. Sajnos nem sikerült a szétválasztást tökéletesen megvalósítani, legalábbis szerintem, így ha a PortalClassLoaderUtil.getClassLoader() paramétert nem adjuk meg a DynamicQueryFactoryUtilnak, akkora ClassNotFoundException-t kapunk mint a ház :). Ezt az egy "apró" részletet leszámítva viszonylag jól dokumentált a DynamicQuery használata, és az alapvető műveletekre elégséges.
2011. január 29., szombat
Liferay FriendyUrlMapping vs. IPC
A portletek világában sajnos állandó problémát jelent a felhasználóbarát URL-ek kezelése. Ennek oka, hogy egy portletnek két fázisa létezik, az egyik a render-fázis, a másik pedig az action-fázis. Álltalános eset, hogy amíg egy portlet action-fázisban van, addig a többi render-fázisba. A böngészőből átvitt adatoknak a szerver felé tartalmazniuk kell valamiféle hivatkozást az adott portlet-példányról, hogy a portlet-konténer el tudja dönteni kinek is címezték a kérést. Paraméterben szokott szerepelni a portlet nézet beállítása (minimalizált, normál, maximalizált, exclusive, ez utóbbi liferay specifikus), valamint az életciklusra, és portlet módra (view|edit|etc) vonatkozó adat. Ennek eredménye valami ehhez hasonló URN: ?p_p_id=searchaction_WAR_fakeportletshoppingportlet&p_p_lifecycle=1&p_p_state=normal&p_p_mode=view&p_p_col_id=column-2&p_p_col_count=1&_searchaction_WAR_fakeportletshoppingportlet_javax.portlet.action=search. A probléma megoldására a Liferay többféle megoldást is kínál, de erről később. A probléma másik része, hogy a portlet-szabvány második verziója óta lehetőség van portletek közötti kommunikációra, röviden IPC-re. Az IPC lényege tömören, hogy az egyik portletünk kivált, publikál egy eseményt, míg egy vagy több másik portlet, amelyek előzőleg feliratkoztak az eseményre, elkapják azt. Az eseményben lehetőség van adatok átpasszíroázára is, így válik teljessé a kommunikáció. A dolog hátulütője, hogy kiváltani eseményt csak action fázisban lehet, és a kiváltott eseményt a másik portlet render fázisának egy bizonyos szakaszában képes elkapni, tehát érezhetően nem teljes a fejlesztői szabadság. Összegezve a problémát egy friendly url-en (továbbiakban FU) "figyelő" portletnek kell jeleznie állapotáról egy másik portletnek.
IPC kialakítása
A megvalósításhoz nincs más dolgunk, mint bejegyezni a portlet.xml-ben egy globális eseményleírást, ezzel rögzítve magát az eseményt.<event-definition> <qname xmlns:x="http://jpattern.blogspot.hu/events">x:ipc.myEvent</qname> <value-type>java.lang.String</value-type> </event-definition>Ezzel művelettel a http://jpattern.blogspot.hu/events névtéren bejegyeztünk egy ipc.myEvent eseményt.
Ezután mindkét portletünket szerepkörüknek megfelelően felvértezzük a kommunikációval.
<supported-publishing-event> <qname xmlns:x="http://jpattern.blogspot.hu/events">x:ipc.myEvent</qname> </supported-publishing-event> <supported-processing-event> <qname xmlns:x="http://jpattern.blogspot.hu/events">x:ipc.myEvent</qname> </supported-processing-event>Az esemény publikálása az alábbi módon történik:
QName qName = new QName("http://jpattern.blogspot.hu/events", "ipc.myEvent"); response.setEvent(qName, "param"); //ActionResponseAz esemény elkapásához portlet osztályunkban kell delegálnunk egy metódust:
@ProcessEvent(qname = "{http://jpattern.blogspot.hu/events}ipc.myEvent") public void myEvent(EventRequest request, EventResponse response) { Event event = request.getEvent(); String param = (String) event.getValue(); response.setRenderParameter("param", param); }Ezek után a JSP-ben nincs más dolgunk mint kiszedni a requestből a paramétert:
renderRequest.getParameter("param");Ennyi a varázslat, kipróbálásához házi feladat a portlet taglib actionUrl használata.
A FriendlyUrlMapping
Az említett több lehetőség közül nekem szimpatikusabb volt az XML szerkesztgetésnél, hogy létrehozok egy osztályt, amely beállítja a portletet az URL alapján. Az osztállyal szemben támasztott követelmény, hogy a com.liferay.portal.kernel.portlet.BaseFriendlyURLMapper osztály leszármazottja legyen.public class MyFriendlyUrlMapper extends BaseFriendlyURLMapper { private static final String MAPPER = "mappingstring"; //erre az URL darabra fog aktiválódni a mapper @Override public void populateParams(String friendlyURLPath, Map<String, String[]> params, Map<String, Object> requestContext) { addParameter(params, "p_p_id", getPortletId()); addParameter(params, "p_p_lifecycle", "1"); //ez a paraméter kényszeríti action-fázisba a portletet addParameter(params, "p_p_mode", PortletMode.VIEW); addParameter(params, "p_p_state", WindowState.NORMAL); addParameter(getNamespace(), params, "javax.portlet.action", "actionMethod"); //Ez az action-metódus fog meghívódni } @Override public boolean isCheckMappingWithPrefix() { return false; } @Override public String getMapping() { return MAPPER; } @Override public String buildPath(LiferayPortletURL portletURL) { return null; } }Miután elkészítettük az osztályt, be kell jegyeznünk a liferay-portlet.xml fájlban a megfelelő portletnél azt:
<friendly-url-mapper-class>com.blogger.jpattern.MyFriendlyUrlMapper</friendly-url-mapper-class>Érdemes megjegyezni, hogy a FU kezelés a LR-ben úgy működik, hogy felveszünk egy oldalt, pl.: /oldal, amire kihelyezzük a portletet, és a LR /oldal/mappingstring URL esetén aktiválja a Mapper osztályt.
Amennyiben a portletünket render fázisban szeretnénk használni p_p_lifecycle=0, nincs más dolgunk, de mint említettem IPC esemény kiváltására csak action-fázisban van lehetőség. A LR-ben ki kell kapcsolnunk az "Auth token check"-et, ami annyit tesz, hogy minden action típusú kéréshez a LR generál egy egyedi azonosítót, és ennek hiányában nem enged hozzáférést az erőforráshoz. Nyilván FU használata esetén nem rendelkezünk ezzel az azonosítóval, ezért a portal-ext.properties fájlban vegyük fel a "auth.token.check.enabled=false" paramétert.
2010. november 28., vasárnap
Liferay Service Builder
Első lépésként, ha még nem tettük volna, hozzunk létre egy New -> Liferay Plug-in Projectet kedvenc Eclipse fejlesztői környezetünkben. Eclipshez létezik egy Liferay IDE nevű plugin, ami azért nagyban megkönnyíti a fejlesztést, természetesen a Liferay SDK-t is használhatjuk, én a pluginra támaszkodom. Következő dolog amit létre kell hoznunk, az egy New -> Liferay Service. A beállító-panelen állítsuk be a Plug-in Projectet, Package path paraméternek adjuk meg a service réteg ős-csomagjának nevét, ez tetszőleges, Namespace paraméternek a tábla prefixet tudjuk megadni, és végül saját nevünket. A Namespace paraméter üresen is hagyható jelentősége annyi, hogy mivel az érintett táblákat a Liferay alap értelmezetten saját táblái mellé hozza létre könnyebb átlátni, ha prefixeljük a táblaneveket. Ha végeztünk találunk egy service.xml állományt a docroot/WEB-INF mappában. Dolgunk mindössze annyi, hogy XML formában rögzítsük Modelljeink struktúráját.
<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 6.0.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_6_0_0.dtd"> <service-builder package-path="hu.jpattern.model"> <author>mhmxs</author> <namespace>library</namespace> <entity name="Book" local-service="true" remote-service="false"> <column name="bookId" type="long" primary="true" /> <column name="title" type="String" /> <column name="author" type="Collection" entity="Author" mapping-key="authorId" /> <column name="description" type="String" /> <column name="price" type="long" /> <column name="groupId" type="long" /> <column name="companyId" type="long" /> <order by="asd"> <order-column name="title" /> <order-column name="price" order-by="desc" /> </order> <finder return-type="Collection" name="title"> <finder-column name="title" comparator="LIKE" /> </finder> </entity> <entity name="Author" local-service="true" remote-service="false"> <column name="authorId" type="long" primary="true" /> <column name="name" type="String" /> <column name="groupId" type="long" /> <column name="companyId" type="long" /> </entity> </service-builder>Az XMl-ben az entity tag írja le az entitásokat. Először is egy kötelező név attribútumot kell megadnunk, majd a szerviz hívására vonatkozóakat. A példában csak a lokális Servicekkel foglalkozok, de tudni érdemes, hogy létezik remote-service attribútum is, alapértelmezett true!!, mely SOAP szolgáltatásokat valósít meg a Liferayyel együtt szállított Axison keresztül. Következnek a mezőleírások, melyek típusa primitív, egyszerű adat típus (String), vagy Date lehet. A primary="true" paraméterrel a PK-t határozhatjuk meg, értelemszerűen entitásonként legfeljebb 1-et. Lehetőségünk van entitások közötti kapcsolat létrehozására is, erre példa a Book author mezője, ahol a kapcsolat típusát, módját kell megadni, és az összekapcsolás kulcsát. A mapping-key OTM (@OneToMany) a mapping-table pedig MTM (@ManyToMany) kapcsolatot hoz létre entitásaink között. Liferay "poliszi", hogy nem használnak idegen kulcsokat az adatbázisban, minden kapcsolatot Java kódból kell kezelni, ezért a Service Builder sem támogatja ezt a dolgot. Személy szerint nekem ez roppant unszimpatikus, hiszen rengeteg hibát rejt magában a koncepció, a fejlesztők emberek, és óhatatlan, hogy bonyolultabb adat-struktúra esetén valami mégis kimarad a kódból. Az egyetlen "elfogadható" érv a Liferay döntése mellett, hogy így tudják biztosítani a minél szélesebb adatbázis-paletta támogatását. Érdemes felvenni a groupId és companyId mezőket, mert a Liferay ezen mezők szerint fogja tudni összekötni entitásunkat saját jogosultság-kezelő rendszerével. A Service Builder lehetőséget ad egy rendezési elv meghatározására az order mező megadásával, példánkban cím szerint ASC, és ár szerint DESC rendezést valósítunk meg. Sajnos order-ből egyetlen egy szerepelhet. Kereséseket könnyítendő meghatározhatunk előre generált kereső metódusokat a finder mezővel, ez eléggé hasznos, ugyanis tetszőleges számú lehet, és rögtön megadható a comperatorlt, =, !=, <, <=, >, >=, és LIKE lehet, értelemszerűen a = az alapértelmezett. Bár a példában nem szerepel, létezik egy tx-required mező, melynek szöveges tartalmában kell azon metódusokat felsorolni, amikre szeretnénk tranzakciókezelést használni. Ennek értéke alapértelmezetten add*, check*, clear*,
delete*, set*, és update*, így az alap Service metódusok le is vannak vele fedve. További lehetőségekről a liferay-service-builder_6_0_0.dtdből tájékozódhatunk :). Végezetül a service.xml szerkesztőjében nyomjuk meg a Build services gombot, aminek hatására az IDE legenerálja a szükséges osztályokat.
A létrejött struktúrát megvizsgálva az alábbi osztályok "érdekesek" számunkra:
- hu.jpattern.model.service.base.xxxLocalServiceBaseImpl (xxx az adott entitás osztály) ez az osztály tartalmazza az alap szerviz metódusainkat. Ezt az osztályt ne módosítsuk!
- hu.jpattern.model.service.impl.xxxLocalServiceImpl osztályban tudjuk megvalósítani saját szerviz metódusainkat, melyek nincsenek benne a xxxLocalServiceBaseImpl osztályban. Fontos tudni, hogy az ebben az osztályba írt publikus metódusokat a Service Builder beleteszi az interfészbe következő buildeléskor.
- hu.jpattern.model.model.impl.xxxModelImpl az alap entitásunkat reprezentálja. Ezt az osztályt ne módosítsuk, és ne szerezzünk rá referenciát közvetlenül!
- hu.jpattern.model.model.impl.xxxImpl tartalmazza az entitás egyéb, általunk megírt metódusait. Ezt az osztályt szintén beolvasztja a következő build futtatásakor a Liferay.
- Egy állományt emelnék még ki a többi közül, mégpedig a docroot/WEB-INF/lib könyvtárban létrejött ?.service.jar-t, mely az interfészeket és a szerviz "végleges" osztályait tartalmazza. Ezekkel a fejlesztőnek nem kell foglalkoznia.
BookLocalServiceUtil.getBooksCount();
2010. szeptember 1., szerda
Forrásgenerálás CodeModel segítségével
Minden programozó életében egyszer elérkezik a pont, amikor valamilyen előre egyeztetett séma alapján forrás-álloményokat, azaz működő Java osztályokat kell generálnia. Jelenleg is számtalan eszközzel tudunk forrást generálni, gondoljunk csak az adatbázisból létrejövő entitásokra, vagy egy WSDL alapján generált osztály-struktúrára, sőt a legtöbb IDE alapértelmezetten segítséget nyújt ezen a területen, képes konstruktort, getter-setter metódusokat, stb készíteni pár kattintással. Ha saját magunknak szeretnénk készíteni egy forrás-generátort, természetesen arra is megvan a lehetőség. A JAXB-nek van egy al-projektje, a CodeModel, aminek segítségével megoldható a probléma, ha van elég kitartásunk megérteni a mikéntjét, ugyanis dokumentálva mondhatni egyáltalán nincs az eszköz. Rövid írásom célja, hogy ízelítőt adjon a CodeModel lehetőségeiből, ezért kézenfekvőnek tűnik, hogy ismerkedés gyanánt készítsünk egy olyan generátort, ami átadott paraméter-lista alapján összeállít egy DTO osztályt. Először is szükségünk lesz a jaxb-xjc csomagra, ugyanis ez tartalmazza a szükséges osztályokat. Magam részéről a DTO típusú osztály-generátort egy saját osztályba csomagoltam.
import com.sun.codemodel.JAnnotationUse; import com.sun.codemodel.JClassAlreadyExistsException; import com.sun.codemodel.JCodeModel; import com.sun.codemodel.JDefinedClass; import com.sun.codemodel.JDocComment; import com.sun.codemodel.JExpr; import com.sun.codemodel.JMethod; import com.sun.codemodel.JMod; import java.io.Serializable; import java.util.Map; /** * @author mhmxs */ public final class DTO { private final String name; private final Map<String, Class> parameters; public DTO(String name, Map<String, Class> parameters) { this.name = name; this.parameters = parameters; } public JCodeModel generateSource() throws JClassAlreadyExistsException { JCodeModel codeModel = new JCodeModel(); //define class header JDefinedClass clazz = codeModel._class(name); clazz._implements(Serializable.class); JAnnotationUse annotation = clazz.annotate(SuppressWarnings.class); annotation.param("value", "serial"); //Add Java-doc to class header JDocComment jDocComment = clazz.javadoc(); jDocComment.add("Simple DTO class : " + name + "\n"); jDocComment.add("@author mhmxs"); //Create constructor JMethod constr = clazz.constructor(JMod.PUBLIC); //Generate getter and setter methods for all parameters JMethod method; for(String param : parameters.keySet()) { //Add parameter to class declaration clazz.field(JMod.PRIVATE, parameters.get(param), param); //Add parameter to constructor constr.param(parameters.get(param), param); constr.body().directStatement("this." + param + " = " + param + ";"); String methodName = param.replaceFirst(String.valueOf(param.charAt(0)), String.valueOf(param.charAt(0)).toUpperCase()); //Create getter method method = clazz.method(JMod.PUBLIC, parameters.get(param), "get" + methodName); method.body()._return(JExpr._this().ref(param)); //Create setter method method = clazz.method(JMod.PUBLIC, Void.TYPE, "set" + methodName); method.param(parameters.get(param), param); method.body().directStatement("this." + param + " = " + param + ";"); } return codeModel; } }Az osztály-t az alábbi módon tudjuk meghívni.
Map<String, Class> parameters = new HashMap<String, Class>(); parameters.put("parameter", String.class); parameters.put("parameter2", Integer.class); DTO dto = new DTO("a.b.Clazz", parameters); OutputStream out = new FileOutputStream(new File("/a/b/Clazz.java")); dto.generateSource().build( new SingleStreamCodeWriter(out)); out.close();Végül az eredmény.
package a.b; import java.io.Serializable; /** * Simple DTO class : a.b.Clazz * @author mhmxs * */ @SuppressWarnings("serial") public class Clazz implements Serializable { private String parameter; private Integer parameter2; public Clazz(String parameter, Integer parameter2) { this.parameter = parameter; this.parameter2 = parameter2; } public String getParameter() { return this.parameter; } public void setParameter(String parameter) { this.parameter = parameter; } public Integer getParameter2() { return this.parameter2; } public void setParameter2(Integer parameter2) { this.parameter2 = parameter2; } }
2010. augusztus 19., csütörtök
META-INF könyvtár kicsit közelebbről
A JAR-ra vonatkozó paraméterek
Main-Class
Amennyiben önállóan futtatható JAR-t készítünk, a JVM-nek meg kell adnunk paraméterként, hogy mely osztály "main" metódusa a belépési pont. Mivel ezt körülményes minden indításnál megadni, ezért lehetőségünk van a MF-ben tárolni ezt a beállítást.Main-Class: my.package.startApplication
Class-Path
Az osztály-betöltő ha még nem töltött be egy hivatkozott osztályt, akkor az alapértelmezett útvonalakon elkezdi keresni azt. Ha szerencsések vagyunk, akkor sikerrel jár és az osztály bekerül a memóriába. Amennyiben az alapértelmezettől eltérő útvonalon található az osztály, akkor annak pontos helyet specifikálhatjuk a MF-ben. Lehetőség van több JAR-t is felsorolni szóközzel elválasztva. A fejlesztés könnyítése érdekében helyettesítő karaktereket is használhatunk az útvonal megadásakor.Class-Path: ./lib/first.jar /usr/lib/secound.jar /usr/lib/java/*
Egyéb opcionális paraméterek
A teljeség igénye nélkül.Archiver-Version: Plexus Archiver Created-By: Apache Ant Built-By: Joe Build-Jdk: 1.6.0_04 Signature-Version: 1.0
Bejegyzésekre vonatkozó paraméterek
Name
Lehetőségünk van nevet adnia a JAR-on belül az egyes csomagoknak, osztályoknak vagy erőforrásoknak. A névadás konvenciója, hogy a név értéke a csomag esetén relatív útvonala a fájlstruktúrában, és egy lezáró "/"-karakter, osztály vagy egyéb erőforrás-fájl esetén a teljes relatív elérés. Minden további attribútum ami üres-sor nélkül a név után következik, érvényes lesz a Name-ben hivatkozott elemre.Name: my/package/
Sealed
A névvel ellátott csomagokat opcionálisan le lehet zárni, ami azt jelenti, hogy a lezárt csomagban lévő összes definiált osztály egyazon JAR-ban található. A lezárásnak biztonsági és verziózási okai lehetnek.Name: my/package/ Sealed: true
Package Versioning Specification
Verzió-kezelési specifikációban a csomagnak többféle attribútum is megadható opcionálisan, az egyetlen megkötés, hogy ezen beállítások mindegyikének szintén a Name attributumot kell követniük üres-sor hagyása nélkül.Name: my/package/ Specification-Title: "MF Sample" Specification-Version: "0.1" Specification-Vendor: "jpattern" Implementation-Title: "my.package" Implementation-Version: "build1" Implementation-Vendor: "jpattern"
Content-Type
A Content-Type segítségével az adott elem MIME tipusát határozhatjuk meg.Name: my/package/startApplication.properties Content-Type: text/plain
Java-Bean
A Java-Bean attribútum segítségével adható meg, hogy az adott elem Java Bean vagy sem.Name: my/package/startApplication.class Java-Bean: true
*-Digest
Az aláírással ellátott állományoknál kötelező elemként meg kell adni az állomány hashének base64 dekódolt reprezentációját. Az aláírás készítésről később.Name: my/package/startApplication.class SHA1-Digest: TD1GZt8G11dXY2p4olSZPc5Rj64=
MANIFEST.MF állomány készítése Ant taskból
Természetesen a MF-et nem kell minden alkalommal manuálisan megszerkeszteni, hanem lehetőségünk van Ant taskbol előállítani tartalmát (Mavenből szintén).Aláírás és hitelesítés
A digitális aláírás fogalmát, és lényegét azt hiszem senkinek sem kell bemutatnom. Mivel a Java eléggé fejlett biztonsággal rendelkezik, elengedhetetlen, hogy az egyes csomagokat ne lehessen hitelesíteni. A Java a művelethez szükséges állományokat szintén a META-INF könyvtárban tárolja, tipikusan *.SF, *.DSA, *.RSA és SIG-* fájlokban. A hitelesítés menete tömören:
- Az aláíró egy titkos kulcs segítségével aláírja a JAR-t (pontosabban minden benne lévő állományt).
- A felhasználó a publikus kulccsal ellenőrzi az aláírást.
Aláírás készítése
- Aláírás készítésének első lépése, hogy generálnunk kell egy privát kulcsot.
keytool -genkey -alias jarSigner -keystore storedkeys -keypass passwd -dname "cn=jpatter" -storepass stpasswd
Mint is csinál ez a parancs? Generál egy kulcsot jarSigner névvel, és a keypass jelszóval védetté teszi. A keystore paraméterben megadott adatbázis-állományban eltárolja a kulcsot, mely állomány ha nem létezik a keytool létrehozza azt, és a storepass-ban megadott jelszóval védi. A dname paraméter specifikál egy un. "X.500 Distinguished Name" bejegyzést, a cn paraméterben megadott egyszerűsített névvel. Az "X.500 Distinguished Name azonosítja a bejegyzéseket a X.509 hitelesítéshez, értéke tetszőleges lehet. - Az elkészült JAR-unk aláírásához a jarsigner eszközt tudjuk segítségül hívni.
jarsigner -keystore storedkeys -storepass stpasswd -keypass passwd -signedjar myapp_signed.jar myapp.jar jarSigner
A parancs lényegében kiszedi az előzőekben létrehozott kulcsot, és a bemeneti JAR állomány minden elemén elvégzi az aláírást, a végeredményt pedig a kimeneti JAR-ba teszi. A JAR-ba pillanva máris megjelent a JARSIGNE.SF és JARSIGNE.DSA, melyek közül az előbbi az egyes állományok hitelesítéséért felel, az utóbbi a publikus kulcs. A JARSIGN.SF állományt megnyitva valami hasonló tárul a szemünk elé:
Signature-Version: 1.0 SHA1-Digest-Manifest-Main-Attributes: FPLdz3FFeWLdgX0fUdHTqjUNkpE= Created-By: 1.6.0_20 (Sun Microsystems Inc.) SHA1-Digest-Manifest: AJwzuuyn2mg59fYB2qEhUL0PPgI= Name: my/resources/logo.png SHA1-Digest: oVtyix/BpiM9iq1fG/nMpy4Xy4Q= Name: my/package/startApplication.class SHA1-Digest: TD1GZt8G11dXY2p4olSZPc5Rj64=
A MF állományunk tartalma is automatikusan megváltozott, az SHA1-Digest bejegyzések kerültek bele.
Aláírás hitelesítése
A hitelesítés szintén a jarsigner eszközzel történik.
jarsigner -verify myapp_signed.jareredményként pedig "jar verified" vagy "jar is unsigned" értékeket kaphatjuk.