2010. november 28., vasárnap

Liferay Service Builder

A Liferay az egyedi portletek fejlesztését saját MVC keretrendszerével kívánja élvezetesebbé tenni, az un. Liferay MVC-vel. A keretrendszer legizgalmasabb része a Model-t előállító Service Builder, ennek bemutatására teszek most egy kísérletet. Felépítését tekintve az adatbázis réteg fölött Hibernate biztosítja az entitások adatbázissal való megfeleltetését. A Hibernatet a Liferay saját perzisztencia rétege burkolja be. A perzisztencia réteggel a Servicek tartják a kapcsolatot, melyek lehetnek lokálisak és távoliak egyaránt, és általuk szerezhetünk referenciát Entitásainkra, melyeket a Liferay analógia szerint Modeleknek hívunk. A könnyebb megértést segíti a mellékelt ábra.
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.
Használata pofon egyszerű, az xxxLocalServiceUtil osztály statikus metódusain keresztül érhetjük el a Service réteget.
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 META-INF könyvtáron belül található MANIFEST.MF (továbbiakban MF) állománnyal mindenki találkozik, aki Java binárist készít, vagy felhasznál egy mások által fordított Java forrást. Amikor létrehozunk egy JAR-t vagy valamely módozatát (WAR, EAR), akkor automatikusan belekerül egy példány az állományba, melynek tartalma alapértelmezetten csak a Manifest verziószámát tartalmazza "Manifest-Version: 1.0". A MF állomány JDK 1.2 verziója óta lényegesen egyszerűsödött, korára való tekintettel az ez előtti verzió nem kerül tárgyalásra. A MF állomány kulcs-érték pár alapú metaadat-tárolást valósít meg, és egy JAR-ban (WAR, EAR) csak egy példány lehet belőle, így értelem szerűen az egész JAR-ra, és a benne lévő csomagokra vonatkozó információkat is tárol. Rövid írásom célja, hogy bemutassa a lehetséges alap-beállítások egy részét. Mivel a MF funkcionalitása eléggé széleskörű, kezdve a verzió-követéstől, az elektronikus aláíráson át, a szerzői információkig, egy írásban talán össze sem lehetne foglalni az összes területet, ráadásként az egyes keretrendszerek (Spring, OSGi, stb.) is előszeretettel használják saját csomag-információk tárolására.

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.jar
eredményként pedig "jar verified" vagy "jar is unsigned" értékeket kaphatjuk.

2010. július 30., péntek

OSGi távoli kapcsolódás CoolRMI segítségével

Az előző bejegyzésben láthattuk miként készíthetünk OSGi keret-rendszer alá szolgáltatást, és hogyan kapcsolódhatunk ehhez a szolgáltatáshoz a keret-rendszeren belül. A mostani írás célja, hogy segédletet nyújtson távoli kapcsolat kialakítására, hiszen elengedhetetlen, hogy az OSGi konténeren kívül eső kód is használni tudja a szolgáltatásokat. Választásom a CoolRMI nevű eszközre esett, egyrészt mert magyar fejlesztés, és nyílt-forráskóddal lett publikálva, másrészt mert a környezetem ezt használja, és kipróbált alkalmazás-komponensnek minősült. A CoolRMI két részből tevődik össze. Az egyik fele az OSGi konténerben fut mint szolgáltatás, és várja a beérkező kéréseket, a másik fele pedig az alkalmazásunkban a kapcsolódásért felel.
  • Szerezzük be a CoolRMI legfrissebb verzióját.
  • Indítsuk el az OSGi konténert.
  • Installáljuk majd indítsuk el a CoolRMI-t az OSGi konténerben.
    Valami ilyesmit kell látnunk:

    osgi> ss
    
    Framework is launched.
    
    id      State       Bundle
    0       ACTIVE      org.eclipse.osgi_3.6.0.v20100517
    2       ACTIVE      com.rizsi.coolrmi_1.1.0
    
  • Hozzunk létre egy "Plug-in Project"-et az Eclipsben CoolRMI néven, majd a letöltött jar-ból a MANIFEST.MF-et és a forrás-fájlokat másoljuk a projektbe, végül pedig frissítsük a projektet. (nekem a MANIFEST.MF állományba az alábbi sort fel kellett venni: Import-Package: org.osgi.framework;version="1.3.0")
  • Nyissuk meg az előzőekben service néven futó projektünket, és a MANIFEST.MF állomány Import-Package szekcióját bővítsük a CoolRMI exportjaival.
  • Szerver oldalon a a szolgáltatás regisztrációjában van némi különbség. Jelen esetben nem az OSGi konténerbe kell regisztrálnunk a szolgáltatást, hanem a CoolRMI kontextusába, ezért az Activator.start metódusunkat az alábbira kell alakítani:

    InetSocketAddress addr = new InetSocketAddress(9001);
      
    CoolRMIServer server = new CoolRMIServer(Activator.class.getClassLoader(), addr, true);
      
    //Register a service on the id: "service"
    //This id an be used client side to find the service on this server.
    server.getServiceRegistry().addService(new CoolRMIService("service", SimpleService.class, new SimpleServiceImpl()));
      
    server.start();
    System.out.println("CoolRMI Service started.");
    
  • Fordítsuk és telepítsük a szolgáltatást.
  • Hozzunk létre egy "Java Project"-et, és importáljuk be az imént letöltött jar-t.
  • Tegyük elérhetővé a service.SimpleService interfészt, hogy a kliens programunk tudja milyen metódusokat tud meghívni.
  • A kliens osztály kódja az alábbi:

    import java.net.InetSocketAddress;
    import service.SimpleService;
    import com.rizsi.coolrmi.CoolRMIClient;
    import com.rizsi.coolrmi.ICoolRMIProxy;
    
    public class Caller {
     public static void main(String[] args) throws Exception {
      // Create a client that connects to the server on the specified address.
      CoolRMIClient client = new CoolRMIClient( Caller.class.getClassLoader(), new InetSocketAddress("localhost", 9001), true);
      
      try {
       // Get a proxy for the remote service object
       // To access the remote object the remote interface
       // and the service Id must be known
       ICoolRMIProxy remoteService = client.getService(SimpleService.class, "service");
       
       try {
        SimpleService service = (SimpleService) remoteService;
        service.echo("CoolRMI calling.");
       } finally {
        remoteService.disposeProxy();
       }
      } finally {
       client.close();
      }
     }
    }
  • Futtassuk a klienset, és ellenőrizzük a kimenetet az OSGi konzolon.

    osgi> CoolRMI calling.
    

2010. július 10., szombat

OSGi szárnypróbálgatás

Bár biztosan sokan ismeritek, de legalább hallottatok az Open Services Gateway Initiative-ról, ismertebb nevén az OSGi-ről, gondoltam én is kísérletet teszek rendszer a bemutatására. A témában kutakodva azonban találtam egy rövid írást Paller Gábor kollégától, melyben olyan érthetően megfogalmazza a technológia lényegét, hogy bizton állíthatom nekem sem sikerülne jobban, ezért meg sem próbálom. Az elméleti alapok elsajátítása után a gyakorlati megvalósításra szeretném helyezni a hangsúlyt.
OSGi pluginek fejlesztésére Eclipse IDE-t választottam, mely amellett, hogy ismeri a MANIFES.MF állomány függőségeit kezelni, rendelkezik beépített OSGi konténerrel (Equinox) is. Szerencsére a hazai bloggerek már több ízben is feszegették a témát, ennek köszönhetően pl. a jTechnics hasábjain olvashatunk a különféle konténer-implementációkról.
Megvalósítandó feladatként készítsünk egy bundlet, amely kiajánl egy szolgáltatást, amit egy másik bundeből meghívunk.

A szerviz:

  • Első lépésként hozzunk létre az Eclipsben egy "Plug-in Project"-et service néven, és az "on OSGi framework" opciónál állítsuk be az Equinox-ot.
  • Hozzuk létre a service.SimpleService interfészt:
    package service;
    
    public interface SimpleService {
     public void echo(Object message);
    }
    
  • Majd hozzuk létre service.SimpleServiceImpl néven az szolgáltatás implementációját:
    package service;
    
    public class SimpleServiceImpl implements SimpleService {
    
     @Override
     public void echo(Object message) {
      System.out.println(message);
     }
    }
    
  • Szerkesszük a service.Activator osztályt a következőre:
    package service;
    
    import org.osgi.framework.BundleActivator;
    import org.osgi.framework.BundleContext;
    import org.osgi.util.tracker.ServiceTracker;
    
    public class Activator implements BundleActivator {
    
     @Override
     public void start(BundleContext context) throws Exception {
      context.registerService(SimpleService.class.getName(), new SimpleServiceImpl(), new java.util.Hashtable());
      
      //Check service
      ServiceTracker serviceTracker = new ServiceTracker(context, SimpleService.class.getName(), null);
      serviceTracker.open();
      
      SimpleService simpleLogService = (SimpleService) serviceTracker.getService();
      if(simpleLogService != null)
       System.out.println("Service started.");
      else
       System.out.println("Service init failed!");
      
      serviceTracker.close();
     }
    
     @Override
     public void stop(BundleContext context) throws Exception {  
      System.out.println("Service stopped.");
     }
    }
    
  • A META-INF/MANIFEST.MF állomány szerkesztésével készíthetjük fel bundlet a konténerrel való együttműködésre. Eclipsben az Import-Package szerkesztésével tudjuk a "Plug-in Dependencies"-eket szerkeszteni, az IDE automatikusan importálja az itt megadott csomagokat. Fontos továbbá, hogy az Export-Packagenél kiajánljuk a service csomagot, mert ellenkező esetben a kliens oldalon "class SimpleServiceImpl cannot be cast to class SimpleService" exceptiont fogunk kapni.
    Manifest-Version: 1.0
    Bundle-ManifestVersion: 2
    Bundle-Name: Service
    Bundle-SymbolicName: service
    Bundle-Version: 1.0.0
    Bundle-Activator: service.Activator
    Bundle-RequiredExecutionEnvironment: JavaSE-1.6
    Import-Package: org.osgi.framework;version="1.3.0",org.osgi.util.tracker;version="1.3.1"
    Export-Package: service;version="1.0.0"
    
  • A MANIFEST.MF állomány->jobbklikk->Run As->Run Configuration ablakban tudjuk beállítani a konténert a teszteléshez.
  • Fordításhoz vagy írunk saját ant/maven scriptet, vagy Exportáljuk a bundlet JAR formájában. Szeretném megjegyezni, hogy az Eclipse alapértelmezetten generálja a MANIFEST.MF állományt a jar-ba, viszont ebben az esetben a konténer nem fog tudni mit kezdeni a bundleval, ezért a "Use existing manifest from workspace" opció alatt (utolsó lépés) adjuk meg a megfelelő állományt.

