戰(zhàn)排查:從Correct the classpath到依賴樹定位與修復(fù))
前兩周有個(gè)項(xiàng)目啟動(dòng)時(shí)直接拋出一個(gè)看著特別眼熟的異常日志最后一行清清楚楚寫著Correct the classpath of your application so that it contains a single, compatible version of。說實(shí)話看到 classpath、single compatible version 這些詞基本就能斷定是 jar 包沖突了。但這種提示往往不會(huì)直接告訴你到底是哪個(gè) jar、哪幾個(gè)版本在打架只是甩給你一個(gè)讓我保持單一兼容版本的結(jié)論新手面對(duì)這行字很容易懵——本地編譯是通的打包也成功了為什么一到運(yùn)行就炸。這篇文章我就從這句報(bào)錯(cuò)入手把 jar 包沖突的成因、依賴樹的定位方法、以及修復(fù)的完整套路講清楚。內(nèi)容會(huì)覆蓋 Maven 和 Gradle 兩種主流構(gòu)建工具也聊 Spring Boot 項(xiàng)目里最典型的沖突場(chǎng)景適合正被依賴樹折磨的 Java 開發(fā)者參考。如果你遇到過的報(bào)錯(cuò)只有這半句話建議先記住一個(gè)結(jié)論這句提示不是在罵你亂引依賴而是在告訴你classpath 里同一個(gè)庫(kù)出現(xiàn)了不止一個(gè)版本或者某個(gè)類的實(shí)現(xiàn)來源已經(jīng)不一致了。1. 先看報(bào)錯(cuò)現(xiàn)場(chǎng)這句Correct the classpath到底在說什么1.1 一個(gè)很典型的報(bào)錯(cuò)場(chǎng)景先還原一下最常見的情況。一個(gè) Spring Boot 項(xiàng)目啟動(dòng)的時(shí)候日志下方出現(xiàn)一個(gè)比較醒目的入口內(nèi)容大概長(zhǎng)這樣*************************** APPLICATION FAILED TO START *************************** Description: An attempt was made to call a method that does not exist. The attempt was made from the following location: ... Correct the classpath of your application so that it contains a single, compatible version of org.springframework:spring-core前面還會(huì)跟著具體是哪個(gè)方法調(diào)用出了問題比如某個(gè)Foo類調(diào)用了Bar的某個(gè)方法但當(dāng)前 classpath 里的Bar并沒有這個(gè)方法。這種錯(cuò)誤往上追根因往往是同一個(gè) JAR 以不同版本被卷進(jìn)了最終運(yùn)行環(huán)境或者兩個(gè)不同坐標(biāo)的 JAR 里包含了完全相同的類。這句 Correct the classpath 相當(dāng)于是框架層幫你做了一次統(tǒng)一體檢它掃描到運(yùn)行環(huán)境里存在多個(gè)版本的同名類或者某個(gè)類的版本與你當(dāng)前代碼的預(yù)期不一致所以直接拒絕啟動(dòng)。它的潛臺(tái)詞是你光讓編譯通過沒有用運(yùn)行時(shí)我加載到的是不是你寫代碼時(shí)用的那一套才是真正要命的地方。1.2 這句錯(cuò)誤信息的真實(shí)含義single, compatible version 拆開看是兩層意思。第一層是 single即同一個(gè)類在 classpath 中只應(yīng)該出現(xiàn)一次來源。Java 類加載器在加載類時(shí)按 classpath 順序查找如果兩個(gè) JAR 里都有com.example.utils.StringUtil實(shí)際加載哪一個(gè)完全取決于運(yùn)氣和順序。你代碼里調(diào)用的方法可能在另一個(gè)版本里根本沒有于是出現(xiàn)NoSuchMethodError或AbstractMethodError。第二層是 compatible即這個(gè)唯一版本必須和編譯期的版本兼容。我們寫代碼時(shí)引入的是某個(gè)依賴的 3.x 版本編譯期可能正常但如果運(yùn)行時(shí)最終命中 2.x2.x 里沒有 3.x 新增的接口簽名就會(huì)拋出提示。所以這句報(bào)錯(cuò)翻譯成大白話就是你的運(yùn)行環(huán)境里某個(gè)類庫(kù)的版本已經(jīng)亂到框架不敢繼續(xù)往下走了自己把 classpath 理清楚再啟動(dòng)吧。2. 定位沖突源頭靠依賴樹而不是靠肉眼2.1 Maven 項(xiàng)目?jī)?yōu)先看 dependency:tree遇到?jīng)_突時(shí)最容易犯的錯(cuò)誤是用 IDE 的搜索功能在本地倉(cāng)庫(kù)里找一個(gè)版本號(hào)然后手動(dòng)改 pom。這種思路不太靠譜因?yàn)檎嬲鉀Q的是誰引入了舊版本這條依賴鏈而不是單純把某個(gè)坐標(biāo)版本寫高。Maven 項(xiàng)目第一步建議先輸出完整依賴樹命令很簡(jiǎn)單mvn dependency:tree輸出會(huì)非常長(zhǎng)尤其多模塊項(xiàng)目動(dòng)輒上千行所以通常配合過濾條件使用。比如報(bào)錯(cuò)信息指向org.springframework:spring-core就執(zhí)行mvn dependency:tree -Dincludesorg.springframework:spring-core-Dincludes支持多種寫法也可以用 groupId 過濾比如只看某個(gè)組織下的所有依賴mvn dependency:tree -Dincludesorg.springframework如果同樣一個(gè)依賴被多個(gè)路徑引入依賴樹會(huì)把這些路徑都列出來這是排查沖突最重要的依據(jù)。而加不加-Dverbose也很有講究-Dverbose會(huì)把被仲裁掉的依賴也顯示出來比如某個(gè)版本最終沒有被選中它會(huì)標(biāo)記為omitted for conflict或omitted for duplicate。不過這個(gè)參數(shù)會(huì)讓輸出爆炸式增長(zhǎng)建議還是先按坐標(biāo)過濾再?zèng)Q定是否打開 verbose。2.2 Gradle 項(xiàng)目用 dependencies 和 dependencyInsightGradle 項(xiàng)目打印依賴樹的命令是gradle dependencies這個(gè)命令默認(rèn)輸出所有 configuration 的依賴同樣比較長(zhǎng)。通常我建議先指定運(yùn)行時(shí) classpathgradle dependencies --configuration runtimeClasspath如果你已經(jīng)知道沖突的坐標(biāo)更推薦用dependencyInsight直接查某個(gè)依賴的來源gradle dependencyInsight --dependency commons-lang3 --configuration runtimeClasspath這個(gè)命令會(huì)把這個(gè)依賴從哪條鏈路引入、最終解析到哪個(gè)版本、為什么選擇這個(gè)版本都解釋得比較清楚。執(zhí)行效率比看完整依賴樹高很多我實(shí)際排查時(shí)基本靠這一條就能把絕大多數(shù)沖突定位出來。2.3 依賴樹里的幾個(gè)關(guān)鍵信號(hào)看依賴樹不是光找重復(fù)的 artifact 名更要看懂輸出里的幾個(gè)標(biāo)記。我整理了常見的幾類標(biāo)志含義需要做什么omitted for conflict該版本因沖突被 Maven 仲裁排除說明存在另一個(gè)版本被選中需要確認(rèn)選中版本是否兼容omitted for duplicate該依賴在依賴樹中重復(fù)出現(xiàn)但被去重暫不影響運(yùn)行但排查時(shí)容易造成困惑- 5.3.30Gradle 中該依賴實(shí)際解析到了 5.3.30確認(rèn)最終版本是否符合預(yù)期FAILEDGradle 解析依賴時(shí)遇到版本沖突錯(cuò)誤需要配置 resolutionStrategy 或排除沖突看到omitted for conflict不代表萬事大吉恰恰相反這往往意味著 Maven 通過仲裁規(guī)則選了一個(gè)就近的版本但這個(gè)就近版本不一定是你業(yè)務(wù)代碼真正需要的版本最終就會(huì)出現(xiàn)開頭那種啟動(dòng)時(shí)報(bào)錯(cuò)。3. 為什么同一個(gè)庫(kù)里會(huì)出現(xiàn)多個(gè)版本3.1 Maven 的最近優(yōu)先仲裁機(jī)制Maven 默認(rèn)的依賴仲裁規(guī)則是最近定義優(yōu)先nearest wins。簡(jiǎn)單理解就是誰的依賴路徑更短誰就說了算。比如 A 直接依賴 B 的 2.0而 A 依賴 CC 又依賴 B 的 1.0那么 A 最終使用的是 B 2.0因?yàn)?A 到 B 2.0 的路徑深度是 1到 B 1.0 的路徑深度是 2。如果兩條路徑深度一樣Maven 則看聲明順序誰先聲明誰優(yōu)先。這個(gè)機(jī)制本身不復(fù)雜但它容易導(dǎo)致一個(gè)很反直覺的結(jié)果你以為某依賴是通過 Spring Boot 的 BOM 統(tǒng)一管理的應(yīng)該用 5.3.x但某個(gè)內(nèi)部 starter 因?yàn)閷蛹?jí)更淺硬生生把自己的 5.2.x 頂了上去。我看過不少項(xiàng)目就是栽在這里父 pom 里明明寫了依賴管理子模塊只要顯式聲明一個(gè)舊版本或者某個(gè)傳遞依賴剛好路徑更短最終就會(huì)拿到一個(gè)舊版本連編譯都能過直到運(yùn)行時(shí)才爆出問題。3.2 Gradle 的沖突解決策略Gradle 的默認(rèn)策略和 Maven 不一樣它傾向于選擇最高的版本。比如一條路徑引入 commons-lang3 3.4另一條路徑引入 3.12Gradle 默認(rèn)會(huì)把最終版本解析到 3.12。這是大多數(shù)場(chǎng)景下更聰明的做法因?yàn)楦甙姹就ǔ<嫒莸桶姹?。?Gradle 也不是沒有坑。如果你在多個(gè) configuration 中做了不同的依賴聲明或者某個(gè)依賴通過相對(duì)路徑直接以文件形式加入又或者依賴圖里有動(dòng)態(tài)版本依然可能出現(xiàn)運(yùn)行時(shí) classpath 里同時(shí)存在多個(gè)版本 jar 的情況。更常見的問題是你把同一個(gè)依賴以不同坐標(biāo)引入比如commons-lang:commons-lang:2.5和org.apache.commons:commons-lang3:3.12.0這兩個(gè) jar 內(nèi)部的包名和類名高度重疊Gradle 基于坐標(biāo)去仲裁根本不認(rèn)為它們是一個(gè)東西于是兩個(gè) jar 都留在 classpath 里類沖突照樣發(fā)生。3.3 哪些情況會(huì)讓報(bào)錯(cuò)真實(shí)發(fā)生有幾個(gè)非常典型的場(chǎng)景會(huì)觸發(fā) Correct the classpath 這類框架級(jí)檢查一是同一個(gè)坐標(biāo)的不同版本同時(shí)存在。這種情況在普通 Maven 項(xiàng)目中不太容易發(fā)生因?yàn)橹俨脵C(jī)制會(huì)干掉一個(gè)版本但在多模塊復(fù)雜項(xiàng)目、IDE 緩存異常、或者你手動(dòng)把 jar 丟進(jìn)了本地倉(cāng)庫(kù)時(shí)仍有可能出現(xiàn)。二是不同坐標(biāo)包含相同類。比如老的commons-lang和新的commons-lang3前者包名是org.apache.commons.lang后者是org.apache.commons.lang3本來井水不犯河水但如果你遇到的是那種被打包過、repackage 過的 fat jar類路徑就會(huì)變得非?;靵y。三是應(yīng)用服務(wù)器環(huán)境下的類加載層級(jí)。比如在 Tomcat 里跑多個(gè) webapp某個(gè) webapp 的 lib 目錄和 Tomcat 公共 lib 目錄里放了不同版本的同一個(gè) jarclasspath 順序一變沖突就出現(xiàn)了。四是 IDE 里的 classpath 和實(shí)際構(gòu)建產(chǎn)物的 classpath 不一致。本地跑起來沒事打成 jar 之后一堆問題多半是這種。4. 從容修復(fù)統(tǒng)一版本的完整操作路徑4.1 第一選擇鎖定版本而不是手動(dòng)刪 jar拿到依賴樹之后最常見的修復(fù)手段是在dependencyManagement里鎖定版本。Maven 項(xiàng)目可以在父 pom 中這樣寫dependencyManagement dependencies dependency groupIdorg.springframework/groupId artifactIdspring-core/artifactId version5.3.30/version /dependency /dependencies /dependencyManagement這樣做的邏輯是凡是依賴樹中沒有顯式寫版本的地方Maven 都會(huì)參考這個(gè)版本決定最終選擇。也就是說它給了全項(xiàng)目一個(gè)默認(rèn)版本約束尤其適合多模塊工程。相比之下在每個(gè)子模塊里各自寫死版本會(huì)讓問題越來越嚴(yán)重因?yàn)榘姹痉稚_突早晚還會(huì)回來。需要提醒的是dependencyManagement只對(duì)省略了版本的依賴聲明生效。如果某些傳遞依賴路徑里有顯式版本或者某個(gè)模塊自身直接寫了version那它的優(yōu)先級(jí)會(huì)高于依賴管理里的默認(rèn)值。所以在復(fù)雜項(xiàng)目里光加一個(gè) dependencyManagement 還不夠往往還要配合排除操作。4.2 排除不該存在的傳遞依賴排除依賴是定位到具體依賴鏈之后最常用的手段。比如你發(fā)現(xiàn)common-infra這個(gè)內(nèi)部包傳遞了一個(gè)舊版 spring-core而這個(gè)包本身不需要直接使用 spring-core那就可以在引入common-infra時(shí)排除掉dependency groupIdorg.example/groupId artifactIdcommon-infra/artifactId version1.4.0/version exclusions exclusion groupIdorg.springframework/groupId artifactIdspring-core/artifactId /exclusion /exclusions /dependency排除操作的核心原則是誰帶來的問題就在誰身上排除。不要在一個(gè)毫不相關(guān)的依賴上寫一堆 exclusions否則后面的人看代碼會(huì)完全看不懂。但排除操作也要謹(jǐn)慎。如果common-infra真的依賴 spring-core你把它排除掉運(yùn)行時(shí)會(huì)因?yàn)槿鄙兕愔苯訄?bào)NoClassDefFoundError。所以更穩(wěn)妥的思路是排除舊版 在 dependencyManagement 鎖定新版兩者配合使用而不是單純刪除。4.3 使用 BOM 管理一組依賴遇到一組依賴需要統(tǒng)一版本時(shí)比如 Spring Boot 全家桶、某個(gè)開源框架的多個(gè)模塊手動(dòng)一個(gè)個(gè)寫版本很蠢而且容易漏。更好的做法是引入 BOMBill of Materials它本質(zhì)上就是一個(gè)專門做依賴管理的 pom。Maven 中通過 import scope 引入dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version2.7.18/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement引入 BOM 之后你在項(xiàng)目中聲明 spring-boot 相關(guān)的模塊時(shí)可以完全不寫版本所有版本統(tǒng)一由 BOM 決定。Spring Boot 項(xiàng)目本身就用這種方式管理了數(shù)以百計(jì)的依賴版本這也是為什么很多項(xiàng)目只要正確繼承了 spring-boot-starter-parent就不用手動(dòng)管 Spring Framework 的版本。值得留意的是BOM 只是給了默認(rèn)值如果項(xiàng)目中某個(gè)模塊顯式聲明了舊版本沖突依然存在。所以 BOM 是輔助不是萬靈丹配合排除操作才是完整方案。4.4 用構(gòu)建插件把沖突堵在源頭如果團(tuán)隊(duì)經(jīng)常出現(xiàn)依賴版本混亂光靠人肉檢查是不夠的最好把規(guī)則固化到構(gòu)建過程里。Maven 可以用 enforcer 插件做依賴檢查比如禁止某些特定版本的依賴出現(xiàn)plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-enforcer-plugin/artifactId version3.4.1/version executions execution idenforce-banned-dependencies/id goals goalenforce/goal /goals configuration rules bannedDependencies excludes excludeorg.apache.commons:commons-lang3:2.5/exclude /excludes /bannedDependencies /rules /configuration /execution /executions /plugin這樣如果有人再引入被禁用的版本構(gòu)建會(huì)直接失敗問題在開發(fā)階段就暴露出來而不是等到運(yùn)行時(shí)報(bào)錯(cuò)。Gradle 項(xiàng)目可以在build.gradle里配置 resolutionStrategyconfigurations.all { resolutionStrategy { force org.apache.commons:commons-lang3:3.12.0 failOnVersionConflict() } }force表示強(qiáng)制所有依賴最終都解析到指定版本failOnVersionConflict則會(huì)在檢測(cè)到多個(gè)版本時(shí)直接報(bào)錯(cuò)逼著你顯式處理。5. 一次真實(shí)的 jar 沖突排查記錄虛構(gòu)但常見5.1 現(xiàn)象啟動(dòng)即失敗錯(cuò)誤提示直指 classpath我印象比較深刻的一次是某支付項(xiàng)目的后臺(tái)服務(wù)Spring Boot 2.7 構(gòu)建啟動(dòng)時(shí)報(bào)錯(cuò)內(nèi)容指向spring-core。項(xiàng)目本身代碼量不算大模塊結(jié)構(gòu)大概是common、infra、web 三個(gè) Maven 模塊web 依賴 infrainfra 又依賴 common。當(dāng)時(shí)啟動(dòng)日志大概是這個(gè)輪廓APPLICATION FAILED TO START Description: An attempt was made to call a method that does not exist. The attempt was made from the following location: com.example.web.config.WebConfiguration.doInit(WebConfiguration.java:45) The following method did not exist: org.springframework.core.annotation.MergedAnnotations.locate(...) Correct the classpath of your application so that it contains a single, compatible version of org.springframework:spring-core看到這個(gè)提示的第一反應(yīng)不是去查方法簽名而是先篩依賴樹。因?yàn)閳?bào)錯(cuò)給出的類名和方法名已經(jīng)足夠說明問題MergedAnnotations是 Spring 5.2 之后才有的類如果 classpath 里實(shí)際加載的 spring-core 是 5.1.x那這個(gè)方法自然不存在。5.2 篩選依賴樹定位到兩條引入路徑我在項(xiàng)目根目錄執(zhí)行mvn dependency:tree -Dverbose -Dincludesorg.springframework:spring-core spring-core-tree.txt打開文件后看到兩條關(guān)鍵路徑[INFO] --- maven-dependency-plugin:3.1.2:tree (default-cli) on project web --- [INFO] com.example:web:1.0.0 [INFO] \- com.example:infra:1.2.0:compile [INFO] \- org.example:common-infra:1.4.0:compile [INFO] \- org.springframework:spring-core:5.2.20.RELEASE:compile [INFO] [INFO] \- org.springframework.boot:spring-boot-starter-web:2.7.18:compile [INFO] \- org.springframework:spring-core:5.3.30:compile問題非常清楚common-infra:1.4.0這個(gè)內(nèi)部包把 spring-core 死死踩在了 5.2.20.RELEASE而 Spring Boot 2.7.18 需要的是 5.3.30。由于common-infra屬于傳遞依賴它的依賴路徑比 Spring Boot 的路徑淺Maven 仲裁時(shí)就傾向了它提供的版本。于是業(yè)務(wù)代碼編譯期看到的接口是基于 spring-core 5.3.30而運(yùn)行時(shí)卻加載了 5.2.20自然找不到MergedAnnotations.locate這個(gè)方法。5.3 修復(fù)操作與驗(yàn)證過程修復(fù)方案我分兩步走。第一步在 web 模塊引入common-infra時(shí)排除掉 spring-coredependency groupIdorg.example/groupId artifactIdcommon-infra/artifactId version1.4.0/version exclusions exclusion groupIdorg.springframework/groupId artifactIdspring-core/artifactId /exclusion /exclusions /dependency第二步為了確保全項(xiàng)目只有一個(gè) spring-core 版本我在父 pom 的 dependencyManagement 里顯式鎖定dependencyManagement dependencies dependency groupIdorg.springframework/groupId artifactIdspring-core/artifactId version5.3.30/version /dependency /dependencies /dependencyManagement修改完成后重新執(zhí)行mvn clean dependency:tree -Dincludesorg.springframework:spring-core這次輸出里只剩一條5.3.30的路徑。再啟動(dòng)服務(wù)報(bào)錯(cuò)消失MergedAnnotations.locate的問題也沒有再現(xiàn)。這個(gè)案例其實(shí)很典型問題并不在項(xiàng)目自身代碼里而是隱藏在第三方內(nèi)部包的依賴鏈條里。如果不借助依賴樹定位單靠猜是永遠(yuǎn)找不到根源的。6. 避坑速查表與常見問題6.1 典型問題匯總我把實(shí)際排查過程中比較高頻的問題整理成了一個(gè)表方便你對(duì)照現(xiàn)象可能原因處理建議依賴樹里只有一個(gè)版本但啟動(dòng)仍報(bào)沖突IDE 緩存、本地倉(cāng)庫(kù)殘留、手動(dòng)加入 fat jar執(zhí)行mvn clean install檢查本地倉(cāng)庫(kù)重新導(dǎo)入 IDE報(bào)錯(cuò)是NoSuchMethodError而不是 Correct the classpath編譯期版本和運(yùn)行期版本不一致優(yōu)先用 dependencyInsight 或 dependency:tree 定位實(shí)際版本排除依賴后報(bào)NoClassDefFoundError排除的 jar 是某個(gè)功能唯一來源恢復(fù)依賴改為用 dependencyManagement 統(tǒng)一版本多個(gè)坐標(biāo)包含相同類如commons-lang和commons-lang3同時(shí)存在確認(rèn)哪些依賴引用了老坐標(biāo)升級(jí)到新坐標(biāo)或排除老坐標(biāo)同一個(gè)依賴在不同模塊里顯式寫了不同版本多模塊工程缺少統(tǒng)一管理把版本收斂到父 pom 的 dependencyManagement打完包后 lib 目錄里有重復(fù) jar構(gòu)建插件或 assembly 配置把重復(fù)內(nèi)容打了進(jìn)去檢查最終 lib 目錄清理重復(fù) jar調(diào)整打包配置6.2 我的幾條實(shí)操經(jīng)驗(yàn)第一遇到 Correct the classpath 這類提示先把報(bào)錯(cuò)信息里的坐標(biāo)復(fù)制下來再依賴樹過濾定位所有引入路徑。不要上來就刪依賴也不要只看 dependencyManagement。第二Maven 項(xiàng)目?jī)?yōu)先考慮在父 pom 統(tǒng)一版本Gradle 項(xiàng)目?jī)?yōu)先考慮使用resolutionStrategy.force。在 project 里手動(dòng)改某個(gè) jar 版本這種做法效果往往持續(xù)不了幾周因?yàn)橐坏┯腥诵略鲆蕾嚊_突會(huì)再次悄悄出現(xiàn)。第三統(tǒng)一版本之后一定要跑一遍回歸測(cè)試尤其是那些涉及本地緩存、序列化、反射的代碼。不同版本的同一個(gè)庫(kù)行為不一定完全兼容高版本也不一定就是更好的版本。比如某些老項(xiàng)目里用的是commons-lang:2.x強(qiáng)行統(tǒng)一到org.apache.commons:commons-lang3:3.x類名和 API 都不完全一樣反而會(huì)引入新的問題。第四如果團(tuán)隊(duì)里有多個(gè)人在維護(hù)項(xiàng)目建議直接啟用構(gòu)建期檢查。Maven 的 enforcer 插件、Gradle 的failOnVersionConflict都是成本比較低的保護(hù)手段它們能把依賴沖突從運(yùn)行時(shí)問題提前到構(gòu)建期問題省下來的排查時(shí)間非??捎^。第五永遠(yuǎn)記得看一眼最終產(chǎn)物里的 lib 目錄。一個(gè)項(xiàng)目在 IDE 里跑得好好的不代表打包出去沒問題因?yàn)?IDE 的 classpath 和構(gòu)建產(chǎn)物的 classpath 偶爾會(huì)不完全一致。排查依賴問題時(shí)別只看 pom 或 build.gradle實(shí)際打包物才是運(yùn)行時(shí)的真相。我自己的習(xí)慣是遇到這類提示先不慌處理順序基本固定先抄報(bào)錯(cuò)里的坐標(biāo)再用dependency:tree或dependencyInsight找出所有引入路徑最后通過 dependencyManagement 或排除操作收斂版本。踩過幾次坑之后我把所有第三方依賴版本都收口到了父 pom 的 dependencyManagement 里子模塊里只寫 groupId 和 artifactId后續(xù)升級(jí)也只改一處。另外每次改完依賴結(jié)構(gòu)我都會(huì)clean install一次再啟動(dòng)確認(rèn)最終 classpath 里不再有重復(fù) jar。這個(gè)流程看著簡(jiǎn)單但確實(shí)幫我省下了大量和類加載器糾纏的時(shí)間。