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

Integration von Hibernate in die OSGi Service Architektur

Update

Inzwischen bietet Hibernate selbst eine OSGi-Integration an, will man flexibel bleiben bietet es sich jedoch an gleich auf den Standard JPA-OSGi Service zu setzen in Form von Pax-JPA, neben Hibernate werden hier auch Eclipselink und OpenJPA sowie andere JPA-Provider unterstützt.

Einleitung

Dieser Artikel soll zeigen, wie man Hibernate möglichst vollständig in die Service Architektur von OSGi integrieren kann.
Zwar gibt es verschiedenste Ansätze auch im Web zu finden, eine zufriedenstellende Lösung findet man aber selten. In den meisten Fällen wird nur beschrieben, wie spezifische Probleme (insbesondere im Zusammenhang mit dem Classloading) gelöst werden können und die Beschreibung endet auch an diesem Punkt.
Im folgendem soll nun versucht werden, einen Weg zu beschreiben wie es möglich ist Hibernate unter Zuhilfenahme von Standard OSGi Services in das Gesamtsystem zu integrieren und anderen Modulen als Service zugänglich zu machen.
Insbesondere sollen folgende Aspekte untersucht werden:
  • Auflösung der Abhängigkeiten zu externen Bibliotheken
  • Dynamisches laden von Mapping-Klassen ohne direkte Abhängigkeit über das Extender-Pattern
  • Bereitstellen der JDBC Treiber als OSGi-JDBC Service und Nutzung in Hibernate über das Whiteboard-Pattern
  • Dynamische Verwaltung verschiedener Konfigurationen/Datenquellen mittels des ConfigurationAdminService.

Schritt 1: Auflösen der Abhängigkeiten und Schaffung eines Hibernate Basisbundles

Als erstes benötigt man natürlich die aktuelle Hibernate Version, genutzt würde hier die Version 3.6.1 die mit knapp 50 MiB zu buche schlägt.
Nun legt man sich ein neues Bundle an mit dem Namen org.hibernate an.
Als nächstes die hibernate3.jar übernehmen und in den Bundle-Classpath übernehmen (in Eclipse über den Manifest-Editor unter dem Reiter 'Runtime'), und folgende Packages exportieren:
  • org.hibernate
  • org.hibernate.cfg
  • org.hibernate.classic

Das war der einfache Teil. Nun geht es daran die im 'lib' Ordner liegenden externen Bibliotheken einzubinden. Hierbei ist sind prinzipiell folgende drei Verfahren denkbar:
  1. Die Abhängigkeit wird in das Hibernatebundle übernommen, die Packages der entsprechenden Abhängigkeit werden weder importiert noch exportiert.
  2. Die Abhängikeit wird durch ein separates Bundle bereitgestellt, dieses exportiert die nötigen Packages, das Hibernatebundle importiert diese.
  3. Eine Kombination aus beidem: Die Abhängigkeit wird in das Hibernatebundle integriert, gleichzeitig werden die betreffenden Packages als optionaler import hinzugefügt.
Die dritte Methode funktioniert folgendermaßen: Wenn das Package von einem anderem Bundle im OSGi System bereitgestellt wird, so wird diese importiert. Ansonsten kommt der Bundle-Classloader zum Einsatz und die intern vorhandenen Klassen werden geladen (siehe OSGi Core Specification Version 4.2, Abschnitt 3,6.3). Diese hat jedoch den Nachteil, dass einerseits das Bundle unnötig groß wird, das Bundlemanifest extrem unübersichtlich und zumindest mit Equinox unter Eclipse scheint es in bestimmten Konfigurationen Probleme mit optionalen imports zu geben. Wir werden daher im weiterem Verlauf entweder Methode 1 oder Methode 2 anwenden, auch wenn später Methode 3 für einen speziellen Fall von Bedeutung sein wird wenn es um die Integration der JDBC Treiber geht.

Required Libarys

