Maven: Build grün, NoClassDefFoundError zur Laufzeit
Kompilieren klappt, Tests laufen, beim Start fehlt die Klasse. Scope provided liegt im Compile- und Test-Classpath, aber nicht im Runtime-Classpath.
Aufwand:
ca. 20 Min.
Das Symptom
mvn clean package läuft ohne eine einzige Warnung durch, die Tests sind grün, das
JAR liegt im Zielverzeichnis. Beim Start bricht die Anwendung mit
java.lang.NoClassDefFoundError ab und nennt eine Klasse, die der Compiler eben
noch problemlos aufgelöst hat.
Besonders verwirrend wird es, wenn genau dieselbe Anwendung in der
Entwicklungsumgebung startet und nur außerhalb scheitert – oder umgekehrt: auf dem
Anwendungsserver läuft und als eigenständiges JAR nicht.
Die „Layer 8“-Wahrheit
Maven führt nicht einen Classpath, sondern mehrere, und der Scope entscheidet, in
welchen davon eine Abhängigkeit einsortiert wird. Für provided steht es
unmissverständlich in der Dokumentation: hinzugefügt zum Classpath für Kompilierung
und Test, aber nicht zum Runtime-Classpath. Der Build ist also nicht falsch grün –
er prüft schlicht eine andere Umgebung als die, in der die Anwendung später läuft.
provided ist ein Versprechen, und zwar keines von Maven, sondern eines vom
Entwickler an Maven: Diese Klassen bringt zur Laufzeit das JDK oder ein Container
mit. Für die Servlet-API in einem Web-Container stimmt das. Für eine Bibliothek in
einer eigenständig startenden Anwendung stimmt es nicht, und dann hält sich Maven
korrekt an eine Zusage, die nie jemand einlösen wollte. Die häufigste Herkunft
dieses Scopes ist übrigens kein Nachdenken, sondern Kopieren – aus einer Antwort,
in der es um ein Web-Projekt ging.
Der leisere Fall ist die Transitivität, oder besser deren Fehlen. provided ist
nicht transitiv, und in der Auflösungstabelle der Dokumentation ist die Spalte
provided durchgehend leer: Was eine Bibliothek als provided deklariert, kommt im
abhängigen Projekt in keinem Scope an. Wer eine fremde Bibliothek einbindet, erbt
deren Erwartung an einen Container, ohne davon zu erfahren. Der Compiler schweigt,
weil er die Klasse nie braucht, und die Lücke fällt erst beim ersten Aufruf im
Betrieb auf – dort, wo es teuer ist.
Die echte, verifizierte Lösung
1000 % faktenbasiert. Jeder Befehl ist real, jeder Pfad existiert. Schritt für Schritt:
1.
Den Scope der fehlenden Klasse nachschlagen
Die Meldung nennt den Klassennamen; daraus ergibt sich das Artefakt. Zuerst in der pom.xml nachsehen, mit welchem Scope es eingetragen ist. Steht dort provided oder test, ist die Ursache gefunden, bevor man irgendetwas ausprobiert hat. Der Abhängigkeitsbaum zeigt den Scope für jedes Artefakt mit an.
dependency:tree zeigt den vollständigen Abhängigkeitsbaum des Projekts. Mit dem Filter auf runtime sieht man genau das, was zur Laufzeit tatsächlich vorhanden ist – die Liste, die zählt, wenn die JVM startet.
mvn dependency:tree -Dscope=runtime
3.
Entscheiden, ob wirklich jemand liefert
provided ist eine Zusage an Maven, dass das JDK oder ein Container die Abhängigkeit zur Laufzeit bereitstellt. Diese Frage ehrlich beantworten: Läuft die Anwendung in einem Web-Container, der die Servlet-API mitbringt? Oder startet sie eigenständig, sodass es diesen Lieferanten gar nicht gibt?
4.
Den Scope korrigieren, wenn niemand liefert
Startet die Anwendung eigenständig, gehört die Abhängigkeit in den Standard-Scope compile – Compile-Abhängigkeiten stehen in allen Classpaths des Projekts zur Verfügung. Braucht der Compiler sie dagegen gar nicht, sondern nur die laufende Anwendung, ist runtime die genauere Wahl.
Bei fremden Bibliotheken die Lücke selbst schließen
Fehlt eine Klasse, die zu einer Abhängigkeit der Abhängigkeit gehört, hilft kein Scope-Wechsel im eigenen Projekt: provided ist nicht transitiv und wird deshalb nie weitergereicht. Die betroffene Abhängigkeit muss ausdrücklich in die eigene pom.xml, mit passender Version.
Häufige Fragen (FAQ)
?Wenn der Compiler die Klasse findet, warum findet die JVM sie nicht?
Weil Maven nicht einen Classpath führt, sondern mehrere. Eine Abhängigkeit mit Scope provided wird laut Dokumentation dem Classpath für Kompilierung und Test hinzugefügt, aber nicht dem Runtime-Classpath. Der Compiler sieht sie also, die laufende Anwendung nicht. Das ist kein Defekt, sondern genau die Zusage, die provided bedeutet: Zur Laufzeit liefert das jemand anderes.
?Wann ist provided richtig und wann falsch?
Richtig, wenn wirklich eine Laufzeitumgebung die Klassen mitbringt – das Standardbeispiel ist die Servlet-API, die der Web-Container bereitstellt. Falsch, sobald die Anwendung eigenständig startet, etwa als ausführbares JAR. Dann existiert der Container nicht, der liefern sollte, und niemand füllt die Lücke.
?Eine transitive Abhängigkeit fehlt plötzlich. Kann provided daran schuld sein?
Ja, und das ist der unauffälligere Fall. provided ist nicht transitiv. Eine Bibliothek, die eine Abhängigkeit als provided deklariert, reicht sie nicht an das eigene Projekt weiter – dort steht dann eine Lücke, die man selbst füllen muss. In der Tabelle zur transitiven Auflösung ist die Spalte provided durchgehend leer.
Belege
Diese Seite ist gegen die folgenden Primärquellen geprüft – Hersteller, Standard oder Gesetzestext. Wer es nicht glaubt, muss es nicht: Da steht, wo es nachzulesen ist.