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