пятница, 11 января 2008 г.

Пишем свой загручик java-классов

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


Введение
Как известно, Java как платформа состоит из двух частей: компилятора исходного кода в байт-код и интерпретатора байткода (я знаю что в некоторых реализациях - компилятора, но мы рассмотрим общий случай). Понятно, что прежде чем некий код интерпретировать его необходимо загрузить в ОЗУ компьютера. В Java реализована модель так называемой динамической загрузки классов (она же - модель позднего связывания). Динамическая загрузка классов в Java обладает следующими особенностями:

1. Отложенная (lazy) загрузка. Каждый класс загружается в память только при необходимости.

2. Проверка корректности загружаемого кода (type safeness). Все действия, связанные с проверкой валидности кода выполняются на этапе загрузки.

3. Программируемая загрузка. Программист может написать свои загрузчики классов, которые подгружают классы например из БД, архива, по сети или еще каким-либо образом.

4. Множественные пространства имен. Каждый загрузчик имеет свое пространство имен для создаваемых классов. Т.е. если классы одинаковы и находятся в одном пакете, но загружаются разными загрузчиками - они считаются разными.

Очень подробно модель динамической загрузки и модель делегирования загрузки описаны в статье на сайте питерских тестировщиков JVM. Я считаю нецелесообразным перепечатывать эту статью, но настоятельно рекомендую прочесть ее, чтобы понимать дальнейшее.

Пишем свой загрузчик

Собственно лучше один раз попробовать, чем десять - прочитать. Поэтому попробуем написать загрузчик классов из Jar-файла. Я знаю что системный загрузчик умеет загружать классы из jar-файлов, но только тех, которые указаны в CLASSPATH. Что же делать, если jar-файл не указан в CLASSPATH, например это - плагин? Вот загрузчик, решающий эту проблему, мы и напишем.

Но прежде чем писать, необходимо продумать алгоритм загрузчика и требования к нему. Поэтому разберемся как работает системный загрузчик.

Вообще в начале работы приложения создаются три загрузчика:

  • базовый загрузчик (bootstrap/primodial class loader)
  • загрузчик расширений (extention class loader)
  • системный загрузчик (system/application class loader)

Собственно наиболее важен для нас именно системный загрузчик, именно он загружает классы из CLASSPATH. Загрузка классов происходит по следующему алгоритму:

1.  Поиск в списке ранее загруженных классов. Проверяется, запрашивался ли данный класс ранее. В случае, если запрашивался загружается из кэша.

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

3. Загружаем класс сами. Если родительский загрузчик не смог загрузить запрошенный класс, текущий загрузчик сам производит процесс загрузки требуемого класса - находит байт-код и на его основе создает класс.

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

С алгоритмом определились, приступим непосредственно к написанию загрузчика. Собственно код загрузчика будет иметь вид:

package org.beq.classloader;



import java.io.IOException;

import java.io.InputStream;

import java.util.Enumeration;

import java.util.HashMap;

import java.util.jar.JarEntry;

import java.util.jar.JarFile;



/**

 * Загружаем файлы из заданного jar-архива

 * Классы должны относится к заданному пакету - пример валидации при загрузке

 *

 * @author Pavel

 *

 */


public class JarClassLoader extends ClassLoader {

    private HashMap<String, Class<?>> cache = new HashMap<String, Class<?>>();

   

    private String jarFileName;

   

    private String packageName;

       

    private static String WARNING = "Warning : No jar file found. Packet unmarshalling won't be possible. Please verify your classpath";



    public JarClassLoader(String jarFileName, String packageName) {    

        this.jarFileName = jarFileName;        

        this.packageName = packageName;

       

        cacheClasses();

    }



    /**

     * При создании загрузчика извлекаем все классы из jar и кэшируем в памяти

     *

     */


    private void cacheClasses() {

        try {           

            JarFile jarFile = new JarFile(jarFileName);

            Enumeration entries = jarFile.entries();

            while (entries.hasMoreElements()) {

                JarEntry jarEntry = (JarEntry) entries.nextElement();

                // Одно из назначений хорошего загрузчика - валидация классов на этапе загрузки

                if (match(normalize(jarEntry.getName()), packageName)) {                   

                    byte[] classData = loadClassData(jarFile, jarEntry);                   

                    if (classData != null) {

                        Class<?> clazz = defineClass(stripClassName(normalize(jarEntry.getName())), classData, 0, classData.length);

                        cache.put(clazz.getName(), clazz);

                        System.out.println("== class " + clazz.getName() + " loaded in cache");

                    }

                }

            }

        }

        catch (IOException IOE) {

            // Просто выведем сообщение об ошибке

            System.out.println(WARNING);

        }

    }

       

