java動態編譯 (java在線執行代碼後端實現原理)(二)

在上一篇java動態編譯 (java在線執行代碼後端實現原理(一))文章中實現了 字符串編譯成字節碼,而後經過反射來運行代碼的demo。這一篇文章提供一個如何防止死循環的代碼佔用cpu的問題。html

思路:因爲CustomStringJavaCompiler中重定向了System.out的輸出位置,確定不能有多線程併發的狀況,不然會照成System.out輸出內容錯亂,因此我用了 Executors.newFixedThreadPool(1), 經過Future模式來獲取結果,我自定義了一個CustomCallable來處理核心邏輯,在call方法中從新new 了一個Thread來編譯並執行代碼,而後經過join等待N秒以後強制stop掉正在運行的線程。這樣就能及時的kill掉動態運行的代碼。java

CustomStringJavaCompiler 編譯核心類sql

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); //測試kill線程暫時屏蔽

            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);
            }
        }
    }
}

CustomCallable 調用編譯並運行,設置超時時間後端

package compiler.mydemo;

import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.Callable;

/**
 * Create by andy on 2018-12-07 13:10
 */
public class CustomCallable implements Callable<RunInfo> {
    private String sourceCode;

    public CustomCallable(String sourceCode) {
        this.sourceCode = sourceCode;
    }

    //方案1
    //@Override
    //public RunInfo call() throws Exception {
    //    System.out.println("開始執行call" + LocalTime.now());
    //    RunInfo runInfo = new RunInfo();
    //    CustomStringJavaCompiler compiler = new CustomStringJavaCompiler(sourceCode);
    //    if (compiler.compiler()) {
    //        runInfo.setCompilerSuccess(true);
    //        try {
    //            compiler.runMainMethod();
    //            runInfo.setRunSuccess(true);
    //            runInfo.setRunTakeTime(compiler.getRunTakeTime());
    //            runInfo.setRunMessage(compiler.getRunResult()); //獲取運行的時候輸出內容
    //        } catch (Exception e) {
    //            e.printStackTrace();
    //            runInfo.setRunSuccess(false);
    //            runInfo.setRunMessage(e.getMessage());
    //        }
    //    } else {
    //        //編譯失敗
    //        runInfo.setCompilerSuccess(false);
    //    }
    //    runInfo.setCompilerTakeTime(compiler.getCompilerTakeTime());
    //    runInfo.setCompilerMessage(compiler.getCompilerMessage());
    //    System.out.println("call over" + LocalTime.now());
    //    return runInfo;
    //}


    //方案2
    @Override
    public RunInfo call() throws Exception {
        RunInfo runInfo = new RunInfo();
        Thread t1 = new Thread(() -> realCall(runInfo));
        t1.start();
        try {
            t1.join(3000); //等待3秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //無論有沒有正常執行完成,強制中止t1
        t1.stop();
        return runInfo;
    }

    private void realCall(RunInfo runInfo) {
        CustomStringJavaCompiler compiler = new CustomStringJavaCompiler(sourceCode);
        if (compiler.compiler()) {
            runInfo.setCompilerSuccess(true);
            try {
                compiler.runMainMethod();
                runInfo.setRunSuccess(true);
                runInfo.setRunTakeTime(compiler.getRunTakeTime());
                runInfo.setRunMessage(compiler.getRunResult()); //獲取運行的時候輸出內容
            } catch (InvocationTargetException e) {
                //反射調用異常了,是由於超時的線程被強制stop了
                if ("java.lang.ThreadDeath".equalsIgnoreCase(e.getCause().toString())) {
                    return;
                }
            } catch (Exception e) {
                e.printStackTrace();
                runInfo.setRunSuccess(false);
                runInfo.setRunMessage(e.getMessage());
            }
        } else {
            //編譯失敗
            runInfo.setCompilerSuccess(false);
        }
        runInfo.setCompilerTakeTime(compiler.getCompilerTakeTime());
        runInfo.setCompilerMessage(compiler.getCompilerMessage());
        runInfo.setTimeOut(false); //走到這一步表明沒有超時
    }
}

RunInfo 動態編譯、運行信息的bean多線程

public class RunInfo {
    //true:表明超時
    private Boolean timeOut;

