在线Java 动态运行Java源代码-编译器

在线Java 动态运行Java源代码-编译器构造编译器,通过编译器编译Java源码,获取编译后的字节码

在线Java

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

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注