需求:要實現一個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());
}
}
}
下一篇介紹了如果處置動態代碼的死循環的思路:
java動態編譯 (java在線執行代碼后端實現原理)(二)
下面一些文章對我的理解有很大的幫助:
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