ClassLoader של ג'אווה

טוען המחלקות של ג'אווה הוא אחד הרכיבים החשובים אך המעט משמשים בפיתוח פרויקטים. אני לא הררתי טוען המחלקות באף אחד מהפרויקטים שלי. אבל, הרעיון ליצור טוען מחלקות מותאם אישית שיכול להתאים את טעינת המחלקות בג'אווה הוא מרתק. מאמר זה יספק סקירה על טוען המחלקות בג'אווה ויעבור אל יצירת טוען מחלקות מותאם אישית בג'אווה.

מהו טוען המחלקות בג'אווה?

אנחנו יודעים שתכנית ג'אווה רצה על ידי מכונת וירטואלית של ג'אווה (JVM). כאשר אנחנו מקמפים מחלקה בג'אווה, ה-JVM יוצרת את הבייטקוד, שהוא תלוי בפלטפורמה ואינו תלוי במכונה. הבייטקוד מאוחסן בקובץ .class. כאשר אנחנו מנסים להשתמש במחלקה, טוען המחלקות טוען אותה לזיכרון.

סוגי טועני המחלקות המובנים

יש שלושה סוגים של טועני מחלקות מובנים בג'אווה.

  1. Bootstrap Class Loader – טוען מחלקות זה טוען מחלקות פנימיות של ה-JDK. טוען זה טוען את קבצי ה-JAR ואת מחלקות הליבה האחרות, לדוגמה מחלקות בחבילת java.lang.*.
  2. Extensions Class Loader – טוען מחלקות זה טוען מחלקות מספר הרחבות של ה-JDK, בדרך כלל מתיקיית $JAVA_HOME/lib/ext.
  3. מטען מחלקה של המערכת – מטען זה טוען מחלקות מנתיב המחלקות הנוכחי. אנחנו יכולים להגדיר את נתיב המחלקות בעת הפעלת התוכנית באמצעות האפשרות -cp או -classpath בשורת הפקודה.

היררכיה של מטעני מחלקות

מטען המחלקות הוא היררכי בטעינת מחלקה לזיכרון. כל פעם שנשלחת בקשה לטעון מחלקה, המטען מעביר את הבקשה למטען האב. בכך נשמרת הייחודיות בסביבת הריצה. אם מטען האב אינו מוצא את המחלקה, המטען מנסה לטעון את המחלקה עצמו. בואו נבין זאת על ידי ביצוע התכנית ה-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());

    }

}

פלט:

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

איך מטען המחלקות של Java עובד?

בואו נבין את פעולת מטעני המחלקות מתוך פלט התכנית הקודמת.

  • מטען המחלקות של ה-java.util.HashMap מופיע כ- null, מה שמשקף מטען Bootstrap. מטען המחלקות של ה-class DNSNameService הוא ExtClassLoader. מטען המחלקות של המחלקה System טוען אותה מכיוון שהמחלקה עצמה נמצאת ב-CLASSPATH.
  • כאשר אנחנו מנסים לטעון את HashMap, מנגנון הטעינה של ClassLoader המערכת שלנו מעביר את זה ל- Extension ClassLoader. ClassLoader של ההרחבה מעביר את זה ל- Bootstrap ClassLoader. ClassLoader של הסיסמה מוצא את המחלקה HashMap ומטעין אותה לזיכרון ה- JVM.
  • אותו תהליך מתקיים גם עבור המחלקה DNSNameService. אך, ClassLoader של הסיסמה לא מצליח לאתר אותה מכיוון שהיא נמצאת ב־$JAVA_HOME/lib/ext/dnsns.jar. לכן, היא נטענת על ידי Extensions ClassLoader.
  • מחלקת Blob כלולה ב־JDBC Connector של MySql (mysql-connector-java-5.0.7-bin.jar), שנמצא בנתיב הבניה של הפרויקט. היא גם נטענת על ידי ClassLoader של המערכת.
  • מחלקות שנטענות על ידי ClassLoader של ילד יכולות לראות מחלקות שנטענות על ידי ClassLoader של ההורה שלו. לכן מחלקות שנטענות על ידי ClassLoader של המערכת יכולות לראות מחלקות שנטענות על ידי Extensions ו- Bootstrap ClassLoader.
  • אם ישנם ClassLoaderים תחתונים זה לזה, אז הם לא יכולים לגשת למחלקות שנטענות על ידי הזהים.

למה לכתוב ClassLoader מותאם אישית ב-Java?

טעינת הכיתות בתוך ג'אווה נעשית באמצעות ClassLoader בררת המחדל של השפה, שיכול לטעון כיתות מהקובץ המקומי במערכת הקבצים, וזה מספיק עבור רוב המקרים. אבל, אם אתה מצפה לטעון כיתה במהלך ריצה, מהשרת של FTP או דרך שירות אינטרנטי של צד שלישי בזמן טעינת הכיתה, יהיה עליך להרחיב את ClassLoader הקיים. לדוגמה, AppletViewers מטעינים את הכיתות משרת אינטרנט מרוחק.

שיטות של ClassLoader בג'אווה

  • כאשר JVM מבקשת כיתה, הוא מפעיל את הפונקציה loadClass() של ClassLoader ומעביר לה את שם הכיתה המלא.
  • הפונקציה loadClass() קוראת לשיטה findLoadedClass() כדי לבדוק האם הכיתה כבר טעונה או לא. זה נדרש כדי למנוע טעינה כפולה של אותה הכיתה.
  • אם הכיתה טרם נטענה, הוא יעביר את הבקשה ל-ClassLoader האב שלו כדי לטעון את הכיתה.
  • אם ה-ClassLoader האב אינו מוצא את הכיתה, הוא יפעיל את שיטת ה-findClass() כדי למצוא את הכיתות במערכת הקבצים.

