java - objetos - Compartiendo clases cargadas dinĂ¡micamente con la instancia de JShell
instanciar clases dinamicamente java (2)
La solución es crear una implementación personalizada de LoaderDelegate
, que proporcione instancias de clases ya cargadas en lugar de cargarlas de nuevo. Un ejemplo simple es usar la implementación predeterminada, DefaultLoaderDelegate
( source ) y anular el método findClass
de su RemoteClassLoader
interno
@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);
}
Para crear una instancia de JShell que funcione, use el siguiente código
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
Por favor, vea las ediciones a continuación
Estoy tratando de crear una instancia de JShell que me dé acceso y me permita interactuar con los objetos en la JVM en la que se creó. Esto funciona bien con las clases que han estado disponibles en tiempo de compilación, pero falla para las clases que se cargan dinámicamente .
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
}
}
Además, el intercambio de DirectExecutionControl
con LocalExecutionControl
da los mismos resultados, pero no entiendo la diferencia entre las dos clases.
¿Cómo puedo hacer que las clases que se cargan en tiempo de ejecución estén disponibles para esta instancia de JShell ?
Edición: la primera parte de esta pregunta se resolvió, a continuación se encuentra el código fuente actualizado para demostrar la segunda parte del problema
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"
}
}
Este es el resultado esperado, si la JVM y la instancia de JShell no comparten ninguna memoria, sin embargo, agregar com.example.test.C
directamente al proyecto en lugar de cargarlo dinámicamente cambia los resultados de la siguiente manera:
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"
¿Por qué no se comparte la memoria entre la JVM y la instancia de JShell para las clases cargadas en tiempo de ejecución?
EDIT 2: el problema parece ser causado por diferentes cargadores de clases
Ejecutando el siguiente código en el contexto del ejemplo anterior:
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
Esto muestra que la misma clase, com.example.test.C
está cargada por dos cargadores de clases diferentes. ¿Es posible agregar la clase a la instancia de JShell sin volver a cargarla? Si no, ¿por qué la clase cargada estáticamente ya está cargada?
Sólo hablé con una pequeña parte de esta pregunta bastante importante:
Además, el intercambio de DirectExecutionControl con LocalExecutionControl da los mismos resultados, pero no entiendo la diferencia entre las dos clases
LocalExecutionControl extends DirectExecutionControl
y reemplaza solo invoke(Method method)
, cuyos cuerpos son ...
local:
Thread snippetThread = new Thread(execThreadGroup, () -> {
...
res[0] = doitMethod.invoke(null, new Object[0]);
...
});
directo:
Object res = doitMethod.invoke(null, new Object[0]);
así que la diferencia entre las dos clases es que invoca directamente el método en el subproceso actual, y local lo invoca en un nuevo subproceso. el mismo cargador de clases se usa en ambos casos, por lo que esperaría los mismos resultados en términos de compartir memoria y clases cargadas