جافا كلاسلودر

تعد Java ClassLoader واحدة من المكونات الحاسمة ولكن النادر استخدامها في تطوير المشاريع. لم أمتد ClassLoader في أي من مشاريعي من قبل. ولكن، فكرة إنشاء ClassLoader خاص بي الذي يمكنه تخصيص تحميل الصف الجافا مثيرة للإعجاب. ستقدم هذه المقالة نظرة عامة على Java ClassLoader ثم تتقدم لإنشاء Class Loader مخصص في Java.

ما هو Java ClassLoader؟

نحن نعلم أن برنامج Java يعمل على الـJava Virtual Machine (JVM). عندما نقوم بتجميع فئة Java، تقوم JVM بإنشاء البايت كود، الذي هو مستقل عن المنصة والجهاز. يتم تخزين البايت كود في ملف .class. عند محاولة استخدام فئة، يقوم ClassLoader بتحميلها إلى الذاكرة.

أنواع ClassLoader المضمنة

هناك ثلاثة أنواع من ClassLoader المضمنة في Java.

  1. Bootstrap Class Loader – يقوم بتحميل فئات JDK الداخلية. يحمل ملف rt.jar وفئات الحزمة java.lang.* الأخرى على سبيل المثال.
  2. Extensions Class Loader – يقوم بتحميل الفئات من دليل الامتدادات JDK، عادة ما يكون في دليل $JAVA_HOME/lib/ext.
  3. محمّل الصنف للنظام – هذا المحمل يقوم بتحميل الصنف من مسار الصنف الحالي. يمكننا تعيين مسار الصنف أثناء استدعاء برنامج باستخدام الخيار -cp أو -classpath على سطر الأوامر.

تسلسل محملات الصنف

المحمل الفرعي هرمي في تحميل صنف إلى الذاكرة. كلما تمت طلب تحميل صنف، يحيله إلى محمل الصنف الأب. هكذا يتم الحفاظ على الفرادة في بيئة التشغيل. إذا لم يجد محمل الصنف الأب الصنف، فإنه يحاول تحميل الصنف بنفسه. دعونا نفهم هذا من خلال تنفيذ البرنامج جافا التالي.

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.util.HashMap كقيمة null، مما يعكس محمل الصنف Bootstrap. محمل الصنف لصنف DNSNameService هو ExtClassLoader. نظرًا لأن الصنف نفسه موجود في CLASSPATH، يقوم محمل الصنف للنظام بتحميله.
  • عندما نحاول تحميل HashMap، يقوم محمّل النظام بتفويض ذلك إلى محمّل فئة التوسيع. يقوم محمّل فئة التوسيع بتفويض ذلك إلى محمّل فئة Bootstrap. يجد محمّل فئة Bootstrap فئة HashMap ويحمّلها في ذاكرة JVM.
  • يتبع نفس العملية لفئة DNSNameService. ولكن، محمّل فئة Bootstrap غير قادر على تحديد موقعها لأنها في $JAVA_HOME/lib/ext/dnsns.jar. لذا، يتم تحميلها بواسطة محمّلات التوسيع.
  • تم تضمين فئة Blob في ملف جر MySql JDBC Connector (mysql-connector-java-5.0.7-bin.jar)، الذي يتواجد في مسار البناء للمشروع. كما يتم تحميلها بواسطة محمّل النظام.
  • الفئات التي تحمّلها محمّل فئة فرعية لها رؤية إلى الفئات التي تم تحميلها بواسطة محمّلات الفئة الأب. لذا فإن الفئات التي تم تحميلها بواسطة محمّل النظام لديها رؤية إلى الفئات التي تم تحميلها بواسطة محمّلات التوسيع و Bootstrap.
  • إذا كانت هناك محمّلات فئة شقيقة فإنها لا يمكن أن تصل إلى الفئات التي تم تحميلها بواسطة بعضها البعض.

لماذا كتابة محمّل فئة مخصص في جافا؟

تستطيع ClassLoader الافتراضي في Java تحميل الفصول من نظام الملفات المحلي، وهذا يكون كافيًا بشكل جيد في معظم الحالات. ولكن، إذا كنت تتوقع فئة في وقت التشغيل أو من خلال خادم FTP أو عبر خدمة ويب من طرف ثالث أثناء تحميل الفئة، فيجب عليك توسيع ClassLoader الحالي. على سبيل المثال، يقوم مشاهدو التطبيقات بتحميل الفصول من خادم ويب عن بُعد.

