名字都像「類找不到」,但病根完全不一樣。
ClassNotFoundException是 checked exception,只在反射或loadClass()這類顯式按類名加載時拋,多半是 classpath 裡少了依賴。NoClassDefFoundError是Error,編譯期明明存在、運行期卻拿不到定義,常見於靜態初始化塊拋例外、版本衝突或部署缺包。
Issue & Env(案發現場與環境)
兩個詞都圍著 Java 的類加載轉。類不會一啟動就全被加載,而是第一次用到時才由 class loader 去 classpath 裡找 .class 文件。
「類找不到」會以兩種身份出現:顯式按名字找的時候拋 ClassNotFoundException,隱式正常執行程式碼時拋 NoClassDefFoundError。前者可恢復,後者是打破約定的錯誤。
兩者從 JDK 早期版本就存在,和具體版本無關。容易踩坑的環境有三種:
- Maven 裡把某依賴的
<scope>provided</scope>設錯,編譯在、運行不在。 - web 應用(Tomcat 等)部署時,
WEB-INF/lib裡漏了 jar,或者 jar 誤入服務器公共 lib 目錄導致加載衝突。 NoClassDefFoundError常見於 Jar 版本衝突:編譯時用 A 庫的 1.2,運行期 classpath 裡卻是 1.0。
排查 Maven 依賴用 mvn dependency:tree,Gradle 用 ./gradlew dependencies。
StackTrace(堆疊日誌)
ClassNotFoundException 典型報錯來自反射按名加載:
java.lang.ClassNotFoundException: com.example.DatabaseDriver
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:315)
at com.example.DatabaseDriverLoader.load(DatabaseDriverLoader.java:22)
at com.example.Main.main(Main.java:9)
NoClassDefFoundError 的現場不一樣。程式碼編譯時一切正常,運行期卻在 new MyService() 或方法調用處崩掉:
Exception in thread "main" java.lang.NoClassDefFoundError: com/example/MyService
at com.example.Main.run(Main.java:10)
at com.example.Main.main(Main.java:5)
Caused by: java.lang.ClassNotFoundException: com.example.MyService
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:315)
注意兩點:一是 Caused by 裡常跟著一個 ClassNotFoundException,很多人看到它就去補依賴,結果方向跑偏;二是它發生在「已編譯通過」之後,說明不是缺源碼,而是運行環境與編譯環境不一致。
Reproduce(復現程式碼)
ClassNotFoundException 的最小復現,一行反射即可:
public class Main {
public static void main(String[] args) {
try {
Class.forName("com.example.NotExist");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
NoClassDefFoundError 經典復現是靜態初始化塊拋例外。先把類加載失敗,之後每次引用都報 NoClassDefFoundError:
public class Foo {
static {
int x = 1 / 0; // 静态初始化抛 ArithmeticException
}
public static void hello() {}
}
public class Main {
public static void main(String[] args) {
Foo.hello();
}
}
首輪加載拋的是 ExceptionInInitializerError,之後 JVM 記住「這個類定義不可用」,再觸發就報 NoClassDefFoundError。
Root Cause(根因分析)
ClassNotFoundException 的根子在類加載環節。Class.forName() 和 ClassLoader.loadClass() 按字符串名定位類,靠 classpath 一層層找。名字錯了、jar 少了、路徑寫岔了,就直接拋這個檢查型例外。因為是顯式請求,語義上「允許類不存在」,編譯期不校驗,出問題在運行期暴露。
NoClassDefFoundError 的根子更隱蔽,常見三類:
- 編譯期有、運行期沒。類在編譯時被引用過,但打包或部署時對應的
.class或 jar 掉了。 - 依賴的依賴缺失。A 引用了 B,B 的字段類型裡有 C,C 不在 classpath。加載 B 時因解析 C 失敗,B 的定義整個拿不到。
- 靜態初始化失敗。類在連結驗證或初始化階段拋了例外,JVM 標記該類定義不可用,後續引用統一報
NoClassDefFoundError。
Stack Overflow 的一條高票回答把界線劃得清楚:ClassNotFoundException 是「你想加載一個可能不存在的類」,NoClassDefFoundError 是「一個本該存在的類定義沒了」。一個是 Exception,處理它;一個是 Error,多半是環境壞了。
The Fix(解決方案)
ClassNotFoundException 先補依賴、對準名字。命令行用 jar tf 驗證某個類是否打包進去:
jar tf your-library.jar | grep com/example/DatabaseDriver
Maven 項目重點查 provided 作用域,它不進最終產物。類名拼寫、包路徑大小寫都要核對一遍。
NoClassDefFoundError 是被動結果,真正的線索在堆疊最前面幾行,尤其帶 Caused by 的原始例外或 ExceptionInInitializerError。先解決那個根因,別急著補 jar。
mvn dependency:tree
# 或
./gradlew dependencies
找到版本衝突就鎖定一個版本、排除多餘依賴。靜態初始化失敗的情況,把 static 塊裡可能拋例外的程式碼挪到需要時才執行,或包一層 try-catch。最後核對部署產物:war/jar 裡是否真帶上那個類,服務器公共 lib 與 .war 內 lib 有沒有互相打架。
