背景

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

    微软已经开放 Build 2020 线上开发者会议注册,https://mybuild.microsoft.com/.Build 2020 会议将于 5 月 19 日至 20 日召开,核心内容都是围绕 ...

  2. axios的使用小技巧:如何绕过字符串拼接,直接传递对象

     Vue.js官方推荐使用axios作为发送http请求的工具,在使用axios中,有些小技巧是不容易发现的.当我们不知道这些技巧时,我们可能会使用其他"奇技淫巧",比如,我们很容 ...

  3. dijkstra preiority_queue优化 紫书学习

    #include<bits/stdc++.h> using namespace std; const int maxn=1000+10; const int INF=1000000000; ...

  4. Frame Relay Voice Traffic Shaping and Frament

    本文全称应该是:Frame Relay Voice-Adaptive Traffic Shaping and Fragmentation,标题限制字数,没办法了   帧中继的流量整型向来是个头疼的地方 ...

  5. 解决vue页面刷新或者后退参数丢失的问题

    原文链接: 点我 在商城类的项目中,会经常遇到列表数据筛选查询的情景,当要打开某一项的详情页或者暂时离开列表页,再返回(后退时),选择的筛选条件会全部丢失,辛辛苦苦选择好的条件全没了,还得重新选择,如 ...

  6. XML--XML作用

    XML 把数据从 HTML 分离 如果你需要在 HTML 文档中显示动态数据,那么每当数据改变时将花费大量的时间来编辑 HTML. 通过 XML,数据能够存储在独立的 XML 文件中.这样你就可以专注 ...

  7. 洛谷2014 选课(树形DP)树形背包问题

    题目描述 在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习.现在有N门功课,每门课有个学分,每门课有一 ...

  8. Jenkins 项目构建

    一:新建项目 (1)点击新建,输入项目名称--构建一个自由风格的软件项目,点击ok (2)构建触发器-----设置每两分钟执行一次 其中有5个参数 (*****) 第一个是代表分钟  一小时内的分钟数 ...

  9. Redis系列(六):设置/移除键的过期时间

    本篇博客是Redis系列的第6篇,主要讲解以下内容: 数据库数量 切换目标数据库 设置键的过期时间 移除键的过期时间 本系列的前5篇可以点击以下链接查看: Redis系列(一):Redis简介及环境安 ...

  10. thinkphp日志泄露扫描

    import requests,sys dirpath=[] def dirscan(url,year): for i in range(1,13): if i < 10: urls=url+' ...