    /**

     * Собственно метод, который и реализует загрузку класса

     *

     */


    public synchronized Class<?> loadClass(String name) throws ClassNotFoundException {

        Class<?> result = cache.get(name);

       

        // Возможно класс вызывается не по полному имени - добавим имя пакета

        if (result == null)

            result = cache.get(packageName + "." + name);           

           

        // Если класса нет в кэше то возможно он системный

        if (result == null)

            result = super.findSystemClass(name);       

       

        System.out.println("== loadClass(" + name + ")");       

       

        return result;

    }



    /**

     * Получаем каноническое имя класса

     * @param className

     * @return

     */


    private String stripClassName(String className) {

        return className.substring(0, className.length() - 6);

    }



    /**

     * Преобразуем имя в файловой системе в имя класса

     * (заменяем слэши на точки)

     *

     * @param className

     * @return

     */


    private String normalize(String className) {

        return className.replace('/', '.');

    }



    /**

     * Валидация класса - проверят принадлежит ли класс заданному пакету и имеет ли

     * он расширение .class

     *  

     * @param className

     * @param packageName

     * @return

     */


    private boolean match(String className, String packageName) {

        return className.startsWith(packageName) && className.endsWith(".class");

    }   

   

    /**

     * Извлекаем файл из заданного JarEntry

     *

     * @param jarFile - файл jar-архива из которого извлекаем нужный файл

     * @param jarEntry - jar-сущность которую извлекаем

     * @return null если невозможно прочесть файл

     */


    private byte[] loadClassData(JarFile jarFile, JarEntry jarEntry) throws IOException {

        long size = jarEntry.getSize();     

        if (size == -1 || size == 0)

            return null;

       

        byte[] data = new byte[(int)size];

        InputStream in = jarFile.getInputStream(jarEntry);

        in.read(data);

       

        return data;

    }

}



Алгоритм работы данного загрузчика следующий: при создании загрузчика через конструктор вызывается метод private void cacheClasses(), который читает переданный jar-архив и загружает классы из него в кэш. Кэш представлен в виде HashMap где названию соответствует класс. Здесь же и происходит валидация - загружаются только те файлы, которые соответсвуют заданному пакету и имеют расширение .class

upd:
Метод defineClass служит для создания класса из загруженного байт-кода. Им осуществляется валидация загруженного байт-кода и анализ зависимостей.

Через переопределенный метод loadClass(String name) происходит использование загрузчика. В данном случае метод довольно прост - если не можем загрузить класс из кэша - значит он системный и делегируем его загрузку базовому загрузчику.

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

Использование созданного загрузчика

Одним из наиболее сложных моментов является использование созданного загрузчика. Вообще загрузчик можно использовать тремя способами: указава его как базовый для всего приложения через параметр командной строки -Djava.system.class.loader. Тем самым ваш загрузчик станет загрузчиком по-умолчанию.

Более подробно хочется рассмотреть способ программного указания загрузчика. Пусть наш загрузчик не является загрузчиком по-умолчанию. Тогда создать объект с его помощью можно следующим образом:

// Создаем загрузчик

JarClassLoader jarClassLoader = new JarClassLoader("beq.jar", "org.beq.classloader.classes.impl");

// Загружаем класс

Class<?> clazz = loadClass("JarSample");

// Создаем экземпляр класса

IJarSample sample = (IJarSample) clazz.newInstance();



sample.demo("Test");


Класс JarSample и интерфейс IJarSample имеют следующий код:

package org.beq.classloader.classes.impl;



import org.beq.classloader.classes.IJarSample;





/**

 * Демонстрационный класс, будет загружаться из Jar

 * @author Pavel

 *

 */


public class JarSample implements IJarSample {

    public JarSample() {

        System.out.println("JarSample::JarSample()");

    }

   

    public void demo(String str) {

        JarClass2Samle s = new JarClass2Samle();

        s.demo();

       

        System.out.println("JarSample::demo(String str)");     

        System.out.println(str);

    }

}

 


package org.beq.classloader.classes;



