Christoph Läubrich Freiberuflicher Diplom-Informatiker OSGi Supporter seit 2011

JDBC Datenquellen dynamisch unter OSGi nutzen

Beim Umgang mit OSGi trifft man immer wieder auf Technologien, welche sich etwas widerspenstiger zeigen bei der Integration in OSGi, da diese sich nicht so ganz in das Modulkonzept und die Classloader Hierarchie fügen wollen.
Ein (relativ harmloser) Kandidat ist dabei JDBC. Folgende Probleme können einem dabei begegnen:
  1. Der Treiber muss dem System bekannt gemacht werden mittels Class.forName(\"...\")
  2. Für den Verbindungsaufbau müssen ggf. Klassen geladen werden die der Bundleclassloader nicht erreichen kann (ClassNotFoundException)
  3. Es existiert kein Standardisierte Weg den Treiber/Connection zu laden und die benötigten Einstellungen zu setzen
Punkt 1 ließe sich noch relativ einfach über einen Aktivator lösen, das Problem ist hier nur, einmal aktiviert verschwindet der Treiber nie wieder aus dem System.
Punkt 2 und 3 könnte man dadurch lösen, dass man die konkrete JDBC Implementierung importiert, und z.B. benötigte Datenquellen per DNS bekannt macht, das ist aber nicht wirklich dynamisch, man bekommt nicht mit wenn eine DNS Quelle "wegfällt" außerdem muss man dann für jeden Einsatzzweck wieder neue Module schreiben welche die benötigten Quellen bereitstellen, auch wird es dann schwer Modular zu arbeiten, man muss dann schon im Vorfeld wissen welche Modul in der Anwendung aktiv sein werden und welche DNS diese benötigen.
Auch wenn eine Umstellung des JDBC Treibers ansteht hat man das Problem dass nun ggf. wieder Anpassungen nötig sind, wir wollen ja aber gerade erreichen, dass wir echte wiederverwendbare, modulare Software erhalten.

OSGi DataSourceFactory als dynamische Alternative

Scheinbar ist dieses Problem nicht ganz unbekannt, sodass die OSGi Spezifikation bereits eine Lösung vorsieht, den DataSourceFactory Service,wird aber momentan noch recht wenig angewandt, zumindest findet man im Internet kaum Informationen dazu, und auch die JDBC Treiber der Datenbankhersteller scheinen dies nicht standardmäßig bereitzustellen.

Das Interface ist ziemlich simpel und enthält nur die folgenden Methoden:
  • createDataSource
  • createConnectionPoolDataSource
  • createXADataSource
  • createDriver
Damit hat man eigentlich alles was man braucht, konfiguriert werden diese über einen fest definierten Satz von Standardroperties, ggf. gewünschte oder benötigte weitere Einstellungen sind aber weiterhin möglich.

Wer also eine (JDBC)Datenbankverbindung aufbauen möchte, holt sich einfach einen passende DataSourceFactory (ggf. gefiltert über osgi.jdbc.driver.class), füllt ein Standardset von Eigenschaften in ein Properties Objekt und kann sich dann die benötigten Verbindungen erzeugen. Wird der betreffende Service aus dem System entfernt, so bekommen wir das mit (entweder automatisch per Deklarativem Service oder ServiceTracker) und können darauf geeignet reagieren. Da der Service für das Erzeugen von neuen Objekten zuständig ist müssen wir uns keine Sorgen um Classloading-Probleme machen, und durch die standardisierte Übergabe der Verbindungsparameter kann eine Implementierung einfach und kompatible ausgetauscht werden (solange wir nicht von speziellen selbst-definierten Parametern abhängig sind).

DataSourceFactory erstellen und ggf. weitere Vorbereitungen

Was uns wie oben schon angedeutet nicht erspart bleibt ist, einmalig für den gewünschten JDBC Treiber den Service zu implementieren.
Schlimmer noch, Equinox in der Version 3.6 bringt nicht mal das benötigte ServiceInterface mit. Das ist ärgerlich, glücklicherweise bietet die OSGi Alliance das Interface auch als Download an unter http://www.osgi.org/Download/Release4V42 einfach zu den Downloads durchklicken, dort dann den OSGi Service Platform Release 4 Version 4.2 Enterprise Companion Code wählen, man erhält dann ein JAR aus welchem man sich aus dem OSGi-OPT Ordner das entsprechende Interface entleihen kann (Lizenz ist Apache License Version 2.0), schnell noch in ein Bundle gepackt und Package exportieren und schon kann es losgehen.

Implementierung am Beispiel des MySQL Connector/J

Nun können wir mit unserer Implementierung starten, hierfür nutzen ich als Beispiel den MySQL Connector/J welcher praktischerweise schon als Bundle vorliegt und deshalb nur noch in die Targetplattform aufgenommen werden muss.

Für den Service habe ich ein Bundle mit dem Namen com.mysql.jdbc.service angelegt, um die Implementierung zu registrieren nutze ich Deklarative Services, natürlich ist auch der Weg über den BundleContext und einen Aktivator denkbar, die DS-XMl sieht dann folgendermaßen aus:

mysql.xml

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" 
    activate="start" name="com.mysql.jdbc.service">
   <implementation class="com.mysql.jdbc.service.MySQLJDBCDataSourceService"/>
   <property name="osgi.jdbc.driver.class"   type="String" value="com.mysql.jdbc.Driver"/>
   <property name="osgi.jdbc.driver.name"    type="String" value="MySQL Connector/J"/>
   <property name="osgi.jdbc.driver.version" type="String" value="5.1.15"/>
   <service>
      <provide interface="org.osgi.service.jdbc.DataSourceFactory"/>
   </service>
</scr:component>
Also nicht besonders spektakulär, es wird lediglich der Service org.osgi.service.jdbc.DataSourceFactor exportiert, sowie einige Properties, welche es anderen Modulen möglich machen ggf. nach einem bestimmten Service zu filtern.
Die Implementierung zeigt sich ebenfalls als recht übersichtlich

Implementierung der Service Methoden

public void start() throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    //Load driver if not already done...
    Class<?> clazz = Class.forName("com.mysql.jdbc.Driver");
    // The newInstance() call is a work around for some
    // broken Java implementations, see MySQL Connector/J documentation
    clazz.newInstance();
}