דוגמא ל-Custom ClassLoader בג'אווה

ניצור את המחלקה שלנו ClassLoader על ידי הרחבת מחלקת ClassLoader ודריסת השיטה loadClass(String name). אם שם המחלקה מתחיל ב־com.journaldev נטען אותה באמצעות ה־class loader המותאם אישית שלנו, אחרת נפעיל את השיטה loadClass() של ClassLoader האב כדי לטעון את המחלקה.

1. CCLoader.java

זוהי ה־class loader המותאמת אישית שלנו עם השיטות הבאות.

  1. private byte[] loadClassFileData(String name): שיטה זו תקרא את קובץ המחלקה ממערכת הקבצים אל מערך של בתי־מספר.
  2. private Class<?> getClass(String name): שיטה זו תקרא לפונקציית loadClassFileData() ועל ידי קריאה לשיטת defineClass() של האב, היא תיצור את המחלקה ותחזיר אותה.
  3. public Class<?> loadClass(String name): שיטה זו אחראית לטעון את המחלקה. אם שם המחלקה מתחיל ב־com.journaldev (המחלקות הדוגמה שלנו), היא תטען אותה באמצעות שיטת getClass(), אחרת תפעיל את השיטה loadClass() של המחלקה ClassLoader האב כדי לטעון אותה.
  4. public CCLoader(ClassLoader parent): זוהי הבנאי האחראי להגדיר את המחלקה האב.
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 {
            // טוען את נתוני הקוד בייט מהקובץ
            b = loadClassFileData(file);
            // defineClass מורשה מכת המטען של ClassLoader
            // הממירת מערך בייט לקלאס. defineClass היא סופית
            // לכן אין אנו יכולים לדרוס אותה
            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

זהו המחלקה הבדיקה שלנו עם הפונקציה הראשית main. אנחנו יוצרים מופע של הטוען שלנו וטוען מחלקות דוגמה באמצעות השיטה loadClass שלו. לאחר טעינת המחלקה, אנחנו משתמשים ב-API Reflection של ג'אווה כדי לקרוא לשיטותיה.

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);

        // השיטה למטה משמשת לבדיקה שהמחלקה Foo נטענת
        // על ידי הטוען המותאם שלנו, כלומר CCLoader
        Method printCL = clas.getMethod("printCL", null);
        printCL.invoke(null, new Object[0]);
    }
 
}

3. Foo.java ו- Bar.java

אלו הם המחלקות הבדיקה שלנו שמותאמות על ידי הטוען המותאם שלנו. יש להן את השיטה printCL() שמופעלת כדי להדפיס את מידע הטוען של המחלקה. המחלקה Foo תיטען על ידי הטוען המותאם שלנו. המחלקה Foo משתמשת במחלקה Bar, ולכן המחלקה Bar תיטען גם על ידי הטוען המותאם שלנו.

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. שלבי ביצוע טוען מחדש מותאם אישית של ג'אווה

ראשית כל, נקמפל את כל המחלקות באמצעות שורת הפקודה. לאחר מכן, נפעיל את המחלקה CCRun על ידי העברת שלושה ארגומנטים. הארגומנט הראשון הוא שם מלא מסוים עבור מחלקת Foo שתיטען על ידי טוען המחלקות שלנו. שני הארגומנטים האחרים מועברים לפונקציית ה main של מחלקת Foo ולבנאי של מחלקת Bar. שלבי הביצוע והפלט יהיו כמו בהמשך.

$ 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
$

אם תביט בפלט, התוכנית מנסה לטעון את המחלקה com.journaldev.cl.Foo. מכיוון שהיא מרחיבה את מחלקת java.lang.Object, היא מנסה תחילה לטעון את מחלקת Object. לכן, הבקשה מתפנה לשיטת הטעינה loadClass של CCLoader, שמפנה אותה לטעינה על ידי הטוענים הראשיים. לכן, טועני המחלקות הראשיים טוענים את המחלקות Object, String ומחלקות ג'אווה אחרות. טוען המחלקות שלנו טוען רק את המחלקות Foo ו-Bar ממערכת הקבצים. זה ברור מהפלט של פונקציית ה- printCL. אנחנו יכולים לשנות את פונקציית ה- loadClassFileData כדי לקרוא למערכת המידע ממערכת ה- FTP או על ידי הפעלת שירות שלישי כלשהו לקבלת מערך הבייטים של המחלקה בזמן הריצה. אני מקווה שהמאמר יהיה שימושי בהבנת הפעולה של טוען המחלקות בג'אווה ואיך ניתן להרחיב אותו כדי לעשות הרבה יותר מאשר רק לקחת ממערכת הקבצים.

יצירת ClassLoader מותאם אישית כקודם על פני ClassLoader ברירת המחדל

אנו יכולים להפוך את ClassLoader המותאם אישית שלנו לקודם על פני הקודם כאשר ה-JVM מתחיל באמצעות אפשרויות ה-Java. לדוגמה, אני אריץ שוב את התוכנית ClassLoaderTest לאחר שאני נותן את אפשרות ה-Java ClassLoader.

$ 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
$

CCLoader טוען את ה-ClassLoaderTest מחלקה מכיוון שהיא נמצאת בחבילת com.journaldev.

ניתן להוריד את קוד הדוגמה של ClassLoader מתוך מאגר הקוד שלנו ב-GitHub.

Source:
https://www.digitalocean.com/community/tutorials/java-classloader