背景

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

    前言 带着问题学习,事半功倍.本文将对如下几个问题进行总结说明: 1.EurekaServer端服务注册的流程和设计模式 2.Eureka服务续约的bug 3.EurekaClient的启动流程 4. ...

  2. bzoj4173 数学

    bzoj4173 数学 欧拉\(\varphi\)函数,变形还是很巧妙的 求: \[\varphi(n)\cdot\varphi(m)\cdot\sum_{n\bmod k+m\bmod k\ge k ...

  3. 最新Idea超实用告别996插件,都是免费

    Idea告别996插件 在IntelliJ IDEA中,秉着IDEA自带能实现的快捷方式就不用插件的原则,少用些插件,运行性能也提升一些,虽然很少,哈哈.分享下我个人常用的插件,希望对大家有些帮助.插 ...

  4. visibility: hidden 和 display: none的区别

    相同点: 两者都可以将dom元素隐藏 不同点: 1.display: none 隐藏之后不占用文档流,而visibility: hidden却会占用文档流,如果要在隐藏元素的同时获取其尺寸信息,那就可 ...

  5. Xenia and Colorful Gems(二分--思维)

    给定三个数组a,b,c. 要求从每个数字取一个数,使得两两之差和最小. 求出这个数. \(我又懵逼了.我是会O(n^3)的暴力啊,怎么办.\) \(\color{Red}{从结果看,选出来的三个数必定 ...

  6. 搬东西 dp

    搬东西 现有n个扁担以及一辆货车,扁担一次挑两个货物,货车可以装K个货物,货车只能运送一次货物. 现在qwb要把总共2n+k个货物搬到某个地方.现在qwb想选K个货物让货车先运走,然后剩下的2n个货物 ...

  7. dp cf 20190615

    A. Timofey and a tree 这个不算是dp,就是一个思维题,好难想的思维题,看了题解才写出来的, 把点和边分开,如果一条边的两个点颜色不同就是特殊边,特殊边两边连的点就叫特殊点, 如果 ...

  8. dp cf 20190613

    A. Boredom 这个题目不难,但是我做的还比较复杂,不过还是很开心,至少做出来了,开始因为爆int了还wa了一发,搞得我以为自己做错了 #include <cstdio> #incl ...

  9. CSS的基本语法及页面引用

    CSS的基本语法及页面引用 CSS基本语法 CSS的定义方法是: 选择器 { 属性:值; 属性:值; 属性:值;} 选择器是将样式和页面元素关联起来的名称,属性是希望设置的样式属性每个属性有一个或多个 ...

  10. 如何在Windows下安装MySQL5和MySQL8的多实例

    MySQL5和MySQL8多实例安装方法: 1.首先下载MySQL5和MySQL8 官方下载链接:https://dev.mysql.com/downloads/mysql/  下载旧版本: 下载好后 ...