Java 程序打包成 JAR、WAR、Spring Boot Fat JAR、Docker 映像後,文件結構發生劇變:class 文件可能在 JAR 內、類路徑根目錄、BOOT-INF/classes 等任意位置。**絕對路徑(`new File(".")`)在不同啟動方式下極不穩定**,必須通過 ClassLoader 獲取「類路徑相對路徑」的資源 URL/Stream,再轉為絕對路徑。
假設類 com.example.Demo 位於 target/classes/com/example/Demo.class,測試輸出如下:
| 代碼 | 返回值示例 | 含義 | 推薦場景 |
|---|---|---|---|
this.getClass().getResource("") | file:/.../target/classes/com/example/ | 當前類所在包目錄(不含類文件) | 讀取同包下配置文件 |
this.getClass().getResource("/") | file:/.../target/classes/ | Classpath 根目錄 | 讀取 src/main/resources 根下資源 |
this.getClass().getClassLoader().getResource("") | file:/.../target/classes/ | ClassLoader 視角的 Classpath 根 | 通用工具類、靜態方法中使用 |
ClassLoader.getSystemResource("") | file:/.../target/classes/ | 系統類加載器的 Classpath 根 | 無實例對象時的靜態調用 |
Thread.currentThread().getContextClassLoader().getResource("") | file:/.../target/classes/ | 線程上下文類加載器(Web 容器常用) | Tomcat/JBoss 等容器環境、SPI 機制 |
ServletActionContext.getServletContext().getRealPath("/") | /opt/tomcat/webapps/myapp/ | Web 應用部署根目錄(非 Classpath) | 讀取/寫入 Web 目錄下的上傳文件、靜態資源 |
public final class ClassPathUtils {
private ClassPathUtils() {}
/** 獲取 Classpath 根目錄的絕對路徑(以 File 分隔符結尾) */
public static String getClassPathRoot() {
URL url = ClassPathUtils.class.getClassLoader().getResource("");
if (url == null) throw new IllegalStateException("無法定位 Classpath 根");
try {
String path = new File(url.toURI()).getAbsolutePath();
return path.endsWith(File.separator) ? path : path + File.separator;
} catch (URISyntaxException e) {
throw new IllegalStateException("Classpath URL 解析失敗", e);
}
}
/** 讀取 Classpath 下資源為 InputStream(自動關閉由調用方負責) */
public static InputStream getResourceAsStream(String relativePath) {
// 相對路徑不以 / 開頭,視為相對調用類;以 / 開頭視為 Classpath 根
return ClassPathUtils.class.getClassLoader().getResourceAsStream(relativePath);
}
/** 讀取資源為 String(UTF-8) */
public static String readResource(String relativePath) {
try (InputStream is = getResourceAsStream(relativePath);
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
return br.lines().collect(Collectors.joining("\n"));
} catch (IOException e) {
throw new UncheckedIOException("讀取資源失敗: " + relativePath, e);
}
}
}
System.getProperty("user.dir"):僅代表 JVM 啟動工作目錄,打包部署後極大概率不是項目根目錄。getResourceAsStream() 直接返回流,兼容 JAR 內讀取,無需解壓路徑。Class.getResource("config.yml") 相對當前類包;ClassLoader.getResource("config.yml") 相對 Classpath 根。統一約定:工具類全部用 ClassLoader + 相對根路徑。application.yml 或環境變量,啟動時注入。getResource("") 返回 jar:file:...!/ 協議,無法用 FileOutputStream 寫入。需寫入時請複製到臨時目錄或外部卷。BOOT-INF/classes/,資源在 BOOT-INF/classes/ 或 BOOT-INF/lib/。ClassPathUtils.getClassPathRoot() 返回的是 JAR 文件所在目錄,不是 JAR 內部路徑。@Value("classpath:config.yml") 或 ResourceLoader.getResource("classpath:config.yml"),交給 Spring 解析 classpath*: 前綴。掌握上述 6 種獲取方式與避坑原則,即可在開發、測試、打包、容器化全生命周期中穩健訪問資源文件,告別「本機跑通、部署炸裂」的噩夢。