Совместное использование динамически загружаемых классов с экземпляром 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
дает одинаковые результаты, но я не понимаю разницу между двумя классами.
Как сделать классы, загруженные во время выполнения, доступными для этого экземпляра 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, не загружая его снова? Если нет, почему статически загруженный класс уже загружен?
2 ответа
Решение заключается в создании пользовательских LoaderDelegate
реализация, которая предоставляет экземпляры уже загруженных классов вместо их повторной загрузки. Простой пример - использовать реализацию по умолчанию, DefaultLoaderDelegate
( источник) и переопределить 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
Говоря лишь о небольшой части этого довольно существенного вопроса:
Кроме того, обмен 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 вызывает его в новом потоке. в обоих случаях используется один и тот же загрузчик классов, поэтому можно ожидать одинаковых результатов с точки зрения совместного использования памяти и загруженных классов.
Теперь есть лучшее и более простое решение:
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.testValue++;").forEach(p -> {
System.out.println(p.value());
});
System.out.println(TestShell.testValue);
}
}
Механизм выполнения по умолчанию - JDI, но вы можете переключить его на локальный или собственный.