Frage zu ClassLoadern

package rummage.manipulator;
public class Foo {
    public String get() {   return "foo";   }
} 

package rummage.manipulator;
public class Bar extends Foo {
    @Override
    public String get() {  return "bar";   }
}

public class Test {
    public static class MyRunnable implements Runnable {
        @Override
        public void run() {    System.out.println(new Foo().get());  }
    }

    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = new ClassLoader(Test.class.getClassLoader()) {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                return name.equals("rummage.manipulator.Foo")
                ? super.loadClass("rummage.manipulator.Bar")
                : super.loadClass(name);
            }};
        Thread thread = new Thread((Runnable)
             classLoader.loadClass(MyRunnable.class.getName()).newInstance());
        thread.setContextClassLoader(classLoader);
        thread.start();
    }
}

Warum liefert hier new Foo() ein Foo, obwohl mein ClassLoader in dem Thread aktiv ist (und bei Aufruf von loadClass auch brav Bar.class liefert)?

Weil loadClass des SystemClassLoaders bereits durch new Foo() explizit aufgerunfen wird, bevor dir dein Classloader überhaupt Bar liefern kann.

Aber so wie du das hier machst, wird das nichts, weil Bar in jedem Fall als Bar geladen wird und deshalb auch nur mit new Bar instanziert werden kann. Das new Foo() müsste also auch über deinen Classloader erfolgen und die Instanzierung ebenfalls mit newInstance().

Schade, dann kann ich das wohl knicken.

Nur aus Neugier: Was sollte damit denn erreicht werden? (Ja, eine andere Klasse geladen… aber - mit welchen Ziel?)

Das Äquivalent von Foo wird innerhalb einer Bibliothek in einer anderen Klasse instanziiert und ausgelesen. Leider sind in dem Foo Sachen fest verdrahtet, die wir gerne geändert hätten. Da wir die Objekterzeugung dort nicht unter Kontrolle haben und auch nicht an die Instanz rankommen (um sie z.B. mit Reflection zu manipulieren, bevor sie ausgelesen wird), war das meine Idee, um zu reinzuschummeln.

Nun, es gibt ja andere Wege, um der JVM irgendwas unterzujubeln. Ich denke, eine komplett andere Klasse wird schwierig. Wer soll wo sicherstellen, dass die Klassen die gleichen Methoden haben? Selbst wenn es ginge, müßte man/ich mal drüber nachdenken, ob man dann nicht einfach mit sowas wie

class MyClassLoader {
    ...
    if (name.equals("SecurityManager")) return MyVerySpecialSecurityManager();
};
someGlobalThreadGroup.setClassLoader(new MyClassLoader());

sämtliche Security-Mechanismen aushebeln könnte.

Wie auch immer: Mit

java -Xbootclasspath:fixedLibrary.jar;C:\jre\lib\rt.jar YourProgram

solltest du ihm jede beliebige Klasse unterjubeln können, die in „fixedLibrary.jar“ liegt - sofern sie den gleichen Namen hat, wie das Original. (Das hatte ich mal für https://stackoverflow.com/a/22098863/3182664 - d.h. das geht sogar mit solchen Innereien wie java.lang.String ! :astonished: ). Ob das hilft oder bei dir andwendbar ist, ist nicht ganz klar…

Deshalb leitet mein Bar ja auch von Foo ab.

Auf sowas ähnliches ist es am Ende auch hinausgelaufen.

Was du benötigst ist ein FilteringClassLoader, der entsprechend verhindert, dass bereits geladene Klassen genutzt werden. Dies geschieht dadurch, dass du den Parent-ClassLoader löscht (um die automatische Delegation zu verhindern) und dann Packages die der FCL laden soll, selber behandelst und erst nachfolgend an den ehemaligen Parent weitergibst (oder ganz abblockst).

Allerdings ist bei dir ein anderes Problem. Du willst von der Original-Klasse ableiten, ergo müsste diese wiederbekannt sein. Am einfachsten kannst du das fixen, indem Foo und Bar ein gemeinsames Interface implementieren. Im letzten Schritt muss MyRunnable.class dann, ähnlich wie du es auch schon machst, vom FilteringClassLoader geladen werden. Denk hier daran, dass der FCL entsprechend das original Package auf der Blacklist hat.

In Hazelcast nutzen wir das übrigens in den Client Tests um sicherzustellen, dass, obwohl Client und Node in der selben JVM laufen, der Node keinen Zugriff auf die Modellklassen hat, also rein mit den serialisierten Objekten arbeiten muss.

Falls ich das richtig verstanden habe, wird das mit dem gemeinsamen Interface schwierig: Wenn man Kontrolle über alles hätte, könnte man ja direkt die Implementierung ändern :wink: Aber zumindest gut zu wissen, dass solches „Filtering“ zumindest grundsätzlich möglich ist.

OT: parent.setAccessible(true); Gerüchten zufolge soll das mit Java 9 doch krachen, oder?