hello, I'm Shendi
因为最近在写自己的工具包,在控制台模块有个需求,就是想与 JShell 一样,将用户输入的字符串转成代码执行
这里附上我写的工具包地址: https://github.com/1711680493/ShendiKit
目录
编译思路及方法
Javax.tools
最简单的编译方式
使用 CompilationTask
编译字符串的Java代码
控制台接收Java语句执行
对于将字符串解析成代码运行,这个有很多博客写到了,都是用的都是 Commons Jexl 包
还有一种是Java语言内置的脚本解析器,不过那个是解析 js 的,并不是Java(可以进行一些+-*/等js操作)
对于脚本的这种方法,存在于 javax.script 包中, ScriptEngine
编译思路及方法
因为Java是静态语言,所以只能另择他法
可以通过将代码写入Java文件中,并将其编译,使用 ClassLoader 加载进来运行
这篇文章的重点为Java代码编译Java文件
对于编译Java文件,有多种思路
- 调用使用 Javac 命令,这种方式可能会出现很多问题,比如用户未设置环境变量什么的
- 这种方式使用Runtime类就行
- 在Java 1.6开始,提供了 javax.tools 包用来编译java文件
Javax.tools
最简单的编译方式
一个简单的编译代码的操作如下
// 获取系统编译器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// 进行编译操作
compiler.run(null, null, null, "C:/Users/Administrator/Desktop/Test.java");
JavaCompiler 的 run 函数接收四个参数
分别为
-
in
- “标准”输入; 使用System.in如果为null -
out
- “标准”输出; 如果为空,请使用System.out -
err
- “标准”错误; 如果为空,请使用System.err -
arguments
- 传递给工具的参数
第四个参数为 javac 命令的参数,是一个可变参数,没有时,则控制台会出现 Javac 的提示
返回值为 0 则代表编译成功
运行后 Test.class 文件就在同目录出现了
使用 CompilationTask
还有一种方法是通过CompilationTask来进行编译,例子在 JavaCompiler 文档中有,如下
public class CompilerTest {
public static void main(String[] args) throws Exception {
// 要编译的文件集合
File[] files1 = new File[] {new File("C:/Users/Administrator/Desktop/Test.java")};
// 获取系统编译器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// 从编译器中获取基础Java文件管理器
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
// 文件管理器中获取Java文件,并添加到任务执行
Iterable<? extends JavaFileObject> compilationUnits1 =
fileManager.getJavaFileObjectsFromFiles(Arrays.asList(files1));
compiler.getTask(null, fileManager, null, null, null, compilationUnits1).call();
// 关闭文件管理器
fileManager.close();
}
}
上面代码的结果与上一种方法一致
主要是 JavaCompiler 的 getTask 函数,此函数将获取编译任务,并可以执行
对于最后一个参数 compilationUnits,可以在 StandardJavaFileManager 中进行转换获取
也可以自行定义JavaFileObject
编译字符串的Java代码
下面的例子将字符串编译成 class 文件
package shendi.test.compiler;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
public class TestCompiler extends SimpleJavaFileObject {
private String code;
protected TestCompiler(String className, String code) throws URISyntaxException {
super(new URI(className), Kind.SOURCE);
this.code = code;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return code;
}
public static void main(String[] args) throws URISyntaxException {
JavaCompiler c = ToolProvider.getSystemJavaCompiler();
String code = "class Test {"
+ "public static void main(String[] args) {"
+ "System.out.println(\"hello, world\");"
+ "}}";
// 自定义 JavaFileObject 可以将字符串转Java文件对象
JavaFileObject jo = new TestCompiler("Test", code);
Iterable<? extends JavaFileObject> it = Arrays.asList(jo);
// 这个文件管理器可有可无
StandardJavaFileManager manager = c.getStandardFileManager(null, null, null);
CompilationTask task = c.getTask(null, manager, null, null, null, it);
task.call();
}
}
奇怪的是,使用 IDE 或者 javac 将代码编译后,运行出错
经过种种排查,我写测试文件都在一个项目,之前写注解处理器,... 删除后问题解决
上述代码执行后,有一个疑问,class文件在哪呢?
因为没有设置参数,默认保存在项目根目录
控制台接收Java语句执行
因为上面这种代码我运行出错,所以使用 run 的方式来编译文件
思路为接收用户输入代码,封装进文件并编译此文件,读取此文件流使用ClassLoader加载进JVM运行
package shendi.test.compiler;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Scanner;
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
public class TestCompiler extends ClassLoader {
public static void main(String[] args) throws Exception {
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print("请输入代码: ");
String code = sc.nextLine();
// 退出死循环命令
if ("exit".equals(code)) break;
// 将语句封装成代码编译得到字节码
byte[] classByte = compiler(code);
// 使用类加载器来将byte[]定义成类,需要自定义类加载器(此类继承ClassLoader)
Class<?> tempClass = cl.defineClass("Temp", classByte, 0, classByte.length);
// 需要注意的是,一个类加载器不能加载相同的类(重新加载),所以定义完后要重写new一遍
cl = new TestCompiler();
// 反射调用函数
Method tempMethod = tempClass.getMethod("exec");
tempMethod.invoke(tempClass.getConstructor().newInstance());
}
sc.close();
}
/** 类加载器 */
private static TestCompiler cl = new TestCompiler();
/**
* 将语句转换成Java文件并编译
* @param code 语句
* @return 编译后的class文件的字节码
*/
private static byte[] compiler(String code) {
// 源文件输入输出流
FileOutputStream output = null;
FileInputStream input = null;
try {
output = new FileOutputStream("./Temp.java");
output.write(("public class Temp { public void exec() {" + code + "} }").getBytes());
JavaCompiler c = ToolProvider.getSystemJavaCompiler();
if (c.run(null, null, null, "./Temp.java") == 0) {
input = new FileInputStream("./Temp.class");
// readAllByte 可以读取此文件所有字节,1.9版本以上才支持
return input.readAllBytes();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (input != null) input.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (output != null) output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
}
执行后,我的需求就达到了