背景

最近同事告诉我一个很有趣的需求:让用户(应用场景中,一般为其他开发者)自己填入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中引入:

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

使用

新建组件JavaIDE.vue

<template>
<codemirror ref="codeMirrorEditor" :value="code" :options="cmOptions" @changes="onChange">
</codemirror>
</template>
<script>
import codemirror from "codemirror/lib/codemirror";
require("codemirror/mode/clike/clike.js");
require("codemirror/addon/edit/closebrackets.js");
components: {
codemirror;
}
export default{
data(){
return{
code: "",
cmOptions:{
mode: "text/x-java", //Java语言
theme: "darcula", // 默认主题
autofocus: true,
lineNumbers: true, //显示行号
smartIndent: true, // 自动缩进
autoCloseBrackets: true// 自动补全括号
}
}
}
</script>

组件化地使用它,我们可以方便地操作它绑定的值(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组件支持主题切换:

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

样式覆写

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

.CodeMirror {
height: 500px !important;
}

JavaCompiler

不用将传入的代码保存成.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<JavaFileManager>,实现从内存字符串中读取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包依赖

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

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

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

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

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

    完整方法如下:
public Map<String, byte[]> 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<String> 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<String, byte[]> results = javaStringCompiler.compile(className + ".java", CODE_TO_COMPILE, libDir);

自定义ClassLoader

参考《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);
} }

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

  • 可以自定义读取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. 【linux三剑客】grep命令

    grep, egrep, fgrep - print lines matching a pattern grep 命令用于查找文件里符合条件的字符串. grep 指令用于查找内容包含指定的范本样式的文 ...

  2. Android Resourse

    为什么80%的码农都做不了架构师?>>>   使用情景: 实现帧动画步骤的控制,这样动态的获取Drawable资源对应的R id,播放到那一步就加载到哪一步 private void ...

  3. swift 3.0字符串的简单使用

    let str:String = "12314124" 获取某个指定位置的元素 print(str.characters[str.index(str.startIndex, off ...

  4. 工具 在 Nuget 发布自己的包

    MSDN : https://docs.microsoft.com/zh-cn/nuget/quickstart/create-and-publish-a-package-using-visual-s ...

  5. flask学习笔记(二)

    一.视图函数的传参方式 修改前: 目标: 传参方式改成 途径: 通过request获取参数 注意:args并不是地点类型,而是dict的一个子类,如图: immutable意思是不可变 不可变的字典转 ...

  6. 使用PHP-Beast加密你的PHP源代码

    PHP-Beast是一个PHP源码加密的模块,其使用DES算法加密,用户可以自定义加密的key来加密源代码. 1. PHP-Beast的安装 $ wget https://github.com/lie ...

  7. INTERVIEW #5

    笔试 150min,3题,每题100分,自己果然还是个蒟蒻呢~ 最近状态好差,虽然做了一些题,但还是考得稀烂,大概有几点需要加强: 独立做题,不要一边看板子一边写代码,更不要一开始就看题解: 对之前研 ...

  8. vue父组件向子组件传对象,不实时更新解决

    vue报错:void mutating a prop directly since the value will be overwritten whenever the parent componen ...

  9. 【Scala】用实例弄清楚scala几种函数的定义和特点

    文章目录 作为参数的函数 匿名函数 柯里化函数(currying) 闭包函数 作为参数的函数 scala> val a1 = Array(1,2,3,4) //这是一个数组 a1: Array[ ...

  10. 设计模式之GOF23建造者模式

    组件很多,装配顺序不定 本质: 1,分离了对象子组件的单独构造(Builder负责)和装配(Director负责),从而可以构造出复杂的对象,这个模式适用于某个对象的构建过程复杂的情况下使用 2,实现 ...