@Override
public DataSource createDataSource(Properties props) throws SQLException {
    MysqlDataSource source = new MysqlDataSource();
    setup(source, props);
    return source;
}

@Override
public ConnectionPoolDataSource createConnectionPoolDataSource(Properties props) throws SQLException {
   MysqlConnectionPoolDataSource source = new MysqlConnectionPoolDataSource();
   setup(source, props);
   return source;
}

@Override
public XADataSource createXADataSource(Properties props) throws SQLException {
   MysqlXADataSource source = new MysqlXADataSource();
   setupXSource(source, props);
   return source;
}

@Override
public Driver createDriver(Properties props) throws SQLException {
   com.mysql.jdbc.Driver driver = new com.mysql.jdbc.Driver();
   //Any setup neccessary?
   return driver;
}
In der Init Methode wird der Treiber "aktiviert", und somit auch gleich geprüft ob dieser Tatsächlich verfügbar ist, würde hier eine Exception auftreten, so würde die Komponente nicht aktiviert und somit der Service nicht zur Verfügung stehen, so sollte es ja auch sein.
Die anderen Methoden erzeugen nur noch die MySQL-Spezifischen Klassen und delegieren dann die Aufgabe des Setups über die Properties an eine Hilfsmethode, sodass wir nicht jedes mal den gleichen Code hinschreiben müssen.

Hilfmethoden für Properties Setup

/**
 * Setups the basic properties for {@link DataSource}s
 */
