Java中從類別載入器取得類別路徑
1. 概述
在本文中,我們將探討如何從最新 OpenJDK 發行版中的ClassLoader
取得類別路徑。
2. ClassLoaders
和Classpath
Java 虛擬機器使用ClassLoaders
在執行時間解析類別或其他資源。儘管ClassLoader
載入器實作可以自由選擇解析機制,但類別路徑是標準選擇。
類別路徑**是 Java 程式使用相對命名語法來定位類別和其他資源的抽象。**例如,如果我們使用以下命令啟動 Java 程式:
java -classpath /tmp/program baeldung.Main
Java 運行時將在 /tmp/program/baeldung/Main.class 位置搜尋文件,假設該文件存在且有效,JVM 將載入Class
並執行main
方法。
程式啟動後,任何其他類別載入都將透過按各自的內部名稱搜尋類別來進行,這意味著帶有“.”的完全限定Class
名稱。 (點)替換為“/”(斜線),作為目錄 /tmp/program 的相對路徑。
此外,類路徑也支援:
- 多個位置,由“:”分隔符號指定
- Jar 檔
最後,我們可以指定任何有效的 URL 作為類路徑位置,而不僅僅是檔案系統路徑。因此,客觀地講,一個**classpath就相當於一組URL** 。
3. ClassLoaders
的常見類型
為了有用, ClassLoader
實作必須能夠透過重寫以下方法來解析java.lang.Class
物件的實例:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
實作客製化的ClassLoader
可能很棘手。 ClassLoaders
設計類似於LinkedList
結構,而每個 ClassLoader 都可能有一個父ClassLoader
,可以透過getParent()
方法查詢該父 ClassLoader 以支援委託模型。
最後,類別路徑和ClassLoader
之間沒有強制耦合,我們將把自己限制在有效利用類別路徑機制**進行資源解析的**眾所周知的類型上。
3.1. java.net.URLClassLoader
URLClassLoader
透過getURLs() and it'
是通常的選擇。例如,Tomcat 擴展了URLClassLoader
以隔離 Web 應用程式的類別路徑:容器為每個部署的應用程式提供獨立的類別路徑,與其他應用程式分開。
此外, URLClassLoader
配備的邏輯不僅可以處理本機目錄,還可以處理託管在磁碟上或**透過 HTTP 或其他協定(例如 FTP)遠端存取的 jar 文件,只要它們支援URL::openConnection()
方法。**
3.2.應用程式ClassLoader
負責啟動Java程式的ClassLoader
,即載入具有main
方法的類別並執行的ClassLoader
,稱為Application ClassLoader.
在較舊的 JDK (<=9)中**,Application ClassLoader
是URLClassLoader
的子類別**。然而,自 OpenJDK 10 以來,層次結構發生了變化,應用程式ClassLoader
繼承自模組私有jdk.internal.loader.BuiltinClassLoader
,它不是URLClassLoader
的子類別。
根據定義,應用程式ClassLoader
將其類別路徑綁定到-classpath
啟動選項。我們可以在運行時透過查詢系統屬性java.class.path
來檢索此信息,但是,這可能隨時被無意中覆蓋,如果我們想確定實際的運行時類路徑,我們必須查詢ClassLoader
本身。
4. 從ClassLoader
取得Classpath
要從給定的ClassLoader
取得類別路徑,我們先定義一個ClasspathResolver
接口,該介面公開了查詢單一ClassLoader
及其整個層次結構的類別路徑的功能:
package com.baeldung.classloader.spi;
public interface ClasspathResolver {
void collectClasspath(ClassLoader loader, Set<URL> result);
default Set<URL> getClasspath(ClassLoader loader) {
var result = new HashSet<URL>();
collectClasspath(loader, result);
return result;
}
default Set<URL> getFullClasspath(ClassLoader loader) {
var result = new HashSet<URL>();
collectClasspath(loader, result);
loader = loader.getParent();
while (loader != null) {
collectClasspath(loader, result);
loader = loader.getParent();
}
return result;
}
}
正如我們之前提到的,Application ClassLoaders
不再繼承自URLClassLoader
,因此**我們不能簡單地透過呼叫URLClassLoader::getURLs()
方法來取得關聯的類別路徑**。然而, URLClassLoader
和BuiltinClassLoader classes:
都包裝了jdk.internal.loader
的實例。 URLClassPath
,它是負責定位基於 URL 的資源的實際類別。
因此, ClasspathResolver
介面的有效實作必須修改 JDK 中的不可見類,從而需要存取非匯出的套件。
由於將 JDK 內部暴露給整個程式是一種不好的做法,因此如果我們透過將ClasspathResolver
介面及其實作放在模組化 jar中並定義一個模組資訊檔案來隔離此功能會更好:
module baeldung.classloader {
exports com.baeldung.classloader.spi;
}
4.1.基本實現
ClasspathResolver
的基本實作將僅處理URLClassLoaders
,無論程式是否有權限存取 JDK 內部,都始終可用:
public class BasicClasspathResolver implements ClasspathResolver {
@Override
public void collectClasspath(ClassLoader loader, Set<URL> result) {
if(loader instanceof URLClassLoader ucl) {
var urls = Arrays.asList(ucl.getURLs());
result.addAll(urls);
}
}
}
4.2.擴展實施
此實作將需要非匯出的 OpenJDK 類,並且可能會出現一些問題,即:
- 我們庫的使用者可能沒有設定正確的運行時標誌
- 該程式在不同的 JDK 中運行,該 JDK 可能沒有
BuiltinClassLoaders
- OpenJDK 的未來版本可能會更改或刪除
BuiltinClassLoader
,使此實作無用
當使用內部 JDK API 時,我們必須進行防禦性編程,這意味著需要採取一些額外的步驟來正確編譯和執行我們的程式。
首先,我們必須透過為編譯器添加適當的命令列選項,將jdk.internal.loader
包匯出到我們的模組。然後,我們還必須打開支援運行時反射存取的套件來測試我們的實作。
當使用 Maven 作為建置和測試工具時,必須如下設定插件:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>22</source>
<target>22</target>
<compilerArgs>
<arg>--add-exports</arg>
<arg>java.base/jdk.internal.loader=baeldung.classloader</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>--add-opens java.base/jdk.internal.loader=baeldung.classloader</argLine>
</configuration>
</plugin>
</plugins>
</build>
注意:**如果您不想使用模組化項目,可以省略 module-info 檔案。相反,在匯出和開放子句中將baeldung.classloader
模組替換為 ALL-UNNAMED** 。
某些 IDE 可能還需要額外的配置。例如,在Eclipse中,需要在Module Dependency中暴露包,如下圖所示:
現在,我們準備好編寫擴展實現的程式碼。本質上,我們需要的呼叫鍊是:
BuiltinClassLoader -> URLClassPath -> getURLs()
URLClassPath
實例被提升到BuiltinClassLoader
# ucp
私有欄位中,因此我們必須透過反射來取得它。這可能會在運行時失敗,如果它確實發生了,我們應該做什麼?
我們將選擇回退到BasicClasspathResolver
,為此,我們應該寫一個支援類別來告訴我們是否可以存取BuiltinClassLoader
# ucp
:
public class InternalJdkSupport {
static final Class<?> BUILT_IN_CLASSLOADER;
static final VarHandle UCP;
static {
var log = LoggerFactory.getLogger(InternalJdkSupport.class);
var version = System.getProperty("java.version");
Class<?> clazz = null;
VarHandle ucp = null;
try {
var ucpClazz = Class.forName("jdk.internal.loader.URLClassPath");
clazz = Class.forName("jdk.internal.loader.BuiltinClassLoader");
var lookup = MethodHandles.privateLookupIn(clazz, MethodHandles.lookup());
ucp = lookup.findVarHandle(clazz, "ucp", ucpClazz);
} catch (ClassNotFoundException e) {
log.warn("JDK {} not supported => {} not available.", version, e.getMessage());
} catch (NoSuchFieldException e) {
log.warn("JDK {} not supported => BuiltinClassLoader.ucp not present", version);
} catch (IllegalAccessException e) {
log.warn("""
BuiltinClassLoader.ucp requires \
--add-opens java.base/jdk.internal.loader=baeldung.classloader
""");
}
BUILT_IN_CLASSLOADER = clazz;
UCP = ucp;
}
public static boolean available() {
return UCP != null;
}
public static Object getURLClassPath(ClassLoader loader) {
if (!isBuiltIn(loader)) {
throw new UnsupportedOperationException("Loader not an instance of BuiltinClassLoader");
}
if (UCP == null) {
throw new UnsupportedOperationException("""
Program must be initialized with \
--add-opens java.base/jdk.internal.loader=baeldung.classloader
""");
}
try {
return UCP.get(loader);
} catch (Exception e) {
throw new InternalError(e);
}
}
static boolean isBuiltIn(ClassLoader loader) {
return BUILT_IN_CLASSLOADER != null && BUILT_IN_CLASSLOADER.isInstance(loader);
}
}
有了InternalJdkSupport
,我們擴展的ClasspathResolver
實作(能夠從應用程式ClassLoaders
中提取URL)變得簡單:
import jdk.internal.loader.BuiltinClassLoader;
import jdk.internal.loader.URLClassPath;
public final class InternalClasspathResolver implements ClasspathResolver {
@Override
public void collectClasspath(ClassLoader loader, Set<URL> result) {
var urls = switch (loader) {
case URLClassLoader ucl -> Arrays.asList(ucl.getURLs());
case BuiltinClassLoader bcl -> {
URLClassPath ucp = (URLClassPath) InternalJdkSupport.getURLClassPath(loader);
yield ucp == null ? Collections.<URL> emptyList() : Arrays.asList(ucp.getURLs());
}
default -> {
yield Collections.<URL> emptyList();
}
};
result.addAll(urls);
}
}
4.3.公開ClasspathResolver
由於我們的模組僅導出com.baeldung.classloader.spi
包,因此客戶端將無法直接實例化實作。因此,我們應該透過工廠方法提供對實現的存取:
public interface ClasspathResolver {
static ClasspathResolver get() {
if (InternalJdkSupport.available()) {
return new InternalClasspathResolver();
}
return new BasicClasspathResolver();
}
}
5. 結論
在本文中,我們探討如何建立一個安全的模組化解決方案來取得常見 OpenJDK ClassLoaders
的類別路徑。
擴展實作被設計為與 OpenJDK 一起工作,因為它需要存取特定於實現的類,但是,它將與其他供應商(例如Zulu 、 Graal和Temurin )一起工作。
模組化將公開的 JDK 內部表面限制為單一受信任的模組,從而防止第三方依賴項進行不必要的惡意存取。
本文的完整原始碼可 在 GitHub 上取得。