Java ClassLoader: Klasse ersetzen, die bereits geladen wurde

Tobias0

Banned
Registriert
Aug. 2025
Beiträge
117
Hallo, ich wollte fragen, ob man eine bestehende Klasse, die schon einmal vom System Class Loader geladen wurde, dynamisch durch eine andere ersetzen kann, oder ob das Java Sicherheitsmodell dies verhindert:

Java:
  private static Object compileSupplierCode(String codeText) throws Exception {
    // Save the code to a temporary file and compile it
    Path parentDir = Paths.get("temp");
    try (Stream<Path> paths = Files.walk(parentDir)) {
      paths.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
    }
    Files.createDirectories(parentDir);
    Path tempFile = Paths.get(parentDir.toString(), "MySupplier.java");
    Files.writeString(tempFile, codeText);
    JavaCompiler jc = javax.tools.ToolProvider.getSystemJavaCompiler();
    jc.run(null, null, null, tempFile.toFile().getAbsolutePath());
    Path classFile = Objects.requireNonNull(tempFile.getParent()).resolve("MySupplier.class");
    URLClassLoader classLoader =
        URLClassLoader.newInstance(
            new URL[] {Objects.requireNonNull(classFile.getParent()).toUri().toURL()}, null);
    //    ClassLoader redefineClassLoader =
    //        new ClassLoader() {
    //          @Override
    //          public Class<?> loadClass(String name) throws ClassNotFoundException {
    //            if (name.equals("MySupplier")) {
    //              try {
    //                byte[] buf = Files.readAllBytes(classFile);
    //                int len = buf.length;
    //                return defineClass(name, buf, 0, len);
    //              } catch (IOException e) {
    //                throw new ClassNotFoundException("", e);
    //              }
    //            }
    //            return getParent().loadClass(name);
    //          }
    //        };
    return Class.forName("MySupplier", true, classLoader)
        .getDeclaredConstructors()[0]
        .newInstance();
  }

Ich kann zwar das geladene Objekt (das Ergebnis von Class.forName ...) mittels Reflection verwenden, aber wenn ich versuche, es nach MySupplier zu casten, kracht es, weil es diese Klasse bereits gibt:

Java:
public class MySupplier {
  public int numberOfSeries() {
    return 2; // Return the number of series to be generated
  }

  public String getTitle(int seriesIndex) {
    return "Series " + (seriesIndex + 1); // Return a title for each series
  }

  public double[][] generateSeries(int seriesIndex) {
    double[][] series = new double[10][2];
    double a = seriesIndex * 10; // Example coefficient based on series index
    for (int i = 0; i < series.length; i++) {
      double y = f(i, a);
      series[i][0] = i; // x value
      series[i][1] = y; // y value based on the function
    }
    return series;
  }

  private double f(double x, double a) {
    return x * x * a; // Example function
  }
}

Wenn ihr eine Idee habt, würde ich mich freuen.
 
Tobias0 schrieb:
ob das Java Sicherheitsmodell dies verhindert
Ich musste kurz schmunzeln. Java und Sicherheit 😄

Aber für dein Problem gibt es verschiedene Ansätze. Der stabilste ist wohl, einen eigenen Classloader zu verwenden. Ich habe einen Stackoverflow-Eintrag gefunden, in dem Leute verschiedene Ansätze angebracht haben, teils sogar schon mit fertigen Implementierungen: https://stackoverflow.com/questions/148681/unloading-classes-in-java. Ich denke mal, dass du dort hilfreiche Denkanstöße, Hintergründe und sogar Implementierungen findest 🙂
 
  • Gefällt mir
Reaktionen: Tobias0
SoDaTierchen schrieb:
Ich musste kurz schmunzeln. Java und Sicherheit 😄

Wird da wieder das Java Browser Plugin und die Sprache an sich in einen Topf geworfen?
 
  • Gefällt mir
Reaktionen: Tobias0
Danke, Stack Overflow hatte ich eigentlich schon "abgegrast", aber ich kenne alle JVM Eigenschaften noch nicht im Detail. 😒
 