private void setup(MysqlDataSource source, Properties props) {
    if (props == null) {
        return;
    }
    if (props.containsKey(JDBC_DATABASE_NAME)) {
        source.setDatabaseName(props.getProperty(JDBC_DATABASE_NAME));
    }
    if (props.containsKey(JDBC_DATASOURCE_NAME)) {
        //not supported?
    }
    if (props.containsKey(JDBC_DESCRIPTION)) {
        //not suported?
    }
    if (props.containsKey(JDBC_NETWORK_PROTOCOL)) {
        //not supported?
    }
    if (props.containsKey(JDBC_PASSWORD)) {
        source.setPassword(props.getProperty(JDBC_PASSWORD));
    }
    if (props.containsKey(JDBC_PORT_NUMBER)) {
        source.setPortNumber(Integer.parseInt(props.getProperty(JDBC_PORT_NUMBER)));
    }
    if (props.containsKey(JDBC_ROLE_NAME)) {
        //not supported?
    }
    if (props.containsKey(JDBC_SERVER_NAME)) {
        source.setServerName(props.getProperty(JDBC_SERVER_NAME));
    }
    if (props.containsKey(JDBC_URL)) {
        source.setURL(props.getProperty(JDBC_URL));
    }
    if (props.containsKey(JDBC_USER)) {
        source.setUser(props.getProperty(JDBC_USER));
    }
}

/**
 * Setup the basic and extended properties for {@link XADataSource}s and
 * {@link ConnectionPoolDataSource}s
 */
private void setupXSource(MysqlXADataSource source, Properties props) {
    if (props == null) {
        return;
    }
    setup(source, props);
    if (props.containsKey(JDBC_INITIAL_POOL_SIZE)) {
        //not supported?
    }
    if (props.containsKey(JDBC_MAX_IDLE_TIME)) {
        //not supported?
    }
    if (props.containsKey(JDBC_MAX_STATEMENTS)) {
        //not supported?
    }
    if (props.containsKey(JDBC_MAX_POOL_SIZE)) {
        //not supported?
    }
    if (props.containsKey(JDBC_MIN_POOL_SIZE)) {
        //not supported?
    }
}
An dieser Stelle zunächst ein Hinweis: Die Spezifikation fordert eigentlich, dass "If the property cannot be set on the [...] being created then a SQLException must be thrown", dahingehend ist die Implementierung etwas großzügiger und ignoriert unbekannte Eigenschaften, was meiner Meinung nach praktikabler ist.

Somit wäre unsere Implementierung nun fertig und bereit für den Einsatz.

Ein kleiner Test

Als nächstes sollte man natürlich einmal seine Arbeit testen, z.B. folgendermaßen

Simple Testklasse

public void setDataSourceFactory(DataSourceFactory factory) throws SQLException {
    System.out.println("Starting MySQL OSGi Test...");
    Properties prop = new Properties();
    prop.put(DataSourceFactory.JDBC_DATABASE_NAME, "test");
    prop.put(DataSourceFactory.JDBC_SERVER_NAME, "localhost");
    prop.put(DataSourceFactory.JDBC_USER, "test_user");
    DataSource source = factory.createDataSource(prop);
    Connection connection = source.getConnection();
    Statement stm = connection.createStatement();
    ResultSet result = stm.executeQuery("SELECT * FROM test_table");
    while (result.next()) {
        System.out.println(result.getObject(1));
    }
    result.close();
    stm.close();
    connection.close();
}

Fazit

Wie wir gesehen habe ist es letztendlich gar nicht so kompliziert einen JDBC Treiber OSGi fit zu machen. Um so verwunderlicher, dass nicht zu jedem Treiber bereits ein passender OSGi-Wrapper bereitgestellt wird.

Über Kommentare, Feedback oder Hinweise wie die fehlenden Eigenschaften bei MySQL gesetzt werden würde ich mich freuen.
Vollständiges Bundle und Sourcecode als Download

Der Code wird unter der GPL v2.0 Lizenz unter der auch (unter anderem) der MySQL JDBC Connector zur Verfügung gestellt wird.

Geändert am 09.04.2011 Zuletzt runtergeladen 10.05.2018 (973 mal) Größe 11,72 KiB
Alternatives Bundle für die H2 Datenbank

Geändert am 02.09.2011 Zuletzt runtergeladen 10.05.2018 (853 mal) Größe 7,87 KiB
Letzte Änderung 07.08.2015