玩轉(zhuǎn) Java 動(dòng)態(tài)編譯,太秀了~!
點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)
來(lái)源:https://zhenbianshu.github.io
問(wèn)題
之前的文章從Spring 的環(huán)境到 Spring Cloud 的配置中提到過(guò),我們?cè)谑褂?Spring Cloud 進(jìn)行動(dòng)態(tài)化配置,它的實(shí)現(xiàn)步驟是先將動(dòng)態(tài)配置通過(guò) @Value 注入到一個(gè)動(dòng)態(tài)配置 Bean,并將這個(gè) Bean 用注解標(biāo)記為 @RefreshScope,在配置變更后,這些動(dòng)態(tài)配置 Bean 會(huì)被統(tǒng)一銷(xiāo)毀。
之后 Spring Cloud 的 ContextRefresher 會(huì)將變更后的配置作為一個(gè)新的 Spring Environment 加載進(jìn) ApplicationContext,由于 Scoped Bean 都是 Lazy Init 的,它們會(huì)在下一次使用時(shí)被使用新的 Environment 重新創(chuàng)建。
這套動(dòng)態(tài)配置加載流程在使我們服務(wù)更加靈活的同時(shí),也帶來(lái)了很大的風(fēng)險(xiǎn)。首先從業(yè)務(wù)上,修改配置不像上線這么”重量級(jí)”,不必要找 QA 進(jìn)行回歸測(cè)試,這就有可能引發(fā)一系列奇怪的 Bug,而且長(zhǎng)時(shí)間發(fā)現(xiàn)不了,另外,Spring Cloud 本身沒(méi)有 “fallback” 機(jī)制,一旦配置的數(shù)據(jù)類(lèi)型出了問(wèn)題,就會(huì)導(dǎo)致服務(wù)不可用。
為此,我給 Spring Cloud 提了個(gè) issue,但作者認(rèn)為變動(dòng)太大,不好改也不必改。
其實(shí)我也明白這個(gè)問(wèn)題的困境,每個(gè)人都得為自己要修改的配置負(fù)責(zé),即使框架支持了 fallback,但將錯(cuò)誤吞掉,配置修改后不生效也沒(méi)什么變化可能也并不符合用戶(hù)的期望。所以,盡量讓用戶(hù)要修改的配置正確成為了新的目標(biāo)。
基于這種需求,我添加了一個(gè)動(dòng)態(tài)配置的校驗(yàn)器,但實(shí)現(xiàn)里一部分代碼來(lái)自 github,所以本文在總結(jié)思路的同時(shí),也幫助我理解所有代碼。
整體思路
由于框架層沒(méi)法做太多事情,所以我的計(jì)劃是將這些配置取出來(lái),構(gòu)造出一個(gè)獨(dú)立的 Java 類(lèi),并在服務(wù)外新建一個(gè) ApplicationContext 試圖通過(guò)構(gòu)造出來(lái)的 Java 類(lèi)初始化一個(gè) Spring Bean,如果這個(gè) Spring Bean 初始化過(guò)程中報(bào)錯(cuò)了,說(shuō)明配置是有問(wèn)題的。
動(dòng)態(tài)編譯
通過(guò)配置構(gòu)造 Java 類(lèi)
首先要通過(guò) .properties 文件構(gòu)造出一個(gè) Java 類(lèi),但問(wèn)題是在配置里我們是不知道這些配置將要被怎么使用的,不知道它要被 Spring EL 如何處理,又將被轉(zhuǎn)成什么類(lèi)型。
這里我采用的策略是給配置添加注釋?zhuān)⑨尷锸褂靡欢ǖ母袷铰暶?EL 表達(dá)式和要生成的字段類(lèi)型,當(dāng)然這種實(shí)現(xiàn)有點(diǎn) low,有人提議把這些信息放到配置項(xiàng)的 key 里,之后會(huì)再進(jìn)行優(yōu)化。
把各個(gè)字段解析完成后放到準(zhǔn)備到的類(lèi)模板中,就生成了一個(gè) Config.java 類(lèi)字符串,之后就要將這個(gè)字符串編譯成字節(jié)碼并由 Spring 加載成 Bean。另外,Spring 系列面試題和答案全部整理好了,微信搜索Java技術(shù)棧,在后臺(tái)發(fā)送:面試,可以在線閱讀。
JavaCompiler
由于 Config.java 是在運(yùn)行時(shí)生成的,所以編譯也只能在運(yùn)行時(shí)了,萬(wàn)幸 Java 有提供 javax.util.JavaCompiler 類(lèi)進(jìn)行 Java 類(lèi)的動(dòng)態(tài)編譯,省去了”寫(xiě)入文件 —— 命令行編譯 —— 類(lèi)加載 —— 清理文件” 的復(fù)雜流程。
Java 核心技術(shù)教程和示例源碼可以看這里:https://github.com/javastacks/javastack
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 通過(guò) JavaFileManager 管理輸入和輸出文件,使用時(shí)通過(guò) getTask() 方法提交一個(gè)異步 CompilationTask 進(jìn)行代碼編譯,代碼編譯時(shí),JavaCompiler 通過(guò) getCharContent() 從傳入的 compilationUnits 獲取到 .java 文件內(nèi)容,把編譯后的結(jié)果調(diào)用 CompiledByteCode 的 openOutputStream() 方法寫(xiě)到 CompiledByteCode 對(duì)象里。