Gemini:
Es ist in Java nicht möglich, eine bereits vom System Class Loader geladene Klasse direkt durch eine andere zu ersetzen. 🚫 Das liegt daran, dass der System Class Loader die geladenen Klassen im Speicher cacht. Einmal geladen, kann eine Klasse nicht mehr entladen oder ausgetauscht werden. Dies ist ein grundlegendes Sicherheitsmerkmal der Java Virtual Machine (JVM).
Aber es wird auch ein Workaround genannt:
Verwendung mehrerer Class Loader: Man kann einen eigenen benutzerdefinierten Class Loader erstellen, der Klassen aus verschiedenen Quellen lädt. Wenn man eine Klasse aktualisieren möchte, kann man einfach einen neuen Class Loader instanziieren, der die aktualisierte Version lädt, und die Anwendung so anpassen, dass sie den neuen Loader verwendet.
 
  • Gefällt mir
Reaktionen: Tobias0
Das hört ein bischen wie ein XY Problem an.
Die Frage ist eher wieso willst du das machen?
Für Tests oder so?
Dann schau dir eher entsprechende Mocking Frameworks für Java an.

Für Plugin Architekturen?
Entsprechende Plugin Frameworks
 
Ich arbeite seit Jahren beruflich mit Java und stehe hier komplett auf dem Schlauch.
Für was braucht man denn einen ClassLoader? Wat für'n Ding?
 
  • Gefällt mir
Reaktionen: Tobias0
  • Gefällt mir
Reaktionen: Tobias0
cbmik schrieb:
Wird da wieder das Java Browser Plugin und die Sprache an sich in einen Topf geworfen?
Ne, das nicht. Aber ich werfe hier die Plattform selbst und die Programmiersprache in einen Topf, das muss ich gestehen. Sicher programmieren in Java ist ja möglich. Aber die java-Plattform macht es enorm zeitaufwendig die Umgebung sicher zu halten auf der sie läuft. Und da meine ich nicht mal die gefühlt uralten Bibliotheken, die gerne verwendet werden, sondern ganz real dass Plattform & Anwendung voneinander entkoppelt sind und separat Pflege benötigen. Ein sicherer Betrieb geht. Ist aber aufwendig. Unter Windows nervt die Doppelpflege, unter Linux stört mich, dass ich ständig in die Dependency-Hölle abtauchen darf. Und das führt leider auch dazu, dass viele Entwickler sich so sehr davon gegängelt fühlen, dass sie eine einmal funktionierende Version erst Updaten, wenn sie einen Zwang dazu verspüren. Das ist bad practice, ja, aber eben auch sehr üblich.

Also ich gestehe: Ich habe der Sprache angelastet, was die Plattform für Probleme mitbringt.
 
  • Gefällt mir
Reaktionen: Tobias0 und cbmik
SoDaTierchen schrieb:
Aber die java-Plattform macht es enorm zeitaufwendig die Umgebung sicher zu halten auf der sie läuft. Und da meine ich nicht mal die gefühlt uralten Bibliotheken, die gerne verwendet werden, sondern ganz real dass Plattform & Anwendung voneinander entkoppelt sind und separat Pflege benötigen.

Das liegt eben am Design, JVM für viele mögliche Architekturen und dein code läuft damit (fast) "überall".

Wer die Runtime Umgebung nicht aktualisiert wenn CVEs bekannt werden, hat aber nicht nur bei Java ein problem.
Das ist bei .NET/Mono oder Node auch so.

Besonders die dependency hell bei nodeJS mit ihrem fehlenden Sicherheitskonzept sehe ich als riesen Problem (obwohl ich privat node für kleine Dinge sehr mag).
 
  • Gefällt mir
Reaktionen: SoDaTierchen, Tobias0 und Marco01_809
Also, ich kann euch auch ein Beispiel zeigen.

Natürlich kann es nicht sicher sein, wenn der Benutzer beliebigen Code zur Laufzeit injizieren, übersetzen und ausführen darf. Deshalb nutze zurzeit nur ich das.

