Der Prozess mit dem die JVM den Bytecode einer Klasse sucht, findet und lädt wird als Classlaoding bezeichnet. Das Classloading wird von Classloadern durchgeführt, das sind Instanzen der abstrakten Klasse ClassLoader. Das Standardverhalten beim Classloading kann durch konkrete Implementierungen dieser Klasse verändert werden, davon wird in Java EE Laufzeitumgebungen ausgiebig Gebrauch gemacht. Jede geladene Klasse bleibt mit ihrem Classloader für die Laufzeit der JVM assoziiert, dieser Classloader kann mit obj.getClassLoader() referenziert werden. Wird derselbe Bytecode von unterschiedlichen Classloadern geladen gelten die dadurch definierten Klassen aus der Sicht der Laufzeit als unterschiedlich, Instanzen dieser Klassen lassen sich nicht aufeinander casten.
Jede Classloader-Instanz hat eine übergeordnete Classloader-Instanz. Die Ausnahme bildet der Bootstrap-Classloader, er ist keine Instanz von ClassLoader sondern integraler Bestandteil der JVM und die Wurzel in der Classloader-Hierarchie. Diese Classloader-Hierarchie ist die Grundlage für den Standard-Suchalgorithmus zum Laden von Klassen:
Seit Java2 ist jeder Thread mit einer Classloader-Instanz assoziierbar. Diese Instanz wird als Context-Classloader bezeichnet und kann mit Thread.currentThread().setContextClassLoader() gesetzt werden. Der betroffene Thread wird ab diesen Zeitpunkt den übergebenen Classloader zum Laden der Klassen benutzen. Der Unterschied zwischen Class.forName() und Thread.currentThread().getContextClassLoader().loadClass() wird hier noch einmal deutlich - Class.forName() nutzt den definierenden Classloader der Klasse, in der der Aufruf erfolgt. Thread.currentThread().getContextClassLoader().loadClass() nutzt den Context-Classloader. Das Ergebnis ist normalerweise verschieden.
In Java SE Umgebungen bestimmen drei Classloader die Classloader-Hierarchie:
Bootstrap-Classloader | Extension-Classloader | System-Classloader
Eine umfassende Lösung für dieses Problems bietet das deklarative Classloading der Eclipse-Runtime. Jedes Plugin bekommt dort seinen individuellen Classloader zugewiesen und die Klassen verschiedener Versionen leben dadurch in unterschiedlichen Namensräumen.
Classloading in Java EE Umgebungen
Das Classloading in Java EE Umgebungen basiert technisch gesehen auf dem Classloading in Java SE Umgebungen und erweitert die Classloading-Hierarchie zur Laufzeit. Eigentlich kann, da nicht detailliert spezifiziert, das Classloading in jedem Server einer ganz eigenen Strategie folgen. Jedoch erzwingen Java EE Laufzeitumgebungen gewisse grundlegende Anforderungen, diese bedingen vergleichbare Strategien der serverspezifischen Umsetzungen für das Classloading in Java EE Umgebungen:
Der Java SE Classloader-Stack (Bootstrap / Extension / System) lädt den Server. Jede Java EE Anwendung (app.ear) bekommt einen eigenen Classloader der die Klassen der Clientkomponenente (client.jar) Integrationskomponenten (adapter.rar) und Hilfsklassen (utils.jar) auffindet. Das EJB-Tier jeder Anwendung bekommt einen eigenen aber für alle EJB einheitlichen Classloader (alle ejb.jar) und jede Webanwendung (web.war) bekommt einen eigenen Classloader.
Bootstrap | Extension | +---- System ----+ Server-Laufzeit | | App1 App2 Application: Application-Client, Resource Adapter, Utilities | | + EJB1 + + EJB2 + EJB Schicht: alle EJB-Klassen dieser Application | | | | Web1_1 Web1_2 Web2_1 Web2_2 Webschicht
In diesem Bild muss man auch die Vorgaben der Spec verorten, mit welcher Strategie jede Classloaderschicht in einem EAR nach Klassen sucht:
Während der Entwicklung von Java EE Anwendungen muss diese Classloader-Hierarchie für die einzelnen Komponenten der Anwendung exakt nachgebaut sein, damit es zur Laufzeit nicht zu Konflikten beim Laden der Klassen kommt. Das erledigt man manuell, man bedient sich der Komfort-Funktionen seiner IDE, oder man nutzt die Java EE Funktionen von Build-Systemen wie Maven.
Eine Classloader-Hierarchie bei der der übergeordnete Classloader zuerst Klassen auflöst wird als parent first bezeichnet und vermeidet zuverlässig Classloading-Konflikte. Allerdings ergeben sich auch starke Einschränkungen für das Packaging einer Java EE Anwendung. Angenommen zur Entwicklung wurde eine util.jar in unterschiedlichen Versionen in der WEB-INF/lib einer Webapplikation und im Classpath der ejb.jar des EJB-Tiers genutzt. Dann kommt es zur Laufzeit zu Konflikten, denn die Webapplikation wird mit den Klassen der util.jar aus dem EJB-Tier versorgt. Deshalb wird in der Java EE Spezifikation klar festgelegt: There must be only one version of each class in an application. If one component depends on one version of a library, and another component depends on another version, it may not be possible to deploy an application containing both components. Eine starke Einschränkung, insbesondere weil Abhängigkeiten zu Bibliotheken oftmals indirekt über die Nutzung von Frameworks ins Spiel kommen und nicht immer zu vermeiden sind.
Viele Serverimplementierungen erlauben aus diesem Grund eine Beeinflussung der beschriebenen Classloading-Strategie, bei der die Java EE Module einer Anwendung Klassen zunächst bei sich suchen. Diese Strategie wird mit parent last bezeichnet und dann können durchaus in ein und derselben Anwendung mehrere Versionen einer Klasse Verwendung finden. Sind die Java EE Module untereinander über lokale Interfaces, die direkt oder indirekt von solchen gemeinsam verwendeten Klassen abhängen, stark gekoppelt, kommt es zur Laufzeit zu Versionskonflikten. Bei der von der Spezifikation empfohlenen parent first Strategie passiert das schon zur Compilezeit und ist damit betrieblich wesentlich unproblematischer. Außerdem verliert durch parent last Classloading die Anwendnung ihre Portabilität, eigentlich einer der großen Vorteile der Java EE Technologie.
Dieses Problem ist erkannt und wird aktuell im Rahmen des Specification Requests JSR-277 diskutiert (Zitat: 'The specification defines a distribution format and a repository for collections of Java code and related resources. It also defines the discovery, loading, and integrity mechanisms at runtime.'). Das Ergebnis ist wohl irgendwas zwischen RPM und OSGi und muss zwangsläufig die Ambiguitäten beim Classloading in Java EE Umgebungen beseitigen. Bis dahin können wir nicht warten und wenden uns lieber praktischen Problemen zu.
Wie bereits erwähnt bieten Serverimplementierungen verschiedene Möglichkeiten, vom Standardverhalten des Classloadings abzuweichen. Zum Test reicht eine Java EE Anwendung aus zwei Webmodule WebA und WebB und zwei EJB Module, EJB1 und EJB2. Alle Module nutzen die Funktion einer Klasse in einer utils1.jar, die in unterschiedlichen Versionen in den einzelnen Modulen betrieben wird.
Beim OC4J 10g (9.0.4.0.0) sehe ich dieses Verhalten: werden zwei Webapplikationen ohne EJB-Modul deployed, dann haben sie getrennte Classloader mit einem gemeinsamen Parent-Classloader (dem Server-Classloader). Soweit ist alles normal. Die EJB-Module bekommen erwartungsgemäß einen gemeinsamen Classloader. Dieser ist nun der Parent-Classloader der Web-Classloader der Anwendung. Damit ist es unmöglich, unterschiedliche Versionen einer utils.jar in der EJB-Schicht und den Modulen der Webschicht laufen zu lassen. Es wird in diesem Falle immer die Version aus der EJB-Schicht benutzt. Ebenso lassen sich nicht unterschiedliche Versionen einer utils.jar in verschiedenen EJB-Modulen betrieben. Es werden immer nur die Klassen einer utils.jar geladen.
Dieses Verhalten kann gewollt sein, da mit dieser Strategie zur Laufzeit ClassCastExceptions verhindert werden. Andererseits ist der Ansatz, die verschiedenen Java EE-Module unabhängig voneinander zu entwickeln und zu testen und gemeinsam zu deployen nicht umsetzbar. Aus diesem Grund kann in der orion-web.xml die Option
<orion-web-app> <web-app-class-loader include-war-manifest-class-path="true" <--- Nutzt auch Classpath-Einträge im MANIFEST des WAR search-local-classes-first="true"/> <--- Erlaubt separierbare Module </orion-web-app>gesetzt werden, die dafür sorgt, dass die Web-Classloader zunächst nach den Klassen im lib-Verzeichnis suchen. Damit ließen sich Web-Module und die EJB-Schicht mit unterschiedlichen Versionen einer Drittanbieter-Bibliothek betreiben. Dann dürfen aber auch die von beiden Modulen gemeinsam benutzten Klassen keine Abhängigkeiten zu diesen Klassen haben. In diesem Falle gäbe es zur Laufzeit einen java.lang.LinkageError.
Der JBoss verfolgt eine ganz ähnliche Strategie. In seiner Standardkonfiguration werden alle Klassen mittels Delegation in allen Modulen einheitlich geladen. Das entspricht exakt dem Standard-Verhalten des OC4J und vermeidet zuverlässig Konflikte beim Classloading. Allerdings gilt auch hier: die Module der Anwendung müssen notwendig einer einheitlichen Versionen einer Klassenbibliothek laufen.
Sobald unterschiedliche Versionen eines Moduls oder seiner Abhängigkeiten gemeinsam deployed werden müssen, trägt dieser Ansatz nicht mehr. Beim JBoss kann man dann für jedes EAR ein eigenes Classloader-Repository deklarieren. Die Classloder der Module des EAR werden dann mit der Suche nach Klassen in diesen Repositories beginnen:
<jboss-app> <loader-repository> misc.example:loader=application.ear <-- Modularisierung auf EAR-Ebenen <loader-repository-config> java2ParentDelegation=false <-- Erlaubt separierbare Module </loader-repository-config> </loader-repository> </class-loading> : </jboss-app>Auch hier gilt selbstverständlich, dass unterschiedliche Versionen von Klassen in den Modulen nur benutzt werden können, wenn sie nicht direkt oder indirekt über die Schnittstellen der Module exponiert werden.
Der WebSphere Application Server ab Version 4.x bietet vier verschiedene Ebenen der Granularität beim Classloading:
Ähnliches ist auch beim BEA Weblogic realisiert. Auch hier liegt im Standardfall eine Parent-Child Beziehung zwischen EJB-JAR und WAR vor, wenn diese Module in ein und demselben EAR deployed werden. (Separat deployed besitzen diese Module voneinander isolierte Classloader-Hierarchien und die Webanwendung muss beispielsweise mit den Interfaces und Stubklassen der EJB Anwendung versorgt werden - dieser Fall wird hier nicht beleuchtet).
Man kann, wie im OC4J und JBoss, den Classloader eines Webmoduls anweisen, zuerst die Modul-eigenen Klassen zu nutzen, erst dann übergeordnete Classloader zu fragen. Dazu wird in der weblogic.xml folgender Eintrag ergänzt:
<container-descriptor> <prefer-web-inf-classes>true</prefer-web-inf-classes> <--- Erlaubt separierbare Module : </container-descriptor>
Die Weblogic-Entwickler gehen noch einen Schritt weiter und erlauben die Deklaration einer Classloader-Hierarchie (über das classloader-structure Element in der weblogic-application.xml). In Verbindung mit der prefer-web-inf-classes-Option lassen sich damit schon sehr flexible Konfigurationen realisieren.
Die Java EE Spezifikation definiert in welcher Weise nach den Klassen einer Anwendung gesucht wird. Um Konflikte beim Classloading zu vermeiden wird implizit vorausgesetzt, dass per Java EE Anwendung, per EJB-Tier und für jede Webanwendung ein eigenständiger Classloader das Laden der Klassen organisiert und dabei in beschriebener Weise als Teil einer Classloader-Hierarchie fungiert, bei der Klassen immer zuerst im übergeordneten Classloader gesucht werden. Vorteil: Versionskonflikte werden sicher vermieden, Nachteil: von jeder Klasse kann immer nur eine Version per Anwendung zum Einsatz kommen, alle Komponenten einer Anwendung müssen auf ein und derselben Codebasis entwickelt und gepflegt werden!
In der Praxis kann das Classloading-Verhalten eines Servers durch (serverspezifische) Konfiguration beeinflusst werden. Modularisierung wird erreicht, indem das Classloading mit der Suche nach Klassen im eigenen Modul beginnt. Serverhersteller können sich nun mit feingranularen Classloading-Konfigurationen gegeneinander differenzieren. Leider kostet diese Freiheit in letzter Konsequenz die Portabilität der Anwendungen, da sich die Module in unterschiedlichen Servern in einem unterschiedlichen Konfigurationszustand befinden. Nicht wenige Entwickler sehen das inzwischen als ein Defizit der Java EE Spezifikation und der JSR 277 befasst sich intensiv mit diesen Fragen.