1. 程式人生 > >java動態編譯 (java線上執行程式碼後端實現原理)

java動態編譯 (java線上執行程式碼後端實現原理)

需求:要實現一個web網頁中輸入java程式碼,然後能知道編譯結果以及執行結果
類似於菜鳥java線上工具的效果:https://c.runoob.com/compile/10

剛開始從什麼概念都沒有到最後封裝成一個完整的工具類,中間查閱了很多資料才瞭解其中的概念以及流程,參考文獻在文章最後面。

重點需要了解的概念是:
JavaFileManage、JavaFileObject
推薦先看這篇文章:http://blog.onlycatch.com/post/java-Compiler-API

這裡是一個封裝的demo程式碼:

package compiler.mydemo;

import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Create by andy on 2018-12-06 21:25
 */
public class CustomStringJavaCompiler {
    //類全名
    private String fullClassName;
    private String sourceCode;
    //存放編譯之後的位元組碼(key:類全名,value:編譯之後輸出的位元組碼)
    private Map<String, ByteJavaFileObject> javaFileObjectMap = new ConcurrentHashMap<>();
    //獲取java的編譯器
    private JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    //存放編譯過程中輸出的資訊
    private DiagnosticCollector<JavaFileObject> diagnosticsCollector = new DiagnosticCollector<>();
    //執行結果(控制檯輸出的內容)
    private String runResult;
    //編譯耗時(單位ms)
    private long compilerTakeTime;
    //執行耗時(單位ms)
    private long runTakeTime;


    public CustomStringJavaCompiler(String sourceCode) {
        this.sourceCode = sourceCode;
        this.fullClassName = getFullClassName(sourceCode);
    }

    /**
     * 編譯字串原始碼,編譯失敗在 diagnosticsCollector 中獲取提示資訊
     *
     * @return true:編譯成功 false:編譯失敗
     */
    public boolean compiler() {
        long startTime = System.currentTimeMillis();
        //標準的內容管理器,更換成自己的實現,覆蓋部分方法
        StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(diagnosticsCollector, null, null);
        JavaFileManager javaFileManager = new StringJavaFileManage(standardFileManager);
        //構造原始碼物件
        JavaFileObject javaFileObject = new StringJavaFileObject(fullClassName, sourceCode);
        //獲取一個編譯任務
        JavaCompiler.CompilationTask task = compiler.getTask(null, javaFileManager, diagnosticsCollector, null, null, Arrays.asList(javaFileObject));
        //設定編譯耗時
        compilerTakeTime = System.currentTimeMillis() - startTime;
        return task.call();
    }

    /**
     * 執行main方法,重定向System.out.print
     */
    public void runMainMethod() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, UnsupportedEncodingException {
        PrintStream out = System.out;
        try {
            long startTime = System.currentTimeMillis();
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            PrintStream printStream = new PrintStream(outputStream);
            //PrintStream PrintStream = new PrintStream("/Users/andy/Desktop/tem.sql"); //輸出到檔案
            System.setOut(printStream);

            StringClassLoader scl = new StringClassLoader();
            Class<?> aClass = scl.findClass(fullClassName);
            Method main = aClass.getMethod("main", String[].class);
            Object[] pars = new Object[]{1};
            pars[0] = new String[]{};
            main.invoke(null, pars); //呼叫main方法
            //設定執行耗時
            runTakeTime = System.currentTimeMillis() - startTime;
            //設定列印輸出的內容
            runResult = new String(outputStream.toByteArray(), "utf-8");
        } finally {
            //還原預設列印的物件
            System.setOut(out);
        }

    }

    /**
     * @return 編譯資訊(錯誤 警告)
     */
    public String getCompilerMessage() {
        StringBuilder sb = new StringBuilder();
        List<Diagnostic<? extends JavaFileObject>> diagnostics = diagnosticsCollector.getDiagnostics();
        for (Diagnostic diagnostic : diagnostics) {
            sb.append(diagnostic.toString()).append("\r\n");
        }
        return sb.toString();
    }

