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 多个文件怎么编译 javacompiler编译多java文件_java 多个文件怎么编译

 

编译思路及方法

因为Java是静态语言,所以只能另择他法

可以通过将代码写入Java文件中,并将其编译,使用 ClassLoader 加载进来运行

这篇文章的重点为Java代码编译Java文件

 

对于编译Java文件,有多种思路

  • 调用使用 Javac 命令,这种方式可能会出现很多问题,比如用户未设置环境变量什么的
  • 这种方式使用Runtime类就行
  • 在Java 1.6开始,提供了 javax.tools 包用来编译java文件

 

Javax.tools

java 多个文件怎么编译 javacompiler编译多java文件_java 多个文件怎么编译_02

最简单的编译方式

一个简单的编译代码的操作如下

// 获取系统编译器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// 进行编译操作
compiler.run(null, null, null, "C:/Users/Administrator/Desktop/Test.java");

 

JavaCompiler 的 run 函数接收四个参数

分别为

  1. in - “标准”输入; 使用System.in如果为null
  2. out - “标准”输出; 如果为空,请使用System.out
  3. err - “标准”错误; 如果为空,请使用System.err
  4. arguments - 传递给工具的参数

第四个参数为 javac 命令的参数,是一个可变参数,没有时,则控制台会出现 Javac 的提示

java 多个文件怎么编译 javacompiler编译多java文件_java_03

 

返回值为 0 则代表编译成功

运行后 Test.class 文件就在同目录出现了

java 多个文件怎么编译 javacompiler编译多java文件_java_04

java 多个文件怎么编译 javacompiler编译多java文件_java_05

java 多个文件怎么编译 javacompiler编译多java文件_ide_06


使用 CompilationTask

还有一种方法是通过CompilationTask来进行编译,例子在 JavaCompiler 文档中有,如下

java 多个文件怎么编译 javacompiler编译多java文件_ide_07

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 函数,此函数将获取编译任务,并可以执行

java 多个文件怎么编译 javacompiler编译多java文件_java_08

对于最后一个参数 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 将代码编译后,运行出错

java 多个文件怎么编译 javacompiler编译多java文件_java 多个文件怎么编译_09

经过种种排查,我写测试文件都在一个项目,之前写注解处理器,

java 多个文件怎么编译 javacompiler编译多java文件_java 多个文件怎么编译_10

... 删除后问题解决

 

上述代码执行后,有一个疑问,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;
	}

}

执行后,我的需求就达到了

java 多个文件怎么编译 javacompiler编译多java文件_Java_11