De Java ClassLoader is een van de cruciale maar zelden gebruikte componenten in projectontwikkeling. Ik heb nog nooit ClassLoader uitgebreid in een van mijn projecten. Maar het idee om mijn eigen ClassLoader te hebben die het laden van Java-klassen kan aanpassen, is opwindend. Dit artikel geeft een overzicht van Java ClassLoader en gaat dan verder met het maken van een aangepaste class loader in Java.
Wat is Java ClassLoader?
We weten dat een Java-programma wordt uitgevoerd op de Java Virtual Machine (JVM). Wanneer we een Java-klasse compileren, maakt de JVM de bytecode, die platform- en machineonafhankelijk is. De bytecode wordt opgeslagen in een .class-bestand. Wanneer we een klasse proberen te gebruiken, laadt de ClassLoader deze in het geheugen.
Ingebouwde ClassLoader-types
Er zijn drie soorten ingebouwde ClassLoader in Java.
- Bootstrap Class Loader – Het laadt JDK-interne klassen. Het laadt rt.jar en andere kernklassen, bijvoorbeeld klassen uit het java.lang.*-pakket.
- Extensions Class Loader – Het laadt klassen uit de JDK-extensiemap, meestal de $JAVA_HOME/lib/ext-map.
- System Class Loader – Deze classloader laadt klassen van het huidige classpath. We kunnen het classpath instellen tijdens het aanroepen van een programma met behulp van de -cp of -classpath opdrachtregeloptie.
ClassLoader Hiërarchie
ClassLoader is hiërarchisch bij het laden van een klasse in het geheugen. Telkens wanneer een verzoek wordt gedaan om een klasse te laden, wordt dit doorgegeven aan de bovenliggende classloader. Zo wordt uniciteit gehandhaafd in de runtime-omgeving. Als de bovenliggende classloader de klasse niet vindt, probeert de classloader zelf de klasse te laden. Laten we dit begrijpen door het onderstaande Java-programma uit te voeren.
package com.journaldev.classloader;
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println("class loader for HashMap: "
+ java.util.HashMap.class.getClassLoader());
System.out.println("class loader for DNSNameService: "
+ sun.net.spi.nameservice.dns.DNSNameService.class
.getClassLoader());
System.out.println("class loader for this class: "
+ ClassLoaderTest.class.getClassLoader());
System.out.println(com.mysql.jdbc.Blob.class.getClassLoader());
}
}
Output:
class loader for HashMap: null
class loader for DNSNameService: sun.misc.Launcher$ExtClassLoader@7c354093
class loader for this class: sun.misc.Launcher$AppClassLoader@64cbbe37
sun.misc.Launcher$AppClassLoader@64cbbe37
Hoe werkt Java ClassLoader?
Laten we begrijpen hoe de werking van classloaders is aan de hand van de uitvoer van het bovenstaande programma.
- De ClassLoader van de java.util.HashMap komt als null, wat de Bootstrap ClassLoader weerspiegelt. De ClassLoader van de DNSNameService-klasse is ExtClassLoader. Aangezien de klasse zelf in het CLASSPATH staat, laadt de System ClassLoader deze.
- Wanneer we proberen de HashMap te laden, delegeert onze System ClassLoader het naar de Extension ClassLoader. De extension class loader delegeert het naar de Bootstrap ClassLoader. De bootstrap class loader vindt de HashMap-klasse en laadt deze in het JVM-geheugen.
- Hetzelfde proces wordt gevolgd voor de DNSNameService-klasse. Maar de Bootstrap ClassLoader kan het niet vinden omdat het zich bevindt in
$JAVA_HOME/lib/ext/dnsns.jar
. Daarom wordt het geladen door Extensions Classloader. - De Blob-klasse is opgenomen in de MySql JDBC Connector-jar (mysql-connector-java-5.0.7-bin.jar), die aanwezig is in het build-pad van het project. Het wordt ook geladen door de System Classloader.
- Klassen geladen door een kind class loader hebben zicht op klassen geladen door hun ouder class loaders. Dus klassen geladen door de System Classloader hebben zicht op klassen geladen door Extensions- en Bootstrap Classloader.
- Als er sibling class loaders zijn, kunnen ze geen toegang krijgen tot klassen geladen door elkaar.
Waarom een aangepaste ClassLoader schrijven in Java?
Java’s standaard ClassLoader kan klassen laden vanaf het lokale bestandssysteem, wat meestal voldoende is. Maar als je een klasse verwacht tijdens runtime, vanaf een FTP-server, of via een externe webservice op het moment van laden van de klasse, dan moet je de bestaande class loader uitbreiden. Bijvoorbeeld, AppletViewers laden klassen vanaf een externe webserver.
Methoden van Java ClassLoader
- Wanneer de JVM vraagt om een klasse, roept het de functie
loadClass()
van de ClassLoader aan door de volledig gekwalificeerde naam van de klasse door te geven. - De loadClass()-functie roept de
findLoadedClass()
-methode aan om te controleren of de klasse al is geladen of niet. Dit is nodig om te voorkomen dat dezelfde klasse meerdere keren wordt geladen. - Als de klasse nog niet is geladen, zal het het verzoek delegeren aan de ouder ClassLoader om de klasse te laden.
- Als de ouder ClassLoader de klasse niet vindt, zal het de findClass()-methode aanroepen om de klassen op het bestandssysteem te zoeken.
Voorbeeld van aangepaste Java ClassLoader
We zullen onze eigen ClassLoader maken door de ClassLoader-klasse uit te breiden en de loadClass(String name) methode te overschrijven. Als de klassenaam begint met com.journaldev
, laden we deze met onze aangepaste klassenlader, anders roepen we de loadClass() methode van de ouder ClassLoader aan om de klasse te laden.
1. CCLoader.java
Dit is onze aangepaste klassenlader met onderstaande methoden.
private byte[] loadClassFileData(String name)
: Deze methode leest het klassenbestand van het bestandssysteem naar een byte-array.private Class<?> getClass(String name)
: Deze methode roept de loadClassFileData() functie aan en door de defineClass() methode van de ouder aan te roepen, genereert het de Klasse en retourneert het.public Class<?> loadClass(String name)
: Deze methode is verantwoordelijk voor het laden van de klasse. Als de klassenaam begint met com.journaldev (onze voorbeeldklassen) dan zal het deze laden met getClass() methode, anders zal het de loadClass() functie van de ouder aanroepen om het te laden.public CCLoader(ClassLoader parent)
: Dit is de constructor, die verantwoordelijk is voor het instellen van de ouder ClassLoader.
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
/**
* Our Custom ClassLoader to load the classes. Any class in the com.journaldev
* package will be loaded using this ClassLoader. For other classes, it will delegate the request to its Parent ClassLoader.
*
*/
public class CCLoader extends ClassLoader {
/**
* This constructor is used to set the parent ClassLoader
*/
public CCLoader(ClassLoader parent) {
super(parent);
}
/**
* Loads the class from the file system. The class file should be located in
* the file system. The name should be relative to get the file location
*
* @param name
* Fully Classified name of the class, for example, com.journaldev.Foo
*/
private Class getClass(String name) throws ClassNotFoundException {
String file = name.replace('.', File.separatorChar) + ".class";
byte[] b = null;
try {
// Dit laadt de bytecode-gegevens uit het bestand
b = loadClassFileData(file);
// defineClass wordt geërfd van de ClassLoader-klasse
// die een byte-array omzet in een Klasse. defineClass is Final
// dus we kunnen het niet overschrijven
Class c = defineClass(name, b, 0, b.length);
resolveClass(c);
return c;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
/**
* Every request for a class passes through this method. If the class is in
* com.journaldev package, we will use this classloader or else delegate the
* request to parent classloader.
*
*
* @param name
* Full class name
*/
@Override
public Class loadClass(String name) throws ClassNotFoundException {
System.out.println("Loading Class '" + name + "'");
if (name.startsWith("com.journaldev")) {
System.out.println("Loading Class using CCLoader");
return getClass(name);
}
return super.loadClass(name);
}
/**
* Reads the file (.class) into a byte array. The file should be
* accessible as a resource and make sure that it's not in Classpath to avoid
* any confusion.
*
* @param name
* Filename
* @return Byte array read from the file
* @throws IOException
* if an exception comes in reading the file
*/
private byte[] loadClassFileData(String name) throws IOException {
InputStream stream = getClass().getClassLoader().getResourceAsStream(
name);
int size = stream.available();
byte buff[] = new byte[size];
DataInputStream in = new DataInputStream(stream);
in.readFully(buff);
in.close();
return buff;
}
}
2. CCRun.java
Dit is onze testklasse met de main-functie. We maken een instantie van onze ClassLoader en laden voorbeeldklassen met behulp van de loadClass()-methode. Na het laden van de klasse gebruiken we de Java Reflection API om de methoden ervan aan te roepen.
import java.lang.reflect.Method;
public class CCRun {
public static void main(String args[]) throws Exception {
String progClass = args[0];
String progArgs[] = new String[args.length - 1];
System.arraycopy(args, 1, progArgs, 0, progArgs.length);
CCLoader ccl = new CCLoader(CCRun.class.getClassLoader());
Class clas = ccl.loadClass(progClass);
Class mainArgType[] = { (new String[0]).getClass() };
Method main = clas.getMethod("main", mainArgType);
Object argsArray[] = { progArgs };
main.invoke(null, argsArray);
// Onderstaande methode wordt gebruikt om te controleren of Foo wordt geladen
// door onze aangepaste klasse-lader, d.w.z. CCLoader
Method printCL = clas.getMethod("printCL", null);
printCL.invoke(null, new Object[0]);
}
}
3. Foo.java en Bar.java
Dit zijn onze testklassen die worden geladen door onze aangepaste klasse-lader. Ze hebben een printCL()
-methode, die wordt aangeroepen om de informatie van de ClassLoader af te drukken. Foo-klasse wordt geladen door onze aangepaste klasse-lader. Foo gebruikt de Bar-klasse, dus Bar-klasse zal ook worden geladen door onze aangepaste klasse-lader.
package com.journaldev.cl;
public class Foo {
static public void main(String args[]) throws Exception {
System.out.println("Foo Constructor >>> " + args[0] + " " + args[1]);
Bar bar = new Bar(args[0], args[1]);
bar.printCL();
}
public static void printCL() {
System.out.println("Foo ClassLoader: "+Foo.class.getClassLoader());
}
}
package com.journaldev.cl;
public class Bar {
public Bar(String a, String b) {
System.out.println("Bar Constructor >>> " + a + " " + b);
}
public void printCL() {
System.out.println("Bar ClassLoader: "+Bar.class.getClassLoader());
}
}
4. Uitvoeringsstappen van aangepaste Java ClassLoader
Ten eerste compileren we alle klassen via de opdrachtregel. Daarna voeren we de klasse CCRun uit door drie argumenten door te geven. Het eerste argument is de volledig geclassificeerde naam voor de Foo-klasse die zal worden geladen door onze klasse-lader. De andere twee argumenten worden doorgegeven aan de hoofdfunctie van de Foo-klasse en de constructor van Bar. De uitvoeringsstappen en de uitvoer zullen als volgt zijn.
$ javac -cp . com/journaldev/cl/Foo.java
$ javac -cp . com/journaldev/cl/Bar.java
$ javac CCLoader.java
$ javac CCRun.java
CCRun.java:18: warning: non-varargs call of varargs method with inexact argument type for last parameter;
cast to java.lang.Class<?> for a varargs call
cast to java.lang.Class<?>[] for a non-varargs call and to suppress this warning
Method printCL = clas.getMethod("printCL", null);
^
1 warning
$ java CCRun com.journaldev.cl.Foo 1212 1313
Loading Class 'com.journaldev.cl.Foo'
Loading Class using CCLoader
Loading Class 'java.lang.Object'
Loading Class 'java.lang.String'
Loading Class 'java.lang.Exception'
Loading Class 'java.lang.System'
Loading Class 'java.lang.StringBuilder'
Loading Class 'java.io.PrintStream'
Foo Constructor >>> 1212 1313
Loading Class 'com.journaldev.cl.Bar'
Loading Class using CCLoader
Bar Constructor >>> 1212 1313
Loading Class 'java.lang.Class'
Bar ClassLoader: CCLoader@71f6f0bf
Foo ClassLoader: CCLoader@71f6f0bf
$
Als je naar de uitvoer kijkt, probeert het de klasse com.journaldev.cl.Foo
te laden. Omdat het de klasse java.lang.Object uitbreidt, probeert het eerst de Object-klasse te laden. Dus het verzoek komt binnen bij de loadClass-methode van CCLoader, die het doorgeeft aan de ouderklasse. Dus de ouderklasse-laders laden de Object, String en andere Java-klassen. Onze ClassLoader laadt alleen de Foo- en Bar-klasse vanuit het bestandssysteem. Dit is duidelijk uit de uitvoer van de printCL() functie. We kunnen de functionaliteit van loadClassFileData() wijzigen om de byte-array van een FTP-server te lezen of door een externe service aan te roepen om de klasse byte-array on-the-fly te krijgen. Ik hoop dat het artikel nuttig zal zijn om te begrijpen hoe de Java ClassLoader werkt en hoe we deze kunnen uitbreiden om veel meer te doen dan alleen vanuit het bestandssysteem te halen.
Het maken van een aangepaste ClassLoader als standaard ClassLoader
We kunnen onze aangepaste classloader instellen als de standaard wanneer de JVM start door Java-opties te gebruiken. Bijvoorbeeld, ik zal het ClassLoaderTest-programma opnieuw uitvoeren nadat ik de java classloader-optie heb opgegeven.
$ javac -cp .:../lib/mysql-connector-java-5.0.7-bin.jar com/journaldev/classloader/ClassLoaderTest.java
$ java -cp .:../lib/mysql-connector-java-5.0.7-bin.jar -Djava.system.class.loader=CCLoader com.journaldev.classloader.ClassLoaderTest
Loading Class 'com.journaldev.classloader.ClassLoaderTest'
Loading Class using CCLoader
Loading Class 'java.lang.Object'
Loading Class 'java.lang.String'
Loading Class 'java.lang.System'
Loading Class 'java.lang.StringBuilder'
Loading Class 'java.util.HashMap'
Loading Class 'java.lang.Class'
Loading Class 'java.io.PrintStream'
class loader for HashMap: null
Loading Class 'sun.net.spi.nameservice.dns.DNSNameService'
class loader for DNSNameService: sun.misc.Launcher$ExtClassLoader@24480457
class loader for this class: CCLoader@38503429
Loading Class 'com.mysql.jdbc.Blob'
sun.misc.Launcher$AppClassLoader@2f94ca6c
$
De CCLoader laadt de ClassLoaderTest-klasse omdat deze zich in het com.journaldev
-pakket bevindt.
Je kunt de voorbeeldcode van de ClassLoader downloaden vanuit onze GitHub Repository.
Source:
https://www.digitalocean.com/community/tutorials/java-classloader