close

老闆讓我把一個項目從 Java 8 遷移到 Java 11,我該怎麼辦呢?

最簡單的辦法,當然是直接強行升級,遇到一個錯就改一個錯,別看它 low,但是對於一個小型且非核心的項目來說,已經足夠了。

當然,對於比較重要的項目,且代碼行數不少的情況,最標準的姿勢就是對着官方文檔,就是這份 Java 11 的遷移說明文檔。

https://docs.oracle.com/en/java/javase/11/migrate/index.html

裡面詳細說明了 Java 8 到 Java 11 可能出現的兼容性問題,並給出了修改措施或建議。



理論上來說,對着官方文檔一個字一個字去讀,並且把代碼的每一行都肉眼掃描一遍,該修改的地方就做出修改,肯定是可以完美遷移的。

但人畢竟不是機器,自己寫的代碼可能就有上千甚至上萬行,還得算上引入的第三方類庫,這顯然就不是人幹的事情了。

所以,就有一款神奇的工具,可以幫我們自動掃描 JDK 升級過程中需要修改或注意的地方,並直接生成一個可視化的 HTML 報告,厲害了!

它是什麼

這款工具的名字叫做 EMT4J,即 Eclipse Migration Toolkit for Java,直譯過來就是 Eclipse 基金會旗下的,用來遷移的,工具集,為 Java 準備的。

官網地址為:

https://projects.eclipse.org/projects/adoptium.emt4j



官網介紹它的方式非常親民,直接來了個以 Tom 為主人公的小故事,大概說的是 Tom 準備把項目從 8 遷移到 11,非常痛苦,用上了 EMT4J 後就爽得飛起。

我非常喜歡這種方式,大家感興趣可以讀一讀,沒有什麼難的詞彙。

不過官網和推文都包含很多宣傳色彩,有很多雜亂信息,作為一枚呆萌的開發者,我還是更喜歡看簡單粗暴的代碼倉庫:

https://github.com/adoptium/emt4j



僅僅一張截圖,就包含了核心功能,下載地址,快速上手體驗這三個開發者最關注的點。

它怎麼用

點擊下載地址下載好 EMT4J 後,查看它的目錄結構,發現非常簡單明了直觀。

➜Downloadstreeemt4j-0.3emt4j-0.3├──bin│├──analysis.bat│└──analysis.sh└──lib├──agent│├──emt4j-agent-jdk11-0.3.jar│└──emt4j-agent-jdk8-0.3.jar├──analysis│├──asm-9.2.jar│├──commons-io-2.4.jar│├──commons-lang3-3.8.jar│├──emt4j-analysis-0.3.jar│├──emt4j-common-0.3.jar│├──gson-2.9.0.jar│├──lombok-1.18.8.jar│├──mvel2-2.4.12.Final.jar│├──slf4j-api-1.7.30.jar│└──velocity-engine-core-2.3.jar└──maven-plugin└──emt4j-maven-plugin-0.3.jar

如果只使用 javaagent 方式來分析項目,那麼對於使用者來說只需要關注 agent 目錄下的兩個 jar 即可。

從 8 遷移到其他 JDK 就使用:

emt4j-agent-jdk8-0.3.jar

從 11 遷移到其他 JDK 就使用:

emt4j-agent-jdk11-0.3.jar

我們寫一個簡單的 Hello World 程序,並用 JDK8 編譯。

publicclassHello{publicstaticvoidmain(String[]args){System.out.println("HelloWorld!");}}

然後對其進行 8 到 11 的遷移分析。

java-javaagent:emt4j-agent-jdk8-0.3.jar=to=11Hello

對輸出結果可視化為 HTML 格式。

shanalysis.sh-oreport.htmlemt4j-XXX.dat

打開這個 HTML 文件,發現輸出的結果正符合我們預期。

對嘛,一個 Hello World 程序自然不需要考慮遷移的兼容性問題~

拿一個複雜的項目舉例

我們再嘗試分析一個較為複雜的項目,依然進行剛剛那幾步動作,這回輸出的報告有點意思了。



我們就通過目錄中的摘要,就可以看到這個項目從 8 遷移到 11 所需要考慮的全部問題了。比如從 JDK 9 java.version 的 schema 發生了變化,點進去。

