Статьи

Понимание и расширение Java ClassLoader

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

Эта статья предоставит обзор загрузки классов Java, а затем перейдет к созданию пользовательского ClassLoader и его использованию.

Что такое ClassLoader?

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

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

  1. Bootstrap Class Loader — загружает внутренние классы JDK, обычно загружает rt.jar и другие базовые классы, например, java.lang. * Классы пакетов
  2. Загрузчик классов расширений — загружает классы из каталога расширений JDK, обычно из каталога $ JAVA_HOME / lib / ext.
  3. System Class Loader — загружает классы из текущего пути к классам, которые могут быть установлены при вызове программы с использованием параметров командной строки -cp или -classpath.

Давайте лучше поймем это, выполнив следующую Java-программу:

ClassLoaderTest.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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());
  
    }
  
}

Вывод вышеуказанной программы:

загрузчик классов для HashMap: null
загрузчик классов для DNSNameService: sun.misc.Launcher$ExtClassLoader@51f12c4e
загрузчик класса для этого класса: sun.misc.Launcher$AppClassLoader@799134f4
sun.misc.Launcher$AppClassLoader@799134f4

Как вы можете видеть, thr java.util.HashMap ClassLoader прибывает как ноль, который отражает Bootstrap ClassLoader, тогда как DNSNameService ClassLoader является ExtClassLoader. Поскольку сам класс находится в CLASSPATH, System ClassLoader загружает его.

Когда мы пытаемся загрузить HashMap, наш System ClassLoader делегирует его расширению ClassLoader, которое, в свою очередь, делегирует его Bootstrap ClassLoader, который нашел класс и загрузил его в JVM. Тот же процесс выполняется для класса DNSNameService, но Bootstrap ClassLoader не может найти его, поскольку он находится в $ JAVA_HOME / lib / ext / dnsns.jar и, следовательно, загружается загрузчиком классов расширений.

Еще один важный момент, на который следует обратить внимание, заключается в том, что классы, загружаемые загрузчиком дочерних классов, имеют видимость классов, загружаемых его загрузчиками родительских классов. Таким образом, классы, загруженные System ClassLoader, имеют видимость классов, загружаемых Extensions и Bootstrap ClassLoader.

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

Зачем писать ClassLoader?

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

Как работает ClassLoader?

Когда JVM запрашивает класс, он вызывает функцию loadClass класса ClassLoader, передавая полностью классифицированное имя класса.

Функция loadClass вызывает метод findLoadedClass () для проверки того, что класс уже загружен или нет. Необходимо избегать загрузки класса несколько раз.

Если класс еще не загружен, он делегирует запрос родительскому ClassLoader для загрузки класса.

Если родительский ClassLoader не находит Class, он вызывает метод findClass () для поиска классов в файловой системе.

Создание нашего собственного ClassLoader

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

Структура проекта будет похожа на изображение ниже:

CCLoader.java: это наш пользовательский загрузчик классов с методами ниже.

1. приватный байт [] loadClassFileData (имя строки)

Этот метод будет читать файл класса из файловой системы в байтовый массив.

2. закрытый класс getClass (имя строки)

Этот метод вызовет функцию loadClassFileData () и, вызвав родительский метод defineClass (), сгенерирует класс и вернет его.

3. открытый класс loadClass (имя строки)

Этот метод отвечает за загрузку класса. Если имя класса начинается с com.journaldev (наши примеры классов), то оно загрузит его с помощью метода getClass (), иначе вызовет родительскую функцию loadClass для его загрузки.

4. публичный CCLoader (родительский класс ClassLoader)

Это конструктор, который отвечает за установку родительского ClassLoader.

Исходный код CCLoader:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
  
/**
 * Our Custom Class Loader 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 class, for example com.journaldev.Foo
     */
    private Class getClass(String name) throws ClassNotFoundException {
        String file = name.replace('.', File.separatorChar) + ".class";
        byte[] b = null;
        try {
            // This loads the byte code data from the file
            b = loadClassFileData(file);
            // defineClass is inherited from the ClassLoader class
            // that converts byte array into a Class. defineClass is Final
            // so we cannot override it
            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 its not in Classpath to avoid
     * any confusion.
     *
     * @param name
     *            File name
     * @return Byte array read from the file
     * @throws IOException
     *             if any 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;
    }
}

