Start - Publikationen - Wissen - TOGAF - Impressum -

Grundlagen


An anderer Stelle habe ich die Grundlagen des Classloadings in Java-Umgebungen beschrieben und dabei auf ein grundlegendes Problem im Java-Umfeld hingewiesen: die Standardprozedur beim Classloading unterstützt nicht das Zusammenspiel isolierter Laufzeitmodule. Die Lösungen die im J2EE Umfeld von Serverentwicklern bereitgestellt werden sind kein Standard und nicht portabel. Ich habe auch darauf hingewiesen, dass an diesem Problem im Rahmen des JSR-277 gerade aktiv gearbeitet wird.

Vor diesem Hintergrund bietet die Eclipse Runtime eine Lösung die zum einen OSGi-konform ist, zum anderen inzwischen ein de facto Standard repräsentiert.

In der Terminologie der Eclipse Runtime sind die top level Komponenten sogenannte Plugins. Die in diesem Zusammenhang wichtigste Eigenschaft dieser Plugins: Erweiterungsmöglichkeiten (extension points) und die Abhängigkeiten zu externem Code und anderen Plugins werden im Rahmen eines außersprachlichen Mechanismus deklarativ festgelegt. Zur Laufzeit wird die Eclipse Runtime diese Deklarationen berücksichtigen. Zur Entwicklungszeit übernimmt das die Eclipse IDE wenn das betreffende Projekt ein Plugin-Projekt ist.

Verschiedene Plugins dürfen Abhängigkeiten zu unterschiedlichen Versionen desselben Codes besitzen. Die Eclipse Runtime ermöglicht das, indem sie jedem Plugin zur Laufzeit einen eigenen Classloader zuordnet. Statt einer Classloader-Kette (wie in J2SE) oder einer Classloader-Hierarchie (wie in vielen J2EE Containern) erzeugt die Eclipse Runtime also ein System isolierter Classloader.

Diese Isolierung wird an den Schnittstellen der Plugins unweigerlich verletzt. Zur Laufzeit muss sichergestellt sein, dass die über die Schnittstelle ausgetauschten Objektinstanzen von den Clients der Schnittstelle verwendet werden können. Dazu werden (trivialerweise) die Klassen der Schnittstelle und alle impliziten Abhängigkeiten "exportiert", indem ein entsprechender Eintrag in der Plugin-Deklaration vorgenommen wird. Der Classloader eines Clients des Plugin lädt diese Klassen nun über den Classloader des Plugins - vermittelt wird dieser Mechanismus von der Eclipse Runtime.

Die Praxis


Zur Illustration dieser Feststellungen wird das Laufzeitverhalten voneinander abhängiger Plugins untersucht. Ein Plugin Service S nutzt eine Bibliothek Utilities U für einen seiner extension points. Damit ist gemeint, dass in der Schnittstelle dieses extension point eine direkte oder indirekte Abhängigkeit zu U besteht. Ein weiteres Plugin Client C erweitert diese Extension und besitzt damit eine Abhängigkeit zu S.

S -> U
^
|
C
Nun gibt es verschiedene Möglichkeiten, C mit der Abhängigkeit zu U zu versorgen. Für die richtige Lösung wird U in S exportiert (indem unter "Exported Packages" die entsprechenden Pakete aus U freigegeben werden).
richtig
S -> U -> export
^
|
C
Hier sind Widersprüche zwischen Deklaration, Entwicklung und Laufzeit vermieden. Allerdings kann C nicht mit einer eigenen Version von U umgesetzt werden. Alle Versuche in dieser Richtung müssen scheitern.

In einem zweiten Szenario wird es dennoch versucht, in C ist nun ein Classpath Eintrag auf eine zu U nicht konforme Version U' deklariert.

falsch!
S -> U -> export
^
|
C -> U'
Eclipse informiert nicht über diesen Widerspruch, nimmt in C zur Entwicklungszeit U', zur Laufzeit allerdings U was in einem java.lang.LinkageError resultiert sobald der C-ClassLoader versucht, Klassen aus C mit Abhängigkeiten zu U' zu nutzen.

Im dritten Szenario wird U nicht exportiert und C behält den Classpath Eintrag auf U'.

falsch!
S -> U
^
|
C -> U'
Auch hier gibt Eclipse keine Warnungen zur Deklarationszeit, für die Entwicklung und Laufzeit in C wird U' benutzt. Zu Problemen führt das sobald der C-ClassLoader versucht, Klassen aus S mit ihren Abhängigkeiten zu U aufzulösen und wieder wird es einen java.lang.LinkageError geben.