/**

 * Интерфейс к загружаемому классу

 *

 * @author Pavel

 *

 */


public interface IJarSample {

    public void demo(String; str);

}


Наиболее важно в данном коде то, что интерфейс обязательно должен загружаться системный загрузчиком. Как я уже говорил, классы загруженые разными загрузчиками для системы разные. Если интерфейс IJarSample будет загружаться нами написанным загрузчиком мы не сможем его использовать в классах, загружаемых системным загрузчиком - будем получать ClassCastException.

И, напоследок, хотелось бы отметить еще одну особенность: если класс загружается сторонним загрузчиком, то и все классы на которые он ссылается (например JarClass2Samle) будут загружаться этим же загрузчиком. Собственно данное правило вытекает из правила различных областей видимости - если бы класс загружался другим загрузчиком (например системным) он был бы просто недоступным.

Изложенная выше информация целиком и полностью основана на личных экспериментах и наблюдениях и не претендует на истину в последней инстанции. Я буду очень рад всем Вашим коментариям, особенно замечаниями и исправлениям допущенных неточностей.


upd:

Хотелось бы привести еще один пример загрузчика и его правильного использования.

Интерфейс:

package sexypackage;



public interface ISexyInterface {

    public void makeBar ();

}



Демо-класс, который будет грузится загрузчиком

package sexypackage;



public class SexyClassForLoader implements ISexyInterface {

    public static String stat_foo = "hello stat_foo";



    static {

        System.out.println("SexyClassForLoader$$static");

    }



    public SexyClassForLoader {

         System.out.println("SexyClassForLoader$$init");

    }



    public static String getStatFoo() {

         return stat_foo;

    }



    public String getSimpleFoo() {

        return simple_foo;

    }



    public String simple_foo = "hello simple_foo";



    public void makeBar() {

        System.out.println ("make bar");

    }

}


Как видем класс и интерфейс лежат в одном пакете, но мы сделаем так, чтобы они грузились разными загрузчиками.

Собственно загрузчик:

package jmxtest.test;



import java.io.File;

import java.io.FileInputStream;

import java.io.IOException;

import java.util.HashMap;



/**

* Класс-загрузчик

*/


class XLoader extends ClassLoader

{

    // карта отображения имен классов на файлы .class, где хранятся их определения

    HashMap mappings;



    XLoader(HashMap mappings)

    {

        this.mappings = mappings;

    }



    public synchronized Class loadClass(String name) throws ClassNotFoundException

    {

        try

        {

            System.out.println("loadClass (" + name + ")");

           

            // важно!

            // приоритет отдан именно загрузке с помощью встроенного загрузчика

            if (!mappings.containsKey(name))

            {

                return super.findSystemClass(name);

            }

           

            String fileName = mappings.get(name);

            FileInputStream fin = new FileInputStream(fileName);            

            byte[] bbuf = new byte[(int)(new File(fileName).length())];

            fin.read(bbuf);

            fin.close();

           

            return defineClass(name, bbuf, 0, bbuf.length);

        }

        catch (IOException e)

        {

            e.printStackTrace();

            throw new ClassNotFoundException(e.getMessage(), e);

        }

    }

}


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

Ну и собственно пример вызывающей программы:

package jmxtest.test;



import java.util.HashMap;



import sexypackage.ISexyInterface;



public class Loader

{

    public static void main(String[] args) throws Exception

    {



        HashMap mappings = new HashMap();

        mappings.put("sexypackage.SexyClassForLoader", "E:\\@Pavel\\MyTeach\\ClassLoader\\bin\\sexypackage\\SexyClassForLoader.class");



        // Если убрать комментарий - будет больно

        /*

            mappings.put("sexypackage.ISexyInterface",

            "путь\\classes\\sexypackage\\ISexyInterface.class");

        */




        XLoader xloa = new XLoader(mappings);

        Class sexy_cla = xloa.loadClass("sexypackage.SexyClassForLoader");

        System.out.println("class was loaded");

        System.out.println("begin object creation");

       

        Object sexy_ob = sexy_cla.newInstance();

        System.out.println("object was created");

        System.out.println("invoke: getFoo" + sexy_cla.getMethod("getSimpleFoo").invoke(sexy_ob));

        System.out.println("get: stat_foo" + sexy_cla.getField("stat_foo").get(sexy_ob));



        ISexyInterface local_sexy = (ISexyInterface) sexy_ob;

        local_sexy.makeBar();

    }

}


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