在如何修復那裡,給出了官方說明文檔,我們繼續點進去。

簡單說就是 java.version 這個系統變量所輸出的字符串格式發生了變化,你的程序要是依賴這個字符串做截取和判斷啥的,就需要小心了。

比如報告中的問題上下文中,就給出了所有可能受之影響的方法,我們舉個例子。



在大名鼎鼎的 log4j 中的 AbstractStringLayout 類中有個 isPreJava8 方法,用來判斷是否是 Java 8 以前的版本。

//log4j-core:2.10.0privatestaticbooleanisPreJava8(){Stringversion=System.getProperty("java.version");String[]parts=version.split("\\.");try{intmajor=Integer.parseInt(parts[1]);returnmajor<8;}catch(Exceptionvar3){returntrue;}}

如果使用 Java 11 來運行這個方法,會得到 true,也就是認為 11 是 8 之前的版本,這顯然是不對的,簡單 debug 一下就知道錯在哪了。

當然,我使用的版本是 log4j-core:2.10.0,我相信大名鼎鼎的 log4j 項目一定在之後的版本修復了這個問題。

果然,在某一次 commit 上就專門修復了這個問題。



修復的方式也很 low,就是判斷第一個點前面如果是 1,就按照新的方式做判斷,即把點後面的數字作為主版本。



嗯,那看來,把這個項目從 8 升級到 11,最穩妥的方式是連這個使用老版本的 log4j 三方依賴也同時升級了。

什麼原理

下面探索一下這個項目的原理,GitHub 官網中給出了一張架構圖。



左邊是 agent 方式分析運行中的項目,上面是分析靜態的 Jar 和 Class 文件等,他們僅僅是解析符號時有所不同。

中間部分是,當符號解析完畢後,都需要讀取規則 Rules,這些規則 Rules 就是判斷是否會出現兼容性問題的邏輯,如果匹配到了,則記錄下來。

右下方是,把剛剛記錄下來的兼容性問題,用更友好的方式比如 HTML 的形式輸出,給最終的用戶看。

所以整個架構還是十分清晰的,重點是裡面的細節是如何處理的,我這裡只說關鍵的環節。

agent 方式分析可能不太直觀,如果你想了解,可以從這個標準 javaagent 項目的 premain 方法開始看起。



我這裡正好了解一下另一種直接靜態掃描 class 文件或 jar 文件的檢查方式,這個就很直觀了。

首先我們故意寫一個可以觸發上面說的 java.version 格式兼容性問題的代碼。

classHello{publicstaticvoidmain(String[]var0){System.out.println("hello");System.out.println(System.getProperty("java.version"));}}

然後把它編譯成 class 文件,放入一個 classFiles 文件夾中,作為我們的掃描文件夾。

然後執行 sh 命令,表示檢查該文件夾中 8 遷移到 11 的風險項。

shanalysis.sh-f8-t11-oreport.htmlclassFiles

這個 sh 腳本其實就是執行 AnalysisMain 的主方法,並且將上面幾個參數作為 args 傳入進去。

//org/eclipse/emt4j/analysis/AnalysisMain.javapublicclassAnalysisMain{publicstaticvoidmain(String[]args)throwsIOException,ClassNotFoundException,InterruptedException,URISyntaxException{...ReportConfigconfig=doAnalysis(args,...);doReport(config,...);}...}

所以從這個入口開始看起就好了。

主方法的結構也很簡單,doAnalysis 就是對這些 class 文件進行分析,得出結果。doReport 就是將分析結果可視化,比如生成 HTML 文件。

具體調用鏈我不一一展開,只看很關鍵的環節。首先 ClassAnalyzer 中的 processClass 方法使用了 ASM 將 class 文件解析為各種符號,保存在 ClassSymbol 對象里。

//org/eclipse/emt4j/analysis/analyzer/ClassAnalyzer.javaprotectedstaticvoidprocessClass(byte[]classFileContent...){ClassSymbolsymbol=ClassInspectorInstance.getInstance().getSymbolInClass(classFileContent);...}

