<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          如何玩 Java 的動態(tài)編譯

          共 4762字,需瀏覽 10分鐘

           ·

          2021-07-14 18:50

          不點(diǎn)藍(lán)字,我們哪來故事?

          每天 11 點(diǎn)更新文章,餓了點(diǎn)外賣,點(diǎn)擊 ??《無門檻外賣優(yōu)惠券,每天免費(fèi)領(lǐng)!》

          來源 | https://zhenbianshu.github.io

          問題

          之前的文章從Spring 的環(huán)境到 Spring Cloud 的配置中提到過,我們在使用 Spring Cloud 進(jìn)行動態(tài)化配置,它的實(shí)現(xiàn)步驟是先將動態(tài)配置通過 @Value 注入到一個動態(tài)配置 Bean,并將這個 Bean 用注解標(biāo)記為 @RefreshScope,在配置變更后,這些動態(tài)配置 Bean 會被統(tǒng)一銷毀。

          之后 Spring Cloud 的 ContextRefresher 會將變更后的配置作為一個新的 Spring Environment 加載進(jìn) ApplicationContext,由于 Scoped Bean 都是 Lazy Init 的,它們會在下一次使用時被使用新的 Environment 重新創(chuàng)建。

          這套動態(tài)配置加載流程在使我們服務(wù)更加靈活的同時,也帶來了很大的風(fēng)險。首先從業(yè)務(wù)上,修改配置不像上線這么”重量級”,不必要找 QA 進(jìn)行回歸測試,這就有可能引發(fā)一系列奇怪的 Bug,而且長時間發(fā)現(xiàn)不了,另外,Spring Cloud 本身沒有 “fallback” 機(jī)制,一旦配置的數(shù)據(jù)類型出了問題,就會導(dǎo)致服務(wù)不可用。

          為此,我給 Spring Cloud 提了個 issue,但作者認(rèn)為變動太大,不好改也不必改。

          其實(shí)我也明白這個問題的困境,每個人都得為自己要修改的配置負(fù)責(zé),即使框架支持了 fallback,但將錯誤吞掉,配置修改后不生效也沒什么變化可能也并不符合用戶的期望。所以,盡量讓用戶要修改的配置正確成為了新的目標(biāo)。

          基于這種需求,我添加了一個動態(tài)配置的校驗(yàn)器,但實(shí)現(xiàn)里一部分代碼來自 github,所以本文在總結(jié)思路的同時,也幫助我理解所有代碼。

          另外,如果您正在學(xué)習(xí)Spring Cloud,推薦一個連載多年還在繼續(xù)更新的免費(fèi)教程:https://blog.didispace.com/spring-cloud-learning/

          整體思路

          由于框架層沒法做太多事情,所以我的計(jì)劃是將這些配置取出來,構(gòu)造出一個獨(dú)立的 Java 類,并在服務(wù)外新建一個 ApplicationContext 試圖通過構(gòu)造出來的 Java 類初始化一個 Spring Bean,如果這個 Spring Bean 初始化過程中報錯了,說明配置是有問題的。

          動態(tài)編譯

          通過配置構(gòu)造 Java 類

          首先要通過 .properties 文件構(gòu)造出一個 Java 類,但問題是在配置里我們是不知道這些配置將要被怎么使用的,不知道它要被 Spring EL 如何處理,又將被轉(zhuǎn)成什么類型。

          這里我采用的策略是給配置添加注釋,注釋里使用一定的格式聲明 EL 表達(dá)式和要生成的字段類型,當(dāng)然這種實(shí)現(xiàn)有點(diǎn) low,有人提議把這些信息放到配置項(xiàng)的 key 里,之后會再進(jìn)行優(yōu)化。

          把各個字段解析完成后放到準(zhǔn)備到的類模板中,就生成了一個 Config.java 類字符串,之后就要將這個字符串編譯成字節(jié)碼并由 Spring 加載成 Bean。

          JavaCompiler

          由于 Config.java 是在運(yùn)行時生成的,所以編譯也只能在運(yùn)行時了,萬幸 Java 有提供 javax.util.JavaCompiler 類進(jìn)行 Java 類的動態(tài)編譯,省去了”寫入文件 —— 命令行編譯 —— 類加載 —— 清理文件” 的復(fù)雜流程。

          JavaCompiler 的典型應(yīng)用示例如下:

          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 進(jìn)行代碼編譯,代碼編譯時,JavaCompiler 通過 getCharContent() 從傳入的 compilationUnits 獲取到 .java 文件內(nèi)容,把編譯后的結(jié)果調(diào)用 CompiledByteCode 的 openOutputStream() 方法寫到 CompiledByteCode 對象里。

          委托模式

          由于 JavaCompiler 的默認(rèn)實(shí)現(xiàn)都是通過文件進(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)后重寫部分方法即可。


          我參考的源碼:https://github.com/trung/InMemoryJavaCompiler

          如果您正在學(xué)習(xí)Spring Boot,推薦一個連載多年還在繼續(xù)更新的免費(fèi)教程:http://blog.didispace.com/spring-boot-learning-2x/

          Spring Bean 實(shí)例化

          要將 Config 類實(shí)例化成 Bean,我們可以在 xml 里預(yù)定義它,在編譯結(jié)束后創(chuàng)建一個簡易的 FileSystemXmlApplicationContext 實(shí)例化這個 xml 內(nèi)的 Bean。

          類加載器

          首先要讓 Spring 能夠加載到這些編譯好的字節(jié)碼,這就需要 ClassLoader 的配合。

          類加載器的默認(rèn)實(shí)現(xiàn)不可能知道去加載我們內(nèi)存里編譯好的字節(jié)碼,只好新加一個 ClassLoader,實(shí)現(xiàn)也很簡單,繼承 ClassLoader 抽象類,并實(shí)現(xiàn) findClass 方法即可。

          class MemoryClassLoader extends ClassLoader {
           @Override
           protected Class<?> findClass(String name) throws ClassNotFoundException {
               // 在 CompiledByteCode 類里將編譯后的字節(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 的初始化依賴動態(tài)配置,我們還要把這些配置也添加到 Spring 環(huán)境內(nèi),我們知道 Spring 環(huán)境配置是由多個 PropertySource 構(gòu)成的,向里面添加一個實(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)目完成的過程中,復(fù)習(xí)了很多知識,也嘗試了業(yè)務(wù)代碼中幾乎不會用到的設(shè)計(jì)模式,充滿了挑戰(zhàn)性。

          當(dāng)然它現(xiàn)在還有配置不夠方便、錯誤提示不夠明確、沒解決配置 namespace 等問題,留到后面慢慢優(yōu)化吧~

          往期推薦

          最全的代碼生成器平臺

          Redis需要關(guān)注哪些監(jiān)控指標(biāo)?

          程序員路邊崩潰,美團(tuán)騎手:我來看看!

          還在用 Random生成隨機(jī)數(shù)了?試試 ThreadLocalRandom,好用!

          下方二維碼關(guān)注我

          技術(shù)草根堅(jiān)持分享 編程,算法,架構(gòu)

          看完文章,餓了點(diǎn)外賣,點(diǎn)擊 ??《無門檻外賣優(yōu)惠券,每天免費(fèi)領(lǐng)!》

          朋友,助攻一把!點(diǎn)個在看!
          瀏覽 54
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产三级片电影成人久久久 | 欧美一级特黄A片免费看视频小说 | 欧美大香蕉专区网 | av又黄又爆力 | 靠逼视频网站在线观看 |