Совместное использование динамически загружаемых классов с помощью экземпляра JShell

Пожалуйста, просмотрите изменения ниже

Я пытаюсь создать экземпляр JShell, который дает мне доступ, и позволяет мне взаимодействовать с объектами в JVM, в котором он был создан. Это отлично работает с классами, которые были доступны во время компиляции, но не выполняется для динамически загружаемых классов.

public class Main {

    public static final int A = 1;
    public static Main M;

    public static void main(String[] args) throws Exception {
        M = new Main();
        ClassLoader cl = new URLClassLoader(new URL[]{new File("Example.jar").toURL()}, Main.class.getClassLoader());
        Class<?> bc = cl.loadClass("com.example.test.Dynamic");//Works
        JShell shell = JShell.builder()
                .executionEngine(new ExecutionControlProvider() {
                    @Override
                    public String name() {
                        return "direct";
                    }

                    @Override
                    public ExecutionControl generate(ExecutionEnv ee, Map<String, String> map) throws Throwable {
                        return new DirectExecutionControl();
                    }
                }, null)
                .build();
        shell.eval("System.out.println(com.example.test.Main.A);");//Always works
        shell.eval("System.out.println(com.example.test.Main.M);");//Fails (is null) if executionEngine is not set
        shell.eval("System.out.println(com.example.test.Dynamic.class);");//Always fails
    }
}

Кроме того, обмен DirectExecutionControl с LocalExecutionControl дает те же результаты, но я не понимаю разницу между LocalExecutionControl двумя классами.

Как сделать классы, загруженные во время выполнения, доступными для этого экземпляра JShell?

Изменение: первая часть этого вопроса была решена, ниже приведен обновленный исходный код, чтобы продемонстрировать вторую часть проблемы

public class Main {

    public static void main(String[] args) throws Exception {
        ClassLoader cl = new URLClassLoader(new URL[]{new File("Example.jar").toURL()}, Main.class.getClassLoader());
        Class<?> c = cl.loadClass("com.example.test.C");
        c.getDeclaredField("C").set(null, "initial");
        JShell shell = JShell.builder()
                .executionEngine(new ExecutionControlProvider() {
                    @Override
                    public String name() {
                        return "direct";
                    }

                    @Override
                    public ExecutionControl generate(ExecutionEnv ee, Map<String, String> map) throws Throwable {
                        return new DirectExecutionControl();
                    }
                }, null)
                .build();
        shell.addToClasspath("Example.jar");
        shell.eval("import com.example.test.C;");
        shell.eval("System.out.println(C.C)"); //null
        shell.eval("C.C = \"modified\";");
        shell.eval("System.out.println(C.C)"); //"modified"
        System.out.println(c.getDeclaredField("C").get(null)); //"initial"
    }
}

Это ожидаемый результат, если JVM и экземпляр JShell не используют никакой памяти, однако, добавив com.example.test.C непосредственно в проект вместо загрузки, он динамически меняет результаты следующим образом:

shell.eval("import com.example.test.C;");
shell.eval("System.out.println(C.C)"); //"initial"
shell.eval("C.C = \"modified\";");
shell.eval("System.out.println(C.C)"); //"modified"
System.out.println(c.getDeclaredField("C").get(null)); //"modified"

Почему память между JVM и экземпляром JShell не используется для классов, загруженных во время выполнения?

РЕДАКТИРОВАТЬ 2: проблема, по-видимому, вызвана различными загрузчиками классов

Выполнение следующего кода в контексте приведенного выше примера:

System.out.println(c.getClassLoader()); //java.net.URLClassLoader
shell.eval("System.out.println(C.class.getClassLoader())"); //jdk.jshell.execution.DefaultLoaderDelegate$RemoteClassLoader
shell.eval("System.out.println(com.example.test.Main.class.getClassLoader())"); //jdk.internal.loader.ClassLoaders$AppClassLoader

Это показывает, что тот же класс com.example.test.C загружается двумя разными загрузчиками классов. Можно ли добавить класс в экземпляр JShell, не загружая его снова? Если нет, то почему статически загруженный класс уже загружен?

Ответ 1

Решение заключается в создании пользовательской реализации LoaderDelegate, которая поставляет экземпляры уже загруженных классов вместо их повторной загрузки. Простым примером является использование реализации по умолчанию, DefaultLoaderDelegate (source) и переопределение метода findClass его внутреннего RemoteClassLoader

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    byte[] b = classObjects.get(name);
    if (b == null) {
        Class<?> c = null;
        try {
            c = Class.forName(name);//Use a custom way to load the class
        } catch(ClassNotFoundException e) {
        }
        if(c == null) {
            return super.findClass(name);
        }
        return c;
    }
    return super.defineClass(name, b, 0, b.length, (CodeSource) null);
}

Чтобы создать рабочий экземпляр JShell, используйте следующий код

JShell shell = JShell.builder()
    .executionEngine(new ExecutionControlProvider() {
        @Override
        public String name() {
            return "name";
        }

        @Override
        public ExecutionControl generate(ExecutionEnv ee, Map<String, String> map) throws Throwable {
            return new DirectExecutionControl(new CustomLoaderDelegate());
        }
    }, null)
    .build();
shell.addToClasspath("Example.jar");//Add custom classes to Classpath, otherwise they can not be referenced in the JShell

Ответ 2

только говоря с небольшой частью этого довольно существенного вопроса:

Кроме того, обмен DirectExecutionControl с LocalExecutionControl дает те же результаты, но я не понимаю разницы между этими двумя классами

LocalExecutionControl extends DirectExecutionControl и переопределяет только invoke(Method method), тела которого...

локальный:

    Thread snippetThread = new Thread(execThreadGroup, () -> {
            ...
            res[0] = doitMethod.invoke(null, new Object[0]);
            ...
    });

непосредственный:

    Object res = doitMethod.invoke(null, new Object[0]);

поэтому разница между двумя классами заключается в том, что direct вызывает метод в текущем потоке, а local вызывает его в новом потоке. тот же загрузчик классов используется в обоих случаях, поэтому вы ожидаете тех же результатов с точки зрения обмена памятью и загруженных классов

Ответ 3

Теперь есть лучшее и более простое решение:

package ur.pkg;

import jdk.jshell.JShell;
import jdk.jshell.execution.LocalExecutionControlProvider;

public class TestShell {
    public static int testValue = 5;

    public static void main(String[] args) {
        JShell shell = JShell.builder().executionEngine(new LocalExecutionControlProvider(), null).build();
        TestShell.testValue++;
        System.out.println(TestShell.testValue);
        shell.eval("ur.pkg.TestShell.dupa++;").forEach(p -> {
            System.out.println(p.value());
        });
        System.out.println(TestShell.testValue);

    }

}

Механизм выполнения по умолчанию - JDI, но вы можете переключить его на локальный или собственный.