然後,由各種規則文件利用這些符號信息做判斷,比如 WholeClassRule,表示需要整個 class 文件信息才能做判斷的規則。

@RuleImpl(type="whole-class")publicclassWholeClassRuleextendsExecutableRule{...@OverrideprotectedCheckResultcheck(Dependencydependency){Map<String,Object>mvelMap=newHashMap<>();mvelMap.put("typeSet",dependency.getClassSymbol().getTypeSet());mvelMap.put("methodSet",toMethodIdentifierSet(dependency.getClassSymbol().getCallMethodSet()));mvelMap.put("cpSet",dependency.getClassSymbol().getConstantPoolSet());Objectresult=MVEL.eval(mvel2Rule,mvelMap);if(resultinstanceofBoolean){return((Boolean)result)?CheckResult.FAIL:CheckResult.PASS;}else{thrownewJdkMigrationException("Mvel2rulefile"+mvel2RuleFile+"mustreturnabooleanresult!Nowresulttypeis:"+result.getClass());}}}

這裡將剛剛的符號信息取出來,放入 mvelMap 中,分別是 typeSet 類型集合、methodSet 方法集合、cpSet 常量池集合。

接下來使用 MVEL 這個第三方類庫進行判斷,這是一個可以使用表達式進行匹配判斷的工具類,很方便,規則表達式就寫在這個 mvel2Rule 里。

這個 mvel2Rule 有很多,其中檢查 java.version 這個兼容性問題的表達式寫在下面這個文件里。

//emt4j-common/src/main/resources/default/rule/8to11/data/mvel2-rule-getjavaversion.cfgmethodSet.contains('java.lang.System.getProperty')&&(cpSet.contains('java.version')||cpSet.contains('java.specification.version')||cpSet.contains('java.runtime.version'))

這個規則表達式很好理解,你不用了解它的寫法也能看懂,就是當使用了

System.getProperty

方法,並且字符串常量池中有

java.version 或 java.specification.version 或 java.runtime.version

時,就視作有兼容性問題。

可以看出,這個判斷非常粗糙,就是簡單的字符串包含判斷而已,由此可見,兼容性問題的判斷,也逃脫不了這種方式,不要以為裡面利用了什麼智能分析方法。

當這個兼容性問題被記錄下來後,最終輸出 HTML 文件的時候,會通過 ResourceBundle 查看官方文檔說明,將詳細信息寫入 HTML 文件中。



最終就看到了效果,就是這麼簡單。

再說兩句

通過體驗和了解這個項目,我們可以學到很多東西,麻雀雖小,五臟俱全。

首先它通過 agent 和 sh 兩種方式提供給用戶使用,但中間的分析判斷邏輯都是共用的。

然後,這個項目使用了專門解析 class 文件的 ASM 工具,把符號信息提取出來方便後面使用。



又使用了 MVEL 作為規則判斷的工具,使得規則判斷只需要寫好表達式即可。

最後輸出為 HTML 時,為了查詢對應兼容性問題對應的官方說明,使用了 ResourceBundle 查找官方文檔說明,由此我們可以繼續深入,了解下官方文檔的查詢規範,而且是從代碼層面的,十分嚴謹。



好的工具就是利用了這麼多知名的工具,使自己便利。那我們也可以利用這個項目,做些自己的事情。

比如你做的某款工具,甚至你定製的某款 JDK,需要檢查業務代碼中是否有兼容性問題,或者你就單純想掃描下業務代碼中是否有什麼什麼你關心的東西,那麼你可以僅僅修改這個項目中的各種規則表達式,並且定製化自己的 HTML 報告格式即可出色完成這個功能。

其它的,如何掃描 class,如何解析 class 符號,如何調用規則進行判斷,如何輸出渲染 HTML,都不用再自己做了。

- EOF -

推薦閱讀點擊標題可跳轉

放棄FastDFS,Spring Boot 整合 MinIO 實現分布式文件服務,真香!萬字長文,SpringSecurity實現權限系統設計

面試官:單核 CPU 支持 Java 多線程嗎?什麼?

看完本文有收穫?請轉發分享給更多人

關注「ImportNew」,提升Java技能

點讚和在看就是最大的支持❤️

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()