    private Long compilerTakeTime;
    private String compilerMessage;
    private Boolean compilerSuccess;

    private Long runTakeTime;
    private String runMessage;
    private Boolean runSuccess;

    //省略get和set方法
}

CompilerUtil 把一整套流程封裝了一個工具類併發

package compiler.mydemo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * Create by andy on 2018-12-07 16:32
 */
public class CompilerUtil {
    //這裏用一個線程是由於防止System.out輸出內容錯亂
    private static ExecutorService pool = Executors.newFixedThreadPool(1);

    public static RunInfo getRunInfo(String javaSourceCode) {
        RunInfo runInfo;
        CustomCallable compilerAndRun = new CustomCallable(javaSourceCode);
        Future<RunInfo> future = pool.submit(compilerAndRun);
        //方案1
        try {
            runInfo = future.get();
        } catch (Exception e) {
            e.printStackTrace();
            //代碼編譯或者運行超時
            runInfo = new RunInfo();
            runInfo.setTimeOut(true);
        }

        //方案2:不可行的緣由:future.get超時會有問題,因爲線程池只有1個線程,同時提交10個任務, 當前面幾個任務執行時間很長,後面調用get就會立馬失敗,也就是說get的超時時間是從調用get開始算的,並非線程真正執行時間開始計算的
        //try {
        //    runInfo = future.get(5, TimeUnit.SECONDS);
        //    return runInfo;
        //} catch (InterruptedException e) {
        //    System.out.println("future在睡着時被打斷");
        //    e.printStackTrace();
        //} catch (ExecutionException e) {
        //    System.out.println("future在嘗試取得任務結果時出錯");
        //    e.printStackTrace();
        //} catch (TimeoutException e) {
        //    System.out.println("future時間超時");
        //    e.printStackTrace();
        //    future.cancel(true);
        //}
        //runInfo = new RunInfo();
        //runInfo.setTimeOut(true);
        return runInfo;

    }
}

測試類:app

package compiler.mydemo;

/**
 * Create by andy on 2018-12-10 10:43
 */
public class Test3 {

    public static void main(String[] args) throws InterruptedException {
        String loop = "public class HelloWorld {\n" +
                "    public static void main(String[] args) {\n" +
                "        while(true){\n" +
                //"            System.out.println(\"Hello World!\");\n" +
                "        }\n" +
                "       \n" +
                "    }\n" +
                "}";

        String sleep_loop = "public class HelloWorld {\n" +
                "    public static void main(String[] args) {\n" +
                "    try {\n" +
                "            Thread.sleep(6000);\n" +
                "        } catch (InterruptedException e) {\n" +
                "            e.printStackTrace();\n" +
                "        }\n" +
                "       System.out.println(\"Hello World!\");\n" +
                "        while(true){\n" +
                //"            System.out.println(\"Hello World!\");\n" +
                "        }\n" +
                "    }\n" +
                "}";

        String ok = "public class HelloWorld {\n" +
                "    public static void main(String[] args) {\n" +
                "       System.out.println(\"Hello World!\");\n" +
                "    }\n" +
                "}";

        TestRun t = new TestRun(ok, "thread:ok");
        t.start();

        TestRun t1 = new TestRun(loop, "thread:loop:");
        t1.start();
        //
        TestRun t2 = new TestRun(sleep_loop, "thread:sleep_loop:");
        t2.start();


    }


}

class TestRun extends Thread {
    String code;

    TestRun(String code, String name) {
        this.code = code;
        super.setName(name);
    }

    @Override
    public void run() {
        System.out.println(CompilerUtil.getRunInfo(code));
    }
}
相關文章
相關標籤/搜索