相對路徑總是相對於「當前工作目錄」,但在 Web 容器、模組化框架、外掛架構中,工作目錄不可預期。以 **自身 Class 檔案位置** 為基準,才能可靠定位同層級資源 (設定檔、腳本、原生庫等),實現真正的封裝與可移植性。
Class.getProtectionDomain().getCodeSource().getLocation() 取得程式碼來源 URL (目錄或 JAR 檔)。jar:file:/path/to.jar!/),剝離 !/ 之後路徑取得 JAR 檔案本身。package/Class.class)。File.getCanonicalPath() 正規化路徑 (解析符號連結、相對路徑)。ClassLoader.getResource("package/Class.class")。package mytools;
import java.io.*;
import java.net.*;
import java.security.*;
public class Path {
/**
* 取得指定類別的 .class 檔案絕對路徑 (若在 JAR 中則回傳 JAR 檔案路徑)。
* @param cls 任意可被當前類別載入器載入的類別
* @return 絕對路徑;找不到回傳 null
*/
public static String getPathFromClass(Class> cls) throws IOException {
if (cls == null) throw new NullPointerException();
URL url = getClassLocationURL(cls);
if (url == null) return null;
String path = url.getPath();
if ("jar".equalsIgnoreCase(url.getProtocol())) {
// jar:file:/path/to.jar!/pkg/Class.class → file:/path/to.jar
try {
path = new URL(path).getPath();
} catch (MalformedURLException ignored) {}
int idx = path.indexOf("!/");
if (idx != -1) path = path.substring(0, idx);
}
return new File(path).getCanonicalPath();
}
/**
* 以指定類別為基準,解析相對路徑為絕對路徑。
* @param relatedPath 相對路徑 (如 "../../config/app.properties")
* @param cls 基準類別
* @return 解析後的絕對路徑
*/
public static String getFullPathRelateClass(String relatedPath, Class> cls) throws IOException {
if (relatedPath == null) throw new NullPointerException();
String clsPath = getPathFromClass(cls);
File clsFile = new File(clsPath);
// 若 clsPath 是 JAR 檔,getParent() 為 JAR 所在目錄
String baseDir = clsFile.getParent();
File target = new File(baseDir, relatedPath);
return target.getCanonicalPath();
}
/** 內部核心:解析類別位置 URL */
private static URL getClassLocationURL(final Class> cls) {
if (cls == null) throw new IllegalArgumentException("null input: cls");
// 1. 透過 ProtectionDomain → CodeSource
ProtectionDomain pd = cls.getProtectionDomain();
if (pd != null) {
CodeSource cs = pd.getCodeSource();
if (cs != null) {
URL loc = cs.getLocation();
if (loc != null) {
// 處理 file: 協定:目錄 vs JAR
if ("file".equals(loc.getProtocol())) {
try {
String ext = loc.toExternalForm();
if (ext.endsWith(".jar") || ext.endsWith(".zip")) {
// JAR 檔案 → 轉為 jar: 協定指向類別項目
return new URL("jar:" + ext + "!/" + cls.getName().replace('.', '/') + ".class");
} else if (new File(loc.getFile()).isDirectory()) {
// 目錄 → 直接拼接類別相對路徑
return new URL(loc, cls.getName().replace('.', '/') + ".class");
}
} catch (MalformedURLException ignored) {}
} else {
return loc; // 非 file 協定 (如 jrt:, http:) 直接回傳
}
}
}
}
// 2. 兜底:ClassLoader.getResource()
ClassLoader cl = cls.getClassLoader();
return (cl != null) ? cl.getResource(cls.getName().replace('.', '/') + ".class")
: ClassLoader.getSystemResource(cls.getName().replace('.', '/') + ".class");
}
// 測試主程式
public static void main(String[] args) {
try {
System.out.println("Path.class 位置: " + getPathFromClass(Path.class));
System.out.println("相對路徑測試: " + getFullPathRelateClass("../test/abc/..", Path.class));
} catch (Exception e) { e.printStackTrace(); }
}
}
| 場景 | 程式碼 | 結果 |
|---|---|---|
| 讀取同目錄下 properties | Path.getFullPathRelateClass("config.properties", MyClass.class) | /deploy/app/config.properties |
| 載入 JNI 原生庫 | System.load(Path.getFullPathRelateClass("../lib/libmylib.so", MyClass.class)) | /deploy/lib/libmylib.so |
| 定位 JAR 內資源 (需改用 getResourceAsStream) | — | 本工具回傳 JAR 檔路徑,非 JAR 內部路徑 |
Class.getResourceAsStream()。jrt:/ 協定不支援 File 操作,需改用 Module.getResourceAsStream()。getResource(),兜底策略可能失效。