A kliens:

  • Ismét egy hasonlóan paraméterezett "Plug-in Projct"-re lesz szükségünk, de ezúttal caller néven.
  • A caller.Activate osztályt hozzuk az alábbi formára:
    package caller;
    
    import org.osgi.framework.BundleActivator;
    import org.osgi.framework.BundleContext;
    import org.osgi.framework.ServiceReference;
    
    import service.SimpleService;
    
    public class Activator implements BundleActivator {
    
     @Override
     public void start(BundleContext context) throws Exception {
      //Another way to connect a service.
      ServiceReference reference = context.getServiceReference(SimpleService.class.getName());
      SimpleService service = (SimpleService) context.getService(reference);
      
      if (service != null)
       service.echo("Service called.");
      else
       System.out.println("Service call error!");
     }
    
     @Override
     public void stop(BundleContext context) throws Exception {
     }
    }
    
  • A service.SimpleService interfészt mindenképpen tegyük elérhetővé ebben a projektben, az egyszerűség kedvéért hozzuk létre az objektumot.
  • A META-INF/MANIFEST.MF állomány tartalma legyen a következő:
    Manifest-Version: 1.0
    Bundle-ManifestVersion: 2
    Bundle-Name: Caller
    Bundle-SymbolicName: caller
    Bundle-Version: 1.0.0
    Bundle-Activator: caller.Activator
    Bundle-RequiredExecutionEnvironment: JavaSE-1.6
    Import-Package: org.osgi.framework;version="1.3.0",service;version="1.0.0"
    
    Fontos, hogy a szervizben exportált service csomagot itt importáljuk.
  • A szerviznél leírtakkal azonos módon tesztelhetjük és fordíthatjuk a bundlet (nekem voltak problémák, amikor az IDE-ből próbáltam tesztelni a két bundlet együtt).

A konténer:

Utolsó egységként a konténert kell megismernünk, melynek vezérlése eltérő a különböző implementációk esetén. Az Equinoxot választva az alábbiakat kell tennünk.
  • Szerezzük be a nekünk megfelelő verziót, jelenleg a 3.6-os a legfrissebb.
  • A `java -jar org.eclipse.osgi_3.6.0(.*?).jar -console` parancs futtatásával indíthatjuk el a konténert.
  • Installáljuk a bundlekat:
    osgi> install file:///path/to/file/service.jar
    Bundle id is 6
    
    osgi> install file:///path/to/file/caller.jar
    Bundle id is 7
    
    Eltávolítani az uninstall paranccsal, frissíteni pedig az updatetel lehet.
  • Az ss paranccsal lekérdezhetjük a telepített bundlek állapotát, mindkettőnek INSTALLED állapotban kell lennie:
    osgi> ss
    
    Framework is launched.
    
    id      State       Bundle
    0       ACTIVE      org.eclipse.osgi_3.6.0.v20100517
    6       INSTALLED   service_1.0.0.qualifier
    7       INSTALLED   caller_1.0.0.qualifier
    
  • Utolsó lépésként indítsuk el mindkét bundlet:
    osgi> start service
    Service started.
    
    osgi> start caller
    Service called.
    
    A bundle neve helyett használhatjuk az azonosítóját.
  • A stop paranccsal állítjuk le a bundlekat:
    osgi> stop service
    Service stopped.
    

2010. június 24., csütörtök

Barátkozás a Liferay Portlettel, telepítés