Hier ist das Beispiel, das zurzeit auf localhost im Browser läuft:

1754897702761.png

Ergänzung ()

Und das plotte ich dann so (und hier liegt der Knackpunkt, was vermutlich die Sicherheit angeht):

Java:
  public static String plot(Object obj) throws Exception {
    Method numberOfSeries = obj.getClass().getMethod("numberOfSeries");
    Method generateSeries = obj.getClass().getMethod("generateSeries", int.class);
    Method getTitle = obj.getClass().getMethod("getTitle", int.class);

    XYChart chart =
        new XYChartBuilder()
            .width(800)
            .height(400)
            .title("Area Chart")
            .xAxisTitle("x")
            .yAxisTitle("y")
            .build();
    for (int i = 0; i < (Integer) numberOfSeries.invoke(obj); i++) {
      double[][] series = (double[][]) generateSeries.invoke(obj, i);
      double[] xData = new double[series.length];
      double[] yData = new double[series.length];
      for (int j = 0; j < series.length; j++) {
        xData[j] = series[j][0];
        yData[j] = series[j][1];
      }
      chart.addSeries((String) getTitle.invoke(obj, i), xData, yData);
    }
    // Return the base64 encoded string
    return "data:image/png;base64," + imgToBase64String(BitmapEncoder.getBufferedImage(chart));
  }
 
Zuletzt bearbeitet:
Hab ich das richtig verstanden, du willst beliebige DIagramme, die ausgewählt werden können, zur Laufzeit laden?

Ich kenne das so, dass man eine Elternklasse hat, welche das Diagramm darstellt. Dort kannst Du auch die "draw" oder "render" Methode implementieren. In den konkreten Klassen überschreibst Du dann die MEthode, mit dem Speziellen Diagramm. Dann implementierst Du den Code mit der Elternklasse, wo immer die "draw/render" methode aufgerufen wird. Dann musst du nur zur Laufzeit, je nach Auswahl, ein Objekt der Kinderklasse erzeugen und übergeben. Stichwort Polimorphie.


Ansonsten kannst du auch mit überladenen Funktionen Arbeiten....oder mit Generics....gibt verschiedene Wege zum Ziel
 
@Andarkan: Der tut genau das was der Name sagt. Ohne so ein Ding läuft gar nix; der lädt eine Klasse in die JVM wenn sie das erste mal gebraucht wird. Davon muss man nicht unbedingt was mitkriegen, Java setzt dir von Haus einen System Class Loader auf, der Klassen aus .class files laden kann (ggf. in jar files) und sucht diese in den Orten die du angegeben hast (über java -cp lib:bla:... oder java -jar x.jar) und den Class-Path Einträgen in den Manifesten deiner jar. Macht man alles by-the-book merkt man gar nicht dass es ihn gibt.

Den ClassLoader der die aktuelle Klasse geladen hat bekommt man mit getClass().getClassLoader(). Wenn du in deinem Code jetzt eine neue Klasse das erste mal referenzierst, z.B. new Foo(), dann wird der ClassLoader deiner Klasse benutzt um auch die Klasse Foo zu laden. Es kann natürlich sein dass der ClassLoader die Klasse gar nicht finden kann, dann gäbe es eine ClassNotFoundException.

Über Reflection kann man allerdings auch gezielt einen anderen ClassLoader angeben. Der kann die Klassen dann auch aus ganz anderen Quellen laden. Spring Boot & Co. nutzen das z.B. für ihre jar-in-jar files ("uber jars") wo alle dependencies ihre eigene jar behalten die in die haupt-jar verschachtelt ist. Diverse Spiele und Gameserver nutzen das auch für Modding/Plugin Support wo man zur Laufzeit Plugins hinzufügen kann.

Ein großer Vorteil ist dass man zwei verschiedene Klassen unter dem gleichen Namen in einer JVM haben kann. z.B. hast du eine App in die du zwei Plugins laden willst:
  • Plugin-A bundled Dependency-X in Version 1.7
  • Plugin-B bundled Dependency-X in Version 2.1
