1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        玩轉(zhuǎn) Java 動態(tài)編譯,太秀了~!

        共 2908字,需瀏覽 6分鐘

         ·

        2021-07-04 10:49

        點擊關(guān)注公眾號,Java干貨及時送達(dá)

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

        問題

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

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

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

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

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

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

        整體思路

        由于框架層沒法做太多事情,所以我的計劃是將這些配置取出來,構(gòu)造出一個獨立的 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)然這種實現(xiàn)有點 low,有人提議把這些信息放到配置項的 key 里,之后會再進行優(yōu)化。

        把各個字段解析完成后放到準(zhǔn)備到的類模板中,就生成了一個 Config.java 類字符串,之后就要將這個字符串編譯成字節(jié)碼并由 Spring 加載成 Bean。另外,Spring 系列面試題和答案全部整理好了,微信搜索Java技術(shù)棧,在后臺發(fā)送:面試,可以在線閱讀。

        JavaCompiler

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

        Java 核心技術(shù)教程和示例源碼可以看這里:https://github.com/javastacks/javastack

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

        委托模式

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

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

        Spring Bean 實例化

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

        類加載器

        首先要讓 Spring 能夠加載到這些編譯好的字節(jié)碼,這就需要 ClassLoader 的配合。46 張 PPT 弄懂 JVM 調(diào)優(yōu)!這個分享給你。

        類加載器的默認(rèn)實現(xiàn)不可能知道去加載我們內(nèi)存里編譯好的字節(jié)碼,只好新加一個 ClassLoader,實現(xiàn)也很簡單,繼承 ClassLoader 抽象類,并實現(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);
         }
        }

        配置和實現(xiàn)

        由于 Config Bean 的初始化依賴動態(tài)配置,我們還要把這些配置也添加到 Spring 環(huán)境內(nèi),我們知道 Spring 環(huán)境配置是由多個 PropertySource 構(gòu)成的,向里面添加一個實現(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é)

        小項目完成的過程中,復(fù)習(xí)了很多知識,也嘗試了業(yè)務(wù)代碼中幾乎不會用到的設(shè)計模式,充滿了挑戰(zhàn)性。

        當(dāng)然它現(xiàn)在還有配置不夠方便、錯誤提示不夠明確、沒解決配置 namespace 等問題,留到后面慢慢優(yōu)化吧~另外,關(guān)注公眾號Java技術(shù)棧,在后臺回復(fù):面試,可以獲取我整理的 Java 系列面試題和答案,非常齊全。






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



        獲取 Spring Boot 實戰(zhàn)筆記!
        瀏覽 55
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

          <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            精品一区二区三区观看 | 国产自产视频 | 国产区在线观看视频 | 中国免费一级毛片 | 在线黄色小说 | 人妻免费视频 | 日韩中文字幕网站 | 高潮毛片无遮挡高清视频 | 走光无码一区二区三区 | 熟女视频91 |