طرق ClassLoader في Java

  • عندما يطلب JVM فئة، يقوم باستدعاء وظيفة loadClass() في ClassLoader عن طريق تمرير الاسم المصنف بالكامل للفئة.
  • تقوم وظيفة loadClass() باستدعاء طريقة findLoadedClass() للتحقق مما إذا كانت الفئة قد تم تحميلها بالفعل أم لا. وهذا يتطلب لتجنب تحميل نفس الفئة عدة مرات.
  • إذا لم يتم تحميل الفئة بالفعل، فسيقوم بتفويض الطلب إلى ClassLoader الأب لتحميل الفئة.
  • إذا لم يجد ClassLoader الأب الفئة، فسيستدعي طريقة findClass() للبحث عن الفصول في نظام الملفات.

مثال على ClassLoader مخصص في Java

سنقوم بإنشاء ClassLoader الخاص بنا عن طريق تمديد فئة ClassLoader واستبدال طريقة loadClass(String name). إذا بدأ اسم الفئة من com.journaldev ، فسنقوم بتحميلها باستخدام ClassLoader المخصص لدينا. وإلا ، سنقوم باستدعاء طريقة تحميل الفئة الأصلية الأم loadClass() لتحميل الفئة.

1. CCLoader.java

هذا هو ClassLoader المخصص الخاص بنا مع الأساليب التالية.

  1. private byte[] loadClassFileData(String name): ستقوم هذه الطريقة بقراءة ملف الفئة من نظام الملفات إلى مصفوفة بايت.
  2. private Class<?> getClass(String name): ستقوم هذه الطريقة باستدعاء وظيفة loadClassFileData() وعن طريق استدعاء طريقة defineClass() الأم الأصلية ، ستنشئ الفئة وتعيدها.
  3. public Class<?> loadClass(String name): تتولى هذه الطريقة مسؤولية تحميل الفئة. إذا بدأ اسم الفئة بـ com.journaldev (فئاتنا المثالية) ، فسيتم تحميلها باستخدام طريقة getClass() وإلا فسيتم استدعاء وظيفة تحميل الفئة الأم loadClass() لتحميلها.
  4. public CCLoader(ClassLoader parent): هذا هو البناء، الذي يسؤول عن تعيين ال 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 {
            // يقوم هذا بتحميل بيانات البايت القابلة للتنفيذ من الملف 
            b = loadClassFileData(file);
             // تمت الإرث من الفئة ClassLoader 
             // التي تحويل مصفوفة البايت إلى فئة. defineClass هو Final 
             // لذلك لا يمكننا تجاوزه 
            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

هذه هي فئتنا الاختبارية مع وظيفة رئيسية. نحن ننشئ مثيلًا من ClassLoader الخاص بنا ونحمل فئات عينة باستخدام طريقته loadClass(). بعد تحميل الفئة، نستخدم واجهة برمجة التطبيقات 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()، التي يتم استدعاؤها لطباعة معلومات ClassLoader. ستتم تحميل فئة 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. خطوات تنفيذ Java Custom ClassLoader

أولاً وقبل كل شيء، سنقوم بتجميع جميع الفصول من خلال سطر الأوامر. بعد ذلك، سنقوم بتشغيل فصل 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 أولاً. لذا، الطلب قادم إلى طريقة تحميل الفئة CCLoader loadClass، التي تقوم بتفويضها إلى الفئة الأم. لذا، محملات الفئة الأم تقوم بتحميل Object، String، وغيرها من فئات Java. محمل الفصل الخاص بنا يقوم فقط بتحميل فصل Foo وفصل Bar من نظام الملفات. من الواضح من إخراج وظيفة printCL() يمكننا تغيير وظيفة loadClassFileData() لقراءة مصفوفة البايت من خادم FTP أو عن طريق استدعاء أي خدمة طرف ثالث للحصول على مصفوفة البايت للفئة على الطاير. آمل أن يكون المقال مفيدًا في فهم عمل Java ClassLoader وكيف يمكننا توسيعه للقيام بأشياء أكثر من مجرد أخذه من نظام الملفات.

جعل ClassLoader المخصص كـ ClassLoader افتراضي

يمكننا جعل محمل الصف المخصص الخاص بنا كمحمل افتراضي عند بدء JVM باستخدام خيارات Java. على سبيل المثال، سأقوم بتشغيل برنامج ClassLoaderTest مرة أخرى بعد توفير خيار 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