【问题标题】:Sharing dynamically loaded classes with JShell instance与 JShell 实例共享动态加载的类
【发布时间】:2018-07-31 10:47:54
【问题描述】:

请查看下面的修改

我正在尝试创建一个 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
    }
}

此外,将DirectExecutionControlLocalExecutionControl 交换会得到相同的结果,但我不明白这两个类之间的区别。

如何使 运行时 加载的类可用于此 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"
    }
}

这是预期的输出,如果 JVMJShell 实例 不共享任何内存,但是将 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"

为什么在运行时加载的类不共享 JVMJShell 实例 之间的内存?

编辑 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 实例而不再次加载它?如果不是,为什么已经加载了静态加载的类?

【问题讨论】:

  • 您使用哪个 IDE 运行您的代码?
  • 我尝试了两个 Netbeans 并直接运行它。
  • 这个不可否认的非常有趣的练习有最终目标吗?
  • 好吧,它现在实际上正在工作,请参阅下面的我的答案。我这样做是为了调试 Minecraft 服务器插件,但这主要是一个有趣的练习。我希望该解决方案对其他想要了解 JShell 如何与启动它的 JVM 交互的人有所帮助。

标签: java java-9 dynamic-class-loaders jshell


【解决方案1】:

解决方案是创建一个自定义的LoaderDelegate 实现,它提供已加载类的实例,而不是再次加载它们。一个简单的例子是使用默认实现,DefaultLoaderDelegate(source),并覆盖其内部RemoteClassLoaderfindClass方法

@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]);

所以这两个类的区别在于直接调用当前线程中的方法,而本地调用新线程中的方法。两种情况下都使用相同的类加载器,因此在共享内存和加载的类方面,您会期望得到相同的结果

【讨论】:

    【解决方案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.testValue++;").forEach(p -> {
                System.out.println(p.value());
            });
            System.out.println(TestShell.testValue);
    
        }
    
    }
    
    

    默认执行引擎是JDI,但你可以切换到本地或自己的。

    【讨论】:

    • 对我来说打印:6, null, 6. JDK 9.0.4。我需要使用特定的 Java 版本吗?
    猜你喜欢
    • 2020-10-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-21
    • 2015-03-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多