Start - Publikationen - Wissen - TOGAF - Impressum -

Einleitung


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:

  1. finde die Klasse im Cache des Classloaders
  2. wenn nicht gefunden: finde die Klasse mittels des übergeordneten Classloaders (der die gleiche Suchstrategie anwenden wird), ist der nicht gesetzt, benutze findBootstrapClass()
  3. wenn nicht gefunden: suche die Klasse mittels der Implementierung von findClass
  4. wenn nicht gefunden: ClassNotFoundException wird geworfen, andernfalls wird die Klasse gecached
Die Definition des Suchalgorithmus ist so noch nicht vollständig. Es muss geklärt werden, welcher Classloader den Einstiegspunkt bildet, dieser wird als initialer Classloader bezeichnet. In den meisten Fällen wird der initiale Classloader implizit von der JVM ausgewählt:
  1. In Klasse A wird der new-Operator der Klasse B ausgeführt: für das Laden von B's Klasse wird die JVM den Classloader von A als initialen Classloader verwenden.
  2. Klasse A ist abhängig von Klasse B: für das Laden von B's Klasse wird die JVM den Classloader von A als initialen Classloader verwenden.
  3. Class.forName() ohne Angabe eines Classloaders oder ObjectInputStream.resolveClass() wird gerufen: die JVM untersucht den Callstack, das erste Objekt, dessen Klasse nicht vom Bootstrap-Classloader geladen wurde bestimm den initialen Classloader.
Der initiale ClassLoader kann aber auch explizit ausgewählt werden: ClassLoader.getSystemClassLoader(), Thread.currentThread().getContextClassLoader() oder obj.getClassLoader() liefert eine Classloader-Instanz an der zum Beispiel loadClass() gerufen werden kann.

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.

Classloading in Java SE


In Java SE Umgebungen bestimmen drei Classloader die Classloader-Hierarchie:

  1. Bootstrap-Classloader: lädt Klassen aus rt.jar und i18n.jar (kann manipuliert werden, zB mit -Xbootclasspath für Suns JVM)
  2. Extension-Classloader: lädt alle Klassen in den JARs in jre/lib/ext (kann manipuliert werden, zB mit der "java.ext.dirs" system property für Suns JVM)
  3. System-Classloader: lädt alle durch '-cp', '-classpath' oder die 'java.class.path' system property addressierten Klassen. Dieser Classloader kann mit ClassLoader.getSystemClassLoader() referenziert werden und ist der übergeordnete Classloader aller selbstdefinierten Classloader.
  4.   Bootstrap-Classloader
              |
      Extension-Classloader
              |
        System-Classloader
    
Durch diese strikte Hierarchie werden Versionskonflikte zur Laufzeit vermieden. Andererseits ist es unmöglich, in ein und derselben VM verschiedene Versionen einer Klasse zu benutzen. Das Problem ist weniger akademisch als man zunächst annehmen möchte. Stellen Sie sich vor, sie entwickeln Code und benutzen eine Bibliothek utils.jar in der Version 1.1. Gleichzeitig verwenden Sie ein Framework framework.jar in Ihrer Software. Wenn framework.jar utils.jar in der Version 2.0 erwartet, wird es zur Laufzeit zu Konflikten kommen, wenn das Framework Klassen aus utils.jar direkt oder indirekt über seine Schnittstellen exponiert. Der Standardmechanismus zum Classloading besitzt für dieses Problem keine Lösung.

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:

  1. In einer Serverlaufzeit sollen verschiedenen Anwendungen mit unterschiedlichen Konfigurationen unabhängig voneinander laufen. Jede Java EE Anwendungen hat deshalb ihren eigenen Namensraum und ist die Wurzel einer eigenen Classloading-Hierarchie.
  2. Java EE Anwendungen sind komponentenbasiert, die verschiedenen Komponenten werden typische Abhängigkeiten zueinander aufweisen, die zum Teil in den Deploymentdeskriptoren festgelegt werden. Webanwendungen oder Richclients haben Abhängigkeiten zum EJB-Tier, von hier aus gibt es Abhängigkeiten zu Integrationskomponenten (Resource Adapter) und Hilfsklassen.
  3. Wegen lokaler Interfaces müssen Abhängigkeiten im EJB-Tier berücksichtigt werden.
Auch diese Herausforderungen löst eine geeignete Classloading-Hierarchie bei der immer zuerst die Schicht zu der Abhängigkeiten bestehen nach Klassen durchsucht wird. Technisch ist das gewöhnlich implementiert, indem die Classlaoding-Hierarchie von einer entsprechenden Classloader-Hierarchie repräsentiert wird. Auch wenn das nicht zwingend so sein muss, wird ab jetzt nur noch von einer Classloader-Hierarchie die Rede sein.

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:

  1. Der App-Classloader findet die Klassen der Server-Laufzeit und Klassen in allen JARs im /lib Verzeichnis des EAR (aber nicht in Unterverzeichnissen und hier werden auch keine Java EE Komponenten erwartet), und Klassen in den Archiven in der Wurzel des EAR in Application-Clients (JARs) und Resource Adapter (RARs).
  2. Der EJB-Classloader einer jeden Anwendung findet alle Klassen des App-Classloaders und die Klassen in allen ejb.jar.
  3. Die Classloader der Web-Apps finden die Klassen des EJB-Classloaders ihrer Application, Klassen im WEB-INF/classes Verzeichnis und in allen JARs im WEB-INF/lib Verzeichnis.
Anmerkungen:
  • Zusätzlich müssen die Angaben für das Class-Path Attribut in den META-INF/manifest.mf aller so referenzierten JARs aufgelöst werden. Das sind alle client.jar, util.jar, web.war, adapter.rar und ejb.jar. Es können also in der Wurzel des EAR noch weitere JARs liegen, die mit Class-Path Einträgen referenziert sind und deren Klassen gefunden werden.
  • Im Beispiel sind Resource Adapter per Anwendung deployed. Sie können aber auch serverweit deployed werden, wenn die angesprochene Ressource allen Anwendungen gleichermaßen zur Verfügung stehen soll.
In der META-INF/application.xml des EAR werden Clients, Resource Adapter, EJBs und Webanwendungen deklariert. Server suchen aber auch ohne diese Angaben im EAR nach Java EE Komponenten und folgen dabei einfachen Regeln: sie suchen nach den Dateien META-INF/application-client.xml, META-INF/ra.xml, META-INF/ejb.xml und WEB-INF/web.xml in allen Archiven, die sie in der Wurzel des EAR finden. Des weiteren werden JARs mit einem Main-Class Attribut in der META-INF/manifest.mf als Application Clients und mit Klassen mit EJB-Annotationen als EJBs interpretiert.

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.

Konflikte


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.

Beispiele


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:

  • Server - die gesamte Serverinstanz nutzt einen Classloader (genauer: serverweit wird jede Klasse eindeutig einem Classloader zugeordnet)
  • Application - in der Serverinstanz nutzt jede Java EE Anwendung einen Classloader (genauer: per Anwendung wird jede Klasse eindeutig einem Classloader zugeordnet)
  • Module - per Modul wird jeder Klasse eindeutig ein Classloader zugeordnet
  • Compatibility - für Abwärtskompatibilität (hier nicht diskutiert)
Die Ebene Application entspricht damit der Standardkonfiguration des OC4J 10g.

Ä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.

Zusammenfassung


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.

Referenzen


Java EE Specifications

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