Blog

Classpath considered harmful

„The classpath is dead“ 

— Mark Reinhold (Chief Architect Java Platform Group, Oracle)

Als diese Aussage 2009 die Runde machte, erregte sie einiges Aufsehen in der Java-Community. Seitdem sind fünf Jahren vergangen. Nach den Maßstäben technologischer Entwicklung also eine Ewigkeit, aber dennoch ist der Class Path in den meisten Java-Projekten immer noch sehr lebendig und die damit einhergehenden Probleme sind nach wie vor weit verbreitet. Eine Besserung ist nicht in Sicht: Auf der einen Seite gibt es Lösungsansätze, die sehr lange existieren, aber aus verschiedenen Gründen keine Verbreitung gefunden haben. Und auf der anderen Seite hat das Projekt einer standardisierten Lösung für das gesamte Ökosystem seit 2006 eine sehr lange und erfolglose Historie hinter sich und ist nun erst einmal auf Java 9 (!) vertagt. Angesichts dieser Perspektive scheint es, als müsste man sich mit der Situation abfinden. Oder doch nicht?

Class Loading 101

Eine Lösung kann man erst dann bewerten, wenn man das Problem genau verstanden hat – wie funktioniert also aktuell Class Loading in der JVM? Die Spezifikation für die JVM definiert ein Modell von hierarchischen, delegierenden Class Loadern. Dabei ist der Class Loader, der als Erstes eine Klasse laden soll, also dessen loadClass als Erstes für eine bestimmte Klasse aufgerufen wird, der sogenannte initiale Class Loader. Dieser versucht nun, die Klasse in bis zu drei Schritten zur Verfügung zu stellen:

  1. Wenn die Klasse bereits durch ihn geladen wurde, stellt er sie direkt zur Verfügung.
  2. Falls dies nicht der Fall ist, ruft er die loadClass-Methode des übergeordneten, sogenannten Parent Class Loaders auf.
  3. Sollte die Klasse durch diesen Aufruf nicht gefunden worden sein, versucht er selbst die Klasse zu finden.

Durch dieses Verfahren wird eine Klasse immer durch den „höchsten“ Class Loader in der Hierarchie geladen, der Zugriff auf eine Klasse hat. Dabei „sehen“ Class Loader immer nur die Klassen, die sie selbst oder die übergeordneten Parent Class Loader geladen haben. Die Class Loader stellen also einen gerichteten Baum dar. An dessen Spitze steht der mit nativem Code implementierte Bootstrap Class Loader, der die Klassen der Java Runtime lädt. Also unter anderem alles, was sich unter java.lang befindet und alle anderen Dateien, die im rt.jar, also in der Standard Runtime zu finden sind. Daran schließt sich der Extension Class Loader an. Dieser Class Loader ist zuständig für Klassen, die zwar nicht zur Standard Runtime gehören, die aber als so zentral angesehen werden, dass sie jedem Java-Programm zur Verfügung stehen sollen. Dafür werden alle Jars geladen, die sich in den Verzeichnissen, die in dem System Property java.ext.dirs aufgelistet sind, finden lassen. Dazu gehört bei der Oracle JVM 7 z. Bsp. die Java Cryptography Extension und Internationalisierungsinformationen für java.text und java.util. Diese vorgegebene Hierarchie endet nun mit dem System Class Loader, der den beim Start angegebenen Class Path zur Verfügung stellt.

Welcome To Jar Hell!

Dieses Modell ist durchaus angemessen, wenn sich die Anzahl der Abhängigkeiten in Grenzen hält. Allerdings steigt in den meisten Projekten die Anzahl der Abhängigkeiten sehr schnell stark an. Schließlich will man auf bestehende Funktionalität zurückgreifen, anstatt diese selbst zu implementieren, um effizient zu arbeiten. Das hat jedoch zur Folge, dass die Anzahl der eingesetzten Middlewarekomponenten, Frameworks und Libraries im Projektverlauf schnell wächst. Und damit beginnen die Probleme mit dem hierarchischen Modell.

Diese Probleme kommen meist durch transitive Abhängigkeiten zustande: Wenn man schon eine direkte Abhängigkeit auf eine populäre Bibliothek besitzt, bringt eine andere Abhängigkeit eine inkompatible Version dieser Bibliothek als eigene Abhängigkeit mit. Nun wird entweder die eine oder die andere Version geladen – mit den entsprechenden problematischen Folgen. Zwar können Gradle, Maven, Ivy und Co. solche Konflikte erkennen, aber ihre Beseitigung ist schwierig. Darüber hinaus macht es das Modell kompliziert, Bibliotheken zwischen verschiedenen Applikationen (z.B. auf einem Applikationsserver) zu teilen. Mit diesem Problem ist man als Java-Entwickler nicht allein. Gem Hell, DLL Hell, RPM Hell – sie alle teilen dieselbe Ursache: Ohne eine Möglichkeit der Isolierung zwischen Abhängigkeiten bekommt man irgendwann Probleme.

