Java学习之基于SpringBoot的Java在线编译⼯具
在Java开发⼯具中,有⼀种是基于Spring Boot的Java在线编译⼯具,下⾯⼩编来给⼤家介绍。
项⽬运⾏流程
程序运⾏流程图如下
接下来开始具体分析每⼀步的实现⽅法
⼀个Java程序是怎样运⾏起来的
想要实现在线运⾏Java代码的需求,我们⾸先需要了解Java程序正常的编译和运⾏流程。
⾸先源代码⽂件(.java)经由编译器编译成字节码
例如JDK中的javac命令就是实现字节码⽣成技术的程序
接下来有Java虚拟机解释并运⾏字节码⽂件,运⾏过程有分为两个步骤
类的加载
常用的java编译器有哪些
应⽤程序运⾏后,系统会启动⼀个虚拟机进程。JVM进程在类的加载阶段⾸先会通过⼀个类的全限定类名获取定义此类的⼆进制字节流,然后将这个字节流所代表的静态存储结构转化为⽅法区的运⾏时数据结构,并且在内存中⽣成⼀个代表这个类的java.lang.Class对象,作为⽅法区这个类的各种数据访问⼊⼝。
类加载的相关的内容⽐较复杂,⽣成对应的Class对象后还会进⾏验证、准备、解析、初始化等⼀系列步骤才算加载完成,但考虑到篇幅问题这⾥就不再展开说明了。
类的执⾏
当类加载完成后JVM就可以到main⽅法执⾏了。
本项⽬中使⽤反射来完成这⼀步骤。
明确了以上步骤后,我们发现有三个问题需要解决:
如何编译提交到服务器的Java代码?
在本地运⾏Java代码的时候我们可以选⽤Javac命令编译。对于本项⽬⽽⾔,这种⽅式需要我们先将源代码写⼊⼀个.java⽂件,再编译得到.class⽂件。但是这样⼀来不仅⾮常耗时,⽽且还会⽣成额外的⽂件,导致服务器环境被污染。因此我们选择使⽤JDK1.6以后添加的动态编译API来解决这⼀问题。
如何执⾏编译之后的代码?
⼀段程序往往不是编写、运⾏⼀次就能达到效果的。同⼀个类可能需要反复的修改、提交、运⾏。另外,提交的类也要能访问服务端的其他类库才⾏,对于这⼀问题,需要我们⾃⼰编写类加载器来实现需求。
如何收集Java代码的执⾏结果?
我们需要把程序向标准输出(System.out)和标准错误输出()中打印的信息收集起来返回给客户端。但是标准输出设备是整个虚拟机进程全局共享的资源。如果使⽤System.setOut()/System.setErr()⽅法将输出流重定向到⾃⼰定义的PrintStream上固然可以收集信息,但在多线程情况下这样会连带其他线程的信息⼀起收集了,这显然不是我们希望看到的。因此我们选择将程序中的System替换为我们⾃⼰写
的HackSystem类。
也就是说,我们的重点在于实现编译模块和运⾏模块。在理清以上思路后,我们就可以正式开始代码的编写了。
Spring Boot相关
在正式开始编码前还要罗嗦⼀下,本项⽬选择使⽤Spring Boot仅仅是看中了它在开发web应⽤时的⽅便、快捷,项⽬中并不会涉及太多框架⽅⾯的知识。
如果对于Spring Boot的⾃动配置原理感兴趣,可以阅读下笔者写的另⼀篇⽂章,记录了笔者对于Spring Boot⾃动配置原理的⼀些粗浅认识,欢迎各位⼤神斧正。
编译模块:compile
使⽤动态编译的⽅式可以直接在内存中对⼀个Java程序进⾏编译并输出到内存中,提⾼程序运⾏效率的同时还不会污染服务器环境,可谓⼀举两得。具体实现步骤如下。
动态编译
关于动态编译的API全部放在ls包下,本项⽬中主要涉及到的类和接⼝如下所⽰:
编译器:
JavaCompiler
ToolProvider
源代码⽂件:
JavaFileObject
SimpleJavaFIleObject
⽂件管理器:
JavaFileManager
StandardJavaFileManager
ForwardingJavaFileManager
收集诊断信息:
DiagnosticListener
DiagnosticCollector
接下来开始具体介绍实现动态编译的步骤
准备编译器对象
只有⼀种⽅法:
//获取Java语⾔编译器
JavaCompiler compiler = SystemJavaCompiler();
//开始执⾏编译,通过传⼊⾃⼰的JavaFileManager为编译器创建存放字节码的JavaFIleObject对象
Boolean result = Task(null,javaFileManager,compileCollector,
null,null, Arrays.asList(sourceJavaFileObject)).call();
关于ToolProvider这⾥有⼀个坑,如果使⽤的是OpenJDK,tools.jar⽂件是放在%JAVA_HOME%/lib下的,
运⾏起来就会报空指针异常。因为启动java的⽬录默认是%JAVA_HOME%/jre/,这个⽬录的lib⽬录为%JAVA_HOME%/jre/lib,⾥⾯没有tools.jar。因此要么把⽂件拷到指定的lib下,要么⼲脆使⽤Oracle JDK也是⼀切正常。
可以看到执⾏编译这个⽅法要填⼀⼤堆参数,这些参数就是我们实现在内存中编译源代码的关键。
API中对于这个⽅法参数的解释如下
JavaCompiler.CompilationTask getTask(Writer out,
JavaFileManager fileManager,
DiagnosticListener<? super JavaFileObject> diagnosticListener,
Iterable<String> options,
Iterable<String> classes,
Iterable<? extends JavaFileObject> compilationUnits)
out - ⽤于编译器的附加输出; 如果为null使⽤的就是使⽤
fileManager - ⽂件管理器; 如果null使⽤编译器的标准⽂件管理器diagnosticListener - 诊断信息收集器; 如果为null则使⽤编译器的默认⽅法来报告诊断options - 编译器选项, null表⽰没有选项
classes - 通过注释处理类的名称, null表⽰没有类名
compilationUnits - 编译单元, null表⽰⽆编译单位