Die Klassennamen überschneiden sich großteils, aber die Funktionalität ist nicht die gleiche.
Lädst du beide Plugins in den system class loader gibt es entweder einen Konflikt das eine Klasse mehrfach vorhanden ist oder es gibt einen Crash im Code von Plugin-B weil in Dependency-X 1.7 Methoden fehlen die zur Compile-Zeit von Plugin-B in Version 2.1 noch da waren. Die Lösung ist, jedes Plugin in seinem eigenen ClassLoader zu laden. Dann kann jedes Plugin Klassen haben wie es lustig ist.

Wichtig ist natürlich zu bedenken dass die App selbst die Klassen der Plugins gar nicht sieht (und damit weder Dependency-X Version 1.7 noch 2.1), weil es die im system class loader gar nicht gibt!
Daraus folgt dass die App jetzt nicht einfach new PluginA() machen kann! Außerdem wäre es auch kein richtiges Plugin-System wenn in der App die Namen aller Plugins einprogrammiert sein müssen. Darum zwangsläufig newInstance per Reflection.

Andersrum können die Plugins aber alle Klassen der App sehen weil ihr class loader vom system class loader erbt, welcher die app.jar geladen hat. Das machen wir uns zu nutze damit wir nicht dauerhaft mit Relfection weiterarbeiten müssen; wir definieren in der App gemeinsame Basisklassen bzw. Interfaces, angefangen z.B. mit einem interface Plugin.
In Plugin-A gibt es dann eine class PluginA implements Plugin und in Plugin-B eine class PluginB implements Plugin.

Die App lädt die Plugins dann mit einem Code wie

Java:
List<Plugin> plugins = new ArrayList<>();
for (String pluginName : pluginNames) {
  ClassLoader pluginClassLoader = new PluginClassLoader(getClass().getClassLoader(), "plugins/" + pluginName + ".jar");
  plugins.add((Plugin) Class.forName(pluginName, true, pluginClassLoader).newInstance());
}
und von da an wird jedes Plugin generisch behandelt. Übliche Verwendung ist dann z.B. über ein Event-System, oder trivialste Lösung: die App called einfach Methoden auf jedem Plugin wenn etwas bestimmtes passiert.

@Tobias0: Daraus sollte auch klar werden warum dein Cast nicht funktioniert. Die Klasse die du lädst wird nur in deinen neuen ClassLoader geladen! Die Klasse in der die compileSupplierCode Methode steht läuft aber unter dem System class loader! Der kennt MySupplier entweder gar nicht oder eine völlig andere Version.

Wenn du in dieser Methode also einen Cast (MySupplier) machst dann versucht es zu dem MySupplier zu casten den der system class loader kennt. Genau wie wenn du new MySupplier() machen würdest. Das ist eine ganz andere Klasse! Das kann nie klappen!

Du musst entweder mit Reflection weiterarbeiten oder besser; in deiner App ein Interface Supplier deklarieren das alle Methoden hat die du brauchst. Zur Laufzeit class MySupplier implements Supplier kompilieren. Und in der App kannst du dann das Ergebnis vom newInstance-Call zumindest auf einen Supplier casten mit dem du angenehm arbeiten kannst.

Ich würde dir auch raten die Klasse MySupplier NICHT mit in die App zu kompilieren um Konflikte und Verwirrung zu vermeiden. Dann ist auch sofort klar warum du nicht in MySupplier casten kannst: Weil dein Code die Definition dieser Klasse zur Compile-Zeit gar nicht kennt!
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: Tobias0
Tobias0 schrieb:
Natürlich kann es nicht sicher sein, wenn der Benutzer beliebigen Code zur Laufzeit injizieren, übersetzen und ausführen darf. Deshalb nutze zurzeit nur ich das.
Woher weißt du dass du der einzige Nutzer bist? Der Angreifer fragt nicht :D Guck bloß dass dein Webserver nur auf localhost binded, also nicht übers Netzwerk erreichbar ist.

