请查看下面的修改
我正在尝试创建一个 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得到相同的结果,但是我不理解两个类之间的区别。
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直接添加到项目中而不是动态加载它会如下更改结果:
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实例而无需再次加载它?如果否,为什么已经加载了静态加载的类?
解决方案是创建一个自定义LoaderDelegate实现,该实现提供已加载类的实例,而不是再次加载它们。一个简单的示例是使用默认实现DefaultLoaderDelegate(source)并覆盖findClass其内部的方法RemoteClassLoader
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