Az utóbbi időben sajnos, vagy inkább szerencsére több időt szántam a tanulásra, mint az írásra. Egy munka-hely váltás kapcsán  találkoztam a Liferay Portletek téma-körével, és mivel hosszas küzdelem árán sikerült beüzemelni a dolgot, gondoltam talán másoknak is hasznos lehet, ha lejegyzem.
A Liferay egy Java alapú, nyílt forrás-kódú, "Enterprise" CMS rendszer, ami annyiba tér el más hasonló alkalmzásoktól, hogy a tartalmat Portletek segítségével állítja elő. A Portletek lényegét röviden úgy foglalhatnánk össze, hogy az egyes hagyományos értelembe vett modulok külön életet élnek, és egy HTML oldal legenerálása a szerveren nem a megszokott módon történik, hanem a Portlet konténer, a megjelenítendő HTML-t "össze-ollózza" a Portletektől, azaz a Portleteket megkéri, hogy az állapotuknak megfelelő HTML kimenetet biztosítsák számára. Portlet konténerből számtalan implementáció létezik, ki jobban, ki kevésbé tér el a specifikációtól, a Liferay fejlesztői úgy döntöttek, hogy saját megoldást dolgoznak ki, így született meg a Liferay Portlet. Számomra a megismerést nagyban nehezítette a név-választás, ami egyébként logikus, ugyanis a netet böngészve, linkre kattintva nehéz hirtelen eldönteni, hogy vajon a portálról, vagy a portletről van éppen szó.
  • A tanulás/fejlesztés első lépése, hogy letöltjük a conténert. Bár széles palettán válogathatunk a társított szerverek terén - Tomcat, JBoss, Glassfish...-, mégiscsak a Liferay fejlesztők száj-íze, és verziói szerint kell dolgoznunk. Személy szerint nekem nem szimpatikus, hogy csak egyben tudjuk beszerezni a web-konténert, a Portlet-konténert, és ráadásként a Liferay Portalt, találkoztam olyan Portlet-konténerrel, amely meglévő alkalmazás-szerverünket frissítette, meghagyva a választás szabadságát, illetve futó alkalmazásainkat.
  • Kicsomagolás közben bőven lesz időnk tanulmányozni a dokumentációt.
  • Miután elindítottuk a szervert azonmód elérhető a Lifery Portal rendszer (jó esetben a http://localhost:8080 címen), amire Portlet fejlesztőként egyelőre nem lesz szükségünk.
  • A dokumentáció szerint függőségként telepítenünk kell az Apache Antot (Mavennel is működésre lehet bírni, de ezt nem próbáltam), valamint a Jikes fordítót.
  • Az első Portletünk létrehozását érdemes a Liferay Plugins SDKn keresztül elvégezni, amely számtalan mintával könnyíti meg a fejlsztést.
  • Az SDK gyökér-könyvtárában található build.properties állományban állítsuk be a kívánt web-konténert, a megfelelő elérésekkel. Amennyiben több felhasználó is használja ugyanazt az SDK-t, a buil.properties mellé hozzunk létre felhasználónként egy-egy build.${username}.properties fájlt, amelyben perszonalizálni tudjuk a beállításokat.
  • Következő lépésként az SDK portlets könyvtárában hozzunk létre egy hello-world projektet.
    ./create.sh hello-world "Hello World"
    Buildfile: build.xml
    
    create:
        [unzip] Expanding: /media/data/code/liferay/sdk/portlets/portlet.zip into /media/data/code/liferay/sdk/portlets/hello-world-portlet
        [mkdir] Created dir: /media/data/code/liferay/sdk/portlets/hello-world-portlet/docroot/WEB-INF/tld
         [copy] Copying 6 files to /media/data/code/liferay/sdk/portlets/hello-world-portlet/docroot/WEB-INF/tld
    
    BUILD SUCCESSFUL
    Total time: 1 second
    
  • A script által létrehozott hello-world-portlet könyvtárba lépve vegyük rá az Antot, hogy készítsen egy buildet.
    ant deploy
    Buildfile: build.xml
    
    compile:
    
    merge:
        [mkdir] Created dir: /media/data/code/liferay/sdk/portlets/hello-world-portlet/docroot/WEB-INF/classes
        [mkdir] Created dir: /media/data/code/liferay/sdk/portlets/hello-world-portlet/docroot/WEB-INF/lib
         [copy] Copying 5 files to /media/data/code/liferay/sdk/portlets/hello-world-portlet/docroot/WEB-INF/lib
        [javac] Compiling 1 source file to /media/data/code/liferay/sdk/portlets/hello-world-portlet/docroot/WEB-INF/classes
    
    merge:
    
    war:
    
    clean-portal-dependencies:
          [zip] Building zip: /media/data/code/liferay/sdk/dist/hello-world-portlet-5.2.3.1.war
    
    deploy:
         [copy] Copying 1 file to /media/data/code/liferay/deploy
    
    BUILD SUCCESSFUL
    Total time: 4 seconds
    
  • Utolsó simításként annyi van hátra, hogy Bruno (Admin) nevében jelentkezzünk be az alapértelmezett Portal oldalon (email: bruno@7cogs.com, password: bruno, de szerencsére kattintós módszer is van), és a Welcome -> Add Application menüpont alatt adjuk hozzáadjuk a "Hello World"-öt az oldalhoz (érdemes rákeresni).
Szeretném megjegyezni, hogy eléggé sok fejfájást okozott az, hogy eleinte a megszokott módon szerettem volna az alkalmazás-fejlesztést vezérelni az IDE-ből. Ennek érdekében először a Liferay Eclipse Pluginjével próbálkoztam (elég sokat), amit végül nem sikerült rávenni, hogy tegye a dolgát. Ezután megpróbáltam olyan projektet létrehozni az IDE-ben, ami kapcsolatban áll a web-konténerrel, és vezérli a szinkronizációt és magát a szervert (itt is próbálkoztam pár kombinációval hátha csak én vagyok a hüje), de sajnos ez sem vezetett eredményre. Az járható út egyelőre, hogy az IDE-ből fejlesztés közben/után WAR fájlba csomagolva a konténer auto-deploy könyvtárába exportáljuk a produktumot, és a konzolból indított szerver pedig teszi a dolgát. Szerény véleményem, hogy ez eléggé fa-pados, és időigényes módszer, hiszen a konténer az egész alkalmazást újra húzza, és nem csak a szükséges részt frissíti. Szerk.: A Liferay Eclipse Plugin sajnos csak 6-os verziónál >= Liferayyel kompatibilis, így csak az új fejlesztések tehetők kényelmesebbé vele. Régebbi verzió esetén eléggé fapados sajnos a fejlesztés.
Pozitívumként tapasztaltam, hogy létezik Magyar Liferay közösség, továbbá egy hazai Liferay specialista az I-Logic képében, és az elengedhetetlen Facebook csoport.

2010. május 16., vasárnap

Adott pont vonzáskörzetében való keresés MySQL segítségével

Azt a feladatot kaptam, hogy saját adatbázisból vendéglátó-egységeket tegyek kereshetővé, és a találatokat jelenítsem meg egy térképen. Hogy ne legyen olyan egyszerű a feladat, további kérés volt, hogy Budapesten a szomszédos kerületek találatait is mutassam meg, egyébként pedig 50 km-es körzetben lévőket. Bár a megoldás nem kapcsolódik közvetlenül a Java-hoz, mégis úgy gondoltam talán érdekes lehet. Igyekeztem úgy kivitelezni a dolgot, hogy a MySQL adatbázisból ne kerüljön ki olyan érték, amelyet nem kell megjeleníteni.
Lássuk először az 50 km-es körzet problémáját:
Először is létrehoztam két triggert, amik a vendéglátó-egység beszúrása vagy módosítása után futnak le, és egész egyszerűen annyit tesznek, hogy a beállított longitude és latitude értékből a coordinate mezőben eltárolnak egy POINT objektumot. A POINT beépített MySQL objektum, így értelem-szerűen a mező típusának is az van deklarálva.
DELIMITER //

CREATE TRIGGER RefreshHorecaPointInsert BEFORE INSERT ON `horeca`
FOR EACH ROW
BEGIN
    SET NEW.`coordinate` = POINT(NEW.`longitude`, NEW.`latitude`);
END//

CREATE TRIGGER RefreshHorecaPointUpdate BEFORE UPDATE ON `horeca`
FOR EACH ROW
BEGIN
    SET NEW.`coordinate` = POINT(NEW.`longitude`, NEW.`latitude`);
END//

DELIMITER ;
A következő lépés már igen egyszerű, mivel a MySQL (is) rendelkezik beépített geometriai funkciókkal. Feladatunk annyi, hogy a POLYGONE objektumot felhasználva az első találat koordinátái köré egy tetszőleges sokszöget rajzoljunk. Bár az 50 km-es körzet meghatározásához egy kört kellene rajzolni, de annak bonyolultsága és gyakorlati haszna között fennálló aránytalanság miatt, én a négyszöget választottam.
String polygone = "POLYGON((" + (horeca.getLongitude() - 0.32) + " " + (horeca.getLatitude() - 0.22) + ", " + (horeca.getLongitude() + 0.32) + " " + (horeca.getLatitude() - 0.22) + ", " + (horeca.getLongitude() + 0.32) + " " + (horeca.getLatitude() + 0.22) + ", " + (horeca.getLongitude() - 0.32) + " " + (horeca.getLatitude() + 0.22) + ", " + (horeca.getLongitude() - 0.32) + " " + (horeca.getLatitude() - 0.22) + "))";
String query = "SELECT h FROM Horeca h WHERE MBRContains(PolygonFromText('" + polygone + "'), coordinate)";
Megjegyzem a geometriai funkciók elméletileg 5.0-ás verzió óta léteznek MySQL-ben, elég sok helyen olvastam, hogy hibásan működik, meg, hogy aki ilyet akar használjon PostgreSQL-t. Én is belefutottam olyan kellemetlenségbe, ami miatt frissíteni kellett a szervert 5.1.x-re (a SELECT POINT(12, 12) lefutott, de a UPDATE horeca SET coordinate = POINT(longitude, latitude) már nem).
A Budapest szomszédos kerületeinek kérdés-köre nem egy bonyolult dolog, pusztán azért tárgyalom, hogy teljes legyen a kép. Az adatbázis kímélése érdekében ismét egy triggert írtam, ami budapesti cím beszúrása vagy szerkesztése esetén az irányító számból eltárolja a középső 2 karaktert, mint kerületet. Az egységes kereshetőség érdekében hagytam minden kerületet két karakteresre.
DELIMITER //

CREATE TRIGGER SetDistrict BEFORE INSERT ON `horeca`
FOR EACH ROW
BEGIN
    IF NEW.`postal_code` < 2000 THEN
        SET NEW.`district` = SUBSTR(NEW.`postal_code`, 2, 2);
    END IF;
END;

DELIMITER ;
Ezek után létrehoztam egy táblát, amely a szomszédos kerületek mátrixát tartalmazza.
INSERT INTO `district_neighbors` (`reference`, `neighbor`) VALUES
('01', '02'),('01', '12'),('01', '11'),('01', '05'),('02', '12'),('02', '03'),('02', '05'),('02', '13'),('03', '04'),('03', '13'),('04', '13'),('04', '14'),('04', '15'),('05', '06'),('05', '07'),('05', '13'),('06', '07'),('06', '13'),('06', '14'),('07', '08'),('07', '09'),('07', '14'),('08', '09'),('08', '10'),('08', '14'),('09', '10'),('09', '11'),('09', '19'),('09', '20'),('09', '21'),('10', '14'),('10', '16'),('10', '17'),('10', '18'),('10', '19'),('11', '12'),('11', '21'),('11', '22'),('13', '18'),('13', '19'),('13', '20'),('13', '21'),('14', '15'),('14', '16'),('15', '16'),('17', '18'),('18', '19'),('18', '20'),('18', '23'),('19', '20'),('20', '21'),('20', '23'),('21', '22'),('21', '23');
Az alábbi lekérdezéssel pedig könnyedén kinyerhető adott kerület és a vele szomszédosak az adatbázisból.
String query = "SELECT h FROM Horeca h WHERE district = :district OR district IN (SELECT IF(reference = :district, neighbor, reference) FROM district_neighbors WHERE reference = :district OR neighbor = :district)";

2010. április 28., szerda

Weboldal beüzemelése Linux, Apache, Glassfish alapon

Java-s web-alkalmazások fejlesztése során gyorsan felmerül az igény arra, hogy az oldal elérhető legyen mindenféle portszám megadása nélkül, hiszen vég-felhasználóink általában nem szakavatott fejlesztők, vagy rendszer tervező mérnökök, akiktől nem idegen az efféle címzés. A probléma megoldására több lehetséges megoldás is létezik, az általam felvázolt lehetőség csak egy a sok közül. Operációs rendszernek Linuxot választottam, kedvenc alkalmazás-szerverem pedig a Glassfish.
A beállítás lépései:
  • Glassfish domain létrehozása.
# asadmin create-domain --adminport 4848 --savemasterpassword domain1
A savemasterpassword opcióra azért van szükség, hogy a szerver indításakor és leállításakor ne kelljen a masterjelszavat megadni. Ellenkező esetben csak kézzel tudjuk a szervert indítani és/vagy leállítani.
  • Indító script megírása, és a megfelelő runlevel-be helyezése.
#!/bin/sh
ase "$1" in
start)
    ulimit -Hn 10240
    ulimit -Sn 10240
    su glassfish /path_to_glassfish/bin/asadmin start-domain domain1
    ;;
stop)
    su glassfish /path_to_glassfish/bin/asadmin stop-domain domain1
    ;;
restart)
    su glassfish /path_to_glassfish/bin/asadmin stop-domain domain1
    ulimit -Hn 10240
    ulimit -Sn 10240
    su glassfish /path_to_glassfish/bin/asadmin start-domain domain1
    ;;
*)
    echo $"usage: $0 {start|stop|restart}"
    exit 1
esac
Az első említésre méltó dolog, hogy én létrehoztam egy glassfish felhasználót a rendszerben, és annak nevében/jogosultságával telepítettem az alkalmazás-szervert. Ennek elsősorban biztonsági okai vannak, hiszen így az alkalmazás-szerver csak a "mezei" felhasználó hatáskörében tud tevékenykedni. A második dolog ami szemet szúrhat az ulimit parancs. A Linux kernelben meg van határozva, hogy mekkora darab-számú állományt nyithat meg egy alkalmazás/felhasználó. Ez a szám alapértelmezetten 1024, amiből a Glassfish indulás után elhasznál 8-900-at, így eléggé kis terhelés esetén is átlépi a határt. A kiadott ulimit parancs az adott processre, és az abból induló alprocessekre vonatkozik, ezért nem érdemes rendszer-szinten növelni a limitet, elég az init scriptben beállítani a kívánt értéket. A pontos érték megállapítása terheléses teszt után hangolható, ám kezdésnek érdemes 10240-re venni.
  • Apache web-szerver beállítása
NameVirtualHost *:80

<VirtualHost *:80>
    ServerName www.foo.bar
    ProxyPass / http://localhost:8080/foo.bar-war/
    ProxyPassReverse / http://localhost:8080/foo.bar-war/
    ProxyPassReverseCookieDomain localhost:8080/foo.bar-war www.foo.bar
    ProxyPassReverseCookiePath / /
    ProxyVia Off
    ProxyPreserveHost On
</VirtualHost>
Mivel a felhasználók az URL begépelésével a szerver 80-as portjára csatlakoznak, ésszerű megoldás, ha egy web-szervert telepítünk erre a portra, és a web-szerverből proxyzzuk át a megfelelő kéréseket az alkalmazás-szerver felé. A proxyzáshoz a mod_proxy és mod_proxy_http modulokat kell betölteni. A ProxyPass és ProxyPassReverse opciókkal magát a proxyzás útvonalát állítjuk be, míg a ProxyPassReverseCookieDomain és ProxyPassReverseCookiePath opciókkal a Cookie-k tárolásának módját írjuk elő. Az utóbbi 2 opció elhagyása esetén nem tudjuk a tárolt Cookie-kat elérni, mivel azok a www.foo.bar domainen lesznek bejegyezve, ebből kifolyólag Session azonosítót sem tudunk Cookie-ban tárolni.

2010. április 27., kedd

JDBC loggolás jdbcdslog segítségével

