Du änderst eine Zeile Java-Code, startest deine Anwendung neu und erwartest ein anderes Verhalten. Stattdessen passiert exakt das Gleiche wie vorher. Du setzt zusätzlich einen Breakpoint in IntelliJ, aber auch der wird nicht erreicht. Also prüfst du noch einmal deinen Code. Die Änderung ist da. Maven baut das Projekt ohne Fehler. WildFly startet ebenfalls sauber. Trotzdem verhält sich die Anwendung so, als hättest du überhaupt nichts geändert.

Genau dieser Moment ist einer der verwirrendsten im Java-Alltag: Du siehst eindeutig neuen Code vor dir, aber die Anwendung führt offenbar alten aus. Das wirkt zunächst so, als würde Java deine Änderung ignorieren. Tatsächlich liegt das Problem oft gar nicht in der Codezeile selbst. Entscheidend ist vielmehr, welche kompilierte Klasse tatsächlich ausgeführt wird - und genau hier kommen Classloader ins Spiel.

 

Was ein Classloader eigentlich macht

Java führt deinen Quellcode nicht direkt aus. Eine Datei wie UserService.java wird zunächst kompiliert und daraus entsteht eine UserService.class. Diese Datei enthält den Bytecode, den die JVM ausführen kann. Wenn deine Anwendung nun com.example.UserService benötigt, muss die JVM wissen, wo sich diese Klasse befindet. Genau dafür gibt es Classloader. Vereinfacht gesagt suchen sie nach der passenden Klasse, laden sie und stellen sie der JVM zur Verfügung. Interessant wird das dadurch, dass eine Java-Anwendung nicht zwingend nur einen einzigen Classloader besitzt. Schon die JVM selbst arbeitet mit mehreren Classloadern für unterschiedliche Bereiche. Ein Application Server wie WildFly geht noch deutlich weiter. Dort können Servermodule, Libraries und einzelne Deployments in unterschiedlichen Classloading-Kontexten laufen.

Dadurch kann dieselbe Klasse theoretisch mehrfach vorhanden sein. Angenommen, com.example.UserService steckt einmal direkt in deiner Anwendung und zusätzlich in einer eingebundenen JAR-Datei. Dann entscheidet nicht IntelliJ darüber, welche Variante verwendet wird, sondern die Classloading-Struktur der laufenden Anwendung. Genau deshalb kann es passieren, dass du in UserService.java eine Rückgabe von "alt" auf "neu" änderst, im Browser aber weiterhin "alt" angezeigt bekommst. Deine Änderung ist korrekt, nur die laufende Anwendung verwendet nicht die Klasse, die du gerade vor dir siehst.

Eine bereits geladene Klasse wird außerdem normalerweise nicht einfach ersetzt, nur weil du die entsprechende .class-Datei auf der Festplatte verändert hast. Wenn WildFly eine Klasse beim Start eines Deployments geladen hat und du anschließend den Quellcode neu kompilierst, bedeutet das nicht automatisch, dass der laufende Server plötzlich diese neue Version verwendet. Genau hier entsteht der Eindruck, die Anwendung würde Änderungen ignorieren. In Wahrheit führt sie einfach weiterhin den Bytecode aus, den sie zuvor geladen hat.

 

Zwischen IntelliJ und Maven können mehrere Versionen deines Codes entstehen

Im Entwicklungsalltag beginnt die Verwirrung häufig schon lange bevor WildFly ins Spiel kommt. IntelliJ kann Java-Code selbst kompilieren und Maven kann denselben Java-Code ebenfalls kompilieren. Beide Wege müssen aber nicht zwangsläufig dieselben Ausgabeverzeichnisse verwenden. Maven schreibt kompilierte Klassen normalerweise nach target/classes, Testklassen landen unter target/test-classes. IntelliJ kann abhängig von deiner Projektkonfiguration eigene Compiler-Ausgaben verwenden. Gleichzeitig kann WildFly wiederum ein WAR oder EAR deployen, das aus einem ganz anderen Build stammt. Damit können auf deinem Rechner problemlos mehrere Versionen derselben Klasse existieren, obwohl du im Editor natürlich nur eine davon siehst.

Das führt zu einem sehr typischen Fehlerbild: Du änderst UserService.java, startest die Anwendung und nichts ändert sich. Also prüfst du die Zeile erneut und stellst fest, dass sie definitiv angepasst wurde. Was du dabei aber zunächst nicht siehst: IntelliJ zeigt dir lediglich die aktuelle Quelldatei. Ob Maven daraus bereits die richtige .class-Datei gebaut hat und ob genau diese Datei später im Deployment steckt, ist eine ganz andere Frage.

Deshalb ist mvn clean package bei solchen Effekten ein sinnvoller erster Schritt.

mvn clean package

Das clean löscht zunächst das komplette target-Verzeichnis, bevor Maven das Projekt neu baut. Alte .class-Dateien und alte Build-Artefakte verschwinden damit aus dem Maven-Build. Gerade nach Refactorings ist das wichtig. Wenn du beispielsweise eine Klasse verschoben oder umbenannt hast, kann eine alte kompilierte Variante sonst weiterhin im Build-Verzeichnis liegen, obwohl es den dazugehörigen Quellcode längst nicht mehr an dieser Stelle gibt.

