最近同事告诉我一个很有趣的需求:让用户(应用场景中,一般为其他开发者)自己填入Java代码片段,代码片段的内容为已经规定好的模板类的继承类,实现模板类定义的方法。我们的项目要实现动态编译代码片段,存储代码片段和用户操作记录的映射关系,并能够在业务中载入代码片段执行。
这有点像我们提供一个模板模式的架构,只不过模板类的实现类由外部接口填入代码片段动态实现。相较让其他开发者直接参与项目开发,无疑:
……
这相当于要实现一个简单的在线Java开发环境,提供基础的代码填写、编译和保存的功能。
基于vue-codemirror
和Java Compiler
的动态编译,实现了上述需求,目前完成的Web端IDE主要功能点包括:
CodeMirror是一个JS库,可以支持实现有丰富的附加功能和多种语言支持。我们项目的前端使用Vue框架,可以很方便地集成并使用CodeMirror提供的插件,实现我们的在线IDE多种特性。
参考:CodeMirror官网
安装依赖:"vue-codemirror": "^4.0.6"
在src
目录下的main.js
中引入:
import VueCodeMirror from 'vue-codemirror'
import 'codemirror/lib/codemirror.css'
Vue.use(VueCodeMirror)
新建组件JavaIDE.vue
组件化地使用它,我们可以方便地操作它绑定的值(code)和其他附加选项(cmOption)。
在组件创建时为code赋值,即可实现加载模板代码。
根据官网,我们可以直接使用CodeMirror的默认构造函数,也可以提供一个
textarea DOM
元素作为构造CodeMirror对象的参数。
可以使用readOnly
参数将代码块设置为只读。
希望实现:在上面顶栏中填写类名,在代码中联动填写。
实现方式: 使用正则匹配替换代码片段,再进行替换
使用相同的方法,也可以实现动态补全类名等功能
参考更多JavaScript的正则表达式
为输入框加上监听函数@input="changeClassName"
changeClassName(className) {
var reg = new RegExp(/public class .*? extends ActionParamBuilder/);
this.code = this.code.replace(reg,
"public class " + className + " extends ActionParamBuilder"
);
}
引入主题css
样式文件
import "codemirror/theme/eclipse.css";
import "codemirror/theme/darcula.css";
import "codemirror/theme/blackboard.css";
使用String数组定义支持的主题,并使用 Element-UI
提供的Select
组件支持主题切换:
slot
实现在选择器中嵌入图标,并支持tooltip
功能,使工具栏更加紧凑。 slot
意为插槽,是封装好的组件预留的可以自定义的空间,我们可以使用slot = ""
把DOM元素置入到组件内部,非常灵活。使用!important
关键字覆盖原有CodeMirror样式。注意,将该样式放在全局而不是局部scoped
样式表中。
.CodeMirror {
height: 500px !important;
}
不用将传入的代码保存成.java
文件写入磁盘,直接就可以使用JavaCompiler
工具对字符串进行编译。
为了实现实时动态编译功能,我搜索了关于如何将字符串编译成class的方法,还看了一些动态代理的实现思路。后来看到这一篇:
Java运行时动态生成class的方法,发现这就是我想要的!
使用Java SDK(since 1.6)提供的JavaCompiler工具。该工具提供编译方法:
CompilationTask getTask(Writer out,
JavaFileManager fileManager,
DiagnosticListener<? super JavaFileObject> diagnosticListener,
Iterable<String> options,
Iterable<String> classes,
Iterable<? extends JavaFileObject> compilationUnits);
JavaFileManager
MemoryJavaFileManager
,继承ForwardingJavaFileManager
,实现从内存字符串中读取JavaFileObject JavaFileObject makeStringSource(String name, String code) {
return new MemoryInputJavaFileObject(name, code);
}
static class MemoryInputJavaFileObject extends SimpleJavaFileObject {
final String code;
MemoryInputJavaFileObject(String name, String code) {
super(URI.create("string:///" + name), Kind.SOURCE);
this.code = code;
}
@Override
public CharBuffer getCharContent(boolean ignoreEncodingErrors) {
return CharBuffer.wrap(code);
}
}
options
,可选参数列表,可以增加外部Jar包依赖option
将这些依赖加进去。这一步踩了坑,因为之前没用过,不知道怎么写……最后终于找到了正确的写法:List optionList =Arrays.asList("-extdirs",extLib);
extLib
是外部jar包的路径(目录地址)。可以使用路径分隔符填入多个路径。DiagnosticListener
诊断信息监听DiagnosticCollector diagnosticCollector = new DiagnosticCollector();
JavaFileObject
待编译的Java对象,调用自定义类MemoryJavaFileManager
的makeStringSource
方法。可以传入一组编译单元。public Map compile(String fileName, String source,String extLib) throws IOException {
try (MemoryJavaFileManager manager = new MemoryJavaFileManager(stdManager)) {
JavaFileObject javaFileObject = manager.makeStringSource(fileName, source);
// 传入诊断监听器 size和传入的javaObject相同
DiagnosticCollector diagnosticCollector = new DiagnosticCollector();
List optionList =Arrays.asList("-extdirs",extLib);
CompilationTask task = compiler.getTask(null, manager,diagnosticCollector, optionList, null, Arrays.asList(javaFileObject));
Boolean result = task.call();
if (result == null || !result.booleanValue()) {
throw new RuntimeException("Compilation failed.");
}
return manager.getClassBytes();
}
}
调用代码:
Map results = javaStringCompiler.compile(className + ".java", CODE_TO_COMPILE, libDir);
参考《Java编程的逻辑》中24.5中内容,我们可以使用自定义的
ClassLoader
来加载用户代码片段,成为可调用的Class对象。
URLClassLoader
findClass
方法class MemoryClassLoader extends URLClassLoader {
// class name to class bytes:
Map<String, byte[]> classBytes = new HashMap<String, byte[]>();
public MemoryClassLoader(Map<String, byte[]> classBytes) {
super(new URL[0], MemoryClassLoader.class.getClassLoader());
this.classBytes.putAll(classBytes);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] buf = classBytes.get(name);
if (buf == null) {
return super.findClass(name);
}
classBytes.remove(name);
return defineClass(name, buf, 0, buf.length);
}
}
自定义类加载器有如下好处:
本篇中主要涉及知识点:
vue-codemirror
集成和使用JavaCompiler
的使用JavaScript
正则和Vue
中的插槽(slot
)ClassLoader
实现动态加载