識(shí)別Java代碼中的設(shè)計(jì)模式與類指標(biāo))
簡介IBM Pattern Modeling and Analysis Tool for Java是IBM推出的Java GC專項(xiàng)分析工具面向Java開發(fā)者與性能調(diào)優(yōu)工程師核心價(jià)值在于解析JVM生成的GC日志將抽象回收行為轉(zhuǎn)化為可視化報(bào)告幫助定位GC停頓過長、內(nèi)存分配異常及潛在泄漏風(fēng)險(xiǎn)。壓縮包共17個(gè)文件、約7.9MB以13語種HTML說明文檔為主體覆蓋中、英、日、韓及歐洲多語種另附可執(zhí)行JAR包、配套說明與許可協(xié)議便于直接運(yùn)行與查閱。已有308人學(xué)習(xí)瀏覽。通過該工具可系統(tǒng)掌握串行、并行、CMS與G1等主流GC策略的日志解讀方法理解各策略暫停頻率、回收效率及適用場景并據(jù)此調(diào)整堆大小、回收閾值等JVM參數(shù)以降低停頓、提升吞吐量還可依據(jù)堆內(nèi)存分配趨勢排查泄漏隱患為生產(chǎn)環(huán)境策略選型與容量規(guī)劃提供數(shù)據(jù)支撐。1. PMAT 是什么讓設(shè)計(jì)模式從 Java 代碼里自動(dòng)現(xiàn)形PMATPattern Modeling and Analysis Tool for Java是一套知名的 Eclipse 插件干的事非常直接把一份 Java 工程源碼或編譯后的 class丟進(jìn)去它自動(dòng)找回代碼里真實(shí)存在的設(shè)計(jì)模式實(shí)例同時(shí)算出一組面向?qū)ο笾笜?biāo)比如內(nèi)聚度、耦合度、繼承深度。和很多 IDE 自帶的結(jié)構(gòu)檢查不同它不靠類名和注釋猜測而是從類關(guān)系、引用圖、方法簽名里提取結(jié)構(gòu)證據(jù)做匹配。最適合誰用常年維護(hù)別人代碼的工程師、做技術(shù)債盤點(diǎn)的人、想給老系統(tǒng)補(bǔ)架構(gòu)圖的人。這個(gè)工具的定位從來不是實(shí)時(shí)提示代碼風(fēng)格而是批量掃描、結(jié)構(gòu)建模、輸出候選結(jié)論。下面從安裝開始一步步講中間拆匹配邏輯和指標(biāo)解讀最后把誤報(bào)、漏報(bào)、跑不動(dòng)的現(xiàn)場列一遍。你拿到手的將是一套可復(fù)現(xiàn)的分析流程而不是一篇泛泛的產(chǎn)品介紹。2. 把 PMAT 裝進(jìn) Eclipse本地安裝、導(dǎo)入工程與跑通第一次分析2.1 本地插件安裝與菜單驗(yàn)證PMAT 最可靠的形態(tài)是 Eclipse 插件拿到插件包后常見做法是放進(jìn) Eclipse 根目錄下的 dropins 文件夾。之所以不直接丟進(jìn) plugins是為了讓 Eclipse 的擴(kuò)展點(diǎn)緩存能通過 dropins 機(jī)制統(tǒng)一重建以后想移除工具時(shí)也方便整包刪除不會(huì)留下碎文件。操作步驟關(guān)閉 Eclipse進(jìn)入安裝目錄下的 dropins 文件夾。把插件壓縮包原樣放進(jìn)去不要提前解壓成散文件。用干凈緩存模式啟動(dòng)一次 Eclipse在命令行執(zhí)行eclipse -clean。啟動(dòng)后右鍵點(diǎn)擊任意 Java 工程確認(rèn)菜單里出現(xiàn) PMAT 相關(guān)入口同時(shí)打開 Window → Preferences檢查是否有 PMAT 配置頁。# 以 -clean 啟動(dòng)強(qiáng)制重建插件擴(kuò)展點(diǎn)緩存避免菜單不出現(xiàn) eclipse -clean這個(gè)步驟里最容易被忽略的是-clean。很多裝完菜單消失的情況不是插件放錯(cuò)位置而是 Eclipse 沒重新掃描 dropins。如果插件是在全新建成的 Eclipse 目錄里首次啟動(dòng)問題不大但如果你是在已經(jīng)跑過多次的工作臺(tái)上加插件我的習(xí)慣是每次都把-clean當(dāng)作必做動(dòng)作而不是出錯(cuò)后的補(bǔ)救手段。注意獨(dú)立準(zhǔn)備一個(gè)專門跑分析的 Eclipse 環(huán)境別和日常開發(fā)環(huán)境混用。PMAT 偶爾要處理幾十萬個(gè)類堆內(nèi)存設(shè)置和工作空間狀態(tài)都會(huì)互相干擾獨(dú)立環(huán)境用起來更省心。2.2 導(dǎo)入工程用編譯產(chǎn)物還是源碼PMAT 分析的對(duì)象可以是源碼目錄也可以是編譯后的 class 目錄。我的習(xí)慣是優(yōu)先用編譯產(chǎn)物因?yàn)?class 文件里已經(jīng)去掉了注釋和格式干擾而且能覆蓋到那些只有二進(jìn)制、沒有源碼的歷史包。但無論選哪種前提都是 class 文件必須完整存在。導(dǎo)入步驟在 Eclipse 中用 Import → Existing Projects into Workspace 導(dǎo)入工程。對(duì)工程做一次 Project → Clean把過期的 class 清理掉再重新編譯。打開工程屬性確認(rèn)輸出目錄bin 或 target/classes里已有 .class 文件。Maven 多模塊工程要特別注意PMAT 讀的是 Eclipse 工程里能訪問到的 class而 Maven 的 target/classes 是按模塊生成的。如果從工作空間根目錄導(dǎo)入整個(gè)聚合工程分析會(huì)跨模塊執(zhí)行模塊間依賴以 Eclipse 的 classpath 為準(zhǔn)。為了讓結(jié)果干凈我會(huì)把分析范圍限制在單模塊右鍵時(shí)選模塊目錄而不是工程根目錄。如果一個(gè)工程源碼齊全分析后輸出卻是空的先去查輸出目錄。只導(dǎo)入了源碼而沒觸發(fā)編譯的工程PMAT 根本讀不到 class結(jié)果必然是空的。這是第一次用這個(gè)工具時(shí)最常見的翻車點(diǎn)不是工具壞了是輸入不完整。2.3 第一次完整分析分析范圍與三個(gè)結(jié)果視圖在工程或包上右鍵選擇 PMAT 的解析入口等待進(jìn)度條走完。分析范圍可以在右鍵入口處決定按類、按包、按工程。按類最快適合驗(yàn)證單個(gè)模式假設(shè)按工程最全但耗時(shí)和內(nèi)存開銷會(huì)明顯上升。我一般先按包跑一遍拿到全局印象再對(duì)重點(diǎn)包按類細(xì)看。分析結(jié)束后會(huì)打開多個(gè)結(jié)果視圖最常用的三個(gè)表PMAT 結(jié)果視圖速查視圖內(nèi)容用途模式實(shí)例樹按模式類型列出命中的類與角色判斷哪個(gè)類在扮演 Singleton、Observer 等類關(guān)系圖繼承、聚合、依賴的可視化展示補(bǔ)架構(gòu)圖、排查循環(huán)依賴指標(biāo)表LCOM、DIT、NOC、CBO 等逐類數(shù)值排序篩選重構(gòu)候選第一次跑完建議先展開模式實(shí)例樹對(duì)照你自己的業(yè)務(wù)常識(shí)看。比如一個(gè)配置類被標(biāo)成 Singleton它是否真的只有全局唯一實(shí)例入口如果一批類全部命中 Observer但業(yè)務(wù)上沒有任何事件通知機(jī)制那多半是誤報(bào)。誤報(bào)怎么逐一排查在第 5 章專門講。還有一條實(shí)戰(zhàn)經(jīng)驗(yàn)每次改完源碼必須重新編譯再跑分析。PMAT 讀取的是 class 而不是源碼不重編譯等于分析舊版本。我在這上面吃過虧改完代碼直接重跑指標(biāo)紋絲不動(dòng)折騰半天才發(fā)現(xiàn) class 根本沒過期。3. PMAT 的模式識(shí)別邏輯從結(jié)構(gòu)證據(jù)到 Singleton、Observer 實(shí)例3.1 先建中間表示再按約束做圖匹配PMAT 處理 Java 代碼的方式可以拆成兩步。第一步把類、接口、枚舉、字段、方法簽名、方法內(nèi)調(diào)用、實(shí)例化點(diǎn)全部解析成一張中間表示的關(guān)系圖相當(dāng)于從 class 的字段表、方法表、常量池里恢復(fù)出完整的類結(jié)構(gòu)。第二步根據(jù)每種設(shè)計(jì)模式定義好的結(jié)構(gòu)約束在關(guān)系圖上做匹配。約束通常包括類的可見性、字段類型與靜態(tài)性、方法返回類型、調(diào)用關(guān)系、繼承與實(shí)現(xiàn)關(guān)系。匹配命中的類組合輸出為候選模式實(shí)例。理解這個(gè)流程就能理解這個(gè)工具的兩條內(nèi)在邊界其一它不依賴命名類名叫不叫 Manager、Service 都不影響判斷其二它判斷的是結(jié)構(gòu)像不像不是業(yè)務(wù)意圖是不是。所以輸出叫候選實(shí)例需要人復(fù)核。這個(gè)定位決定了它適合做架構(gòu)盤點(diǎn)、技術(shù)債評(píng)估不適合做代碼審查這類對(duì)語義要求更高的場景。3.2 Singleton 的命中條件一個(gè)例子看清匹配規(guī)則拿最典型的單例來舉例。PMAT 對(duì) Singleton 的約束一般包括類非抽象、構(gòu)造函數(shù)訪問級(jí)別不是 public、存在類型為本類的靜態(tài)字段、存在公開靜態(tài)方法且返回類型為本類。代碼樣本如下public class ConfigStore { private static final ConfigStore INSTANCE new ConfigStore(); private ConfigStore() {} public static ConfigStore getInstance() { return INSTANCE; } }兩點(diǎn)提醒。第一靜態(tài)方法不要求叫 getInstance叫 create()、instance() 都能命中匹配的是靜態(tài)方法返回本類類型這個(gè)結(jié)構(gòu)特征。第二構(gòu)造方法寫成 protected 同樣算滿足條件因?yàn)槠ヅ湟?guī)則只關(guān)心訪問級(jí)別不是 public。這看起來不嚴(yán)謹(jǐn)?shù)珜?duì)工具是合理的它先篩掉完全開放的類剩下的交給人工判斷工作量已經(jīng)小很多。另一個(gè)容易翻車的點(diǎn)是枚舉單例。枚舉類里只寫一個(gè)枚舉常量的寫法語義上天然是單例但部分版本的匹配規(guī)則對(duì)枚舉的構(gòu)造處理不完整枚舉單例常常檢測不到。如果分析結(jié)果里 Singleton 一個(gè)都沒有而代碼里明明有一堆枚舉常量先確認(rèn)工具版本行為再考慮人工標(biāo)注。3.3 可識(shí)別的模式范圍與誤報(bào)控制思路常見的部署場景里PMAT 能覆蓋三類 GoF 設(shè)計(jì)模式列表如下表可識(shí)別的模式類別與典型成員類別常見模式關(guān)注點(diǎn)創(chuàng)建型Singleton、Factory Method、Abstract Factory、Builder、Prototype對(duì)象創(chuàng)建方式結(jié)構(gòu)型Adapter、Composite、Proxy、Decorator、Facade、Bridge類與對(duì)象的組織方式行為型Observer、Strategy、Template Method、Command、State、Iterator、Mediator、Visitor職責(zé)分配與算法封裝誤報(bào)控制的核心思路是看證據(jù)鏈。PMAT 輸出的候選實(shí)例通常帶命中證據(jù)比如命中了哪些字段、哪些方法、哪些調(diào)用點(diǎn)。排查誤報(bào)時(shí)先展開證據(jù)看是否完整。拿 Observer 來說如果命中的類確實(shí)有訂閱注冊方法和回調(diào)方法只是方法名和書上不一樣那基本可信如果只因?yàn)閷?shí)現(xiàn)了同一個(gè)接口就被標(biāo)記證據(jù)不足應(yīng)降級(jí)處理。我看結(jié)果的第一步永遠(yuǎn)是點(diǎn)開證據(jù)而不是只看模式名稱。4. 指標(biāo)解讀PMAT 輸出的類級(jí)體檢表怎么用4.1 常用指標(biāo)速查與計(jì)算口徑PMAT 的指標(biāo)全部落在類級(jí)別一張表說清楚表PMAT 類級(jí)指標(biāo)速查指標(biāo)含義值高說明什么LCOM方法內(nèi)聚缺失度方法間不共享字段的配對(duì)比例越高越不內(nèi)聚類可能承擔(dān)了多種職責(zé)DIT從根類到當(dāng)前類的繼承鏈長度越高繼承體系越深理解成本越高NOC直接子類數(shù)量越多影響面越大基類改動(dòng)牽連廣CBO與其它類的耦合計(jì)數(shù)越高依賴越重越難獨(dú)立測試和復(fù)用WMC類內(nèi)方法復(fù)雜度或方法數(shù)加權(quán)越高實(shí)現(xiàn)越重出問題概率越大RFC類可響應(yīng)的方法總數(shù)越高交互范圍越大調(diào)試定位越難NPM公開方法數(shù)越高接口面越大使用成本越高這些指標(biāo)不是 PMAT 首創(chuàng)但它把指標(biāo)和模式檢測放在同一個(gè)分析結(jié)果里省去了來回切換工具的麻煩。這也是它的價(jià)值之一模式和指標(biāo)共用同一套中間表示你可以直接從哪些類命中了模式跳到這些類的指標(biāo)是否健康。4.2 指標(biāo)組合的判讀單看一個(gè)數(shù)沒有意義單獨(dú)看一個(gè)指標(biāo)容易誤判。LCOM 高聽著像壞味道但純工具類里全是靜態(tài)方法、不共享字段LCOM 天然高這類類不需要重構(gòu)。反過來LCOM 高、CBO 也高、而且類被頻繁實(shí)例化這才是重構(gòu)優(yōu)先級(jí)高的組合??吹礁?LCOM 先問一句它的高是結(jié)構(gòu)導(dǎo)致的還是職責(zé)混亂導(dǎo)致的另一個(gè)常用組合是 DIT 高加 NOC 大。它表示基類在繼承體系里位置很高、子類很多改基類的影響面會(huì)非常大。這種類在動(dòng)手重構(gòu)之前必須先把它的調(diào)用鏈完整畫出來否則就是在拿線上穩(wěn)定性開玩笑。再比如 RFC 和 CBO 同時(shí)偏高說明類對(duì)外交互范圍大、依賴多這種類往往適合做為引入 Facade 的切分點(diǎn)。數(shù)值閾值不要照搬網(wǎng)上說的固定線不同業(yè)務(wù)復(fù)雜度下指標(biāo)分布差很多。更實(shí)用的做法是把整個(gè)工程的指標(biāo)拉出來看分布排前 20% 的類是重點(diǎn)其余的先不動(dòng)。分布思維比絕對(duì)閾值可靠得多。4.3 用指標(biāo)篩選重構(gòu)對(duì)象一個(gè)可復(fù)用的工作流把指標(biāo)和模式綁在一起篩我一般按下面順序操作導(dǎo)出指標(biāo)表導(dǎo)出方法在第 6 章按 CBO 降序排列。取前 20% 的類逐個(gè)檢查這些類命中了哪些模式角色。命中創(chuàng)建型模式的類通常保留不動(dòng)命中行為型且 CBO 高的類重點(diǎn)檢查分支邏輯是否能用策略或狀態(tài)模式拆分。對(duì) LCOM 高且職責(zé)明顯的類把字段分組組間互不訪問就考慮拆類。這個(gè)流程的實(shí)際產(chǎn)出不是立刻動(dòng)手改代碼而是一張按風(fēng)險(xiǎn)排序的重構(gòu)候選清單。有了清單你可以跟團(tuán)隊(duì)說這個(gè)類有 8 個(gè)外部依賴、內(nèi)聚缺失度 0.87風(fēng)險(xiǎn)排第三而不是我覺得這里該重構(gòu)。給重構(gòu)找到量化依據(jù)這是 PMAT 最值錢的地方。5. PMAT 避坑與排查誤報(bào)、漏報(bào)和分析跑不動(dòng)的五個(gè)現(xiàn)場5.1 現(xiàn)象同一工程在兩個(gè) JDK 版本下分析結(jié)果不一致原因不同版本 javac 編譯出的字節(jié)碼在方法簽名、泛型簽名、枚舉構(gòu)造上存在差異PMAT 讀 class 時(shí)對(duì)描述符的解釋不同匹配結(jié)果自然不一樣。這不算工具 bug而是輸入字節(jié)碼變了。解決固定分析用的 JDK。拿同一批代碼做對(duì)比實(shí)驗(yàn)時(shí)全程用同一個(gè) JDK 編譯最好把 JDK 路徑固化到分析環(huán)境的配置說明里。遇到跨版本結(jié)果不一致時(shí)以 JDK 8 編譯出的 class 為基準(zhǔn)兼容性表現(xiàn)最好。5.2 現(xiàn)象分析結(jié)果里 Singleton 一片誤報(bào)原因匹配規(guī)則只要求構(gòu)造函數(shù)非 public、靜態(tài)方法返回本類。大量工具類把構(gòu)造方法私有化只是為了禁止實(shí)例化業(yè)務(wù)上根本沒有全局唯一意圖于是被批量誤判成 Singleton。解決點(diǎn)開命中證據(jù)看靜態(tài)字段。沒有 static 本類字段的候選實(shí)例直接排除模式實(shí)例樹里通常能看到命中的字段列表憑這一條就能把工具類誤報(bào)大批量清掉。剩下的少量候選再用是否真的存在共享狀態(tài)來判斷。5.3 現(xiàn)象大工程分析到一半內(nèi)存溢出或界面卡死原因中間表示的關(guān)系圖在內(nèi)存里全量構(gòu)建幾十萬類的工程會(huì)吃掉數(shù) GB 堆內(nèi)存Eclipse 默認(rèn)堆設(shè)置根本不夠。解決先按模塊分包分析不要一上來跑整個(gè)工程。給 Eclipse 加大 -Xmx取值建議不超過物理內(nèi)存的一半8GB 機(jī)器給 3GB 左右。同時(shí)保持工作空間里只打開一個(gè)分析工程其它無關(guān)項(xiàng)目全部關(guān)掉。分模塊跑雖然要多執(zhí)行幾次但每次都能拿到完整結(jié)果比一次跑掛強(qiáng)得多。提示加大 -Xmx 后如果仍然溢出優(yōu)先懷疑是關(guān)系圖規(guī)模問題而不是內(nèi)存參數(shù)問題。把分析范圍再縮小往往比繼續(xù)加內(nèi)存更有效。5.4 現(xiàn)象泛型和嵌套類導(dǎo)致關(guān)系鏈斷裂模式漏報(bào)原因泛型擦除后方法參數(shù)類型變成 Object 或邊界類型中間表示里精確類型信息丟失嵌套類的構(gòu)造器簽名攜帶外部類引用匹配時(shí)這類關(guān)系容易被繞過。漏報(bào)集中在 Decorator、Adapter 這類依賴方法參數(shù)精確類型的模式上。解決分析前把源碼里的泛型在分析副本里替換成具體類型List?這類寫法改成ListString嵌套類過多的模塊先做一次抽類再分析。注意這些改動(dòng)只作用于分析副本不提交到業(yè)務(wù)代碼。5.5 現(xiàn)象Eclipse 一升級(jí)PMAT 菜單整個(gè)消失原因插件是按舊版 Eclipse 的擴(kuò)展點(diǎn)編譯的新版工作臺(tái)改了擴(kuò)展點(diǎn)定義或移除了對(duì)應(yīng) API插件沒有被加載菜單自然消失。解決分析環(huán)境與開發(fā)環(huán)境分離。單獨(dú)保留一個(gè)舊版 Eclipse 跑 PMAT日常開發(fā)用新版。這也是一條血淚經(jīng)驗(yàn)分析工具是手段別讓它拖累你的主開發(fā)環(huán)境。每次升級(jí) Eclipse 前先確認(rèn)插件有對(duì)應(yīng)支持再?zèng)Q定動(dòng)不動(dòng)否則就保持舊環(huán)境原樣不動(dòng)。6. 讓 PMAT 貼合你的代碼庫自定義模式描述與結(jié)果復(fù)驗(yàn)6.1 補(bǔ)一條自定義模式描述PMAT 的思路是模式 一份結(jié)構(gòu)約束描述。實(shí)際代碼庫里經(jīng)常有團(tuán)隊(duì)自己的框架設(shè)施比如統(tǒng)一的注冊器、統(tǒng)一的分發(fā)入口這些結(jié)構(gòu)在 GoF 之外默認(rèn)檢測不到。常見做法是照著模式描述語言的語法補(bǔ)一條自定義模式讓工具按你的約束去匹配。示意結(jié)構(gòu)如下pattern nameFacadeRegistry categorystructural class roleclient field nameFACADE statictrue type*Registry/ method nameregister* returnsvoid/ /class /pattern解讀這份描述定義了一個(gè)叫 FacadeRegistry 的結(jié)構(gòu)模式匹配條件是某個(gè)類存在名稱 FACADE 的靜態(tài)字段且字段類型以 Registry 結(jié)尾同時(shí)存在 register 開頭、返回 void 的方法。實(shí)際語法以你拿到的工具版本為準(zhǔn)但骨架是類似的。加一條自定義模式后重新分析散落的注冊器實(shí)現(xiàn)就會(huì)被統(tǒng)一標(biāo)出來。6.2 三個(gè)低成本的結(jié)果復(fù)驗(yàn)手段工具給的是候選落地前必須復(fù)驗(yàn)。三個(gè)低成本手段比較實(shí)用抽查每次分析后隨機(jī)抽 10 個(gè)模式實(shí)例打開源碼人工核對(duì)記錄一次準(zhǔn)確率。對(duì)照動(dòng)態(tài)行為選幾個(gè)關(guān)鍵實(shí)例通過運(yùn)行時(shí)日志或調(diào)用棧確認(rèn)它的參與鏈路與靜態(tài)分析一致。版本對(duì)比把兩個(gè)版本的分析結(jié)果導(dǎo)出做 diff觀察模式實(shí)例數(shù)量的變化是否對(duì)應(yīng)真實(shí)架構(gòu)變更。這三個(gè)手段互相獨(dú)立抽查保證基本盤動(dòng)態(tài)對(duì)照驗(yàn)證關(guān)鍵鏈路版本對(duì)比追蹤演進(jìn)趨勢。只要抽查準(zhǔn)確率保持在八成以上工具結(jié)果就可以作為日常架構(gòu)盤點(diǎn)的輸入。6.3 一個(gè)驗(yàn)證習(xí)慣我現(xiàn)在接手任何不熟悉的 Java 系統(tǒng)第一周必做的就是把工程完整跑一遍 PMAT導(dǎo)出指標(biāo)表和模式實(shí)例存檔作為基線。之后每次大版本迭代結(jié)束后重跑一次對(duì)比模式實(shí)例數(shù)量和 CBO 前幾名有沒有異常漂移。之前有次發(fā)現(xiàn)某模塊的 CBO 一個(gè)版本內(nèi)翻了一倍順著導(dǎo)出清單排查原來是為了省事把整個(gè)工具類的靜態(tài)方法引了個(gè)遍。沒有基線表這種隱性耦合很容易漏掉。這個(gè)習(xí)慣幫我擋住了不少后續(xù)維護(hù)成本希望也能幫到你。本文還有配套的精品資源點(diǎn)擊獲取