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.