背景

最近同事告诉我一个很有趣的需求:让用户(应用场景中,一般为其他开发者)自己填入Java代码片段,代码片段的内容为已经规定好的模板类的继承类,实现模板类定义的方法。我们的项目要实现动态编译代码片段,存储代码片段和用户操作记录的映射关系,并能够在业务中载入代码片段执行。

这有点像我们提供一个模板模式的架构,只不过模板类的实现类由外部接口填入代码片段动态实现。相较让其他开发者直接参与项目开发,无疑:

  1. 降低了侵入风险
  2. 向其他开发者隐藏了大部分实现
  3. 降低操作难度和开发门槛
  4. 便于管理

……

这相当于要实现一个简单的在线Java开发环境,提供基础的代码填写、编译和保存的功能。

效果演示

基于vue-codemirrorJava Compiler的动态编译,实现了上述需求,目前完成的Web端IDE主要功能点包括:

  • 页面展示Java代码块(代码高亮,有行号、可自动补全括号等)
  • 从服务端获取模板类代码,并提供示例
  • 实时动态编译并获取编译结果(通过/失败 todo:返回编译错误信息)
  • 将输入字符串加载成Java Class

    以及小的功能点:自动缩进、补全括号、切换主题、联动填写类名等等。

    下面给出涉及到的技术和实现方法。

CodeMirror

CodeMirror是一个JS库,可以支持实现有丰富的附加功能和多种语言支持。我们项目的前端使用Vue框架,可以很方便地集成并使用CodeMirror提供的插件,实现我们的在线IDE多种特性。

参考:CodeMirror官网

引入

安装依赖: "vue-codemirror": "^4.0.6"

src目录下的main.js中引入:

  1. import VueCodeMirror from 'vue-codemirror'
  2. import 'codemirror/lib/codemirror.css'
  3. Vue.use(VueCodeMirror)

使用