Wir beginnen zunächst uns dem laut Hibernate nötigstem zuzuwenden. Unter required finden wir: antlr-2.7.6.jar, commons-collections-3.1.jar, dom4j-1.6.1.jar, javassist-3.12.0.GA.jar, jta-1.1.jar, slf4j-api-1.6.1.jar. Die ersten vier (ANTLR, Commons Collections, dom4j und Javassist) werden auf dem erstem Wege genauso wie das hibernate3.jar direkt in das Hibernatebundle integriert, da ich diese Library bisher nicht einsetze. Wer dies jedoch tut sollte für diese ein separates Bundle anlegen oder z.B. bei Orbit runterladen.
Die jta-1.1.jar enthält die "JBoss Transaction 1.0.1 API", eine alternative Version gibt es bei Orbit als Bundle javax.transaction. Ob man dies also extern einbindet oder direkt bleibt jedem selbst überlassen, ich habe dies vorerst ebenfalls integriert.
Als letztes bleibt noch die SLF4J API. Da ich selbst SLF4J verwende habe ich dieses entfernt und stattdessen einen import für das Package org.slf4j hinzugefügt. Falls man SLF4J nicht einsetzt, so empfiehlt es sich trotzdem dieses auszulagern, da möglicherweise andere Bundles ebenfalls davon abhängen könnten und somit unnötige Doppelung und mögliche Classloadingprobleme vermieden werden.

Optional Libarys

Dieser Ordner enthält vor allem verschiedene Cache Implementierungen und optionale Erweiterungen. Hier muss jeder für sich entscheiden welche Erweiterungen er nutzen möchte. Ich habe zunächst keine dieser Erweiterungen genutzt.

Sonstige Libarys

