老闆讓我把一個項目從 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 後,查看它的目錄結構,發現非常簡單明了直觀。
如果只使用 javaagent 方式來分析項目,那麼對於使用者來說只需要關注 agent 目錄下的兩個 jar 即可。
從 8 遷移到其他 JDK 就使用:
emt4j-agent-jdk8-0.3.jar
從 11 遷移到其他 JDK 就使用:
emt4j-agent-jdk11-0.3.jar
我們寫一個簡單的 Hello World 程序,並用 JDK8 編譯。
然後對其進行 8 到 11 的遷移分析。
對輸出結果可視化為 HTML 格式。
打開這個 HTML 文件,發現輸出的結果正符合我們預期。
對嘛,一個 Hello World 程序自然不需要考慮遷移的兼容性問題~
拿一個複雜的項目舉例
我們再嘗試分析一個較為複雜的項目,依然進行剛剛那幾步動作,這回輸出的報告有點意思了。

我們就通過目錄中的摘要,就可以看到這個項目從 8 遷移到 11 所需要考慮的全部問題了。比如從 JDK 9 java.version 的 schema 發生了變化,點進去。
在如何修復那裡,給出了官方說明文檔,我們繼續點進去。
簡單說就是 java.version 這個系統變量所輸出的字符串格式發生了變化,你的程序要是依賴這個字符串做截取和判斷啥的,就需要小心了。
比如報告中的問題上下文中,就給出了所有可能受之影響的方法,我們舉個例子。
在大名鼎鼎的 log4j 中的 AbstractStringLayout 類中有個 isPreJava8 方法,用來判斷是否是 Java 8 以前的版本。
如果使用 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 格式兼容性問題的代碼。
然後把它編譯成 class 文件,放入一個 classFiles 文件夾中,作為我們的掃描文件夾。
然後執行 sh 命令,表示檢查該文件夾中 8 遷移到 11 的風險項。
這個 sh 腳本其實就是執行 AnalysisMain 的主方法,並且將上面幾個參數作為 args 傳入進去。
所以從這個入口開始看起就好了。
主方法的結構也很簡單,doAnalysis 就是對這些 class 文件進行分析,得出結果。doReport 就是將分析結果可視化,比如生成 HTML 文件。
具體調用鏈我不一一展開,只看很關鍵的環節。首先 ClassAnalyzer 中的 processClass 方法使用了 ASM 將 class 文件解析為各種符號,保存在 ClassSymbol 對象里。
然後,由各種規則文件利用這些符號信息做判斷,比如 WholeClassRule,表示需要整個 class 文件信息才能做判斷的規則。
這裡將剛剛的符號信息取出來,放入 mvelMap 中,分別是 typeSet 類型集合、methodSet 方法集合、cpSet 常量池集合。
接下來使用 MVEL 這個第三方類庫進行判斷,這是一個可以使用表達式進行匹配判斷的工具類,很方便,規則表達式就寫在這個 mvel2Rule 里。
這個 mvel2Rule 有很多,其中檢查 java.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技能
點讚和在看就是最大的支持❤️