新建组件JavaIDE.vue

  1. <template>
  2. <codemirror ref="codeMirrorEditor" :value="code" :options="cmOptions" @changes="onChange">
  3. </codemirror>
  4. </template>
  5. <script>
  6. import codemirror from "codemirror/lib/codemirror";
  7. require("codemirror/mode/clike/clike.js");
  8. require("codemirror/addon/edit/closebrackets.js");
  9. components: {
  10. codemirror;
  11. }
  12. export default{
  13. data(){
  14. return{
  15. code: "",
  16. cmOptions:{
  17. mode: "text/x-java", //Java语言
  18. theme: "darcula", // 默认主题
  19. autofocus: true,
  20. lineNumbers: true, //显示行号
  21. smartIndent: true, // 自动缩进
  22. autoCloseBrackets: true// 自动补全括号
  23. }
  24. }
  25. }
  26. </script>

组件化地使用它,我们可以方便地操作它绑定的值(code)和其他附加选项(cmOption)。

在组件创建时为code赋值,即可实现加载模板代码。

根据官网,我们可以直接使用CodeMirror的默认构造函数,也可以提供一个textarea DOM元素作为构造CodeMirror对象的参数。

可以使用readOnly参数将代码块设置为只读。

联动填写类名功能

希望实现:在上面顶栏中填写类名,在代码中联动填写。

实现方式: 使用正则匹配替换代码片段,再进行替换

使用相同的方法,也可以实现动态补全类名等功能

参考更多JavaScript的正则表达式

为输入框加上监听函数@input="changeClassName"

  1. changeClassName(className) {
  2. var reg = new RegExp(/public class .*? extends ActionParamBuilder/);
  3. this.code = this.code.replace(reg,
  4. "public class " + className + " extends ActionParamBuilder"
  5. );
  6. }

切换主题

引入主题css样式文件

  1. import "codemirror/theme/eclipse.css";
  2. import "codemirror/theme/darcula.css";
  3. import "codemirror/theme/blackboard.css";

使用String数组定义支持的主题,并使用 Element-UI提供的Select组件支持主题切换:

  1. <el-select v-model="cmOptions.theme" placeholder="切换主题" @change="changeTheme">
  2. <span slot="prefix">
  3. <el-tooltip content="更换主题">
  4. <a-icon type="skin" style="fontSize:16px;line-height=50px;"/>
  5. </el-tooltip>
  6. </span>
  7. <el-option v-for="(item,index) in supportThemes" :key="index" :label="item" :value="item">
  8. </el-option>
  9. </el-select>
  • 使用slot实现在选择器中嵌入图标,并支持tooltip功能,使工具栏更加紧凑。 slot意为插槽,是封装好的组件预留的可以自定义的空间,我们可以使用slot = ""把DOM元素置入到组件内部,非常灵活。

样式覆写

使用!important关键字覆盖原有CodeMirror样式。注意,将该样式放在全局而不是局部scoped样式表中。

  1. .CodeMirror {
  2. height: 500px !important;
  3. }

JavaCompiler

不用将传入的代码保存成.java文件写入磁盘,直接就可以使用JavaCompiler工具对字符串进行编译。

为了实现实时动态编译功能,我搜索了关于如何将字符串编译成class的方法,还看了一些动态代理的实现思路。后来看到这一篇:Java运行时动态生成class的方法,发现这就是我想要的!

使用Java SDK(since 1.6)提供的JavaCompiler工具。该工具提供编译方法:

  1. CompilationTask getTask(Writer out,
  2. JavaFileManager fileManager,
  3. DiagnosticListener<? super JavaFileObject> diagnosticListener,
  4. Iterable<String> options,
  5. Iterable<String> classes,
  6. Iterable<? extends JavaFileObject> compilationUnits);
  • JavaFileManager

    自定义MemoryJavaFileManager,继承ForwardingJavaFileManager<JavaFileManager>,实现从内存字符串中读取JavaFileObject

    重点是下面这个方法:
  1. JavaFileObject makeStringSource(String name, String code) {
  2. return new MemoryInputJavaFileObject(name, code);
  3. }
  4. static class MemoryInputJavaFileObject extends SimpleJavaFileObject {
  5. final String code;
  6. MemoryInputJavaFileObject(String name, String code) {
  7. super(URI.create("string:///" + name), Kind.SOURCE);
  8. this.code = code;
  9. }
  10. @Override
  11. public CharBuffer getCharContent(boolean ignoreEncodingErrors) {
  12. return CharBuffer.wrap(code);
  13. }
  14. }
  • options,可选参数列表,可以增加外部Jar包依赖

    因为我们所需要编译的代码里依赖的类来源于外部的Jar包,所以需要将这些Jar包使用option将这些依赖加进去。这一步踩了坑,因为之前没用过,不知道怎么写……最后终于找到了正确的写法:

    List<String> optionList =Arrays.asList("-extdirs",extLib);

    extLib是外部jar包的路径(目录地址)。可以使用路径分隔符填入多个路径。
  • DiagnosticListener 诊断信息监听

    加入诊断信息监听器,我们可以拿到编译错误信息,把这些信息反馈给前端,实现实时编译并报错的功能。

    DiagnosticCollector diagnosticCollector = new DiagnosticCollector();
  • JavaFileObject 待编译的Java对象,调用自定义类MemoryJavaFileManagermakeStringSource方法。可以传入一组编译单元。

    完整方法如下:
  1. public Map<String, byte[]> compile(String fileName, String source,String extLib) throws IOException {
  2. try (MemoryJavaFileManager manager = new MemoryJavaFileManager(stdManager)) {
  3. JavaFileObject javaFileObject = manager.makeStringSource(fileName, source);
  4. // 传入诊断监听器 size和传入的javaObject相同
  5. DiagnosticCollector diagnosticCollector = new DiagnosticCollector();
  6. List<String> optionList =Arrays.asList("-extdirs",extLib);
  7. CompilationTask task = compiler.getTask(null, manager,diagnosticCollector, optionList, null, Arrays.asList(javaFileObject));
  8. Boolean result = task.call();
  9. if (result == null || !result.booleanValue()) {
  10. throw new RuntimeException("Compilation failed.");
  11. }
  12. return manager.getClassBytes();
  13. }
  14. }

调用代码:

  1. Map<String, byte[]> results = javaStringCompiler.compile(className + ".java", CODE_TO_COMPILE, libDir);

自定义ClassLoader

参考《Java编程的逻辑》中24.5中内容,我们可以使用自定义的ClassLoader来加载用户代码片段,成为可调用的Class对象。

  • 继承URLClassLoader
  • 重写findClass方法
  1. class MemoryClassLoader extends URLClassLoader {
  2. // class name to class bytes:
  3. Map<String, byte[]> classBytes = new HashMap<String, byte[]>();
  4. public MemoryClassLoader(Map<String, byte[]> classBytes) {
  5. super(new URL[0], MemoryClassLoader.class.getClassLoader());
  6. this.classBytes.putAll(classBytes);
  7. }
  8. @Override
  9. protected Class<?> findClass(String name) throws ClassNotFoundException {
  10. byte[] buf = classBytes.get(name);
  11. if (buf == null) {
  12. return super.findClass(name);
  13. }
  14. classBytes.remove(name);
  15. return defineClass(name, buf, 0, buf.length);
  16. }
  17. }

自定义类加载器有如下好处:

  • 可以自定义读取class文件字节码方法和形式,如:从内存中、指定jar包中,或从数据库/网络读取等
  • 实现隔离,可以实现使用同一个类的不同版本
  • 实现热部署,动态更新类的内容

总结

本篇中主要涉及知识点:

  • vue-codemirror集成和使用
  • JavaCompiler的使用
  • JavaScript正则和Vue中的插槽(slot
  • 自定义ClassLoader实现动态加载

vue-codemirror + Java Compiler实现Java Web IDE的更多相关文章

  1. Architecture of a Java Compiler

    Architectural Overview   A modern optimizing compiler can be logically divided into four parts:   Th ...

  2. Intellij编译时报“java: System Java Compiler was not found in classpath”

    问题如下: http://stackoverflow.com/questions/19889145/setting-up-intellij-12-idea-with-java-1-7-and-reso ...

  3. Java compiler level does not match the version of the installed Java project facet.问题

    从同事那里拷贝过来的web项目,导入到eclipse中,出现Java compiler level does not match the version of the installed Java p ...

  4. 解决Java compiler level does not match the version of the installed Java project facet.问题

    其实之前遇到过Java compiler level does not match the version of the installed Java project facet.这个问题,因为当时没 ...

  5. Java项目转换成Web项目

    阐述:有时候我们在Eclipse中导入一个web项目,发现导入到项目中后变成一个Java项目,这让人很蛋疼.本篇主要讲述怎样将这个本该为web项目的Java项目变身回去,以及一些在导入过程中遇到的一些 ...

  6. .net基础学java系列(二)IDE 之 插件

    上一篇文章.net基础学java系列(二)IDE "扎实的基础"+"宽广的视野",基本可以帮我们摆脱码畜.码奴.码农的命运! IT领袖:IT大哥:IT精英:IT ...

  7. .net基础学java系列(二)IDE

    上一篇文章.net基础学java系列(一)视野 废话: "视野"这篇文章,管理员说它比较空洞!也许初学者看不懂表格中的大部分内容!多年的neter估计也有很多不知道的! 有.net ...

  8. Java基础14:离开IDE,使用java和javac构建项目

    更多内容请关注微信公众号[Java技术江湖] 这是一位阿里 Java 工程师的技术小站,作者黄小斜,专注 Java 相关技术:SSM.SpringBoot.MySQL.分布式.中间件.集群.Linux ...

  9. eclipse 中springboot2.0整合jsp 出现No Java compiler available for configuration options compilerClassName

    今天使用eclipse创建springboot整合jsp出现一个问题,在idea中并没有遇到这个问题.最后发现是需要在eclipse中添加一个eclipse依赖,依赖如下: <dependenc ...

随机推荐

  1. 009.Ansible模板管理 Jinja2

    一 Jinja2简介 Jinja2是基于python的模板引擎. 假设说现在我们需要一次性在10台主机上安装redis,这个通过playbook现在已经很容易实现.默认情况下,所有的redis安装完成 ...

  2. telnet 636端口不通

    今天发生了一件奇怪的事情,LDAP的636端口突然就不通了报错如下 [www@DC ~]$ telnet 10.219.90.173 636Trying10.219.90.173...Connecte ...

  3. Cisco 交换机启用netflow

    Router2951#configure terminal //Creating Flow Record router2951(config)# flow record NTArecord route ...

  4. 软件——eclipse debug小技巧

    1.开启调试: 在代码编辑处右键单击,在弹出菜单中点击Debug As开始调试 2.几个快捷键: F5:跟入Step into, 一般会跟踪进入到调用函数的函数体,Step Over则不会跟踪进入,直 ...

  5. C. K-Complete Word(小小的并查集啦~)

    永久打开的传送门 \(\color{Pink}{-------------分割-------------}\) \(n最大有2e5,那么暴力一定不行,找规律\) \(我们发现第i位的字符一定和第i+k ...

  6. Git 获取远程仓库指定分支内容

    1. 在本地一个空的文件夹中 git init  (生成本地仓库) 2. 在刚刚的文件夹中随便建立一个文件 ,git add . (为了生成分支)(提交到暂存区) 3. git commit -m'1 ...

  7. Try-Catch包裹的代码异常后,竟然导致了产线事务回滚!

    导读:​一段被try-catch包裹后的代码在产线稳定运行了200天后忽然发生了异常,而这个异常竟然导致了产线事务回滚.这期间究竟发生了什么?日常在项目过程中该如何避免事务异常?就在这个时候,老板拿着 ...

  8. 【Hadoop离线基础总结】工作流调度器azkaban

    目录 Azkaban概述 工作流调度系统的作用 工作流调度系统的实现 常见工作流调度工具对比 Azkaban简单介绍 安装部署 Azkaban的编译 azkaban单服务模式安装与使用 azkaban ...

  9. 杂记---主要关于PHP导出excel表格学习

    今天上午处理了一下WIN7系统的电脑前置话筒和耳机口无法使用的问题,主要现象是耳机插入后没声音,麦插入话筒说话对方也听不到,后置端口一切正常.刚开始判断肯定是设置的问题,于是用另一台电脑百度搜索“wi ...

  10. uCOS2014.1.7

    主要关于任务堆栈: 在计算机中一般设置一个专用的地址寄存器用来存放堆栈的栈顶地址,这个寄存器称为堆栈指针(SP). 任务堆栈有两种,一种是地址向下增长的,PC就是采用这样的堆栈: 另一种是地址向上增长 ...