1、获取JDK编译器主要有两种方式(当然大神也可以有三种,自己写编译器):
- 如果运行的环境System.getProperty(“java.home”)中配置了JDK,则直接通过ToolProvider.getSystemJavaCompiler()获取系统环境中tools.jar里的编译器;
- 如果运行的环境没有配置JDK,就需要加载自己指定tools.jar,然后创建编译器。
public class StandardCompilerFactory implements AbstractCompilerFactory {
private Logger log = Logger.getLogger(StandardCompilerFactory.class.getName());
private URL toolsUrl;
public StandardCompilerFactory() {
this(null);
}
public StandardCompilerFactory(URL toolsUrl) {
super();
this.toolsUrl = toolsUrl;
}
/**
* 当toolsUrl不为null时,获取自定义工具包中的编译器;<br/>
* 否则先尝试加载系统运行环境中JDK的编译器,如果未加载到编译器,则再尝试框架自带tools包(LITE不含)加载编译器。
*
* @return .
*/
@Override
public JavaCompiler getCompiler() {
if (toolsUrl != null) {
try {
log.info("加载tools.jar路径:" + toolsUrl);
// 加载自定义tools中的编译器
return loadJavaCompiler(toolsUrl);
} catch (Exception e) {
log.log(Level.WARNING, e.getMessage(), e);
throw new AssertionError("无法获取编译器,自定义路径:" + toolsUrl);
}
}
// 获取java的编译器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler != null) {
return compiler;
}
// 利用框架自带tools包,兜底创建编译器(LITE版无)
// 创建临时暂存tools.jar
String tempPath = System.getProperty("java.io.tmpdir");
File tmpJar = new File(tempPath, "dynamic_tools_1.8.jat");
URL loadUrl = null;
try {
loadUrl = tmpJar.toURI().toURL();
log.info("加载tools.jar路径:" + loadUrl);
InputStream in = StringJavaCompiler.class.getClassLoader()
.getResourceAsStream("lib/tools-1.8.jar");
if (!tmpJar.exists()) {
// 拷贝到临时目录
FileOutputStream out = new FileOutputStream(tmpJar);
IOUtils.copyByNIO(in, out, 2 << 10);
}
// 加载临时工具包
return loadJavaCompiler(loadUrl);
} catch (Exception e) {
log.log(Level.WARNING, e.getMessage(), e);
if (tmpJar.exists()) {
tmpJar.delete();
}
throw new AssertionError(
"无法获取编译器,默认路径:" + loadUrl + "。如果为LITE版,请务必保证运行环境有JDK或者配置自定义tools.jar路径。");
}
}
private JavaCompiler loadJavaCompiler(URL toolUrl) throws Exception {
// 加载工具包
URL[] urls = { toolUrl };
URLClassLoader loader = new URLClassLoader(urls);
// 因运行环境没有JavacTool类,所以通过反射来创建获取编译工具类
Class<?> javacTool = Class.forName("com.sun.tools.javac.api.JavacTool", true, loader);
// 获取创建编译器方法
Method create = javacTool.getMethod("create");
// 获取编译器
return (JavaCompiler) create.invoke(null);
}
}
2、有编译器,那我们就可以用编译取编译Java源码,获取我们想要编译后的字节码。编译源码用到的核心类JavaFileManager、JavaFileObject。JavaFileObject主要作用是将纯源码文本来替换Java.java文件作为输入源编译,JavaFileManager这里作用是获取编译后的字节码为后面通过字节码加载Class做准备。
输入源:
/**
* 自定义一个字符串的源码对象
*/
public class StringJavaFileObject extends SimpleJavaFileObject {
// 等待编译的源码字段
private String contents;
// java源代码 => StringJavaFileObject对象 的时候使用
public StringJavaFileObject(String className, String contents) {
super(URI.create(
"string:///" + className.replaceAll("\\.", "/") + Kind.SOURCE.extension),
Kind.SOURCE);
this.contents = contents;
}
// 字符串源码会调用该方法
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return contents;
}
}
编译后的字节:
/**
* 自定义一个编译之后的字节码对象
*/
public class ByteJavaFileObject extends SimpleJavaFileObject {
// 存放编译后的字节码
private ByteArrayOutputStream outPutStream;
public ByteJavaFileObject(String className, Kind kind) {
super(URI.create("string:///" + className.replaceAll("\\.", "/") + Kind.SOURCE.extension),
kind);
}
// StringJavaFileManage 编译之后的字节码输出会调用该方法(把字节码输出到outputStream)
@Override
public OutputStream openOutputStream() {
outPutStream = new ByteArrayOutputStream();
return outPutStream;
}
// 在类加载器加载的时候需要用到
public byte[] getCompiledBytes() {
return outPutStream.toByteArray();
}
}
JavaFileManager:
/**
* 自定义一个JavaFileManage来控制编译之后字节码的输出位置
*/
public class StringJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {
private CompileResult result;
public StringJavaFileManager(JavaFileManager fileManager, CompileResult result) {
super(fileManager);
this.result = result;
}
// 获取输出的文件对象,它表示给定位置处指定类型的指定类。
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className,
JavaFileObject.Kind kind, FileObject sibling)
throws IOException {
ByteJavaFileObject javaFileObject = new ByteJavaFileObject(className, kind);
result.setFileObject(javaFileObject);
return javaFileObject;
}
}
3、最后编译源码文本,使用DiagnosticCollector收集编译信息,获取返回最终编译结果。
public CompileResult compile(String fullClassName, String sourceCode, CalTimeDTO calTime)
throws CompileException {
long compilerTakeTime = -1;
boolean compileSuccess;
// 存放编译过程中输出的信息
DiagnosticCollector<JavaFileObject> diagnosticsCollector = new DiagnosticCollector<>();
CompileResult result = new CompileResult(fullClassName);
if (calTime.isCalCompileTime()) {
long startTime = System.currentTimeMillis();
// 编译成字节,并将字节对象放入result
compileSuccess = compile(sourceCode, diagnosticsCollector, result);
// 设置编译耗时(单位ms)
compilerTakeTime = System.currentTimeMillis() - startTime;
} else {
// 编译成字节,并将字节对象放入result
compileSuccess = compile(sourceCode, diagnosticsCollector, result);
}
result.setCompileTime(compilerTakeTime);
if (compileSuccess) {
return result;
}
throw new CompileException(diagnosticsCollector);
}
/**
* 核心编译
*
* @param sourceCode 源码
* @param 编译错误信息
* @param result 结果
* @return 是否编译成功
*/
protected boolean compile(String sourceCode,
DiagnosticCollector<JavaFileObject> diagnosticsCollector,
CompileResult result) {
// 标准的内容管理器,更换成自己的实现,覆盖部分方法
StandardJavaFileManager standardFileManager = getCompiler()
.getStandardFileManager(diagnosticsCollector, null, null);
JavaFileManager javaFileManager = new StringJavaFileManager(standardFileManager, result);
// 构造源代码对象
JavaFileObject javaFileObject = new StringJavaFileObject(result.getFullClassName(),
sourceCode);
// 获取一个编译任务
JavaCompiler.CompilationTask task = getCompiler().getTask(null, javaFileManager,
diagnosticsCollector, null, null, Arrays.asList(javaFileObject));
// TODO 后期扩展支持Processors,实现类似Lombok功能
// task.setProcessors(processors);
return task.call();
}
好啦,编译阶段完成(计划后续自己开发个简单语法的编译器,支持代码块、方法,不再需要完整的java代码文件结构)。
在线Java 动态运行Java源代码
源码已开源至Github,如果无法访问,可访问Gitee。
给个小星星Star哦。
今天的文章在线Java 动态运行Java源代码-编译器分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/79869.html