CCRun.java:

Это наш тестовый класс с функцией main, где мы создаем объект нашего ClassLoader и загружаем примеры классов, используя его метод loadClass. После загрузки класса мы используем Java Reflection API для вызова его методов.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
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);
        // Below method is used to check that the Foo is getting loaded
        // by our custom class loader i.e CCLoader
        Method printCL = clas.getMethod("printCL", null);
        printCL.invoke(null, new Object[0]);
    }
  
}

Foo.java и Bar.java:

Это наши тестовые классы, которые загружаются нашим пользовательским загрузчиком классов. У них также есть метод printCL (), который вызывается для печати ClassLoader, который загрузил класс. Класс Foo будет загружен нашим пользовательским загрузчиком классов, который в свою очередь использует класс Bar, поэтому класс Bar также будет загружен нашим пользовательским загрузчиком классов.

Исходный код Foo.java:

01
02
03
04
05
06
07
08
09
10
11
12
13
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());
    }
}

Исходный код Bar.java:

01
02
03
04
05
06
07
08
09
10
11
12
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());
    }
}

Шаги выполнения:

Прежде всего мы скомпилируем все классы через командную строку. После этого мы запустим класс CCRun, передав три аргумента. Первый аргумент — полностью классифицированное имя для класса Foo, которое будет загружено нашим загрузчиком классов. Два других аргумента передаются главной функции класса Foo и конструктору Bar. Шаги выполнения с выводом будут как ниже.

Pankaj $ javac -cp. ком / journaldev / мл / Foo.java
Pankaj $ javac -cp. ком / journaldev / мл / Bar.java
Pankaj $ javac CCLoader.java
Pankaj $ javac CCRun.java
CCRun.java:18: предупреждение: не-varargs вызов метода varargs с неточным типом аргумента для последнего параметра;
приведение к java.lang. Класс для вызова varargs
приведите к java.lang.Class [] для вызова не varargs и подавления этого предупреждения
Метод printCL = clas.getMethod («printCL», null);
^
1 предупреждение
Pankaj $ java CCRun com.journaldev.cl.Foo 1212 1313
Загрузка класса ‘com.journaldev.cl.Foo’
Загрузка класса с помощью CCLoader
Загрузка класса ‘java.lang.Object’
Загрузка класса ‘java.lang.String’
Загрузка класса ‘java.lang.Exception’
Загрузка класса ‘java.lang.System’
Загрузка класса ‘java.lang.StringBuilder’
Загрузка класса ‘java.io.PrintStream’
Foo Constructor 1212 1313
Загрузка класса ‘com.journaldev.cl.Bar’
Загрузка класса с помощью CCLoader
Бар Конструктор 1212 1313
Загрузка класса ‘java.lang.Class’
Bar ClassLoader: CCLoader @ 71f6f0bf
Foo ClassLoader: CCLoader @ 71f6f0bf
ctk-pcs1313512-2: источник pk93229 $

Если вы внимательно изучите вывод, сначала он пытается загрузить класс com.journaldev.cl.Foo, но, поскольку он расширяет класс java.lang.Object, он сначала пытается загрузить его, а запрос поступает в метод CCCoader loadClass, который делегирует это в родительский класс. Таким образом, загрузчики родительских классов загружают Object, String и другие Java-классы. Наш ClassLoader загружает только классы Foo и Bar из файловой системы, которая становится понятной, когда мы вызываем их функцию printCL ().

Обратите внимание, что мы можем изменить функциональность loadClassFileData () для чтения байтового массива с FTP-сервера или путем вызова любой сторонней службы для получения байтового массива класса на лету.

Ссылка: вопросы интервью Java: Понимание и расширение Java ClassLoader от нашего партнера JCG Панкаджа .

Статьи по Теме: