Java ClassLoader

Java ClassLoader – один из ключевых, но редко используемых компонентов в разработке проектов. Я никогда не расширял ClassLoader в своих проектах. Однако идея иметь свой собственный ClassLoader, который может настраивать загрузку классов в Java, кажется захватывающей. Этот материал предоставит обзор Java ClassLoader, а затем перейдет к созданию собственного загрузчика классов в Java.

Что такое Java ClassLoader?

Мы знаем, что программа на Java выполняется в виртуальной машине Java Virtual Machine (JVM). При компиляции Java-класса JVM создает байткод, который является платформенно-независимым. Байткод сохраняется в файле .class. При попытке использовать класс ClassLoader загружает его в память.

Типы встроенных загрузчиков классов

Существует три типа встроенных загрузчиков классов в Java.

  1. Загрузчик Bootstrap – Загружает внутренние классы JDK. Он загружает файл rt.jar и другие основные классы, например, классы пакета java.lang.*.
  2. Загрузчик расширений – Загружает классы из каталога расширений 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 ClassLoader?

Давайте понять, как работают загрузчики классов по выводу программы выше.

  • Загрузчик классов java.util.HashMap имеет значение null, что отражает Загрузчик Бутстрэпа. Загрузчик класса DNSNameService относится к ExtClassLoader. Поскольку сам класс находится в CLASSPATH, загрузчик классов системы загружает его.
  • Когда мы пытаемся загрузить HashMap, наш System ClassLoader передает это Extension ClassLoader. Extension class loader делегирует это Bootstrap ClassLoader. Bootstrap class loader находит класс HashMap и загружает его в память JVM.
  • Тот же процесс следуется для класса DNSNameService. Но Bootstrap ClassLoader не может найти его, так как он находится в $JAVA_HOME/lib/ext/dnsns.jar. Поэтому его загружает Extensions Classloader.
  • Класс Blob включен в MySql JDBC Connector jar (mysql-connector-java-5.0.7-bin.jar), который присутствует в пути сборки проекта. Его также загружает System Classloader.
  • Классы, загруженные дочерним загрузчиком классов, имеют видимость в классы, загруженные его родительскими загрузчиками классов. Так что классы, загруженные System Classloader, имеют видимость в классы, загруженные Extensions и Bootstrap Classloader.
  • Если есть дочерние загрузчики классов, они не могут получить доступ к классам, загруженным друг другом.

Почему писать собственный ClassLoader в Java?

ClassLoader по умолчанию в Java может загружать классы из локальной файловой системы, что достаточно для большинства случаев. Однако, если вы ожидаете класс во время выполнения или из FTP-сервера или через сторонний веб-сервис во время загрузки класса, то вам необходимо расширить существующий загрузчик классов. Например, AppletViewer загружает классы с удаленного веб-сервера.

Методы Java ClassLoader

  • Когда JVM запрашивает класс, он вызывает функцию loadClass() у ClassLoader, передавая полное имя класса.
  • Функция loadClass() вызывает метод findLoadedClass(), чтобы проверить, был ли класс уже загружен. Это необходимо, чтобы избежать многократной загрузки одного и того же класса.
  • Если класс еще не был загружен, то запрос передается родительскому ClassLoader для загрузки класса.
  • Если родительский ClassLoader не находит класс, то вызывается метод findClass() для поиска классов в файловой системе.

Пример пользовательского ClassLoader в Java

Мы создадим собственный ClassLoader, расширив класс ClassLoader и переопределив метод loadClass(String name). Если имя класса начнется с com.journaldev, то мы загрузим его, используя наш собственный загрузчик классов, в противном случае мы вызовем метод загрузки класса родительского ClassLoader loadClass() для загрузки класса.

1. CCLoader.java

Это наш собственный загрузчик классов со следующими методами.

  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);
            // defineClass унаследован от класса ClassLoader
            // который преобразует массив байт в Class. 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(). После загрузки класса мы используем Java Reflection API для вызова его методов.

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 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. Таким образом, запрос поступает в метод loadClass CCLoader, который делегирует его родительскому классу. Таким образом, загрузчики родительских классов загружают Object, String и другие классы Java. Наш ClassLoader загружает только классы Foo и Bar из файловой системы. Это видно из вывода функции printCL(). Мы можем изменить функциональность loadClassFileData() для чтения массива байтов с FTP-сервера или вызова любого сервиса третьей стороны для получения массива байтов класса “на лету”. Надеюсь, статья будет полезна для понимания работы Java 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.

Вы можете скачать пример кода загрузчика классов из нашего репозитория GitHub.

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