Der Klassenlader ist ein sehr wichtiges Konzept in Java™. Der Klassenlader ist für das Laden des Bytecodes von Java-Klassen in die Java Virtual Machine verantwortlich. In diesem Artikel werden zunächst die Grundkonzepte von Java-Klassenladern ausführlich vorgestellt, einschließlich des Proxy-Modus, des spezifischen Prozesses zum Laden von Klassen und Thread-Kontext-Klassenladern usw., und anschließend wird erläutert, wie um Ihren eigenen Klassenlader zu entwickeln, und führt schließlich die Anwendung des Klassenladers in Webcontainern und OSGi™ ein.
Entwickeln und implementieren Sie Ihre nächste App auf der IBM Bluemix Cloud-Plattform.
Starten Sie Ihre Testversion
Der Klassenlader ist eine Innovation der Java-Sprache und einer der wichtigen Gründe für die Beliebtheit der Java-Sprache. Es ermöglicht das dynamische Laden und Ausführen von Java-Klassen in die Java Virtual Machine. Klassenlader gibt es seit JDK 1.0 und sie wurden ursprünglich entwickelt, um die Anforderungen von Java-Applets zu erfüllen. Das Java-Applet muss Java-Klassendateien von der Fernbedienung in den Browser herunterladen und ausführen. Klassenlader werden mittlerweile häufig in Webcontainern und OSGi verwendet. Im Allgemeinen müssen Entwickler von Java-Anwendungen nicht direkt mit Klassenladern interagieren. Das standardmäßige Verhalten der Java Virtual Machine reicht aus, um die Anforderungen der meisten Situationen zu erfüllen. Wenn Sie jedoch auf eine Situation stoßen, in der Sie mit einem Klassenlader interagieren müssen und nicht viel über den Mechanismus des Klassenladers wissen, kann es leicht sein, viel Zeit mit DebuggenClassNotFoundException und NoClassDefFoundError und andere Ausnahmen. In diesem Artikel wird der Klassenlader von Java ausführlich vorgestellt, um den Lesern ein tiefes Verständnis dieses wichtigen Konzepts in der Java-Sprache zu vermitteln. Im Folgenden stellen wir zunächst einige verwandte Grundkonzepte vor.
Grundkonzept des KlassenladersWie der Name schon sagt, wird der Klassenlader zum Laden von Java-Klassen in die Java Virtual Machine verwendet. Im Allgemeinen verwendet die Java Virtual Machine Java-Klassen wie folgt: Java-Quellprogramme (.java-Dateien) werden nach der Kompilierung durch einen Java-Compiler in Java-Bytecodes (.class-Dateien) konvertiert. Der Klassenlader ist dafür verantwortlich, Java-Bytecode zu lesen und ihn in eine Instanz der Klasse java.lang.Class zu konvertieren. Jede dieser Instanzen repräsentiert eine Java-Klasse. Ein
Objekt dieser Klasse kann über die newInstance()-Methode dieser Instanz erstellt werden. Die tatsächliche Situation kann komplizierter sein. Beispielsweise kann Java-Bytecode dynamisch über Tools generiert oder über das Netzwerk heruntergeladen werden. Grundsätzlich sind alle Klassenlader eine Instanz der Klasse java.lang.ClassLoader. Diese Java-Klasse wird im Folgenden ausführlich beschrieben.
java.lang.ClassLoader**Klasseneinführung**Die grundlegende Aufgabe der Klasse java.lang.ClassLoader besteht darin, den entsprechenden Bytecode basierend auf dem Namen einer angegebenen Klasse zu finden oder zu generieren class und definieren Sie dann eine Java-Klasse aus diesen Bytecodes, also eine Instanz der Klasse java.lang.Class. Darüber hinaus ist ClassLoader auch für das Laden der von Java-Anwendungen benötigten Ressourcen verantwortlich, wie z. B. Bilddateien und
Konfigurationsdateien usw. In diesem Artikel wird jedoch nur die Funktion des Ladens von Klassen erläutert. Um die Verantwortung für das Laden von Klassen zu vervollständigen, stellt ClassLoader eine Reihe von Methoden bereit. Die wichtigsten Methoden sind in Tabelle 1 aufgeführt. Einzelheiten zu diesen Methoden werden unten beschrieben.
Tabelle 1. Methoden im Zusammenhang mit dem Laden von Klassen in ClassLoader
Methodenbeschreibung
getParent()Gibt den übergeordneten Klassenlader dieses Klassenladers zurück. loadClass(String name)
Lädt die Klasse mit dem Namen name und das zurückgegebene Ergebnis ist eine Instanz der Klasse java.lang.Class. findClass(String name)Suchen Sie die Klasse mit dem Namen name, und das zurückgegebene Ergebnis ist eine Instanz der Klasse java.lang.Class. findLoadedClass(String name)Suchen Sie die geladene Klasse mit dem Namen name, und das zurückgegebene Ergebnis ist eine Instanz der Klasse java.lang.Class. defineClass(String name, byte[] b, int off, int len)Konvertieren Sie den Inhalt des Bytearray b in eine Java-Klasse und geben Sie das Ergebnis zurück Ist eine Instanz der Klasse java.lang.Class. Diese Methode wird als final deklariert.
resolveClass(Class> c)Verknüpfen Sie die angegebene Java-Klasse. Für die in Tabelle 1 angegebenen Methoden ist der Wert des Namensparameters, der den Klassennamen darstellt, der binäre Name der Klasse. Zu beachten ist die Darstellung interner Klassen wie com.example.Sample$1 und com.example.Sample$Inner. Diese Methoden werden weiter unten erläutert, wenn der Arbeitsmechanismus des Klassenladers vorgestellt wird. Im Folgenden wird die baumartige Organisationsstruktur des Klassenladers beschrieben.Baumartige Organisationsstruktur der Klassenlader
Klassenlader in Java können grob in zwei Kategorien unterteilt werden: Eine wird vom System bereitgestellt und die andere wird von Java-Anwendungsentwicklern geschrieben. Es gibt drei Hauptklassenlader, die vom System bereitgestellt werden:
Bootstrap Klassenlader): Er wird zum Laden der Kernbibliothek von Java verwendet und ist in nativem Code Erbt von java.lang implementiert .ClassLoader. Erweiterungsklassenlader: Wird zum Laden von Java-Erweiterungsbibliotheken verwendet. Die Implementierung der Java Virtual Machine stellt ein Verzeichnis von Erweiterungsbibliotheken bereit. Der Klassenlader sucht und lädt Java-Klassen in diesem Verzeichnis.
Systemklassenlader: Er lädt Java-Klassen entsprechend dem Klassenpfad (CLASSPATH) der Java-Anwendung. Im Allgemeinen werden Java-Anwendungsklassen dadurch geladen. Es kann über ClassLoader.getSystemClassLoader() abgerufen werden.
Mit Ausnahme des Bootstrap-Klassenladers verfügen alle Klassenlader über einen übergeordneten Klassenlader. Es kann über die in Tabelle 1 angegebene Methode getParent() abgerufen werden. Für den vom System bereitgestellten Klassenlader ist der übergeordnete Klassenlader des Systemklassenladers der Erweiterungsklassenlader, und der übergeordnete Klassenlader des Erweiterungsklassenladers ist der Boot-Klassenlader, der von Entwicklern geschrieben wurde Klassenlader ist der Klassenlader, der Java-Klassen dieses Klassenladers lädt. Denn die Java-Klasse des Klassenladers wird wie
andere Java-Klassen auch vom Klassenlader geladen. Im Allgemeinen ist der übergeordnete Klassenlader eines von einem Entwickler geschriebenen Klassenladers der Systemklassenlader. Klassenlader sind auf diese Weise organisiert und bilden eine Baumstruktur. Der Wurzelknoten des Baums ist der Boot-Klassenlader. Abbildung 1 zeigt ein typisches Organisationsstrukturdiagramm eines Klassenladerbaums, in dem der Pfeil auf den übergeordneten Klassenlader zeigt.
Abbildung 1. Schematische Darstellung der Organisationsstruktur des Klassenladerbaums
Codelisting 1 demonstriert die baumartige Organisationsstruktur des Klassenladers.
Listing 1. Demonstration der Baumstruktur von Klassenladern
public class ClassLoaderTree { public static void main(String[] args) { ClassLoader loader = ClassLoaderTree.class.getClassLoader(); while (loader != null) { System.out.println(loader.toString()); loader = loader.getParent(); } } }
Referenz, diese Referenz kann über die Methode getClassLoader() abgerufen werden. In Codelisting 1 wird die Methode getParent() rekursiv aufgerufen, um alle übergeordneten Klassenlader auszugeben. Die laufenden Ergebnisse von Codelisting 1 werden in Codelisting 2 angezeigt.
Listing 2. Laufergebnisse, die die Baumorganisationsstruktur des Klassenladers veranschaulichen sun.misc.Launcher$AppClassLoader@9304b1
sun.misc.Launcher$ExtClassLoader@190d11
Wie in Codeliste 2 gezeigt, ist die erste Ausgabe der Klassenlader der ClassLoaderTree-Klasse, also der Systemklassenlader. Es handelt sich um eine Instanz der Klasse sun.misc.Launcher$AppClassLoader; die zweite Ausgabe ist der Erweiterungsklassenlader, der eine Instanz der Klasse sun.misc.Launcher$ExtClassLoader ist. Es ist zu beachten, dass der Boot-Klassenlader hier nicht ausgegeben wird. Dies liegt daran, dass einige JDK-Implementierungen null zurückgeben, wenn der übergeordnete Klassenlader der Boot-Klassenlader ist.
Nachdem wir die baumartige Organisationsstruktur des Klassenladers verstanden haben, wird im Folgenden der Proxy-Modus des Klassenladers vorgestellt.
Proxy-Modus des KlassenladersWenn ein Klassenlader versucht, den Bytecode einer Klasse selbst zu finden und ihn zu definieren, stellt er zunächst einen Proxy für seinen übergeordneten Klassenlader und die übergeordnete Klasse dar Loader wird Der Klassenlader versucht zuerst, diese Klasse zu laden, und so weiter. Bevor wir die Motivation hinter dem Proxy-Muster vorstellen, müssen wir zunächst erklären, wie die Java Virtual Machine feststellt, dass zwei Java-Klassen gleich sind. Die Java Virtual Machine prüft nicht nur, ob die vollständigen Namen der Klassen gleich sind, sondern auch, ob die Klassenlader, die diese Klasse laden, gleich sind. Zwei Klassen gelten nur dann als gleich, wenn beide gleich sind. Selbst wenn derselbe Bytecode von verschiedenen Klassenladern geladen wird, sind die erhaltenen Klassen unterschiedlich. Beispielsweise generiert eine Java-Klasse com.example.Sample nach der Kompilierung eine Bytecodedatei Sample.class. Zwei verschiedene Klassenlader, ClassLoaderA und ClassLoaderB, lesen jeweils diese Sample.class-Datei und definieren zwei Instanzen der Klasse java.lang.Class, um diese Klasse darzustellen. Diese beiden Fälle sind nicht identisch. Für die Java Virtual Machine handelt es sich um unterschiedliche Klassen. Der Versuch, Objekte dieser beiden Klassen einander zuzuordnen, löst eine Laufzeitausnahme ClassCastException aus. Das Folgende wird anhand von Beispielen ausführlich erläutert. Die Java-Klasse com.example.Sample ist in Codelisting 3 angegeben.
Listing 3. com.example.Sample-Klasse
package com.example; public class Sample { private Sample instance; public void setSample(Object instance) { this.instance = (Sample) instance; } }
如 代码清单 3所示,com.example.Sample类的方法 setSample接受一个 java.lang.Object类型的参数,并且会把该参数强制转换成com.example.Sample类型。测试 Java 类是否相同的代码如 代码清单 4所示。
清单 4. 测试 Java 类是否相同
public void testClassIdentity() { String classDataRootPath = "C:\workspace\Classloader\classData"; File SystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath); FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath); String className = "com.example.Sample"; try { Class<?> class1 = fscl1.loadClass(className); Object obj1 = class1.newInstance(); Class<?> class2 = fscl2.loadClass(className); Object obj2 = class2.newInstance(); Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class); setSampleMethod.invoke(obj1, obj2); } catch (Exception e) { e.printStackTrace(); } }
代码清单 4中使用了类 FileSystemClassLoader的两个不同实例来分别加载类 com.example.Sample,得到了两个不同的java.lang.Class的实例,接着通过 newInstance()方法分别生成了两个类的对象 obj1和 obj2,最后通过 Java 的反射 API 在对象 obj1上调用方法 setSample,试图把对象 obj2赋值给 obj1内部的 instance对象。代码清单 4的运行结果如 代码清单 5所示。
清单 5. 测试 Java 类是否相同的运行结果
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at classloader.ClassIdentity.testClassIdentity(ClassIdentity.java:26)
at classloader.ClassIdentity.main(ClassIdentity.java:9)
Caused by: java.lang.ClassCastException: com.example.Sample
cannot be cast to com.example.Sample
at com.example.Sample.setSample(Sample.java:7)
... 6 more
从 代码清单 5给出的运行结果可以看到,运行时抛出了 java.lang.ClassCastException异常。虽然两个对象 obj1和 obj2的类的名字相同,但是这两个类是由不同的类加载器实例来加载的,因此不被 Java 虚拟机认为是相同的。
了解了这一点之后,就可以理解代理模式的设计动机了。代理模式是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。
不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。这种技术在许多框架中都被用到,后面会详细介绍。
下面具体介绍类加载器加载类的详细过程。
加载类的过程
在前面介绍类加载器的代理模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用 defineClass来实现的;而启动类的加载过程是通过调用 loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。
方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是java.lang.NoClassDefFoundError异常。
类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。
下面讨论另外一种类加载器:线程上下文类加载器。
线程上下文类加载器
Der Thread-Kontextklassenlader (Kontextklassenlader) wurde ab JDK 1.2 eingeführt. Die Methoden getContextClassLoader() und setContextClassLoader(ClassLoader cl) in der Klasse java.lang.Thread werden verwendet, um den Kontextklassenlader des Threads abzurufen und festzulegen. Wenn es nicht über die Methode setContextClassLoader(ClassLoader cl) festgelegt wird, erbt der Thread den Kontextklassenlader seines übergeordneten Threads. Der Kontextklassenlader für den ersten Thread, auf dem eine Java-Anwendung ausgeführt wird, ist der Systemklassenlader. In einem Thread ausgeführter Code kann Klassen und Ressourcen über diesen Loader-Typ laden.
Der oben erwähnte Proxy-Modus des Klassenladers kann nicht alle Probleme von Klassenladern lösen, die bei der Entwicklung von Java-Anwendungen auftreten. Java bietet viele Dienstanbieterschnittstellen (Service Provider Interface, SPI), sodass Dritte Implementierungen für diese Schnittstellen bereitstellen können. Zu den gängigen SPIs gehören JDBC, JCE, JNDI, JAXP und JBI. Diese SPI-Schnittstellen werden von der Java-Kernbibliothek bereitgestellt. Die SPI-Schnittstellendefinition von JAXP ist beispielsweise im Paket javax.xml.parsers enthalten. Diese SPI-Implementierungscodes sind wahrscheinlich als JAR-Pakete enthalten, von denen Java-Anwendungen abhängen, und können über den Klassenpfad (CLASSPATH) gefunden werden, wie zum Beispiel das in Apache Xerces enthaltene JAR-Paket, das JAXP SPI implementiert. Der Code in der SPI-Schnittstelle muss häufig bestimmte Implementierungsklassen laden. Beispielsweise wird die Methode newInstance() in der Klasse javax.xml.parsers.DocumentBuilderFactory in JAXP verwendet, um eine neue Instanz von DocumentBuilderFactory zu generieren. Die eigentliche Klasse der Instanz wird hier von javax.xml.parsers.DocumentBuilderFactory geerbt, bereitgestellt von der SPI-Implementierung. In Apache Xerces ist die implementierte Klasse beispielsweise org.apache.xerces.jaxp.DocumentBuilderFactoryImpl. Das Problem besteht darin, dass die SPI-Schnittstelle Teil der Java-Kernbibliothek ist und vom Boot-Klassenlader geladen wird. Die von SPI implementierten Java-Klassen werden im Allgemeinen vom Systemklassenlader geladen. Der Boot-Klassenlader kann die SPI-Implementierungsklasse nicht finden, da er nur die Kernbibliothek von Java lädt. Es kann auch kein Proxy für den Systemklassenlader sein, da es der Vorgängerklassenlader des Systemklassenladers ist. Mit anderen Worten: Der Proxy-Modus des Klassenladers kann dieses Problem nicht lösen. Der Thread-Kontextklassenlader löst genau dieses Problem. Wenn keine Einstellungen vorgenommen werden, wird als Kontextklassenlader des Threads der Java-Anwendung standardmäßig der Systemkontextklassenlader verwendet. Durch die Verwendung des Thread-Kontextklassenladers im SPI-Schnittstellencode können Sie die SPI-Implementierungsklasse erfolgreich laden. Der Thread-Kontextklassenlader wird in vielen SPI-Implementierungen verwendet. Im Folgenden wird eine weitere Methode zum Laden von Klassen vorgestellt: Class.forName.
Class.forName
Class.forName ist eine statische
Methode, die auch zum Laden von Klassen verwendet werden kann. Diese Methode hat zwei Formen: Class.forName(String-Name, boolesche Initialisierung, ClassLoader-Loader) und Class.forName(String-Klassenname). Die erste Form des Parameternamens stellt den vollständigen Namen der Klasse dar; initialize gibt an, ob der Klassenlader initialisiert werden soll; Die zweite Form entspricht dem Festlegen des Werts des Parameters initialize auf true und des Werts von Loader auf den Klassenlader der aktuellen Klasse. Eine sehr häufige Verwendung von Class.forName ist das Laden des Datenbank--Treibers . Beispielsweise wird Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance() verwendet, um den Treiber der Apache Derby-Datenbank zu laden. Nachdem wir die grundlegenden Konzepte im Zusammenhang mit Klassenladern vorgestellt haben, erfahren Sie hier, wie Sie Ihren eigenen Klassenlader entwickeln.
Obwohl in den meisten Fällen die vom System standardmäßig bereitgestellte Klassenlader-Implementierung die Anforderungen erfüllen kann. In einigen Fällen müssen Sie jedoch dennoch einen eigenen Klassenlader für Ihre Anwendung entwickeln. Beispielsweise überträgt Ihre Anwendung Bytecodes der Java-Klasse über das Netzwerk. Um die Sicherheit zu gewährleisten, werden diese Bytecodes verschlüsselt. Zu diesem Zeitpunkt benötigen Sie Ihren eigenen Klassenlader, um den verschlüsselten Bytecode von einer bestimmten Netzwerkadresse zu lesen, ihn dann zu entschlüsseln und zu überprüfen und schließlich die Klasse zu definieren, die in der Java Virtual Machine ausgeführt werden soll. Im Folgenden wird die Entwicklung von Klassenladern anhand zweier konkreter Beispiele veranschaulicht.
DateisystemKlassenladerDer erste Klassenlader wird verwendet, um im Dateisystem gespeicherten Java-Bytecode zu laden. Die vollständige Implementierung ist in Code Listing 6 dargestellt.
public class FileSystemClassLoader extends ClassLoader { private String rootDir; public FileSystemClassLoader(String rootDir) { this.rootDir = rootDir; } protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] getClassData(String className) { String path = classNameToPath(className); try { InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; } }
如 代码清单 6所示,类 FileSystemClassLoader继承自类 java.lang.ClassLoader。在 表 1中列出的 java.lang.ClassLoader类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理模式的实现。该方法会首先调用 findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的 loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用 findClass()方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写 findClass()方法。
类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。
网络类加载器
下面将通过一个网络类加载器来说明如何通过类加载器来实现组件的动态更新。即基本的场景是:Java 字节代码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务器上保存的文件即可。通过类加载器可以比较简单的实现这种需求。
类 NetworkClassLoader负责通过网络下载 Java 类字节代码并定义出 Java 类。它的实现与 FileSystemClassLoader类似。在通过NetworkClassLoader加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用 Java 反射 API。另外一种做法是使用接口。需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。使用 Java 反射 API 可以直接调用 Java 类的方法。而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类。在客户端通过相同的接口来使用这些实现类。网络类加载器的具体代码见 下载。
在介绍完如何开发自己的类加载器之后,下面说明类加载器和 Web 容器的关系。
类加载器与 Web 容器
对于运行在 Java EE™容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。
绝大多数情况下,Web 应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则:
每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在 WEB-INF/classes和 WEB-INF/lib目录下面。
多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。
当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。
在介绍完类加载器与 Web 容器的关系之后,下面介绍它与 OSGi 的关系。
Klassenlader und OSGi
OSGi™ ist ein dynamisches Modulsystem auf Java. Es bietet Entwicklern eine serviceorientierte und komponentenbasierte Laufzeitumgebung und bietet eine Standardmethode zur Verwaltung des Lebenszyklus der Software. OSGi wurde auf vielen Produkten implementiert und bereitgestellt und hat in der Open-Source-Community breite Unterstützung erhalten. Eclipse basiert auf der OSGi-Technologie.
Jedes Modul (Bundle) in OSGi enthält Java-Pakete und -Klassen. Ein Modul kann die Java-Pakete und Klassen anderer Module deklarieren, von denen es abhängt (über Import-Package), und es kann auch seine eigenen Pakete und Klassen zur Verwendung durch andere Module deklarieren und exportieren (exportieren) (über Export-Package). . Das bedeutet, dass Sie in der Lage sein müssen, bestimmte Java-Pakete und -Klassen innerhalb eines Moduls auszublenden und zu teilen. Dies wird durch den einzigartigen Klassenlademechanismus von OSGi erreicht. Jedes Modul in OSGi verfügt über einen entsprechenden Klassenlader. Es ist für das Laden der im Modul selbst enthaltenen Java-Pakete und -Klassen verantwortlich. Wenn Klassen aus der Java-Kernbibliothek geladen werden müssen (Pakete und Klassen, die mit Java beginnen), wird der übergeordnete Klassenlader (normalerweise der Startklassenlader) als Proxy verwendet, um den Vorgang abzuschließen. Wenn eine importierte Java-Klasse geladen werden muss, delegiert es an das Modul, das die Java-Klasse exportiert, um den Ladevorgang abzuschließen. Module können auch bestimmte Java-Pakete und -Klassen explizit deklarieren, die vom übergeordneten Klassenlader geladen werden müssen. Legen Sie einfach den Wert des System--Attributs org.osgi.framework.bootdelegation fest.
Angenommen, es gibt zwei Module bundleA und bundleB, die beide über ihre eigenen entsprechenden Klassenlader classLoaderA und classLoaderB verfügen. BundleA enthält die Klasse com.bundleA.Sample und die Klasse ist als exportiert deklariert, was bedeutet, dass sie von anderen Modulen verwendet werden kann. bundleB erklärt, dass es die von bundleA bereitgestellte Klasse com.bundleA.Sample importiert und eine Klasse com.bundleB.NewSample enthält, die von com.bundleA.Sample erbt. Wenn BundleB startet, muss sein Klassenlader classLoaderB die Klasse com.bundleB.NewSample laden, die wiederum die Klasse com.bundleA.Sample laden muss. Da BundleB die Klasse com.bundleA.Sample zum Importieren deklariert, delegiert classLoaderB die Arbeit des Ladens der Klasse com.bundleA.Sample an den Klassenlader classLoaderA von BundleA, der diese Klasse exportiert. classLoaderA sucht in seinem Modul nach der Klasse com.bundleA.Sample und definiert sie. Die resultierende Klasseninstanz com.bundleA.Sample kann von allen Modulen verwendet werden, die diese Klasse deklarieren. Klassen, die mit Java beginnen, werden vom übergeordneten Klassenlader geladen. Wenn die Systemeigenschaft org.osgi.framework.bootdelegation=com.example.core.* deklariert ist, schließt der übergeordnete Klassenlader das Laden der Klassen im Paket com.example.core ab.
Die Klassenladerstruktur des OSGi-Moduls ermöglicht die Koexistenz verschiedener Versionen einer Klasse in der Java Virtual Machine, was große Flexibilität bietet. Allerdings wird dieser Unterschied den Entwicklern auch einige Probleme bereiten, insbesondere wenn das Modul Bibliotheken von Drittanbietern verwenden muss. Hier sind einige gute Vorschläge:
Wenn eine Klassenbibliothek nur von einem Modul verwendet wird, fügen Sie das JAR-Paket der Klassenbibliothek in das Modul ein und geben Sie es im Bundle-ClassPath an.
Wenn eine Klassenbibliothek von mehreren Modulen gemeinsam genutzt wird, können Sie ein separates Modul für diese Klassenbibliothek erstellen und die Java-Pakete, die andere Module verwenden müssen, als exportiert deklarieren. Andere Moduldeklarationen importieren diese Klassen.
Wenn die Klassenbibliothek eine SPI-Schnittstelle bereitstellt und den Thread-Kontext-Klassenlader zum Laden der von SPI implementierten Java-Klasse verwendet, wird die Java-Klasse möglicherweise nicht gefunden. Wenn eine NoClassDefFoundError-Ausnahme auftritt, prüfen Sie zunächst, ob der Kontextklassenlader des aktuellen Threads korrekt ist. Der Klassenlader kann über Thread.currentThread().getContextClassLoader() abgerufen werden. Dieser Klassenlader sollte der entsprechende Klassenlader für dieses Modul sein. Wenn nicht, können Sie zuerst über class.getClassLoader() den dem Modul entsprechenden Klassenlader abrufen und dann über Thread.currentThread().setContextClassLoader() den Kontextklassenlader des aktuellen Threads festlegen.
Zusammenfassung
Der Klassenlader ist eine Innovation in der Java-Sprache. Es ermöglicht die dynamische Installation und Aktualisierung von Softwarekomponenten. In diesem Artikel werden Themen rund um den Klassenlader im Detail vorgestellt, darunter Grundkonzepte, Proxy-Modus, Thread-Kontext-Klassenlader, Beziehung zu Webcontainern und OSGi usw. Wenn Entwickler auf Ausnahmen wie ClassNotFoundException und NoClassDefFoundError stoßen, sollten sie den Klassenlader der Klasse, die die Ausnahme ausgelöst hat, und den Kontextklassenlader des aktuellen Threads überprüfen, um das Problem zu ermitteln. Bei der Entwicklung Ihres eigenen Klassenladers müssen Sie auf die Koordination mit der vorhandenen Organisationsstruktur des Klassenladers achten.
Das obige ist der detaillierte Inhalt vonEin detaillierter Blick auf Java-Klassenlader. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!