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

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

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

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

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

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


package mysqlclasses;

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

public class ClassSaver {

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

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

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

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

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

}
}

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


package mysqlclasses;

import java.sql.Connection;

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

Connection con;

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

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

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

}

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

package test;

public interface Test {
public void some();
}


package test;

public class TestA implements Test {

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

}


package test;

public class TestB implements Test {

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

}

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


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

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

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

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

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


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

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

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

a.some();

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

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


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

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

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

Végezetül pár hasznos link:

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

4 thoughts on “Java osztályok tárolása MySQL adatbázisban”

  1. Csak hogy érdemben hozzászóljak a szokásos kritikámmal: nézzünk utána a működő megvalósításoknak is.

    Én mindenképpen behivatkoznám az Oracle Toplinket (http://www.oracle.com/technology/products/ias/toplink/index.html), ami egy általános objektumrelációs leképezést megvalósító API készlet. J2EE környékén szinte kizárólagosan ezt használják (mert az EJB3 szabvány is ezt tartalmazza 🙂 ), de szerintem J2SE fejlesztéshez is felhasználható. Érdemes ezt is megnézni, mert ez nagyon sok favágást automatikusan megold (persze megvan a dolognak a költsége is, az első használatnál rendszeres WTF jellegű problémák adódnak; nem mellesleg Java 1.5 feltétlen kell hozzá – bár ez már talán nem akkora probléma), és ami talán a legfontosabb: a Toplink segítségével elkészített, adatbázisba leképezett osztályok kódja nagyon szépen olvasható.

  2. Nem ismerem a TopLinket, de az nem perzisztencia réteg? Mert akkor olvasd el figyelmesebben a cikket 😉

    Nem adatbázis kezelő osztályokat generálok, hanem a fájlrendszer helyett adatbázisból töltök be általános célú osztályokat. És egy egyszerű, könnyen használható módszert adok rá, aminek sok előnye van egy hatalmas monolit osztálykönyvtárhoz képest.

    A kritikát egyébként elfogadom, valóban nem néztem utána, hogy létezik-e már hasonló.

  3. No comment. Akkor ezt is benéztem. A Toplink tényleg perzisztenciaréteg.

    Más kérdés, hogy van-e értelme ennek az osztálykönyvtárkezelésnek. Erőforrásszegény környezetben nem igazán használnak Java-t (pl. beágyazott rendszerekben), egyébként meg nem sokat számíthat. És mindezek mellett azt hiszem, ezeknél a megoldásoknál megpróbálják a statikus megoldást optimalizálni.

    Ha meg fejlesztői eszközhöz való a megoldás, és az osztályok könnyebb kezelése, akkor nehezen tudom elképzelni, hogy ez hogyan könnyíti meg a fejlesztést.

  4. Az alap probléma, amihez a fenti megoldás kiindulási alapot ad az az, hogy hogyan lehet tetszőleges helyen tárolt osztályokat betöltetni a java-val. Lehet, hogy egy konkrét problémára nem épp egy mysql adatbázisba szeretnénk osztályokat elhelyezni. Viszont elképzelhető olyan eset, amikor név->bytecode hozzárendelés helyett valamilyen bonyolultabb struktúrában szeretnénk tárolni az osztályokat. Ebben az esetben egy adatbázis sokat segíthet, és akár név helyett valamilyen meta-tulajdonsága alapján választjuk ki a használni kívánt osztályt.

    Tehát a cikk tágabb értelemben csak a ClassLoader használatára ad egy példát, és nem állítom, hogy amit összeraktam az önmagában használható.

Leave a Reply