學習 xxl-job 定時任務時瞭解到基於 JVM 的 Grovvy 指令碼語言、搭建 Jenkins 時知道了編譯API
1. Java 指令碼機制
Java 的指令碼 API 可以讓我們呼叫 JavaScript、Grovvy、Ruby 等指令碼語言,它避免了編譯和連結環節,具有如下優勢:
- 可快速變更,不斷實驗(Java 9 已經有 JShell 可以實驗了)
- 可修改執行著的程式行為
- 支援程式定製化
1.1 使用示例
public static void main(String[] args) throws Exception {
// 獲取 JS 指令碼引擎
ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine jsEngine = scriptEngineManager.getEngineByName("JS");
// 執行指令碼語言
String script = "var num = 1 + 2";
jsEngine.eval(script);
// 也可以從流中獲取指令碼
FileInputStream fileInputStream = new FileInputStream(new File("script.txt"));
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream);
jsEngine.eval(inputStreamReader);
// 繫結變數
jsEngine.put("testKey", "testValue");
// 執行方法
// 指令碼引擎呼叫方法需要實現 Invocable 介面
String jsMethod = "function hello() {return 'Hello World'}";
jsEngine.eval(jsMethod);
Object function = ((Invocable) jsEngine).invokeFunction("hello");
// 獲取結果
Object num = jsEngine.get("num");
Object testValue = jsEngine.get("testKey");
System.out.println(num.toString());
System.out.println(testValue.toString());
System.out.println(function.toString());
}
1.2 思考
指令碼語言不像 Java 修改程式碼後需要再次編譯和部署,這樣想想的話 xxl-job 定時任務框架可能是通過 RPC 呼叫傳輸了 Grovvy 指令碼的流給執行器,那麼 JVM 執行的定時任務都是最新的
指令碼 API 允許從外部讀取指令碼且實時生效,那麼就可以做外掛式的功能介面,只需做一個公用介面或者上層抽象類來呼叫外部指令碼,需定製化或修改時可替換外部指令碼來實現
2. 編譯器 API
在專案中也看到過用 Java 來寫 Java 類然後編譯放入專案中呼叫的,第一次見有點新鮮感。這個編譯器 API 在測試和自動化構建中也會被呼叫
2.1 基本使用
預設編譯之後的位元組碼在同級目錄下
public class CompilerTest1 {
public static void main(String[] args) {
// 獲取編譯器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
/**
* 引數分別是
* InputStream in:輸入流規定為空,預設的編譯器不會接收控制檯輸入
* OutputStream out:輸出,為空輸出到控制檯
* OutputStream err:輸出,為空輸出到控制檯
* String... arguments:引數,若呼叫 javac 則是傳入啟動引數
* result:返回 0 則編譯成功
*/
int result = compiler.run(null, null, null, "D:\\CompilerTest.java");
if (result == 0) {
System.out.println("編譯成功");
} else {
System.out.println("編譯失敗");
}
}
}
2.2 實際事例
專案中編譯的情況相對來說是複雜些,需要發起編譯任務來對編譯過程有更多的控制
public class CompilerTest2 {
public static void main(String[] args) throws URISyntaxException, IllegalAccessException, InstantiationException, NoSuchMethodException, ClassNotFoundException, InvocationTargetException {
// 獲取編譯器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// 類名、字串的類程式碼
String className = "TestClass";
StringBuilder sb = new StringBuilder();
sb.append("public class TestClass {\n");
sb.append("\tpublic void hello() {\n");
sb.append("\t\tSystem.out.println(\"Hello World Compiler\");\n");
sb.append("\t}\n");
sb.append("}");
// 將字串程式碼轉成 JavaFileObject ———— 編譯器需要
StringSource javaFileObject = new StringSource(className, sb.toString());
Iterable<StringSource> fileObjects = Arrays.asList(javaFileObject);
// 檔案管理器 ———— 編譯器需要
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
// 報告診斷資訊物件 ———— 編譯器需要
DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<JavaFileObject>();
// 編譯引數:編譯後的位元組碼輸出地址
File classPath = new File(Thread.currentThread().getContextClassLoader().getResource("").toURI());
String outDir = classPath.getAbsolutePath() + File.separator;
Iterable<String> options = Arrays.asList("-d", outDir);
/**
* Writer out:輸出,為空到控制檯
* JavaFileManager fileManager:檔案管理器,為空用編譯器的標準檔案管理器
* DiagnosticListener<? super JavaFileObject> diagnosticListener:診斷監聽器,為空用編譯器預設方法報告
* Iterable<String> options:編譯引數
* Iterable<String> classes:需要編譯的類,用於註解處理
* Iterable<? extends JavaFileObject> compilationUnits
*/
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnosticCollector, options, null, fileObjects);
// 執行編譯
boolean result = task.call();
// 編譯錯誤資訊
if (result != true) {
for (Diagnostic diagnostic : diagnosticCollector.getDiagnostics()) {
System.out.println("Error on line: " + diagnostic.getLineNumber());
System.out.println("URI: " + diagnostic.getSource().toString());
}
System.exit(-1);
}
// 將位元組碼載入進 JVM
Class<?> clazz = Class.forName(className);
// 建立一個新類,反射執行其方法
Object instance = clazz.newInstance();
Method helloMethod = clazz.getMethod("hello");
helloMethod.invoke(instance);
}
/**
* 字串的類程式碼存在於記憶體之中,而引數需要 FileObject
* 我們將字串程式碼轉成 FileObject 型別
*/
static class StringSource extends SimpleJavaFileObject {
private String code;
StringSource(String name, String code) {
super(URI.create("string:///" + name.replace(".", "/") + Kind.SOURCE.extension), Kind.SOURCE);
this.code = code;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return code;
}
public String getCode() {
return code;
}
}
}