玩轉 Java 動態(tài)編譯,太秀了~!
來源:https://zhenbianshu.github.io
問題
之后 Spring Cloud 的
ContextRefresher會將變更后的配置作為一個新的 Spring Environment 加載進 ApplicationContext,由于 Scoped Bean 都是 Lazy Init 的,它們會在下一次使用時被使用新的 Environment 重新創(chuàng)建。這套動態(tài)配置加載流程在使我們服務更加靈活的同時,也帶來了很大的風險。首先從業(yè)務上,修改配置不像上線這么”重量級”,不必要找 QA 進行回歸測試,這就有可能引發(fā)一系列奇怪的 Bug,而且長時間發(fā)現不了,另外,Spring Cloud 本身沒有 “fallback” 機制,一旦配置的數據類型出了問題,就會導致服務不可用。
為此,我給 Spring Cloud 提了個 issue,但作者認為變動太大,不好改也不必改。
其實我也明白這個問題的困境,每個人都得為自己要修改的配置負責,即使框架支持了 fallback,但將錯誤吞掉,配置修改后不生效也沒什么變化可能也并不符合用戶的期望。所以,盡量讓用戶要修改的配置正確成為了新的目標。
基于這種需求,我添加了一個動態(tài)配置的校驗器,但實現里一部分代碼來自 github,所以本文在總結思路的同時,也幫助我理解所有代碼。
整體思路
由于框架層沒法做太多事情,所以我的計劃是將這些配置取出來,構造出一個獨立的 Java 類,并在服務外新建一個 ApplicationContext 試圖通過構造出來的 Java 類初始化一個 Spring Bean,如果這個 Spring Bean 初始化過程中報錯了,說明配置是有問題的。
動態(tài)編譯
通過配置構造 Java 類
首先要通過
.properties文件構造出一個 Java 類,但問題是在配置里我們是不知道這些配置將要被怎么使用的,不知道它要被 Spring EL 如何處理,又將被轉成什么類型。這里我采用的策略是給配置添加注釋,注釋里使用一定的格式聲明 EL 表達式和要生成的字段類型,當然這種實現有點 low,有人提議把這些信息放到配置項的 key 里,之后會再進行優(yōu)化。
把各個字段解析完成后放到準備到的類模板中,就生成了一個
Config.java類字符串,之后就要將這個字符串編譯成字節(jié)碼并由 Spring 加載成 Bean。另外,Spring 系列面試題和答案全部整理好了,微信搜索互聯架構師,在后臺發(fā)送:8,可以在線閱讀。JavaCompiler
由于 Config.java 是在運行時生成的,所以編譯也只能在運行時了,萬幸 Java 有提供
javax.util.JavaCompiler類進行 Java 類的動態(tài)編譯,省去了”寫入文件 —— 命令行編譯 —— 類加載 —— 清理文件” 的復雜流程。JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
JavaFileManager fileManager = javaCompiler.getStandardFileManager(null, null, null);
CompilationTask task = javaCompiler.getTask(out, fileManager, diagnosticListener, options, classes, compilationUnits);
task.call();
FileObject outputFile = fileManager.getFileForOutput(null, null, null, null);
outputFile.getCharContent(true);流程如下圖:JavaCompiler 通過 JavaFileManager 管理輸入和輸出文件,使用時通過
getTask()方法提交一個異步 CompilationTask 進行代碼編譯,代碼編譯時,JavaCompiler 通過getCharContent()從傳入的 compilationUnits 獲取到 .java 文件內容,把編譯后的結果調用 CompiledByteCode 的openOutputStream()方法寫到 CompiledByteCode 對象里。委托模式
由于 JavaCompiler 的默認實現都是通過文件進行的,這不符合我的期望,我需要的是輸入和輸出都在內存進行,所以需要修改 JavaCompiler 的實現,JavaCompiler、JavaFileManager、JavaFileObject(Input/Output) 分別使用委托模式實現。其中 JavaFileManager 已經有
ForwardingJavaFileManager的實現,JavaFileObject 也有SimpleJavaFileObject的實現,我們繼承其實現后重寫部分方法即可。我參考的源碼:https://github.com/trung/InMemoryJavaCompiler
Spring Bean 實例化
要將 Config 類實例化成 Bean,我們可以在 xml 里預定義它,在編譯結束后創(chuàng)建一個簡易的
FileSystemXmlApplicationContext實例化這個 xml 內的 Bean。類加載器
首先要讓 Spring 能夠加載到這些編譯好的字節(jié)碼,這就需要 ClassLoader 的配合。
類加載器的默認實現不可能知道去加載我們內存里編譯好的字節(jié)碼,只好新加一個 ClassLoader,實現也很簡單,繼承
ClassLoader抽象類,并實現findClass方法即可。class MemoryClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 在 CompiledByteCode 類里將編譯后的字節(jié)碼放到 classLoader 的 classBytes 字段內。
byte[] buf = classBytes.get(name);
if (buf == null) {
return super.findClass(name);
}
return defineClass(name, buf, 0, buf.length);
}
}配置和實現
由于 Config Bean 的初始化依賴動態(tài)配置,我們還要把這些配置也添加到 Spring 環(huán)境內,我們知道 Spring 環(huán)境配置是由多個
PropertySource構成的,向里面添加一個實現即可。然后就可以調用 application 的refresh()方法初始化上下文了,另外 Config Bean 被設置為懶加載了,不要忘記 get 一下使其被創(chuàng)建。最終的代碼如下:
FileSystemXmlApplicationContext applicationContext = new FileSystemXmlApplicationContext();
applicationContext.setClassLoader(memoryClassLoader);
applicationContext.setConfigLocation("classpath*:/test.xml");
Map<String, Object> propertyMap = buildDynamicPropertyMap();
MapPropertySource mapPropertySource = new MapPropertySource("validate_source", propertyMap);
applicationContext.getEnvironment().getPropertySources().addFirst(mapPropertySource);
applicationContext.refresh();
applicationContext.getBean("config");小結
小項目完成的過程中,復習了很多知識,也嘗試了業(yè)務代碼中幾乎不會用到的設計模式,充滿了挑戰(zhàn)性。
當然它現在還有配置不夠方便、錯誤提示不夠明確、沒解決配置 namespace 等問題,留到后面慢慢優(yōu)化吧~
最后,關注公眾號互聯網架構師,在后臺回復:2T,可以獲取我整理和創(chuàng)作的 Java 系列教程非常齊全。
1、2019 年 9 月全國程序員工資統(tǒng)計,你是什么水平?
5、37歲程序員被裁,120天沒找到工作,無奈去小公司,結果懵了...
