在上一篇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)); } }