    /**
     * @return 控制檯列印的資訊
     */
    public String getRunResult() {
        return runResult;
    }


    public long getCompilerTakeTime() {
        return compilerTakeTime;
    }

    public long getRunTakeTime() {
        return runTakeTime;
    }

    /**
     * 獲取類的全名稱
     *
     * @param sourceCode 原始碼
     * @return 類的全名稱
     */
    public static String getFullClassName(String sourceCode) {
        String className = "";
        Pattern pattern = Pattern.compile("package\\s+\\S+\\s*;");
        Matcher matcher = pattern.matcher(sourceCode);
        if (matcher.find()) {
            className = matcher.group().replaceFirst("package", "").replace(";", "").trim() + ".";
        }

        pattern = Pattern.compile("class\\s+\\S+\\s+\\{");
        matcher = pattern.matcher(sourceCode);
        if (matcher.find()) {
            className += matcher.group().replaceFirst("class", "").replace("{", "").trim();
        }
        return className;
    }

    /**
     * 自定義一個字串的原始碼物件
     */
    private 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;
        }

    }

    /**
     * 自定義一個編譯之後的位元組碼物件
     */
    private 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();
        }
    }

    /**
     * 自定義一個JavaFileManage來控制編譯之後位元組碼的輸出位置
     */
    private class StringJavaFileManage extends ForwardingJavaFileManager {
        StringJavaFileManage(JavaFileManager fileManager) {
            super(fileManager);
        }

        //獲取輸出的檔案物件,它表示給定位置處指定型別的指定類。
        @Override
        public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
            ByteJavaFileObject javaFileObject = new ByteJavaFileObject(className, kind);
            javaFileObjectMap.put(className, javaFileObject);
            return javaFileObject;
        }
    }

    /**
     * 自定義類載入器, 用來載入動態的位元組碼
     */
    private class StringClassLoader extends ClassLoader {
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            ByteJavaFileObject fileObject = javaFileObjectMap.get(name);
            if (fileObject != null) {
                byte[] bytes = fileObject.getCompiledBytes();
                return defineClass(name, bytes, 0, bytes.length);
            }
            try {
                return ClassLoader.getSystemClassLoader().loadClass(name);
            } catch (Exception e) {
                return super.findClass(name);
            }
        }
    }
}

測試程式碼:

package compiler.mydemo;

/**
 * Create by andy on 2018-12-06 15:21
 */
public class Test1 {

    public static void main(String[] args) {
        String code = "public class HelloWorld {\n" +
                "    public static void main(String []args) {\n" +
                "\t\tfor(int i=0; i < 1; i++){\n" +
                "\t\t\t       System.out.println(\"Hello World!\");\n" +
                "\t\t}\n" +
                "    }\n" +
                "}";
        CustomStringJavaCompiler compiler = new CustomStringJavaCompiler(code);
        boolean res = compiler.compiler();
        if (res) {
            System.out.println("編譯成功");
            System.out.println("compilerTakeTime:" + compiler.getCompilerTakeTime());
            try {
                compiler.runMainMethod();
                System.out.println("runTakeTime:" + compiler.getRunTakeTime());
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(compiler.getRunResult());
            System.out.println("診斷資訊:" + compiler.getCompilerMessage());
        } else {
            System.out.println("編譯失敗");
            System.out.println(compiler.getCompilerMessage());
        }

    }


}

下面一些文章對我的理解有很大的幫助:
http://blog.onlycatch.com/post/java-Compiler-API (這篇文章迴圈漸進,對理解概念有很大幫助)
https://www.liaoxuefeng.com/article/0014617596492474eea2227bf04477e83e6d094683e0536000
https://www.ibm.com/developerworks/cn/java/j-jcomp/
http://www.cnblogs.com/liaoyu/p/real-time-compile-and-run-java-code-web-app.html
http://www.timehaswingss.top/blog/java/dynamic.html