委托模式
由于 JavaCompiler 的默認(rèn)實(shí)現(xiàn)都是通過(guò)文件進(jìn)行的,這不符合我的期望,我需要的是輸入和輸出都在內(nèi)存進(jìn)行,所以需要修改 JavaCompiler 的實(shí)現(xiàn),JavaCompiler、JavaFileManager、JavaFileObject(Input/Output) 分別使用委托模式實(shí)現(xiàn)。其中 JavaFileManager 已經(jīng)有 ForwardingJavaFileManager 的實(shí)現(xiàn),JavaFileObject 也有 SimpleJavaFileObject 的實(shí)現(xiàn),我們繼承其實(shí)現(xiàn)后重寫(xiě)部分方法即可。

我參考的源碼:https://github.com/trung/InMemoryJavaCompiler
Spring Bean 實(shí)例化
要將 Config 類(lèi)實(shí)例化成 Bean,我們可以在 xml 里預(yù)定義它,在編譯結(jié)束后創(chuàng)建一個(gè)簡(jiǎn)易的 FileSystemXmlApplicationContext 實(shí)例化這個(gè) xml 內(nèi)的 Bean。
類(lèi)加載器
首先要讓 Spring 能夠加載到這些編譯好的字節(jié)碼,這就需要 ClassLoader 的配合。46 張 PPT 弄懂 JVM 調(diào)優(yōu)!這個(gè)分享給你。
類(lèi)加載器的默認(rèn)實(shí)現(xiàn)不可能知道去加載我們內(nèi)存里編譯好的字節(jié)碼,只好新加一個(gè) ClassLoader,實(shí)現(xiàn)也很簡(jiǎn)單,繼承 ClassLoader 抽象類(lèi),并實(shí)現(xiàn) findClass 方法即可。
class MemoryClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 在 CompiledByteCode 類(lèi)里將編譯后的字節(jié)碼放到 classLoader 的 classBytes 字段內(nèi)。
byte[] buf = classBytes.get(name);
if (buf == null) {
return super.findClass(name);
}
return defineClass(name, buf, 0, buf.length);
}
}
配置和實(shí)現(xiàn)
由于 Config Bean 的初始化依賴(lài)動(dòng)態(tài)配置,我們還要把這些配置也添加到 Spring 環(huán)境內(nèi),我們知道 Spring 環(huán)境配置是由多個(gè) PropertySource 構(gòu)成的,向里面添加一個(gè)實(shí)現(xiàn)即可。然后就可以調(diào)用 application 的 refresh() 方法初始化上下文了,另外 Config Bean 被設(shè)置為懶加載了,不要忘記 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");
小結(jié)
小項(xiàng)目完成的過(guò)程中,復(fù)習(xí)了很多知識(shí),也嘗試了業(yè)務(wù)代碼中幾乎不會(huì)用到的設(shè)計(jì)模式,充滿(mǎn)了挑戰(zhàn)性。
當(dāng)然它現(xiàn)在還有配置不夠方便、錯(cuò)誤提示不夠明確、沒(méi)解決配置 namespace 等問(wèn)題,留到后面慢慢優(yōu)化吧~另外,關(guān)注公眾號(hào)Java技術(shù)棧,在后臺(tái)回復(fù):面試,可以獲取我整理的 Java 系列面試題和答案,非常齊全。






關(guān)注Java技術(shù)??锤喔韶?/strong>