Trotzdem ist auch mvn clean package kein magischer Reparaturbefehl. Angenommen, Maven baut dein Projekt korrekt neu und erzeugt ein aktuelles WAR. Wenn WildFly anschließend weiterhin das vorherige Deployment verwendet, hast du zwar einen sauberen Build, aber dein eigentliches Problem nicht gelöst. Deshalb solltest du im Zweifel direkt in das erzeugte Artefakt schauen. Ein WAR ist technisch im Wesentlichen ein ZIP-Archiv. Deine eigenen kompilierten Klassen findest du dort typischerweise unter WEB-INF/classes, eingebundene Libraries unter WEB-INF/lib. Wenn deine Änderung bereits dort fehlt, brauchst du WildFly noch gar nicht zu verdächtigen. Ist die aktuelle Klasse dagegen enthalten, musst du als Nächstes prüfen, welches Artefakt der Server tatsächlich lädt.

 

WildFly macht aus einem einfachen Problem schnell ein interessantes Problem

Bei einer normalen Java-Anwendung besteht die Classloading-Situation meistens aus der JVM, deiner Anwendung und ihren Abhängigkeiten. WildFly bringt zusätzlich ein modulares Classloading-System mit. Der Server stellt selbst zahlreiche APIs und Module bereit, deine Anwendung bringt eigene Libraries mit und bei einem EAR können zusätzlich mehrere WAR-, EJB- oder JAR-Module beteiligt sein. Das ist technisch sinnvoll, sorgt aber dafür, dass du bei Problemen genauer hinschauen musst, wo eine Klasse eigentlich herkommt.

Angenommen, dein Deployment besteht aus einem EAR mit einer service.jar und einer web.war. Die Klasse UserService liegt in der service.jar. Du änderst genau dort eine Methode und wunderst dich anschließend, warum die Anwendung weiterhin den alten Wert liefert. Wenn du versehentlich nur das WAR neu gebaut oder deployed hast, enthält dein EAR weiterhin die alte service.jar. In deinem Editor steht also beispielsweise:

public class UserService {

    public String getVersion() {
        return "neu";
    }
}

WildFly führt aber weiterhin die alte Klasse mit "alt" aus. Der Server ignoriert deine Änderung nicht. Er hat sie schlicht nie bekommen.

Ähnlich problematisch wird es, wenn dieselbe Library mehrfach vorhanden ist. Vielleicht liegt eine gemeinsame JAR bereits im EAR und zusätzlich noch einmal unter WEB-INF/lib eines WAR. Dann existiert dieselbe Klasse an mehreren Stellen und es ist nicht mehr sofort offensichtlich, welche Variante in welchem Modul geladen wird. Besonders deutlich werden solche Probleme manchmal bei einer ClassCastException, obwohl Quell- und Zieltyp auf den ersten Blick identisch aussehen. Für Java gehört zur Identität einer Klasse nämlich nicht nur ihr vollständiger Name. Auch der Classloader, der sie geladen hat, spielt eine Rolle. Vereinfacht können Classloader A -> com.example.User und Classloader B -> com.example.User für die JVM zwei unterschiedliche Typen sein.

Im normalen Entwicklungsalltag ist die Ursache aber häufig deutlich einfacher: WildFly führt ein anderes Deployment aus als das, von dem du glaubst, dass es läuft. Genau hier kann auch IntelliJ zusätzliche Verwirrung erzeugen. Eine Run Configuration legt fest, welches Artifact an WildFly übergeben wird. Du kannst dein Maven-Projekt also korrekt mit mvn clean package bauen und anschließend über IntelliJ trotzdem ein anderes oder älteres Artifact deployen.

Wenn du also wieder vor der Situation stehst, dass du eine Zeile eindeutig geändert hast und die Anwendung trotzdem das alte Verhalten zeigt, solltest du nicht nur auf diese Zeile starren. Verfolge stattdessen die komplette Kette: Welche Quelldatei hast du geändert? Welche .class-Datei wurde daraus erzeugt? In welchem WAR oder EAR steckt sie? Welches Artifact wird tatsächlich deployed? Und wurde dieses Deployment danach auch wirklich neu geladen?

 

Fazit

Wenn Java scheinbar alten Code ausführt, liegt die Ursache nicht automatisch darin, dass der Classloader verrücktspielt. Häufiger existieren zwischen deinem Quellcode und der laufenden Anwendung einfach mehrere Stände derselben Klasse. IntelliJ zeigt dir Version A, unter target/classes liegt möglicherweise Version B, im WAR steckt Version C und WildFly hat noch Version D geladen. Für die JVM ist dabei nur entscheidend, welche Klasse ihr Classloader tatsächlich geladen hat. Was gerade in deinem Editor steht, interessiert sie nicht.

Genau deshalb ist der Satz "Aber ich habe die Zeile doch geändert" zwar vollkommen nachvollziehbar, technisch aber noch kein Beweis dafür, dass die laufende Anwendung diese Änderung auch kennt. Ein mvn clean package sorgt für einen sauberen Maven-Build, danach muss aber auch genau dieses neue Artifact auf WildFly landen und dort neu geladen werden. Wenn du diese Trennung zwischen Quellcode, Build-Artefakt, Deployment und geladener Klasse einmal verstanden hast, verlieren viele dieser scheinbar mysteriösen Fehler ihren Schrecken. Der Code, den du gerade siehst, ist eben nicht automatisch der Code, den deine JVM gerade ausführt.