Результат работы программы:
loadClass (sexypackage.SexyClassForLoader)
loadClass (sexypackage.ISexyInterface)
loadClass (java.lang.Object)
class was loaded
begin object creation
loadClass (java.lang.System)
loadClass (java.io.PrintStream)
SexyClassForLoader$$static
SexyClassForLoader$$init
object was created
loadClass (java.lang.String)
invoke: getFoohello simple_foo
get: stat_foohello stat_foo
make bar


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

Большое спасибо black zorro за существенные и конструктивные замечания, а так же пример разумно построенного загрузчика.

Понравилось сообщение - подпишись на блог через RSS


16 комментариев:

Анонимный комментирует...

Правки к вашему материалу
----
Метод defineClass служит для создания класса из загруженного байт-кода. Ею осуществляется валидация загруженного байт-кода, анализ зависимостей и вызов секции статической инициализации.
----
если быть до конца верным, то стадия инициализации статических переменных и уж тем более, значений класса по умолчанию, никогда не делается на стадии чтения класса. Статика инициализируется при первом обращении к содержимому класса - к статическому полю или методу, или при создании первого экземпляра класса. А про обычные поля - даже говорить смешно.
Можно было бы возразить, что при использовании стандартного загрузчика классов разницы никакой не будет, но это не так. Но если писать собственные загрузчики, то такое заблуждение пагубно.
второе замечание:
-----
Наиболее важно в данном коде то, что интерфейс должен загружаться системный загрузчиком, а значит лежать не в том пакете, который будет обработан нашим загрузчиком.
-----
в каком пакете будет находиться интерфейс используемый классом не имеет ни малейшего значения. главное, чтобы его загрузили с помощью именно системного загрузчика. в приниципе дальше вы об этом говорите, но все же лучше переформулировать предложение, могут быть непонятки.

далее я приведу простенький пример, в котором загружаются класс и его интерфейс:

--- интерфейс ---
package sexypackage;

public interface ISexyInterface {
public void makeBar ();
}

--- класс ---

package sexypackage;


public class SexyClassForLoader implements ISexyInterface{
public static String stat_foo = "hello stat_foo";

static {
System.out.println("SexyClassForLoader$$static");
}

{
System.out.println("SexyClassForLoader$$init");
}


public static String getStatFoo() {
return stat_foo;
}

public String getSimpleFoo() {
return simple_foo;
}

public String simple_foo = "hello simple_foo";


public void makeBar() {
System.out.println ("make bar");
}
}
--- пример использования ---

package jmxtest.test;

import sexypackage.ISexyInterface;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;

/**
* Класс-загрузчик
*/
class XLoader extends ClassLoader {
// карта отображения имен классов на файлы .class, где хранятся их определения
HashMap < String , String > mappings;

XLoader(HashMap < String, String > mappings) {
this.mappings = mappings;
}

public synchronized Class < ? > loadClass(String name) throws ClassNotFoundException {
try {
System.out.println ("loadClass ("+name+")");
// приоритет отдан именно загрузке с помощью встроенного загрузчика
if (! mappings.containsKey(name)){
return super.findSystemClass(name);
}
String fileName = mappings.get(name);
FileInputStream fin = new FileInputStream(fileName);
byte[] bbuf = new byte [(int) (new File(fileName).length())];
fin.read(bbuf);
fin.close();
return defineClass(name, bbuf, 0, bbuf.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException(e.getMessage(), e);
}
}
}


public class loader {
public static void main(String[] args) throws Exception {

HashMap < String , String > mappings = new HashMap< String , String >();
mappings.put("sexypackage.SexyClassForLoader",
"путь\\classes\\sexypackage\\SexyClassForLoader.class");
/*
mappings.put("sexypackage.ISexyInterface",
"путь\\classes\\sexypackage\\ISexyInterface.class");

*/

XLoader xloa = new XLoader(mappings);
Class sexy_cla = xloa.loadClass("sexypackage.SexyClassForLoader");
System.out.println ("class was loaded");
System.out.println ("begin object creation");
Object sexy_ob = sexy_cla.newInstance();
System.out.println ("object was created");
System.out.println ("invoke: getFoo"+sexy_cla.getMethod("getSimpleFoo").invoke(sexy_ob));
System.out.println ("get: stat_foo"+sexy_cla.getField("stat_foo").get(sexy_ob));

ISexyInterface local_sexy = (ISexyInterface) sexy_ob;
local_sexy.makeBar();
}
}
--- результат вызова ---
loadClass (sexypackage.SexyClassForLoader)
loadClass (sexypackage.ISexyInterface)
loadClass (java.lang.Object)
class was loaded
begin object creation
loadClass (java.lang.System)
loadClass (java.io.PrintStream)
SexyClassForLoader$$static
SexyClassForLoader$$init
object was created
loadClass (java.lang.String)
invoke: getFoohello simple_foo
get: stat_foohello stat_foo
make bar

---
к первому замечанию. обратите внимание на то что вызов статики и обычных блоков инициализации произошел после определения класса
---
к второму замечани. если в карту загрузки положить второй интерфейс (он сейчас закоментарен) то при выполнении как и должно произойдет ошибка.
я бы в ваш пример внес бы перестановку местами кода в загрузке. так сначала бы загрузил с помощью системного загрузчика (так бы мне попался интерфейс -- ведь он должен быть доступен использующему коду) и только затем шло бы чтение данных из кэша.

P.S. похоже разрабы google не догадывались что кто-то на их блоггере будет писать коментарии размером больше 10 слов и сделали оооочень маленькое окошечко :)