Reflection und die Eclipse Runtime


Soweit dargestellt ist das Verhalten der Eclipse Runtime unabdingbar, um die gewünschte Isolierung der Plugins untereinander zu erreichen. Problematisch wird es dann, wenn third-party Software oder der eigene Code spezielle Annahmen über das Classloading trifft. Das ist beispielsweise der Fall bei J2EE Containern oder Persistenzframeworks. Bei Schwierigkeiten in dieser Richtung helfen möglicherweise diese Tipps:

  • Der Classloader eines Plugins lässt sich über eine Klasse des Plugins ermitteln: MyClass.class.getClassLoader() oder myObject.getClass().getClassLoader().
  • Aus einem Bundle ist eine Klasse über loadClass ladbar: Platform.getBundle("myplugin").loadClass("service.MyClass"). Zu beachten ist allerdings, dass eine Initialisierung der Klasse erst bei der ersten Benutzung erfolgt! Array-Typen können so nicht geladen werden.
  • Kennt man den Classloader, so funktioniert wie gewohnt: Class.forName("service.MyClass", myClassLoader). Die Klasse ist dann auch initialisiert.
  • Die Eclipse Runtime verkraftet ein vorübergehendes Ändern des Context-Classloaders:
  • ClassLoader old = Thread.currentThread().getContextClassLoader();
    try {
      Thread.currentThread().setContextClassLoader(mySpecialLoader);
      // ... Code, der nur mit mySpecialLoader funktioniert
    } finally {
      Thread.currentThread().setContextClassLoader(old);
    }
    //.. normal weiter
    

Buddy Class Loading


Die beschriebene Strategie des Classloadings in Eclipse/OSGi ist wasserdicht bis zu dem Moment, wenn außersprachliche Mechanismen zum Laden von Klassen benutzt werden. Um das zu illustrieren ist der Begriff der extensible library eingeführt worden. Er bezeichnet eine Bibliothek, die zur Laufzeit Klassen finden muss, die zur Compilezeit nicht zur Verfügung stehen. Typischerweise findet man in solchen Bibliotheken Codestellen wie Class.forName(..) oder contextLoader.loadClass(..).

Ein sehr praktisches Beispiel für eine solche Anforderung ist das Persistenzframework JPA/Hibernate. Integriert als Plugin H in eine Eclipse Laufzeit darf es keine Abhängigkeit zum Domainmodell-Plugin D besitzen. Diese ist schon in der anderen Richtung vergeben D nutzt Funktionalitäten von H). Zur Laufzeit ist das ein Problem insofern, als dass H Klassen aus D laden muss und diese nicht finden kann. Die Lösung des Problems heißt buddy class loading und ist seit Eclipse-M7 nutzbar. Dazu sind in den betroffenen Plugins Anpassungen in den Manifesten nötig. Zunächst muss H mitgeteilt werden, dass es aus eigener Kraft nicht alle nötigen Klassen laden kann. Im manifest.mf ist dazu der Eintrag

Eclipse-BuddyPolicy: registered
nötig. Der Classloader in H wird Klassen mit der Hilfe aller registrierten Plugins zu laden versuchen. Die Registrierung selbst erfolgt in D auch im manifest.mf
Eclipse-RegisterBuddy: H's symbolischer Name
Es gibt neben registered weitere Möglichkeiten einem Bundle mitzuteilen, wie es beim Laden von Klassen fremde Hilfe anzufordern hat
  • registered - nur alle registrierende Bundles (wie im Beispiel)
  • global - der globale Pool aller exportierten Pakete wird konsultiert
  • dependent - alle abhängigen Bundles helfen beim Laden der Klassen
  • app, ext oder boot - der Application-, Ext- beziehungsweise Boot-Classloader wird konsultiert

Wenn Sie noch nicht M7 im Einsatz haben, dann wird der schon beschriebene Trick, den ContextClassLoader vorübergehend zu ändern, helfen.

Referenzen


Finden und gefunden werden - Classloading in Eclipse
Classloading in J2SE und J2EE Umgebungen
Erweiterungen des Context Classloading in Eclipse

copyright © 2003-2021 | Dr. Christian Dürr | prozesse-und-systeme.de | all rights reserved