<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)編譯,秀起來!

          共 5326字,需瀏覽 11分鐘

           ·

          2021-07-10 04:38

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

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

          來源: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 的,它們會在下一次使用時(shí)被使用新的 Environment 重新創(chuàng)建。

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

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

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

          整體思路

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

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

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

                  JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
                  JavaFileManager fileManager = javaCompiler.getStandardFileManager(nullnullnull);
                  CompilationTask task = javaCompiler.getTask(out, fileManager, diagnosticListener, options, classes, compilationUnits);
                  task.call();
                  FileObject outputFile = fileManager.getFileForOutput(nullnullnullnull);
                  outputFile.getCharContent(true);

          流程如下圖:JavaCompiler 通過 JavaFileManager 管理輸入和輸出文件,使用時(shí)通過 getTask() 方法提交一個異步 CompilationTask 進(jìn)行代碼編譯,代碼編譯時(shí),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)后重寫部分方法即可。

          我參考的源碼:GitHub-trung/InMemoryJavaCompiler

          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)化吧~

          - END -

          往期推薦

          比特幣又爆了。。。

          Java 線程池配置的常見誤區(qū)

          快手宣布取消“大小周”,互聯(lián)網(wǎng)公司“996風(fēng)氣”封印松動?

          來瞅一瞅Spring Boot的多種漏洞!

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

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

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

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

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  精品久久电影 | 亚洲无码福利视频 | 国产探花在线视频 | 亚洲无码不卡手机免费观看 | 国产精品 男同 |