Tobias0 schrieb:
Und das plotte ich dann so (und hier liegt der Knackpunkt, was vermutlich die Sicherheit angeht):
Ne der Knackpunkt ist schon früher. Den Code kompilieren und den ClassLoader zu erzeugen mit dem class file geht vielleicht noch klar (würde ich mich aber nicht drauf verlassen, dieses Sicherheitsmodell wurde schließlich auch den Applets zum Verhängnis). Aber spätestens wo du die Klasse mit Class.forName instanziierst werden ja static-Initializer und der Code im Konstruktor ausgeführt, das ist GARANTIERT UNSICHER.

Du müsstest also sicherstellen dass der Code aus einer vertrauenswürdigen Quelle kommt. Die niedrigste Hürde ist den Server nur auf localhost listenen zu lassen. Aber das reicht nicht, denn HTTP erlaubt jeder Website requests an fremde Domains zu schicken. Soll heißen JEDE Website die du in IRGENDEINEM Browser auf dem gleichen System aufrufst kann Requests an deinen Server auf localhost schicken! Daher mal zum Thema XSRF einlesen...

Die Endlösung wäre dass der Code nur geladen wird wenn er mit einem Key signiert wurde der in der App hardocded ist (passt aber nicht zu dem Nutzungsmodell hier).
 
  • Gefällt mir
Reaktionen: Tobias0
Tobias0 schrieb:
Java:
public class MySupplier {
  public int numberOfSeries() {
    return 2; // Return the number of series to be generated
  }
}
Anstatt eine extra Methode dafür zu haben, kann man die Zahl 2 doch als konstante Variable definieren, oder?

Mir ist generell nicht klar, warum man Reflections für eine banale Plotting-Anwendung nutzen sollte.
Reflections sind eine Holzhammer-Technik, die man nur nutzen sollte, wenn es unbedingt nötig ist.
 
Marco01_809 schrieb:
Du müsstest also sicherstellen dass der Code aus einer vertrauenswürdigen Quelle kommt. Die niedrigste Hürde ist den Server nur auf localhost listenen zu lassen. Aber das reicht nicht, denn HTTP erlaubt jeder Website requests an fremde Domains zu schicken. Soll heißen JEDE Website die du in IRGENDEINEM Browser auf dem gleichen System aufrufst kann Requests an deinen Server auf localhost schicken! Daher mal zum Thema XSRF einlesen...
Jetzt übertreib es bitte nicht. Ja, das Teil läuft zurzeit auf meinem Server. Ja, es ist nur über localhost erreichbar. Und ja, es läuft zusätzlich in einem Docker-Container, wobei nur das jar file als volumen gemountet ist ... Das heißt, wenn es einen Angriff geben sollte, den ich theoretisch eigentlich fast ausschließen würde ... könnte das Teil nicht den ganzen Server niederreißen.

Jetzt fragst du dich vielleicht, wie ich das Teil denn eigentlich selbst aufrufen kann?: Über einen anderen Container, in dem ein Linux mit Oberfläche und Browser läuft, und der mit diesem Container kommunizieren darf. Der andere Container mit Webbrowser ist natürlich geschützt, sodass nur ich diesen aufrufen kann ... aber da möchte ich jetzt nicht auf Einzelheiten eingehen. Der Weg ist hierbei also Webbrowser → VNC → Webbrowser → anderer Container - und wieder zurück.

Andarkan schrieb:
Mir ist generell nicht klar, warum man Reflections für eine banale Plotting-Anwendung nutzen sollte.
Es geht darum, jede beliebige Funktion plotten zu können.
 
  • Gefällt mir
Reaktionen: Marco01_809
Tobias0 schrieb:
Es geht darum, jede beliebige Funktion plotten zu können.
Auch dafür braucht man keine Reflections.
Spätestens mit modernem Java und FunctionalInterfaces kann man auch ganze Funktionen als Parameter übergeben.
 
Zurück
Oben