Java 如何实现动态脚本?( 二 )


上面两种方式都有一个明显的缺点,就是需要依赖于 Cat.java 文件,以及必须产生 Cat.class 文件 。在繁星平台中,自然希望这个过程都在内存中完成,尽量减少 IO 操作,因此使用编程方式来编译 Java 代码就显得很有必要了 。代码位于模块 code-javac 下的 CodeJavac.java 文件中,核心代码如下:
???????//类名String className = "Cat";//项目所在路径String projectPath = PathUtil.getAppHomePath();String facadeJarPath = String.format(".:%s/facade/target/facade-1.0.jar", projectPath);//需要进行编译的代码Iterable<? extends JavaFileObject> compilationUnits = new ArrayList<JavaFileObject>() {{ add(new JavaSourceFromString(className, getJavaCode()));}};//编译的选项,对应于命令行参数List<String> options = new ArrayList<>();options.add("-classpath");options.add(facadeJarPath);//使用系统的编译器JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();StandardJavaFileManager standardJavaFileManager = javaCompiler.getStandardFileManager(null, null, null);ScriptFileManager scriptFileManager = new ScriptFileManager(standardJavaFileManager);//使用stringWriter来收集错误 。StringWriter errorStringWriter = new StringWriter();//开始进行编译boolean ok = javaCompiler.getTask(errorStringWriter, scriptFileManager, diagnostic -> { if (diagnostic.getKind() == Diagnostic.Kind.ERROR) { errorStringWriter.append(diagnostic.toString()); }}, options, null, compilationUnits).call();if (!ok) { String errorMessage = errorStringWriter.toString(); //编译出错,直接抛错 。throw new RuntimeException("Compile Error:{}" + errorMessage);}//获取到编译后的二进制数据 。final Map<String, byte[]> allBuffers = scriptFileManager.getAllBuffers();final byte[] catBytes = allBuffers.get(className);//使用自定义的ClassLoader加载类FsClassLoader fsClassLoader = new FsClassLoader(className, catBytes);Class<?> catClass = fsClassLoader.findClass(className);Object obj = catClass.newInstance();if (obj instanceof Animal) { Animal animal = (Animal) obj; animal.hello("Moss");}//会得到结果: Hello,Moss! 我是Cat 。代码中主要使用到了系统编译器 JavaCompiler,调用它的 getTask 方法就相当于命令行中执行 javac,getTask 方法中使用自定义的 ScriptFileManager 来搜集二进制结果,以及使用 errorStringWriter 来搜集编译过程中可能出错的信息 。最后借助一个自定义类加载器 FsClassLoader 来从二进制数据中加载出类 Cat 。
深入讨论
上文介绍了动态脚本的实现关键点,但是还有诸多问题需要讨论,笔者把主要的几个问题抛出来,简单讨论一下 。
ClassLoader 范围问题
JVM 的类加载机制采用双亲委派模式,类加载器收到加载请求时,会委派自己的父加载器去执行加载任务,因此所有的加载任务都会传递到顶层的类加载器,只有当父加载器无法处理时,子加载器才自己去执行加载任务 。下面这幅图相信大家已经很熟悉了 。

Java 如何实现动态脚本?

文章插图
 
JVM 对于一个类的唯一标识是 (Classloader,类全名),因此可能出现这种情况,接口 Animal 已经加载了,但是我们用 CustomClassLoader 去加载 Cat 时,提示说 Animal 找不到 。这就是因为 Animal 和 Cat 不是被同一个 Classloader 加载的 。
由于 defineClass 方法是 protected 的,因此要用 byte[] 来加载 class 就需要自定义一个 classloader,如何指定这个 Classloader 的父加载器就比较有讲究了 。
公司内部的 Java 系统都是采用的 pandora,pandora 有自己的类加载器以及线程加载器,因此我们以接口 Animal 的加载器 animalClassLoader 为标准,将线程 ClassLoader 设置为 animalClassLoader,同时将自定义的 ClassLoader 的父加载器指定为 animalClassLoader 。代码位于模块 advance-discuss 下,参考代码如下:
???????/*FsClassLoader.java*/public FsClassLoader(ClassLoader parentClassLoader, String name, byte[] data) { super(parentClassLoader); this.fullyName = name; this.data = https://www.isolves.com/it/cxkf/yy/JAVA/2020-08-20/data;}/*AdvanceDiscuss.java*///接口的类加载器ClassLoader animalClassLoader = Animal.class.getClassLoader();//设置当前的线程类加载器Thread.currentThread().setContextClassLoader(animalClassLoader);//...//使用自定义的ClassLoader加载类FsClassLoader fsClassLoader = new FsClassLoader(animalClassLoader, className, catBytes);通过这些保障,就不会出现找不到类的问题了 。
类重名问题
当我们只动态加载一个类时,自然不用担心类全名重复的问题,但是如果需要加载多个相同类时,就有必要进行特殊处理了,可以利用正则表达式捕获用户的类名,然后增加随机字符串的方式来规避重名问题 。


推荐阅读