Was bleibt ist jpa und bytecode. Das erste beinhaltet die Java Persistence API, und stellt hauptsächlich die Annotationen für die Persistenzklassen bereit. Um eine direkte Abhängigkeit der späteren Datenklassen vom Hibernatebundle zu verhindern sollte dieses ausgelagert werden. Orbit bietet hier schon ein fertiges Bundle javax.persistence an, leider nur in der 1.0 Version, Hibernate scheint aber zwingend 2.0 zu benötigen. Also muss man sich dieses selbst bauen, oder ebenfalls in das Hibernatebundle integrieren (und dann die packages
  • javax.persistence
  • javax.persistence.criteria
  • javax.persistence.metamodel
exportieren.
Achtung: Beim export darauf achten als Version 2.0.0 exportieren, damit dies nicht mit einer möglicherweise im Target vorkommenden Version 1.0 interferriert, auch beim Import sollte eine minimum Version von 2.0 gefordert werden, ansonsten kann es vorkommen, dass Hibernate die Klasse nicht korrekt mapped und stattdessen eine EntityNotFoundException bescheren.

Die bytecode Anteile bieten noch Alternativen zum in unserem Fall standardmäßig verwendetem Javassist, kann also getrost weggelassen werden.

Der erste Schritt ist damit getan, wir haben ein Hibernate-Basisbundle auf dem unsere weitere OSGi-Integration aufbauen kann.
MANIFEST.MF für das Hibernate-Bundle

Beispiel für das resultierende Hibernate-Bundle Manifest (inklusive Versionsangabe für javax.persistence packages)

Geändert am 01.05.2011 Zuletzt runtergeladen 09.11.2019 (1392 mal) Größe 1,07 KiB

Schritt 2: Definieren eines Hibernate Session Service

Als nächstes definieren wir uns ein Serviceinterface, welches und Sessions von Hibernate für bestimmte Datenquellen bereitstellt.
Zu diesem Zweck habe ich mir ein Bundle mit dem Namen org.hibernate.osgi angelegt, welches auch das zu importierende Package darstellt für alle späteren Bundles welche den Service nutzen wollen.

Service Interface: SessionContext

package org.hibernate.osgi;

import org.hibernate.Session;

/**
 * Service responsible for creating and releasing of sessions in a given Context
 * 
 * @author Christoph Läubrich
 * 
 */
public interface SessionContext {

	/**
	 * Creates and returns a session in this context. Be sure to release this
	 * session some time later even if an exception occurs! To safely access a
	 * session object without the need to take care about sessions and releasing
	 * it, use the # {@link #accessSession(SessionAccess)} method
	 * 
	 * @return the freshly created session
	 */
	public Session getSession();

	/**
	 * Releases a session back to the context. Each session obtained from the
	 * context should be released after usage. If the session has an open
	 * transaction, this one is rolled back. If the session is open, it will be
	 * closed
	 * 
	 * @param session
	 */
	public void releaseSession(Session session);

	/**
	 * 
	 * @return an ID describing this context, in most cases a combination of
	 *         host, port and schema
	 */
	public String getID();

	/**
	 * Provides a way of save accessing a session. This means, a session is
	 * obtained from the context, passed to the access object, and then is
	 * automatically released afterwards even if an exception occurs.
	 * 
	 * @param <T>
	 * @param access
	 * @return
	 */
	public <T> T accessSession(SessionAccess<T> access);

}
Da der Aufwand für erzeugen einer Session und der sichere Zugriff darauf mit der Zeit nerven kann und um zu vermeiden immer und immer wieder die gleichen try/catch/finally blöcke zu schreiben besitzt unser Service Interface eine Methode zum sicherem Zugriff, dafür definieren wir noch folgendes Hilfsinterface:

Hilfsinterface: SessionAccess

package org.hibernate.osgi;

import org.hibernate.Session;
import org.hibernate.Transaction;

/**
 * Access Interface to access a session
 * 
 * @author Christoph Läubrich
 * @param <T>
 */
public interface SessionAccess<T> {
    /**
     * Provides a session for accession data object. The transaction for this
     * session is already started. To commit changes call
     * {@link Session#getTransaction()}{@link Transaction#commit()} otherwhise
     * it will be rolled back automatically
     * 
     * @param session
     * @return
     */
    public T access(Session session);

}

Schritt 3: Interaktion mit dem OSGi Framework

Schematische Übersicht der Beteiligten Komponenten
Als nächstes stellen wir ein paar Überlegungen an wie unser neu erdachter Service mit dem Rest des Frameworks interagieren soll, es sind folgende Module beteiligt:
  • Der LogService für Logausgaben (optional)
  • Der ConfigurationAdminService welcher uns Konfigurationen bereitstellt, z.B. über die Verbindungsurl, Benutzernamen, Passwort und Datenbankschema.
  • Der HibernateManager welcher eine ManagedServiceFactory bereitstellt und die Konfigurationen vom ConfigAdminService empfängt und die nötigen Objekte erstellt oder ggf. wieder frei gibt.
  • Die OSGiHibernateSessionFactory welche die Implementierung für unser ServiceInterface beisteuert, und mittels eines BundleListener sich Informationen über die Bundles holt und Mappings einsammelt.
  • Eine Menge von DataSourceFactorys welche einen oder mehrere JDBC Datenquellen zur Verfügung stellen.

Auf der rechten Seite gibt es das ganze nochmals als Schematische Darstellung. Aus Gründen der Übersichtlichkeit werde ich nicht genauer auf die Implementierung eingehen, sondern im folgendem anhand eines Beispiels einige Probleme näher erläutern, den vollständigen Sourcecode des Bundles werde ich demnächst hier veröffentlichen.
Die Implementierung einer DataSourceFactory anhand des Beispiels des MySQL Connector/J wird im Artikel JDBC unter OSGi nutzen erläutert.

Schritt 4: JDBC Treiber Hibernate bekannt machen

Zunächst müssen wir nun noch Hibernate bekanntmachen wie es an die JDBC Treiber kommen soll, hier wird ein Weg beschrieben über einen ConnectionProvider.
Leider geht Hibernate (und das Beispiel) davon aus, dass ein linearer Classpath existiert, wir können also der Konfiguration keine ConnectionProvider Instanz übergeben, sondern Hibernate versucht diesen über Reflection zu laden, was schief läuft, da der Classloader des Hibernatebundles nicht auf unser OSGi Bundle zugreifen kann.
Wir hätten hier wieder die Möglichkeit, dass das Hibernate-Bundle unser Hibernate-OSGi Bundle als Abhängigkeit enthält. Das Problem ist, das wir dann eine zirkulare Abhängigkeit erzeugen, da das Hibernate-OSGi ja vom Hibernate-Bundle abhängt.
Also doch wieder alles in ein Bundle und unsere Trennung aufgeben? Natürlich nicht. Wir werden uns eines kleines Tricks bedienen, welchen wir im weiterem Verlauf noch benötigen werden.
Glücklicherweise nutzt Hibernate an dieser Stelle den ContextClassLoader, wir erzeuge also einen neuen delegation Classloader, welchen wir für die Zeit der SessionFactory Erzeugung setzen und so Hibernate das laden der Klasse ermöglichen.

Classinjection mit Delegation-Classloader

Thread thread = Thread.currentThread();
ClassLoader contextClassLoader = thread.getContextClassLoader();
ClassLoader loader = new ClassLoader(contextClassLoader) {

    /* (non-Javadoc)
     * @see java.lang.ClassLoader#findClass(java.lang.String)
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        return SessionHandler.class.getClassLoader().loadClass(name);
    }

};
thread.setContextClassLoader(loader);
factory = configuration.buildSessionFactory();
thread.setContextClassLoader(contextClassLoader);
Wir delegieren also alle Anfragen die der aktuelle Classloader nicht beantworten kann an "unseren" Klassenlader weiter. Man könnte jetzt das ganze noch aus sicherheitstechnischer Sicht verbessern indem nur der spezifische Klassenname geladen wird den wir auch tatsächlich weiterleiten wollen, für den Moment soll es uns so aber reichen.

Schritt 5: Unser erstes DAO Bundle... und erste Probleme

Nachdem wir nun alles vorbereitet haben ist es an der Zeit endlich mal ein kleines TestDAO anzulegen.

Test DAO

package de.laeubisoft.hibernatetest.data;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

/**
 * @author Christoph Läubrich
 */
@Entity
@Table(name = "test", schema = "test")
public class TestDAO implements Serializable {

    private static final long serialVersionUID = 7827517804260638035L;

    @Id
    @Column(name = "id")
    private long              id;

    @Column(name = "text")
    private String            text;

    public void setId(long id) {
        this.id = id;
    }

    public long getId() {
        return id;
    }

    public void setText(String text) {
        this.text = text;
    }

    public String getText() {
        return text;
    }

    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "(" + id + ") " + text;
    }
}
Leider begrüßt uns Hibernate gleich mal mit einer ClassNotFoundException... und das obwohl wir der Konfiguration bereits ein korrektes Klassenobjekt mitgeben versucht Hibernate die Klasse nochmals über Reflection zu laden... warum auch immer, normalerweise würde das auch klappen wenn der Kontext der Configuration gleich der der Mappingklassen ist, nur in unserem dynamischem Setup schlägt dies fehl.
Um dies zu umgehen werden wir nochmals die gleiche Technik wie oben anwenden und unseren Klassenlader entsprechend erweitern.

Erweiterter Delegation-Classloader

final Map<String, Class<?>> mappedClasses = new Hashtable<String, Class<?>>();

...

Thread thread = Thread.currentThread();
ClassLoader contextClassLoader = thread.getContextClassLoader();
ClassLoader loader = new ClassLoader(contextClassLoader) {

    /* (non-Javadoc)
     * @see java.lang.ClassLoader#findClass(java.lang.String)
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        if (mappedClasses.containsKey(name)) {
            Class<?> daoClass = mappedClasses.get(name);
            if (daoClass != null) {
                return daoClass;
            }
        }
        if (OSGiConnectionProvider.class.getName().equals(name)) {
            return SessionHandler.class.getClassLoader().loadClass(name);
        }
        return super.findClass(name);
    }

};
thread.setContextClassLoader(loader);
LOG.trace("[{}] build session factory for configuration with delegation contextloader {}", id, contextClassLoader);
factory = configuration.buildSessionFactory();
thread.setContextClassLoader(contextClassLoader);
Nun kann Hibernate unsere DAO Klasse laden und alles könnte perfekt sein... aber eine Überraschung bleibt noch. Auf irgendwelchen verschlungenen Pfaden versucht Hibernate nun mit Hilfe von javassist einen Proxy zu erzeugen und schlägt mit einer weiteren ClassNotFoundException fehl.
Leider geschieht dies jetzt scheinbar über den Classloader der Klasse selbst, sodass uns nun vollständig die Kontrolle darüber abhanden gekommen ist. Als Lösung bleibt uns so nur die folgenden Packages zu importieren:
  • javassist.util.proxy;version=\"3.12.0\"
  • org.hibernate.proxy;version=\"3.6.1\"
Das ist zwar nicht so schön ohne einen weiteren Proxy oder ByteCode-Manipulationen kommen wir nicht aus dieser Misere heraus (falls jemand eine elegante Lösung kennt bitte melden!).
Nun haben wir es aber geschafft und können endlich durchstarten!

Fazit

//to be continued
Letzte Änderung 23.01.2016