Találtam egy remek kis eszközt, amely képes loggolni a JDBC rétegben történt eseményeket. Az SQL-ek mellett eltárolja a lekérdezések eredményeit is, így pontosan képet kaphatunk arról, hogy mi is történik valójában az adatbázis rétegben. Az alkalmzás a jdbcdslog nevet viseli, és a Google Code szolgáltatáson keresztül érhető el nyílt forráskóddal.
Telepítése pár lépésben:
  • Az aktuális verzió beszerezhető az oldalról. Érdemes a "distribution" kiadást letölteni, mert abban van pár függőség is csomagolva.
  • Az Apache Log4j-re épül a logger, így azt is érdemes beszerezni.
  • Mivel a példa-program a legegyszerűbb módozatot mutatja be, így én a jdbcdslog forrását bemásoltam az alkalmazásomba, illetve a slf4j-api-1.5.10.jar, slf4j-log4j12-1.5.10.jar, log4j-1.2.16.jar jar-okat importáltam. (A jdbcdslog forrásában van hiba, azokat érdemes figyelmen kívül hagyni fordításkor!)
A telepítésről bővebben.
Ezután következhet a kapcsolódás megírása. A dolog nyitja abban rejlik, hogy a JDBC kéréseket a jdbcdslog proxyzza át a JDBC providernek, ehhez a kapcsolatot át kell adni a loggernek.
Connection conn = null;
try {
    PropertyConfigurator.configure(Main.class.getResource("log4j.properties").getFile());
    conn = DriverManager.getConnection("jdbc:mysql://localhost/mysql", "root", "password");
    Connection loggingConnection = ConnectionLoggingProxy.wrap(conn);
    Statement statement = loggingConnection.createStatement();
    statement.executeQuery("SELECT * FROM user");
    ResultSet resultSet = statement.getResultSet();
    while (resultSet.next()) {
        System.out.println(resultSet.getString("User"));
    }
    statement.close();
    resultSet.close();
} catch (Exception e) {
} finally {
    if (conn != null) {
        try {
            conn.close();
        } catch (Exception e) {}
    }
}
A Log4j beállítása a log4j.properties fájlban történik, ennek rejtelmeibe nem mélyednék bele, az egy külön bejegyzést igényelne.
#Create logger named A1
log4j.rootLogger=DEBUG, A1
#Write log to test.log over FileAppender
log4j.appender.A1=org.apache.log4j.FileAppender
log4j.appender.A1.File=test.log
log4j.appender.A1.Append=true
#Set the layout to PatternLayout and set pattern
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%4d{dd MMM yyyy HH:mm:ss,SSS} %-5p - %c: %m\n
Futtatva kódunkat az alábbi eredményt kapjuk. Én egy frissen telepített MySQL adatbázis mysql táblájából olvastam ki a felhasználókat.
root
root
root
A test.log tartalma:
27 Apr 2010 14:36:43,992 DEBUG - org.jdbcdslog.LogUtils: createLogEntry()
27 Apr 2010 14:36:43,993 INFO  - org.jdbcdslog.StatementLogger: java.sql.Statement.executeQuery SELECT * FROM user 110 ms.
27 Apr 2010 14:36:43,999 INFO  - org.jdbcdslog.ResultSetLogger: java.sql.ResultSet.next {'localhost', 'root', '*080C34F746094F116AE54467CF39EA994A0FF57F', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', '', [B@4f80d6, [B@4f80d6, [B@4f80d6, 0, 0, 0, 0}
27 Apr 2010 14:36:44,003 INFO  - org.jdbcdslog.ResultSetLogger: java.sql.ResultSet.next {'linux-brsg', 'root', '', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', '', [B@4f80d6, [B@4f80d6, [B@4f80d6, 0, 0, 0, 0}
27 Apr 2010 14:36:44,004 INFO  - org.jdbcdslog.ResultSetLogger: java.sql.ResultSet.next {'127.0.0.1', 'root', '', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', '', [B@4f80d6, [B@4f80d6, [B@4f80d6, 0, 0, 0, 0}
27 Apr 2010 14:36:44,007 INFO  - org.jdbcdslog.ResultSetLogger: java.sql.ResultSet.next {'localhost', '', '', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', '', [B@4f80d6, [B@4f80d6, [B@4f80d6, 0, 0, 0, 0}
27 Apr 2010 14:36:44,008 INFO  - org.jdbcdslog.ResultSetLogger: java.sql.ResultSet.next {'linux-brsg', '', '', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', '', [B@4f80d6, [B@4f80d6, [B@4f80d6, 0, 0, 0, 0}
Perzisztes réteg használata esetén természetesen az itt vázol megoldás nem kivitelezhető, de szerencsére a fejlesztők két további módszert is építettek az alkalmazásba. Van lehetőség "JDBC Driver" és "JDBC DataSource" proxyzásra is, ezeket választva csak a kapcsolódási URL-t kell módosítani, az alkalmazásunk érintetlen marad.

2010. április 14., szerda

HTTPS kapcsolódás self-signed és/vagy lejárt tanusítvánnyal ellátott szolgáltatáshoz

Egy on-line fizetéses alkalmazás fejlesztése közben futottam bele abba a problémába, hogy https protokollon keresztül kell kapcsolódni a banki rendszerhez. Első hallásra nem is tűnik olyan bonyolultnak a dolog, azonban a megvalósítás közben belefutottam egy igen kellemetlen körülménybe. A teszt rendszer tanusítványa lejárt. Az ügyeletes rendszer-gazdával konzultálva megnyugtatott, hogy az éles rendszerben nem lesz probléma,... egy darabig,... még jó, hogy szóltam! Álmomban sem gondoltam volna, hogy ilyen előfordulhat egy banknál, mégha tesztrendszerről is van szó.
A kapcsolódáshoz a jakarta.commons.httpclient-et választottam, abból is a 3.1-es verziót.
HttpClient httpclient = new HttpClient();
Protocol myhttps = new Protocol("https", new SSLProtocolSocketFactory(), 443);
httpclient.getHostConfiguration().setHost("somehost.tld", 443, myhttps);
GetMethod method = new GetMethod("/some_uri"); //PostMethod to post data

try {
    httpclient.executeMethod(method);
    String response = new String(method.getResponseBody());
} finally {
    method.releaseConnection();
}
A kódot futtatva az alábbi Exception-t kaptam:
javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
Első teendőnk ebben az esetben, hogy megszerezzük a tanusítvány publikus részét, majd letöltve hozzáadjuk egy kulcs-tartóhoz, az alábbi paranccsal:
keytool -importcert -trustcacerts -alias untrustedCert -file somecert.crt -keystore /usr/lib/jvm/java/jre/lib/security/cacerts
Mivel én fejlesztői környezetben szeretnék kapcsolódni, a Java alapértelmezett kulcs-tartójához adtam hozzá a tanusítványt, az alapértelmezett "changeit" jelszó segítségével. Ezek után nincs más dolgunk, mint a JVM-nek átadni paraméterként, hogy a kiszemelt kulcs-tartót fogadja el mindenképpen hitelesnek.
-Djavax.net.ssl.trustStore=/usr/lib/jvm/java/jre/lib/security/cacerts

2010. április 4., vasárnap

Képek átméretezése, első nekifutás

Ebben a témában rengeteg bejegyzés található a neten, én személy szerint mégsem találtam elfogadható megoldás, ami egyben megoldaná minden problémámat, kivéve a Java Advanced Imaging-ot. Utóbbival elég nagy problémám volt, hogy nincsen agyondokumentálva, vannak ugyan példák, de azokon eligazodni is felér egy rém-álommal. A rossz tapasztalatok sarkalltak arra, hogy belekezdjek egy, az én igényeimet kielégítő, eszköz megírásába, mely az alapvető webes környezetben előforduló problémákat orvosolja. A projekt elérkezett az első "mérföld-kőhöz", így gondoltam szánok rá pár sort.
Jelenlegi stádiumban képes átméretezni jpg, png, gif, anim gif formátumokat, alkalmazza az élsimítás technológiát, viszont a transzparens képekbe még beletörik a foga. A művelet pilléreit szeretném itt bemutatni, a teljes forráskódot elérhetővé teszem, de mivel felhasználtam pár osztályt, amit letöltöttem és javítottam, ezek pontos liszenszelését még egyeztetem.
Első esetben nézzük az egyszerűbb oldalát a dolognak, amikor nem animált gif-et kell átméretezni. Mivel a legtöbb példa csak a Graphics2D beépített élsimítását használta fel, aminek minősége finoman szólva is vacak, így átméretezés előtt egy blur effektet húzok a képre. A példa csak a kicsinyítés problémájával foglalkozik, mivel a jelentős minőség-romlás ilyen esetben érzékelhető igazán, és a gyakorlatban is ritkán kell nagyítani egy felhasználó által feltöltött képet.
OutputStream out;
BufferedImage bufferedImage;
String extension;
//...
if (!extension.equals("gif")) {
//Első lépésként, amenniyben jpg-a forrás kép, át kell állítanunk a típusát, mivel az alapértelmezett típus nem támogatja a blur készítését, minden más esetben, kompatibilissé tesszük a képet, szintén a blur-ozhatóság érdekében.
    if (mimeType.equals("image/jpeg") || mimeType.equals("image/pjpeg")) {
        bufferedImage = convert(bufferedImage, BufferedImage.TYPE_INT_RGB);
    } else {
        bufferedImage = createCompatibleImage(bufferedImage);
    }

//Elvégezzük a blur-ozást, és átméretezést, majd mehet a kép a kimenetre.
    ImageIO.write(resize(blur(bufferedImage), newWidth.intValue(), newHeight.intValue()), extension, out);

    out.flush();
    out.close();
}
A meghívott metódusok:
//Átméretezi a képet.
private BufferedImage resize(BufferedImage image, int width, int height) {
    BufferedImage result = new BufferedImage(width, height, image.getType() == 0 ? BufferedImage.TYPE_INT_ARGB : image.getType());
    Graphics2D g = result.createGraphics();
    g.setComposite(AlphaComposite.Src);
    g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    g.drawImage(image, 0, 0, width, height, null);
    g.dispose();

    return result;
}

//Blur-ozza a képet.
private BufferedImage blur(BufferedImage image) {
    float ninth = 1.0f / 9.0f;
    float[] blurKernel = {
        ninth, ninth, ninth,
        ninth, ninth, ninth,
        ninth, ninth, ninth
    };

    Map map = new HashMap();
    map.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    map.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    map.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

    BufferedImageOp op = new ConvolveOp(new Kernel(3, 3, blurKernel), ConvolveOp.EDGE_NO_OP, new RenderingHints(map));

    return op.filter(image, null);
}

//Átalakítja a típusát a képnek.
private BufferedImage convert(BufferedImage image, int type) {
    BufferedImage result = new BufferedImage(image.getWidth(), image.getHeight(), type);
    Graphics2D g = result.createGraphics();
    g.drawRenderedImage(image, null);
    g.dispose();

    return result;
}

//Kompatibilissé teszi a képet.
private static BufferedImage createCompatibleImage(BufferedImage image) {
    GraphicsConfiguration gc = BufferedImageGraphicsConfig.getConfig(image);
    BufferedImage result = gc.createCompatibleImage(image.getWidth(), image.getHeight(), Transparency.BITMASK);
    Graphics2D g = result.createGraphics();
    g.drawRenderedImage(image, null);
    g.dispose();

    return result;
}
A nehezebb eset, mint említettem, animált gifek átméretezése, mivel a Graphics2D nem támogatja alapértelmezetten azokat. A megoldás nem egyedüli érdemem, bár a letöltött kódokat javítanom kellett. A lényeg, hogy a gif kép-kockáin egyesével végig kell iterálni, és az előbb említett műveletek végrehajtása után, a cél képbe beletenni az immár átméretezett példányokat.
try {
    GifDecoder d = new GifDecoder();
    d.read(image.getAbsolutePath());

    AnimatedGifEncoder e = new AnimatedGifEncoder();
    e.setRepeat(d.getLoopCount());
    e.start(out);

    int type = bufferedImage.getType() == 0 ? BufferedImage.TYPE_INT_ARGB : bufferedImage.getType();

    for (int i = 0; i < d.getFrameCount(); i++) {
        e.setDelay(d.getDelay(i));
                    
        BufferedImage frameBuffer = new BufferedImage(origWidth, origHeight, BufferedImage.TYPE_BYTE_INDEXED);
        frameBuffer.getGraphics().drawImage(d.getFrame(i), 0, 0, null);

        if (mimeType.equals("image/jpeg") || mimeType.equals("image/pjpeg")) {
            frameBuffer = convert(frameBuffer, BufferedImage.TYPE_INT_RGB);
        } else {
            frameBuffer = createCompatibleImage(frameBuffer);
        }

        e.addFrame(resize(blur(frameBuffer), newWidth.intValue(), newHeight.intValue()));
    }

    e.finish();
    out.flush();
    out.close();
} catch (Exception e) {}
A javított osztályokat egyenlőre pastebin.com-ra töltöttem fel.

Nem állítom, hogy ez a legjobb és legszebb megoldás, és azt sem, hogy tökéletes eredményt hoz minden típusú képnél, csak azt tudom, hogy a transzparens képek kivételével eddig bevált.

2010. március 23., kedd

Egységesített "Global JNDI name" kezelés EJB 3.1 környezetben

Az alkalmazás-szerver, amikor létrehozunk egy Session Bean-t, automatikusan regisztrálja azt a JNDI kontextusban. Az regisztrált Bean-ekre referenciát legegyszerűbben a függőség-injektálással szerezhetünk, a szerver leveszi a direkt név-feloldás terhét vállunkról. Az említett módszer egységesen működik minden alkalmazás-szerveren, viszont a megvalósítása már korántsem egységes. Minden gyártó saját elképzelése szerint oldotta meg a feladatot.
  • JBoss global JNDI name: ejbapp/BeanName/local
  • GlassFish global JNDI name: InterfaceClass
  • WebSphere Community Edition global JNDI name: ejbapp-ejb/BeanName/InterfaceClass
  • Oracle Application Server (OC4J) global JNDI name: BeanName
Mivel az alkalmazás-szerver elrejti előlünk a feloldás problémáját, egészen addig amíg van lehetőségünk a függőség-injektálásra támaszkodni, kódunk hordozható marad. Viszont abban a pillanatban, amikor pl. egy nem menedzselt objektumból, vagy egy unit-tesztelőből kényszerülünk direkt JNDI név-feloldásra, kénytelenek vagyunk gyártó specifikus kódot készíteni.
Egy egyszerű példa szemlélteti a problémát EJB 3 környezetben:
@Remote
public interface JNDITestBeanInterface {
    String test();
}
@Stateless(name="JNDITestBean")
public class JNDITestBean implements JNDITestBeanInterface {
    public String test() {
        return "ok";
    }
}
public class JNDITestBeanClient {
    public static void main(String[] args) throws Exception {
        Context context = new InitialContext();
        JNDITestBeanInterface testBean = (JNDITestBeanInterface) context.lookup("JNDITest/JNDITestBeanInterface/remote");
        System.out.print(testBean.test());
    }
}
Az EJB 3.1 specifikáció megoldást kínál, mégpedig úgy, hogy egységesíti a név-regisztráció módját, így azon alkalmazás-szerverek, melyek implementálni szeretnék a specifikációt kénytelenek követni is azt.


java:global[/<application-name>]/<module-name>/<bean-name>#<interface-name>
@Stateless
@LocalBean
public class JNDITestBean {
    public String test() {
        return "ok";
    }
}
public class JNDITestBeanClient {
    public static void main(String[] args) throws Exception {
        Context context = new InitialContext();
        JNDITestBean testBean = (JNDITestBean) context.lookup("java:global/JNDITest/JNDITest-ejb/JNDITestBean");
        System.out.print(testBean.test());
    }
}
Az "interface-name" elhagyható, amenniyben interface nélküli Bean-re szeretnénk referenciát szerezni, továbbá ha az egyszerűsített deployment technológiát használva a WAR-ba helyeztük az EJB réteget az "application-name" azaz az alkalmazás neve opcionálissá válik.

2010. március 17., szerda

Cassandra

Több irányból is érkeznek a hírek, hogy nagyobb tartalom-szolgáltatók sorra lecserélik adatbázis megoldásaikat Cassandrára. A Cassandra program arany fokozatú Apache projektté nőtte ki magát, melyet kezdetben a Facebook fejlesztett és tett közzé nyílt forrással. Az adatbázis egy jól skálázható, második generációs, strukturált kulcs-érték tárolására alkalmas Java nyelven írt eszköz, amit kifejezetten több-gépes, clusterezett rendszerekre terveztek.
Néhány alap fogalom:

  • Column: Az oszlop, a legkisebb egysége az adat-tárolásnak, melyben kulcs, érték,és egy idő-bélyeg található.
  • ColumnFamily: Adattábla, mely az oszlopokat fogja össze.
  • SuperColumns: Olyan oszlopokat tartalmaz, melyek értékei további oszlopok.
  • Row: Minden adattábla elszeparált fájlban tárolódik, és ezek a fájlok tárolódnak az un.: Row-ban. Minden Row-nak van egy kulcsa, amivel hivatkozni lehet rá.
  • Keyspace: Lényegében az adatbázis név-tér az adattábláknak, tipikusan egy alkalmazáshoz egy név-tér tartozik.
  • Cluster: A cluster kezeli az elosztott név-tereket.
Ez az egyszerű JSON példa jól szemlélteti a Row, ColumnFamily, és Column struktúráját:
{
   "mccv":{
      "Users":{
         "emailAddress":{"name":"emailAddress", "value":"foo@bar.com"},
         "webSite":{"name":"webSite", "value":"http://bar.com"}
      },
      "Stats":{
         "visits":{"name":"visits", "value":"243"}
      }
   },
   "user2":{
      "Users":{
         "emailAddress":{"name":"emailAddress", "value":"user2@bar.com"},
         "twitter":{"name":"twitter", "value":"user2"}
      }
   }
}
Az adat-szerkezet részletesebb leírása megtalálható itt.
Fontos különbség a relációs adatbázisokhoz képest, hogy míg utóbbiakban az entitások és a közöttük fennálló kapcsolat alapján keresünk, és a keresést index-ek tesszük élhetővé, addig a Cassandra fordított paradigmát igényel, és a tervezésnél azt kell figyelembe vennünk, hogy milyen lekérdezéseket szeretnénk kiszolgálni, mert mert egy adattábla lényegében egy lekérdezésnek "felel meg".
Nézzük, hogyan is működik.
A telepítést részletesen leírja a wiki, ezért arra nem térnék ki. Miután sikeresen feltelepítettük a lánykát a conf/storage-conf.xml állományban tudjuk hangolni a rendszert, illetve itt van lehetőség Keyspace-k és ColumnFamily-k létrehozására, az alábbi módon.
<Keyspaces>
    <Keyspace Name="testKeyspace">
        <KeysCachedFraction>0.01</KeysCachedFraction>
        <ColumnFamily CompareWith="UTF8Type" Name="testColumnFamily"/>
    </Keyspace>
</Keyspaces>
Mentsük el az XML-t indítsuk el a Cassandrát, majd a "cassandra-cli -host localhost -port 9160" paranccsal csatlakozzunk az adatbázishoz. Ha minden jól ment a cassandra> felirat jelenik meg a képernyőn jelezve, ahol is megkezdhetjük a munkát. Első kérésként ellenőrizzük, hogy sikeresen létre hoztuk-e a Keyspace-t.
cassandra>show keyspaces
testKeyspace
system
Ne lepődjünk meg van egy system Keyspace, amit a rendszer maga használ. Jelen esetben létrehoztuk egy testColumnFamily-t amiben az adatokat szeretnénk tárolni UTF-8 string formában, de van mód ASCIIType, BytesType, TimeUUIDType, Super tárolási formákra.
Következő lépésként helyezzünk be egy Column-ot a familybe.
cassandra>set testKeyspace.testColumnFamily ['testUser']['foo'] = 'bar'
Value inserted.
És végül kérdezzük le a felvitt értéket.
cassandra>get testKeyspace.testColumnFamily ['testUser']['foo']
=> (column=foo, value=bar, timestamp=1268818855201)
Mivel leggyakrabban nem parancssorból szeretnénk használni az adatbázist, számtalan programozási nyelvhez létezik már implementáció, köztük természetesen Java-hoz is. Az említett kliensben az előbbi példa e-képpen valósítható meg:
CassandraClient cl = pool.getClient();
KeySpace ks = cl.getKeySpace("testKeyspace");

//insert value
ColumnPath cp = new ColumnPath("testColumnFamily" , null, "testUser".getBytes("utf-8"));
ks.insert("foo", cp , "bar".getBytes("utf-8"));

//get value
Column col = ks.getColumn("foo", cp);
String value = new String(col.getValue(),"utf-8");

pool.releaseClient(cl);
pool.close();
Összegzésként elmondható, hogy Cassandra sem váltja meg a világot, mégis megvan a maga helye és szerepe az adatbázisok területén. Lényeges működésbeli sajátossága hátrány, ha bonyolult adastruktúrákban gondolkozunk, viszont előnye, hogy rendkívül gyorsan képes kiszolgálni a kéréseket. Véleményem szerint relációs adatbázissal vegyesen, mindkettő erősségét (ki)használva optimális megoldáshoz jutunk.

2010. március 9., kedd

JRebel, avagy hogyan tegyük egyszerűbbé az EEletet

Bár elkötelezett híve vagyok a nyílt forrású szoftverfejlesztésnek,a címben említett eszköz mellett mégsem bírtam elmenni csukott szemmel. Mentségükre legyen mondva, a fizetős terméküket bármely szabad szoftver fejlesztéséhez ingyen biztosítják. A JRebel projekt célja, hogy olyan megvalósításokkal támogassa meg a fejlesztőket, melyek nem része a JVM-nek, így hagyományos módon vagy sehogy, vagy igen körülményesen oldhatóak csak meg. A fejlesztők 5 fő pontot neveznek meg, melyek az alábbiak:
1. Új osztályok hozzáadása
A HotSwap protokoll csak meglévő osztályok frissítését tesz lehetővé futás-időben, mivel egy egyedi megfeleltetés van az osztály neve és tartalma között. Ahhoz, hogy új osztályt adhassunk hozzá, kell válasszunk egy class loadert, mivel minden osztály egy specializált loaderben töltődik be. Sajnos ezt a szolgáltatást nem támogatja a HotSwap.
2. Erő-forrás-állományok frissítése
Az osztályaink mellé gyakran teszünk erő-forrás-állományokat melyek leggyakrabban xml vagy properties fájlok. A HotSwap technológia nem támogatja ezen állományok dinamikus módosítását, azaz minden változtatást deployolni kell a szerverre. Ez elég nagy probléma, főleg ha a szerveren van már pl. feltöltött állomány, mert a deploy folyamat esetlegesen törli azokat.
3. Webes erő-források
Minden weben megjelenő alkalmazásban vannak képek, HTML, CSS, JS, stb állományok, melyek az előző ponthoz hasonlóan nem frissülnek automatikusan a web-konténerben.
4. Gyorsító-tárak
Enterprise környezet nagy előnye, hogy a konténer, legyen szó web-konténerről vagy alkalmazás-konténerről, gyorsító-tárrazza osztályainkat. Ez ez előny egyben egy hátrány is, hiszen új elemek, pl. metódusok hozzáadása után a tár törléséig az előző állapot érhető el. Megjegyzem a legtöbb IDE támogatja a "deploy on save" metódust, mely az osztály mentésekor automatikusan frissíti a szerveren az osztályt.
5. Menedzselt komponensek
Egy alkalmazásban számtalan un. menedzselt komponens (EJB-k, Spring bean-ek, stb.) vannak. Az ilyen osztályokra jellemző, hogy általában van függőség-injektálással a konténer által behelyezett osztály, némely állapota társítva van un. instance vagy instance pool-lal, a funkcionalitás megvalósítása különböző rétegeken történik. Frissítésénél nem elegendő magát az osztályt frissíteni, hanem a különböző rétegeket, injectált osztályokat is.
 
Mindezen problémák feloldására a JRebel egy sajátságos metodikát ajánl, amit egy egyszerű, mégis hétköznapi példán mutatnék be. Jelenleg a 2.2.1-es verzió a legfrissebb, amit a letöltés oldalról szerezhetünk meg, 30 napos próbára. A telepítés grafikusan történik pár lépésben. A telepítő felajánlja, hogy befejezés után futtatja is a varázslót, amely megjegyzem példásan kivitelezett alkalmazás, minek segítségével pofon egyszerűvé válik a konfigurálás. A varázslót "UNIX like" környezetben a bin/jrebel-config.sh script futtatásával "varázsolhatjuk" elő.
  • Nincs más dolgunk mint első lépésben a megfelelő IDE-t kiválasztani, én Netbeans-el próbálkoztam, de támogatott az Eclipse, IntelliJ is.
  • Második lépésben felajánlja a varázsló az IDE-knek megfelelő plugint, Netbeans esetén a beépített pluginek közül a legegyszerűbb beszerezni.
  • Következő, sorban a harmadik lépés, hogy alkalmazásunkat felkészítsük a JRebel használatára. A segédlet mindent érthetően elmagyaráz, EJB projekt esetén a források gyökerébe, web projekt esetén a WEB-INF/classes mappában kell egy-egy rebel.xml állományt elhelyeznünk, és az xml-ben specifikálni, hogy mely osztályokat és/vagy erő-forrásokat szeretnénk ha a JRebel kezelné. Itt térnék ki kicsit a működésre. A JRebel egy újabb rétegként rátelepszik az alkalmzásunkra, és a betöltendő osztályokat vagy erő-forrásokat a rebel.xml alapján közvetlenül a fejlesztőkörnyezetből tölti be. Biztos mindenkinek volt már problémája a dinamikusan fordított web-oldalakkal, amikor is a WEB-INF/classes-ban elhelyezett translate.properties-t minden apró változtatás után deployolni kellett. Mivel ez egy gyakori eset, ebből a problémából indultam ki.
<?xml version="1.0" encoding="UTF-8"?>
<application
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.zeroturnaround.com"
xsi:schemaLocation="http://www.zeroturnaround.com http://www.zeroturnaround.com/alderaan/rebel-2_0.xsd">
<web>
<link target="/"><dir name="~/NetBeansProjects/webapp/webapp-war/web"/></link>
</web>
</application>
  • Negyedik lépéséként választanunk kell Java verziót, operációs rendszert, és konténert, utóbbiból eléggé széles a paletta, én Glassfish 3-at használok, ezért azt választottam. Glassfish esetén az adminisztrációs felületen a JVM Setting / JVM Options alatt 2 paramétert kell megadnunk a JVM-nek, mégpedig a -noverify és -javaagent:/utvonal/jrebel.jar.
  • Ötödik lépésben az ügynök, na jó az agent beállításait kell megadnunk. Az advanced gombra kattintva állíthatjuk be pl. azt, ha nem akarunk a rebel.xml-ben abszolút eléréseket megadni, ennek módját részletesen tárgyalja a varázsló. A beállító-panelt "UNIX like" környezetben a bin/agent-settings.sh script futtatásával indíthatjuk.
  • Nincs más dolgunk, mint újraindítani az alkalmazás-szervert, deployolni a rebel.xml-el megfejelt alkalmazásunkat, és máris élvezhetjük a produktumot.
Bár a JRebel fizetős eszköz, a honlapján elhelyezett kalkulátor segítségével gyorsan kiszámolható megéri-e az árát, viszont a pénzben nem mérhető könnyebbséget még a kalkulátor sem tudja számításba venni.

2010. március 2., kedd

EJB 3.1 Lite

A Java EE 6 megjelenése számos újdonságot hozott, már itt is volt szó róla, ezek közül egy érdekesség az un. web-profilok létrehozása. Mint EE fejlesztő gyakran megfordult már a fejemben, hogy vajon tényleg mindig szükség van a teljes enterprise környezetre, hogy vajon nem-e ágyúval verébre amit csinálok, hiszen a szolgáltatások tárházának töredék részét használom egy-egy projektben. Az érem másik oldalán viszont mindig ott van az egyszerű fejlesztés, az átlátható logika (amit egy kicsit az architektúra is kikényszerít), valamint az élesen elkülönített szerep-körök. Ezt a problémát gondolták orvosolni az EE 6 fejlesztői a web-profilok segítségével, mégpedig úgy, hogy az alkalmazásunkhoz igazíthatjuk a felhasznált és szállított rétegeket. A szintén újdonságként megjelenő egyszerűsített deployment technológiának köszönhetően, melynek lényege, hogy az ejb-réteg behelyezhető a web-rétegbe megspórolva a két különálló projektre, némi megkötéssel egy web-konténer, is képes futtatni enterprise alkalmazásunkat. Az EJB Lite technológia nem más, mint egy előre csomagolt web-profil, az alábbi kritériumokkal:
  • Stateless, Stateful, és Singleton session beaneket használhatunk
  • csak lokális EJB hívásokat végezhetünk
  • nem támogatja az aszinkron hívásokat
  • támogatja a deklaratív és programozott tranzakciókat
A csomag egy annyira "lebutított" profil, hogy használatához web-konténer sem kell, ennek köszönhetően bármely hagyományos alkalmazásunkban használható, mindössze a javax.ebj és glassfish-embedded-static-shell könyvtár-gyüjteményt (jar) kell importálnunk.
import javax.ejb.Stateless;

@Stateless
public class TestBean {
public String test() {
   return "ok";
}
}
import java.util.Properties;
import javax.ejb.embeddable.EJBContainer;
import javax.naming.Context;
import javax.naming.NamingException;

public class Main {
public static void main(String[] args) throws NamingException {
   Properties properties = new Properties();
   properties.put("org.glassfish.ejb.embedded.glassfish.installation.root", "");
   EJBContainer container = EJBContainer.createEJBContainer(properties);
   Context namingContext = container.getContext();

   TestBean testBean = (TestBean) namingContext.lookup("java:global/classes/TestBean");

   System.out.println(testBean.test());
}
}
Az alkalmazást futtatva az alábbi kimenetet kapjuk:
Mar 3, 2010 8:47:28 AM org.glassfish.ejb.embedded.EJBContainerProviderImpl getValidFile
SEVERE: ejb.embedded.location_not_exists
Mar 3, 2010 8:47:31 AM com.sun.enterprise.v3.server.AppServerStartup run
INFO: GlassFish v3 (74.2) startup time : Embedded(2591ms) startup services(492ms) total(3083ms)
Mar 3, 2010 8:47:31 AM org.glassfish.admin.mbeanserver.JMXStartupService$JMXConnectorsStarterThread run
INFO: JMXStartupService: JMXConnector system is disabled, skipping.
Mar 3, 2010 8:47:31 AM com.sun.enterprise.transaction.JavaEETransactionManagerSimplified initDelegates
INFO: Using com.sun.enterprise.transaction.jts.JavaEETransactionManagerJTSDelegate as the delegate
Mar 3, 2010 8:47:31 AM AppServerStartup run
INFO: [Thread[GlassFish Kernel Main Thread,5,main]] started
Mar 3, 2010 8:47:32 AM com.sun.appserv.connectors.internal.api.ConnectorsUtil extractRar
INFO: could not find RAR [ __ds_jdbc_ra.rar ] in the archive, skipping .rar extraction
Mar 3, 2010 8:47:32 AM com.sun.appserv.connectors.internal.api.ConnectorsUtil extractRar
INFO: could not find RAR [ __cp_jdbc_ra.rar ] in the archive, skipping .rar extraction
Mar 3, 2010 8:47:32 AM com.sun.appserv.connectors.internal.api.ConnectorsUtil extractRar
INFO: could not find RAR [ __xa_jdbc_ra.rar ] in the archive, skipping .rar extraction
Mar 3, 2010 8:47:32 AM com.sun.appserv.connectors.internal.api.ConnectorsUtil extractRar
INFO: could not find RAR [ __dm_jdbc_ra.rar ] in the archive, skipping .rar extraction
Mar 3, 2010 8:47:32 AM com.sun.enterprise.security.SecurityLifecycle 
INFO: security.secmgroff
Mar 3, 2010 8:47:32 AM com.sun.enterprise.security.ssl.SSLUtils checkCertificateDates
SEVERE: java_security.expired_certificate
Mar 3, 2010 8:47:32 AM com.sun.enterprise.security.SecurityLifecycle onInitialization
INFO: Security startup service called
Mar 3, 2010 8:47:32 AM com.sun.enterprise.security.PolicyLoader loadPolicy
INFO: policy.loading
Mar 3, 2010 8:47:33 AM com.sun.enterprise.security.auth.realm.Realm doInstantiate
INFO: Realm admin-realm of classtype com.sun.enterprise.security.auth.realm.file.FileRealm successfully created.
Mar 3, 2010 8:47:33 AM com.sun.enterprise.security.auth.realm.Realm doInstantiate
INFO: Realm file of classtype com.sun.enterprise.security.auth.realm.file.FileRealm successfully created.
Mar 3, 2010 8:47:33 AM com.sun.enterprise.security.auth.realm.Realm doInstantiate
INFO: Realm certificate of classtype com.sun.enterprise.security.auth.realm.certificate.CertificateRealm successfully created.
Mar 3, 2010 8:47:33 AM com.sun.enterprise.security.SecurityLifecycle onInitialization
INFO: Security service(s) started successfully....
Mar 3, 2010 8:47:33 AM com.sun.ejb.containers.BaseContainer initializeHome
INFO: Portable JNDI names for EJB TestBean : [java:global/classes/TestBean, java:global/classes/TestBean!ejblite.TestBean]
ok
Bár nem hiszem, hogy a technológia leggyakoribb alkalmazási területét sikerült megragadnom, mégis ezen egyszerű példából kiindulva csak a fejlesztő kreativitásán múlik, hogy milyen környezetbe ágyazva alkalmazza az EJB réteget.
Aki részletesebben szeretne tájékozódni a témában, megteheti Roberto Chinnici blogjában, vagy a TheServerSide portálon. Az egyszerűsített deploymentről ebben a cikkben található részletesebb leírás.

2010. február 25., csütörtök

JavaScript EE, JSON válaszol

Előző bejegyzésben bemutattam miként lehet Java környezetünket rávenni JavaScript futtatására. Folytatásban egy servlet-en keresztül rávesszük a web-konténert, hogy JSON válaszokat adjon a kérésekre.
Legelső lépésként egy hagyományos servletet kell létrehoznunk, majd a doGet metódusában megvalósítani a JavaScript futtatását.
Java kód:
Compilable scriptEngine = (Compilable) new ScriptEngineManager().getEngineByName("JavaScript");

ScriptContext scriptContext = new SimpleScriptContext();
scriptContext.setWriter(response.getWriter());
scriptContext.setAttribute("config", getServletConfig(), ScriptContext.ENGINE_SCOPE);
scriptContext.setAttribute("response", response, ScriptContext.ENGINE_SCOPE);
scriptContext.setReader(new InputStreamReader(JSONServlet.class.getResourceAsStream("json.js")));

try {
 CompiledScript script = scriptEngine.compile(scriptContext.getReader());
 script.eval(scriptContext);
} catch (ScriptException e) {}
Látható különbség, hogy ScriptEngine helyett Compilable osztályt használunk. A Compilable interfészt megvalósító értelmező annyival nyújt több szolgáltatást, hogy a lefordított JavaScripteket újra fel lehet használni újrafordítás nélkül, így segítségével gyorsító-tárazni lehet a scipteket csökkentve a szerver terhelését. Következő lépésként, mivel most nem a JavaScript visszatérési értékére van szükségünk (és azt sem szeretnénk, hogy az alapértelmezett kimenetre írjon), hanem azt szeretnénk, hogy a JavaScript közvetlenül a servlet kimenetre írjon, példányosítanunk kell egy ScriptContext objektumot, és referenciát adnunk a ServletResponse-ra. Természetesen van lehetőségünk Java objektumok átadására a JavaScriptnek, de ebben az esetben a ScriptContext osztálynak közvetlenül adhatjuk át a paramétereket a setAttribute metódus meghívásával. Ezután nincs más dolgunk, mint megadni a JavaScript fájlunkat, és futtatni a scriptet.
JavaScript kód:
response.setHeader("Cache-Control", "no-cache");
response.setContentType("application/json");

var json = {
response: {
  status: response.getStatus(),
  content_type: String(response.getContentType())
},
config: {
  servlet: String(config.getServletName())
}
};

println(json.toSource());
A scriptben jól látható miként használjuk a Java objektumokat, és végül miként írjunk a kimenetre.
A böngészőben az alábbi válasz jelenik meg:
({response:{status:200, content_type:"text/plain;charset=ISO-8859-1"}, config:{servlet:"JSONServlet"}})
Folyt köv...

2010. február 19., péntek

JavaScript EE, első felvonás

Bár a téma nem új-keletű, én most jutottam el oda, hogy legalább kipróbálás szinten foglalkozzam vele. Java SE 6-os verziója óta megtalálható benne a Mozilla Rhinó-ja, amely egy JavaScript értelmező, és segítségével a JVM-en lehetséges JavaScript kódok futtatása Java kódból. Nincs más dolgunk, mint importálni a javax.script csomagot, és a ScriptEngineManager segítségével példányosítani egy ScriptEngine értelmezőt.
ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
Java kód:
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.StringBuilder;
import javax.script.*;

public class SimpleJavaScriptTest {
public static void main(String[] args) throws Exception {
//Get reference to the engine
ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
//Bind some Java object to the script
Bindings vars = new SimpleBindings();
vars.put("testString", "This is a test string.");
StringBuilder stringBuilder = new StringBuilder();
vars.put("testStringBuilder", stringBuilder);
//Read the JavaScript code
Reader scriptReader = new InputStreamReader(SimpleJavaScriptTest.class.getResourceAsStream("test.js"));
try {
//Run the JavaScript
engine.eval(scriptReader, vars);

System.out.println(stringBuilder.toString());
System.out.println(vars.get("testFile").getClass());
System.out.println(vars.get("jsObject").getClass());
} finally {
scriptReader.close();
}
}
}
JavaScript kód:
function printType(obj) {
if (obj.getClass) println("Java object: " + obj.getClass().name);
else println("JS object: " + obj.toSource());
}
printType(testString);

printType(testStringBuilder);
testStringBuilder.append("This content added by JavaScript.");

var testFile = new java.io.File("/");
printType(testFile);

var jsObject = {x: 1, y: { u: 2, v: 3 }};
printType(jsObject);
A példát futtatva az alábbi eredményt kapjuk:
JS object: (new String("This is a test string."))
Java object: java.lang.StringBuilderJava object: java.io.File JS object: ({x:1, y:{u:2, v:3}})
This content added by JavaScript.
class java.io.File
class sun.org.mozilla.javascript.internal.NativeObject
Mint az jól lát-szódik szinte teljes az átjárás a két kód között, bemenő paramétert megfelelő JavaScript típusra cast-olja az értelmező, ennek sikertelensége esetén Java objektumként viselkedik, annak minden funkciójával. Továbbmenve JavaScript kódunkban is van lehetőség Java objektumok létrehozására, melyeket a script futása után elkérhetünk az értelmezőtől. Amennyiben olyan változót kapunk a scriptből, amit Java oldalon nem lehet megfeleltetni egyik osztálynak sem, akkor azzal egy dolgot tehetünk, paraméterként egy másik JavaScriptnek átadhatjuk.
Ezen a ponton merülhet fel a kérdés, hogy valójában ez mire is való? A válasz erre a kérdésre eléggé összetett, és nem is célom maradéktalanul körbejárni. Egyrészt ezzel a módszerrel tudjuk ötvözni a két nyelv tulajdonságait, tehát a Java robusztusságát a JavaScript egyszerűségével összepárosítva kiszélesedik a programozói paletta. Megjegyzem erre a célra nem csak JavaScriptet használhatunk, akár Groovie, vagy Scala kódot is integrálhatunk Java programunkba, ráadásul utóbbiak Java bytecode-ra fordulnak, ami mélyebb integrációra ad lehetőséget. Másrészt kliens oldali scriptjeinket is futtathatjuk server oldalon, ami azt jelenti, hogy nem kell két helyen, két környezetben implementálnunk az azonos működést, hanem szerver oldalon használhatjuk a kliens oldali program-kódokat. Hagyományosan egy űrlap elküldése esetén a böngészőben történik egy előellenőrzés, hogy az adatok formailag megfelelnek-e a követelménynek, ezzel csökkentve a hálózati forgalmat. Formailag helyes adatok elküldésre kerülnek, ahol szerver oldalon újra ellenőrizni kell őket, mind formailag, mind már adat-logikailag is. Mivel szerver környezetben nem a JavaScript az elterjedt programozási nyelv, a formai ellenőrzést sajnos ott is implementálni kell. Ehelyett a kliens oldali JavaScript-ünket nyugodtan felhasználhatjuk, időt, energiát, költséget, és nem utolsó sorban hiba-lehetőséget megspórolva.
Java kód:
engine.eval(scriptReader);
if (engine instanceof Invocable) {
Invocable invEngine = (Invocable) engine;
Object result = invEngine.invokeFunction("testStringLength", "valid string", 2);
System.out.println("Result: " + result + "(" + result.getClass().getName() + ")");
result = invEngine.invokeFunction("testStringLength", "invalid string", 20);
System.out.println("Result: " + result + "(" + result.getClass().getName() + ")");
} else System.out.println("NOT Invocable");
JavaScript kód:
function testStringLength(input, lenght) {
return input.length >= lenght ? true : false;
}
Eredmény:
Result: true(java.lang.Boolean)
Result: false(java.lang.Boolean)
Bár a példa nem tükrözi a technológia hasznosságát (így még talán bonyolultabb is), viszont bemutatja, hogyan tudunk JavaScript függvényeket meghívni, és azok visszatérési értékét felhasználni Java kódunkban. Folyt köv...
Aki szeretne a témáról bővebben is olvasni, az itt talál információt.

2010. február 6., szombat

@Aszinkron EJB >=3.1 hívás

Az EJB 3.1 megjelenése számos újítást és egyszerűsítést hozott a rendszerbe, a fejlesztők, mint ahogy a 2.x-ről 3.0-ra váltáskor is, igyekeztek még kényelmesebbé tenni az enterprise alkalmazások készítését. Mivel mi Glassfish alkalmazás-szervert használunk, és a kiadást követő napokban kezdtünk bele egy új projektbe, úgy határoztunk, hogy a friss és ropogós EJB-t használjuk (eddig nem bántuk meg) az implementáláshoz. Az új EJB-t tanulmányozva szembeötlött egy lehetőség, ami mellett nem tudtam szó nélkül elmenni. Úgy néz ki, hogy nem csak nekem jutott eszembe, hogy az alkalmazást, amit fejlesztek párhuzamosítsam, hanem az EJB fejlesztőinek is, ezért megalkották az aszinkron EJB hívás fogalmát. A dolog igen egyszerű, a hagyományokhoz híven @Annotációval kell megjelölni azokat a SessionBean metódusokat, amiket nem a hagyományos módon szeretnénk kiajánlani.
package sample;

import javax.ejb.Asynchronous;
import javax.ejb.LocalBean;
import javax.ejb.Stateless;

@LocalBean
@Stateless
public class TestBean {

    @Asynchronous
    public void asyncMethod() {
     //some functionality
    }
}
A metódus meghívása a megszokott módon történik, viszont az abban elhelyezett utasítások egy új szálon fognak futni. Ez eddig egyszerű, de mi történik, ha szeretnénk, hogy a hívás valamilyen visszatérési értéket is prezentáljon? Ezt úgy tudjuk megtenni, hogy visszatérési értékként a java.util.concurrent.Future<V> interfészt jelöljük meg, és annak valamely implementációját adjuk vissza.
package sample;

import java.util.concurrent.Future;
import javax.ejb.AsyncResult;
import javax.ejb.Asynchronous;
import javax.ejb.LocalBean;
import javax.ejb.Stateless;

@LocalBean
@Stateless
public class TestBean {

    @Asynchronous
    public Future<String> asyncMethod() {
        try { Thread.sleep(1000);
        } catch (Exception ex) {}
        return new AsyncResult<String>("finish");
    }
}
A dolog érdekessége a hívó oldalon történik, hiszen valahogy kezelni kell tudni, hogy az aszinkron hívás éppen milyen állapotban van.
Future<String> asyncResponse = testBean.asyncMethod();
//some functionality
while (!asyncResponse.isDone()) {
    try { Thread.sleep(100);
    } catch (Exception ex) {}
}

if (!asyncResponse.isCancelled()) {
    try {
        String response = asyncResponse.get();
    } catch (InterruptedException e) {
    } catch (ExecutionException e) {}
}
Láthatjuk mi módon tudjuk "összeszinkronizálni" a hívásokat a szülő szálban. Természetesen meg is lehet szakítani a kérést, a Future cancel metódusával, illetve a rendelkezésünkre állnak a hagyományos Thread notify, notifyAll, és wait metódusai.

2010. február 5., péntek

Btrace bemelegítés

Egy Sun-os konferencián hallottam először a Dtrace nevű eszközről, mely egy dinamikus nyom-követő. Sajnos használható formában csak Solaris vagy OpenSolaris platformokra érhető el, és mivel eléggé kernel szintű integrációja van a rendszerrel nehézkes a portolása. Sokáig vártam egy Linux-os verzióra, de sajnos még a mai napig sincs használható állapotban (vagy csak én nem boldogultam vele).
A Dtrace működése röviden annyi, hogy van egy saját programozási nyelve a D, és ezen a nyelven kell megírnunk a kernelnek, hogy a vizsgált alkalmazás mely állapot-változásait figyelje. A dolog szépsége, hogy az alkalmzásunkba nem kell "hook" pontokat elhelyezni, tehát a működés megfigyelése teljes mértékben kívülről történik, menet közben megváltoztatható, és nem utolsó sorban, akár éles üzemben is bevethető. A programozó döntése, hogy mit figyel, a megfigyelt eseményekről mekkora mennyiségű információt irat ki a képernyőre, vagy akár egy log fájlba.
Bár a Dtrace platform-függő, létezik egy kifejezetten Java alkalmazásokra szánt Java-s átírat, amit Btrace-nek hívnak (a honlap nem tudom meddig lesz elérhető, mert a project a Kenai forrás-megosztó oldalon van elhelyezve, amit jól megvett az Oracle és jól be is szippantja "belső" használatra). A Btrace egyébként nyílt forrású project, és jelenleg az 1.1.1-es verziónál tart.
Használat:
  • Telepítése igen egyszerű, jóformán nincs, a bináris-t ki kell csomagolni a kívánt helyre.
  • Ezután első lépésként meg kell írnunk első Btrace scriptünket, mely annyit csinál, hogy az application.Main osztálynak figyeli a someMethod metódus hívását.
    package btrace;
    import static com.sun.btrace.BTraceUtils.*;
    import com.sun.btrace.annotations.*;
    @BTrace public class ClassTracer {
       @OnMethod(clazz = "application.Main", method = "someMethod")
       public static void methodcalled() {
           println("Method called");
       }
    }
    
  • Következő lépésként a vizsgálandó alkalmazást kell elindítani, egy apró külöbséggel, hogy a JVM-et meg kell kérni, hogy HotSpot módban induljon, ennek módja az alábbi: `java -client -jar sampleapp.jar`. A jar-ba fordított programocska:
    package application;
    public class Main {
       public static void main(String[] args) {
           for (int i = 0; i < 1000; i++) {
               someMethod();
               try { Thread.sleep(100);
               } catch (Exception ex) {}
           }
       }
       public static void someMethod() {}
    }
    
  • Miután fut az alkalmzásunk egy arra alkalmas eszköz segítségével meg kell tudnunk a futó programunk azonosítóját, PID-jét, Linuxon a top parancs segítségével jutunk sikerre, bár nem ez az egyetlen mód.
  • Majd a Btracet kell ráállítani a JVM-re, hogy dolgozza fel a kapott információkat a `btrace PID ClassTracer.java` paranccsal (ha elsőre Connection refused hiba-üzenettel leáll még 1x érdemes megpróbálni futtatni, van, hogy elsőre nem indul el).
  • Nincs más dolgunk, mint élvezni a konzolt elárasztó Method called kiírásokat.
A módszer legnagyobb előnye, hogy bytecode szinten tudjuk nyomon-követni alkalmazásunkat, azt látjuk, ami a virtuális gépben ténylegesen végrehajtódik.
Még néhány lehetőség (a teljesség igénye nélkül):
  • Van mód arra, hogy a clazz vagy method @Annotáció érték helyére reguláris kifejezést illesszünk, így nem csak egy osztályt vagy metódust, tudunk lekezeleni, hanem a mintára illeszkedő összeset.
  • Mivel a callback metódusokat mi hozzuk létre, paraméterként számtalan bemeneti változó közül választhatunk, melyeket @Annotációkkal jelölhetünk. Pár példa: @ProbeClassName String probeClass paraméterrel az osztályt, @ProbeMethodName String probeMethod paraméterrel a metódust kapjuk meg. @Return Class returnClass-al megkapjuk a metódus visszatérési értékét (ehhez az @OnMethodban fel kell venni egy location=@Location(Kind.RETURN) értéket), a AnyType[] args-el az átadott argumentumokat.
  • Egy Btrace scripten belül párhuzamosan több metódussal is követhetjük az állapotot, ennek köszönhetően a programozó fantáziájára van bízva, hogy pontosan mit is szeretne megfigyelni.
  • Lehetőség van időzített ismétlődő műveletek futtatására, így képet kaphatunk a processzor terheltségéről, vagy akár a memória használatról is, ekkor az @OnMethod helyett @OnTimer(long ms) @Annotációt kell használnunk.
  • Meglévő Dtrace scriptjeinkkel is összeköthetjük Btrace scriptjeinet, bár ennek nem sok értelmét látom, hisz ahol van Dtrace ott minek használnák a Btracet.
  • Létezik egy VisualVM plugin is a Btracehez, így a scripteket a VisualVm-en belül is szerkeszthetjük akár futás-időben is. Mindennapi használatra ezt javaslom egyébként, számtalan ponton könnyíti a munkát.
A felhasználói kézikönyv szerintem magáért beszél, továbbá a példa kódok bőbeszédűek, azokból is elsajátítható az eszköz használata.
Még egy fontos jó-tulajdonsága van a Btracenek, mégpedig az, hogy mivel POJO osztályokat kell létrehoznunk, és @Annotációkat használ a sajátságos működéshez, a jar-okat importálva bármely Java képes fejlesztő-eszközhöz van támogatás, ami valljuk be elég nagy könnyebbség a fejlesztőnek.
Zár-szóként remélem kedvet kapott a kedves Olvasó, hogy kipróbálja ezt a remek eszközt, és bár az írás eléggé bevezető jellegű mégis hasznos volt.