Lösungskandidaten

Da dieses Problem fast genau so alt ist wie Java selbst, wundert es nicht, dass es über die Jahre hinweg viele Projekte gegeben hat, die versucht haben, das Problem auf die eine oder andere Art zu lösen. Manche davon sind schon fast historisch zu nennen, wie z.B. classworlds (letztes Release Anno 2004). Andere wie etwa simple-dm setzen zwingend ein Maven-Repository voraus. Der mit Abstand prominenteste Vertreter ist dabei sicherlich OSGi. Es konnte sich jedoch trotz Unterstützung großer Unternehmen und zahlreicher Implementierungen nicht durchsetzen und gilt als kompliziert. Und die Lösung des Problems mittels einer Standardisierung ist – wie eingangs erwähnt – in weite Ferne gerückt. Muss man sich also mit dem Problem abfinden? Oder gibt es doch eine einfache, praxistaugliche Lösung?

Hallo JBoss Modules!

Ein weiteres Projekt, das angetreten ist, um dieses (und in guter, alter Unix-Tradition auch wirklich nur dieses eine) Problem zu lösen, ist JBoss Modules. Und obwohl es das gut und gründlich tut, ist es außerhalb der JBoss Community so gut wie unbekannt. Das mag zum einen daran liegen, dass der Name vermuten lässt, die Lösung sei JBoss-spezifisch (was nicht stimmt) und zum anderen an der sehr dürftigen Dokumentation. Dass Red Hat, außer im Zusammenhang mit dem JBoss AS, kaum öffentlich über das Projekt spricht, ist auch nicht gerade hilfreich.

modulesWenn man JBoss Modules nutzt, besteht der Class Path der Anwendung aus genau einer Bibliothek: jboss-modules.jar. Weitere Abhängigkeiten teilt man in Module auf. Jedes dieser Module besteht aus einer XML-Datei, die beschreibt, welche Jars Teil des Moduls sind und auf welche anderen Module Abhängigkeiten bestehen, und den dazugehörigen Bibliotheken. Ein Modul wird über seinen Namen (meist groupId und artifactId – z. Bsp. com.google.guice) und optional eine Version (für den Fall, dass man mehrere Versionen vorhalten möchte), der sog. „slot“, gekennzeichnet. Jedes Modul wird dabei zur Laufzeit durch einen eigenen Class Loader geladen. Hat das Modul eine Abhängigkeit auf ein anderes Modul, dann delegiert der Class Loader an den Class Loader dieses Moduls. Es findet jedoch keine weitere Delegation statt. Innerhalb eines Moduls ist also nur der Zugriff auf die direkten, nicht jedoch auf die indirekten Abhängigkeiten möglich. Genau das verhindert das Entstehen einer Jar Hell, in der sich Versionskonflikte entwickeln können. Statt einem hierarchischen Baum erhält man also einen allgemeineren Graph von Class Loadern.

Auch ist es sehr einfach, dieselbe Bibliothek nur einmal zu laden und dann mit verschiedenen Anwendungen darauf zuzugreifen, um Ressourcen zu sparen. Neben der Vermeidung von Versionskonflikten hat diese Lösung auch Vorteile für die Performance: Klassen müssen nicht mehr in einem „Big Ball of Mud“ gefunden werden. Module werden nur bei Bedarf geladen und es werden mehrere Threads gleichzeitig verwendet. Nur dank dieser Verbesserung ist es möglich, dass JBoss AS 7 im Bruchteil einer Sekunde gestartet werden kann, während seine Vorgänger ein Vielfaches an Zeit benötigen.

No Silver Bullet

Auch wenn JBoss Modules eine pragmatische Lösung darstellt, gibt es trotzdem noch Unwägbarkeiten. Bibliotheken, die sich darauf verlassen, Klassen im Thread Context Class Loader zu finden, sind problematisch. Darüber hinaus gehen die aktuellen Dependency-Management-Tools wie Maven, Ivy und Gradle davon aus, dass es keine Isolation zwischen den Abhängigkeiten gibt (auch wenn es bereits ein Maven Plugin gibt, das bei der Erstellung von Modulen hilft). Mit anderen Worten: man muss muss etwas mehr Hand anlegen, als man gewöhnt ist. Man wird jedoch mit besserer Performance, weniger Class Loading-Problemen und einem wesentlich einfacheren Abhängigkeitsmodell belohnt.

Holisticon AG — Teile diesen Artikel

Über den Autor

Die Holisticon AG ist eine Management- und IT-Beratung aus Hamburg. Wir entwickeln beste Individualsoftware, Webplattformen und Apps. Geschäftsprozesse durchdringen wir und automatisieren sie. Große Datenmengen machen wir mit Smart-Data-Ansätzen beherrschbar. ...und das alles agil.

Ein Kommentar

Antwort hinterlassen