与 JShell 实例共享动态加载的 类
Sharing dynamically loaded classes with JShell instance
请查看下面的编辑内容
我正在尝试创建一个 JShell 实例,让我可以访问并与 JVM 中的对象进行交互创建于。这适用于编译时可用的 classes,但对于 动态加载 的 classes 则失败。
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
交换得到相同的结果,但我不明白两者之间的区别 classes.
我如何使在 运行时 加载的 classes 可用于此 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 实例 之间的内存不为运行时加载的 classes 共享?
编辑 2:问题似乎是由不同的 class 装载机引起的
在上述示例的上下文中执行以下代码:
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
这表明,相同的 class、com.example.test.C
由两个不同的 classloader 加载。是否可以将 class 添加到 JShell 实例而不再次加载它?如果没有,为什么静态加载的 class 已经加载了?
解决方案是创建一个自定义 LoaderDelegate
实现,它提供已加载 类 的实例,而不是再次加载它们。一个简单的示例是使用默认实现 DefaultLoaderDelegate
(source) 并重写其内部 RemoteClassLoader
的 findClass
方法
@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
只针对这个相当实质性问题的一小部分说:
Additionally, exchanging DirectExecutionControl with LocalExecutionControl gives the same results, but I do not understand the difference between the two classes
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,但你可以切换到本地或自己的。
请查看下面的编辑内容
我正在尝试创建一个 JShell 实例,让我可以访问并与 JVM 中的对象进行交互创建于。这适用于编译时可用的 classes,但对于 动态加载 的 classes 则失败。
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
交换得到相同的结果,但我不明白两者之间的区别 classes.
我如何使在 运行时 加载的 classes 可用于此 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 实例 之间的内存不为运行时加载的 classes 共享?
编辑 2:问题似乎是由不同的 class 装载机引起的
在上述示例的上下文中执行以下代码:
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
这表明,相同的 class、com.example.test.C
由两个不同的 classloader 加载。是否可以将 class 添加到 JShell 实例而不再次加载它?如果没有,为什么静态加载的 class 已经加载了?
解决方案是创建一个自定义 LoaderDelegate
实现,它提供已加载 类 的实例,而不是再次加载它们。一个简单的示例是使用默认实现 DefaultLoaderDelegate
(source) 并重写其内部 RemoteClassLoader
findClass
方法
@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
只针对这个相当实质性问题的一小部分说:
Additionally, exchanging DirectExecutionControl with LocalExecutionControl gives the same results, but I do not understand the difference between the two classes
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,但你可以切换到本地或自己的。