P.S. если мои заметки вас заинтересует, пожалуйста, отформатируйте пример исходника.

Unknown комментирует...

Большое Вам спасибо за такой полный и развернутый комментарий - я начал лучше понимать как работают загрузчики классов и избавился от некоторых заблуждений.

К сожалению у Blogger не совсем продумана система комментариев и нельзя раскрасить код в комментариях... Я внес код в статью, если вы не против.

Также внес изменения в текст статьи.

Andrey Limachko комментирует...

А есть ли возможность компилировать и пошружать налету классы из исходников?

Unknown комментирует...

Да, такая возможность существует. В конце концов если даже из БД можно подгружать байткод :)) Но я ею пока не пользовался.

Meowth комментирует...

Подумал, что Вам будет интересно:

вот здесь товарищи по ходу решения одной задачи про плагины и безопасность подробно разбирают и java class loader.

Unknown комментирует...

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

Анатоль комментирует...

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

Dmitry комментирует...

А как это заставить работать в RCP или RAP? Там рассмотренный способ не прокатит.

Unknown комментирует...

В RCP/RAP используется OSGi (точнее Equinox) и там необходимо использовать специфичные для OSGi методы. Конкретно сказать пока не могу - свои загрузчики классов для OSGi не писал.

Dmitry комментирует...

В общем, я выкрутился так. Загрузка работает (у меня RAP 1.2.1):
ClasspathEntry ce = new ClasspathEntry(null, Application.class.getProtectionDomain());
Class cl = (DefaultClassLoader)Application.class.getClassLoader()).defineClass("mypack.MyClass", bbuf, ce, null);

ClasspathEntry ce = new ClasspathEntry(null, Application.class.getProtectionDomain());
Class cl = (DefaultClassLoader)Application.class.getClassLoader()).defineClass( "mypack.MyClass", templates,ce, null);

Alexey комментирует...

Спасибо!

Alex комментирует...

У меня с дурацким архивом примера возникла ошибка , помогите разобраться! Вот ссылка : http://hashcode.ru/questions/298847/classloader-%D1%87%D1%82%D0%B5%D0%BD%D0%B8%D0%B5-jar-%D0%B0%D1%80%D1%85%D0%B8%D0%B2%D0%B0-java

Unknown комментирует...

Ссылка "сайт питерских тестировщиков JVM" нерабочая. Было бы интересно увидеть.

Unknown комментирует...

Надо поискать в кеше гугла, сами понимаете, 8 лет прошло со времени публикации заметки.

popierdoliło комментирует...

https://blogs.oracle.com/vmrobot/entry/%D0%BE%D1%81%D0%BD%D0%BE%D0%B2%D1%8B_%D0%B4%D0%B8%D0%BD%D0%B0%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B9_%D0%B7%D0%B0%D0%B3%D1%80%D1%83%D0%B7%D0%BA%D0%B8_%D0%BA%D0%BB%D0%B0%D1%81%D1%81%D0%BE%D0%B2_%D0%B2
это?

Unknown комментирует...
Этот комментарий был удален автором.

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!