Java ClassLoader è uno dei componenti cruciali ma raramente utilizzati nello sviluppo di progetti. Non ho mai esteso ClassLoader in nessuno dei miei progetti. Tuttavia, l’idea di avere il mio ClassLoader personalizzato che può personalizzare il caricamento delle classi Java è eccitante. Questo articolo fornirà una panoramica di Java ClassLoader e poi passerà alla creazione di un class loader personalizzato in Java.
Cos’è Java ClassLoader?
Sappiamo che i programmi Java vengono eseguiti sulla Java Virtual Machine (JVM). Quando compiliamo una classe Java, la JVM crea il bytecode, che è indipendente dalla piattaforma e dalla macchina. Il bytecode viene memorizzato in un file .class. Quando cerchiamo di utilizzare una classe, il ClassLoader la carica in memoria.
Tipi di ClassLoader incorporati
Esistono tre tipi di ClassLoader incorporati in Java.
- Bootstrap Class Loader – Carica le classi interne di JDK. Carica rt.jar e altre classi principali, ad esempio le classi del pacchetto java.lang.*.
- Extensions Class Loader – Carica le classi dalla directory delle estensioni di JDK, di solito la directory $JAVA_HOME/lib/ext.
- Caricatore di Classi di Sistema – Questo classloader carica le classi dal percorso di classe corrente. Possiamo impostare il percorso di classe durante l’invocazione di un programma utilizzando l’opzione della riga di comando -cp o -classpath.
Gerarchia dei ClassLoader
Il ClassLoader è gerarchico nel caricare una classe in memoria. Ogni volta che viene sollevata una richiesta per caricare una classe, la delega al classloader genitore. Questo è come viene mantenuta l’unicità nell’ambiente di runtime. Se il class loader genitore non trova la classe, il class loader stesso cerca di caricare la classe. Capiremo questo eseguendo il seguente programma Java.
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
Come Funziona il ClassLoader di Java?
Capiremo il funzionamento dei class loader dall’output del programma sopra.
- Il ClassLoader della classe java.util.HashMap viene visualizzato come null, il che riflette il Bootstrap ClassLoader. Il ClassLoader della classe DNSNameService è ExtClassLoader. Poiché la classe stessa è nel CLASSPATH, il Caricatore di Classi di Sistema la carica.
- Quando stiamo cercando di caricare HashMap, il nostro System ClassLoader lo delega al Extension ClassLoader. Il class loader dell’estensione lo delega al Bootstrap ClassLoader. Il bootstrap class loader trova la classe HashMap e la carica nella memoria JVM.
- Lo stesso processo viene seguito per la classe DNSNameService. Ma, il Bootstrap ClassLoader non è in grado di trovarlo poiché è in
$JAVA_HOME/lib/ext/dnsns.jar
. Quindi, viene caricato dal Classloader delle estensioni. - La classe Blob è inclusa nel jar del Connnettore MySql JDBC (mysql-connector-java-5.0.7-bin.jar), che è presente nel percorso di compilazione del progetto. Viene anche caricato dal System Classloader.
- Le classi caricate da un class loader figlio hanno visibilità delle classi caricate dai suoi class loader genitori. Quindi le classi caricate dal System Classloader hanno visibilità delle classi caricate da Extensions e Bootstrap Classloader.
- Se ci sono class loader fratelli, allora non possono accedere alle classi caricate l’uno dall’altro.
Perché scrivere un Custom ClassLoader in Java?
Java default ClassLoader può caricare classi dal sistema di file locale, il che è sufficientemente buono per la maggior parte dei casi. Ma, se ti aspetti una classe durante l’esecuzione o da un server FTP o tramite un servizio web di terze parti al momento del caricamento della classe, allora devi estendere il class loader esistente. Ad esempio, gli AppletViewers caricano le classi da un server web remoto.
Metodi Java ClassLoader
- Quando la JVM richiede una classe, invoca la funzione
loadClass()
del ClassLoader passando il nome completamente classificato della classe. - La funzione loadClass() chiama il metodo
findLoadedClass()
per verificare se la classe è già stata caricata o meno. È necessario per evitare il caricamento della stessa classe più volte. - Se la classe non è ancora stata caricata, delega la richiesta al ClassLoader genitore per caricare la classe.
- Se il ClassLoader genitore non trova la classe, invoca il metodo findClass() per cercare le classi nel sistema di file.
Esempio di ClassLoader personalizzato in Java
Creeremo il nostro ClassLoader estendendo la classe ClassLoader e sovrascrivendo il metodo loadClass(String name). Se il nome della classe inizia con com.journaldev
, allora la caricheremo usando il nostro class loader personalizzato; altrimenti, invocheremo il metodo loadClass()
del ClassLoader genitore per caricare la classe.
1. CCLoader.java
Questo è il nostro class loader personalizzato con i seguenti metodi.
private byte[] loadClassFileData(String name)
: Questo metodo leggerà il file di classe dal sistema di file in un array di byte.private Class<?> getClass(String name)
: Questo metodo chiamerà la funzione loadClassFileData() e, invocando il metodo defineClass() del genitore, genererà la Classe e la restituirà.public Class<?> loadClass(String name)
: Questo metodo è responsabile del caricamento della classe. Se il nome della classe inizia con com.journaldev (le nostre classi di esempio), la caricherà usando il metodo getClass(); altrimenti, invocherà la funzione loadClass() del genitore per caricarla.public CCLoader(ClassLoader parent)
: Questo è il costruttore, responsabile per impostare il ClassLoader genitore.
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 {
// Questo carica i dati del bytecode dal file
b = loadClassFileData(file);
// defineClass è ereditato dalla classe ClassLoader
// che converte un array di byte in una Classe. defineClass è Finale
// quindi non possiamo sovrascriverlo
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
Questa è la nostra classe di test con la funzione principale. Stiamo creando un’istanza del nostro ClassLoader e caricando classi di esempio utilizzando il suo metodo loadClass(). Dopo aver caricato la classe, stiamo utilizzando l’API di Reflection di Java per invocare i suoi metodi.
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);
// Il metodo sottostante viene utilizzato per verificare che Foo venga caricato
// dal nostro caricatore di classi personalizzato cioè CCLoader
Method printCL = clas.getMethod("printCL", null);
printCL.invoke(null, new Object[0]);
}
}
3. Foo.java e Bar.java
Queste sono le nostre classi di test che vengono caricate dal nostro caricatore di classi personalizzato. Hanno un metodo printCL()
, che viene invocato per stampare le informazioni del ClassLoader. La classe Foo sarà caricata dal nostro caricatore di classi personalizzato. Foo utilizza la classe Bar, quindi anche la classe Bar verrà caricata dal nostro caricatore di classi personalizzato.
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. Passi di esecuzione di Java Custom ClassLoader
Innanzitutto, compileremo tutte le classi tramite la riga di comando. Successivamente, eseguiremo la classe CCRun passando tre argomenti. Il primo argomento è il nome completamente qualificato della classe Foo che verrà caricata dal nostro class loader. Gli altri due argomenti vengono passati alla funzione principale della classe Foo e al costruttore di Bar. I passi di esecuzione e l’output saranno come segue.
$ 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
$
Se osservi l’output, sta cercando di caricare la classe com.journaldev.cl.Foo
. Poiché estende la classe java.lang.Object, sta cercando di caricare prima la classe Object. Quindi la richiesta arriva al metodo loadClass di CCLoader, che la delega alla classe genitore. Quindi i class loader genitori caricano le classi Object, String e altre classi Java. Il nostro ClassLoader carica solo le classi Foo e Bar dal sistema di file. È chiaro dall’output della funzione printCL(). Possiamo modificare la funzionalità loadClassFileData() per leggere l’array di byte da un server FTP o invocare un servizio di terze parti per ottenere l’array di byte della classe al volo. Spero che l’articolo sia utile per comprendere il funzionamento del ClassLoader di Java e come possiamo estenderlo per fare molto di più rispetto al semplice prenderlo dal sistema di file.
Creazione di un ClassLoader personalizzato come ClassLoader predefinito
Possiamo impostare il nostro class loader personalizzato come predefinito quando JVM si avvia utilizzando le opzioni di Java. Ad esempio, eseguirò nuovamente il programma ClassLoaderTest dopo aver fornito l’opzione del classloader di Java.
$ 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
$
Il CCLoader sta caricando la classe ClassLoaderTest perché è nel pacchetto com.journaldev
.
Puoi scaricare il codice di esempio del ClassLoader dal nostro Repository GitHub.
Source:
https://www.digitalocean.com/community/tutorials/java-classloader