java動態編譯 (java線上執行程式碼後端實現原理)
阿新 • • 發佈:2018-12-07
需求:要實現一個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