Модераторы: LSD, AntonSaburov
  

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> Разработка собственного загрузчика классов 
:(
    Опции темы
LSD
Дата 19.9.2005, 11:58 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Leprechaun Software Developer
****


Профиль
Группа: Модератор
Сообщений: 15718
Регистрация: 24.3.2004
Где: Dublin

Репутация: 210
Всего: 538



Механизм динамической загрузки классов – одна из основных причин по которой платформа Java приобрела значительную популярность. Загрузчики классов (classloaders) как раз и обеспечивают возможность расширения. Стандартный загрузчик java-машины используется для загрузки классов из каталогов или zip-архивов (правда, обычно имеющих расширение *.jar), определенных в системном свойстве java.class.path, содержимое которого определяется с помощью системной переменной CLASSPATH или установки ключа -cp при запуске java-машины. 

Задача создания собственного загрузчика классов возникает довольно часто. Стоит отметить, что собственные загрузчики классов используют все серверы приложений м web-контейнеры, что и понятно – приложения, разворачиваемые на сервере приложений, должны загружаться динамически, в противном случае перечисление в переменной CLASSPATH всех библиотек, используемых приложениями, становится задачей нетривиальной. Скажу больше, серверы приложений, как правило, используют не один загрузчик классов, а целую их иерархию. Если проанализировать какие загрузчики используют компоненты EJB и сервлеты, то, скорее всего, окажется, что они совершенно разные. Собственный загрузчик классов использует Jakarta Ant и множество других приложений, библиотек и серверов.

Ну хорошо, скажете вы, а мне-то это зачем? Необходимость в собственном загрузчике классов возникает достаточно часто в следующих случаях:
  • нет возможности или нежелательно перечислять все используемые библиотеки при старте программы в CLASSPATH;
  • возможностей стандартного загрузчика недостаточно для загрузки нужных классов.
У меня необходимость в своем загрузчике возникла при написании системы плагинов для приложения. Естественно, удобнее просто положить все, что относится к плагину, в отдельный каталог, чем прописывать используемые плагинами библиотеки каждый раз когда их набор меняется. 

Что делать, если библиотеки доступны только удаленно, например, находятся на веб-сервере? Написать собственный загрузчик? На самом деле, в этом нет необходимости. Класс java.net.URLClassLoader, подклассом которого является стандартный загрузчик JVM, позволяет загружать классы не только из ресурсов, находящихся в файловой системе, но и из ресурсов, находящихся в сети, задавая URL. Однако, встречаются такие ситуации, когда и его возможностей недостаточно.

При загрузке классов платформа Java использует механизм делегирования. Основная идея заключается в том, что каждый загрузчик классов имеет «родительский» загрузчик. В процессе загрузки классов загрузчик в первую очередь делегирует задачу поиска родительскому объекту-загрузчику прежде чем пытаться самому найти класс. При разработке загрузчика класса необходимо соблюдать этот принцип.

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

Любой используемый в JVM загрузчик должен расширять java.lang.ClassLoader и переопределить в нем методы findClass() и findResource() (второй только в том случае если есть необходимость загружать не только классы, но и ресурсы). 
Код

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

/**
 * Класс реализует <code>ClassLoader</code>, позволяющий динамически расширять
 * маршрут поиска классов и ресурсов 
 */
public class ExtendableClassLoader extends ClassLoader {

    private Hashtable cache;

    private ArrayList paths;

    private ClassLoader currentLoader = null;

    public ExtendableClassLoader() {
        cache = new Hashtable();
        paths = new ArrayList();
    }
...
}

Здесь атрибут cache будет использоваться для хранения уже найденных классов (не искать же их в самом деле при каждом обращении), paths – список путей поиска классов, включающий в себя каталоги и библиотеки, currentLoader будет хранить ссылку на дефолтный загрузчик классов.
Код

    /**
     * @see java.lang.ClassLoader#findClass(java.lang.String)
     */
    protected synchronized Class findClass(String className) throws ClassNotFoundException {
        Class result;
        byte classData[];
        
        // Проверяем кэш классов. Если класса в кэше нет, тогда применяются другие средства
        result = (Class) cache.get(className);
        if (result != null) {
            return result;
        }

        // Проверяется нет ли такого класса в стандартном пути 
        try {
            
            // если загрузчик классов текущего потока уже является расширенным,
            // используем сохраненное значение загрузчика классов
            if (Thread.currentThread().getContextClassLoader() instanceof ExtendableClassLoader) {
                if (this.currentLoader == null) throw new ClassNotFoundException();
                else result = this.currentLoader.loadClass(className);
            } else
                result = Thread.currentThread().getContextClassLoader().loadClass(className);
            return result;
        } catch (ClassNotFoundException e) {
        } catch (Exception e) {
        }
        
        // Попытка загрузить класс из добавленного пути
        classData = getClassFromAddedClassPaths(className);
        if (classData == null) {
            throw new ClassNotFoundException();
        }

        // определение класса
        result = super.defineClass(className, classData, 0, classData.length);
        if (result == null) throw new ClassFormatError();

        resolveClass(result);

        // Полученный класс добавляется в кэш классов
        cache.put(className, result);

        return result;
    }

Прежде чем искать класс в путях, зарегистрированных внутри загрузчика, пытаемся найти класс сначала в кэше классов, затем в стандартном загрузчике и только если класс найти не удается загрузчик сам пытается найти класс в заданных путях. Поиск класса выполняется в методе getClassFromAddedClassPaths(). Результатом работы этого метода должен быть массив байтов, представляющий нужный класс. Из массива байтов создается объект типа java.lang.Class, для чего можно использовать метод defineClass(), который реализован в  java.lang.ClassLoader.
Код

    /**
     * Метод ищет класс в созданном CLASSPATH и возвращает массив байтов
     *
     * @param className Имя класса, который нужно загрузить
     * @return Массив, содержащий байт-код класса
     */
    private byte[] getClassFromAddedClassPaths(String className) {
        try {
            String fsep = System.getProperty("file.separator");
            for (String path : paths) {
                File pathFile = new File(path);
                if (pathFile.isDirectory()) {
                    File f = new File(path + fsep
                            + classNameToFileName(className));
                    if (f.exists()) {
                        FileInputStream fis = new FileInputStream(f);
                        return createByteArray(fis);
                    }
                } else {
                    JarFile jarFile = new JarFile(pathFile);
                    ZipEntry entry = jarFile
                            .getEntry(classNameToZipEntryName(className));
                    if (entry == null) {
                        continue;
                    }
                    InputStream stream = jarFile.getInputStream(entry);
                    return createByteArray(stream);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * Создает массив байтов из входного потока
     * @param in Входной поток
     * @return Массив байтов
     * @throws IOException
     */
    private byte[] createByteArray(InputStream in) throws IOException {
        final int bufferSize = 2048;
        byte result[] = new byte[bufferSize];
        
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int len = 0;
        while ((len = in.read(result)) != -1) out.write(result, 0, len);
        return out.toByteArray();
    }

    /**
     * Преобразует имя пакета в путь к каталогу
     *
     * @param className преобразуемое имя класса
     * @return полученное имя файла
     */
    private String classNameToFileName(String className) {
        return className.replace(
            '.',
            System.getProperty("file.separator").charAt(0))
            + ".class";
    }

    private String classNameToZipEntryName(String className) {
        return className.replace('.', '/')
        + ".class";
    }

Теперь переопределим метод findResource(), предназначенный для поиска ресурсов. В отличие от findClass() он возвращает объекта класса java.net.URL.
Код

    protected URL findResource(String name) {
        URL res = ClassLoader.getSystemResource(name);
        if (res != null)
            return res;
        try {
            String fsep = System.getProperty("file.separator");
            for (String path : paths) {
                File pathFile = new File(path);
                if (pathFile.isDirectory()) {
                    File f = new File(path + fsep + name);
                    if (f.exists())
                        return f.toURL();
                } else {
                    JarFile jarFile = new JarFile(pathFile);
                    ZipEntry entry = jarFile.getEntry(name);
                    if (entry == null) continue;
                    String url = createJarResourceURL(pathFile, name, "/");
                    return new URL(url);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    
    private String createJarResourceURL(File jarFile, String resourceName, String fsep) throws MalformedURLException {
        String url = "jar:" + jarFile.toURL() + "!" + fsep + resourceName;
        return url; 
    }

Ну и, наконец, реализуем набор методов, которые позволяли бы изменять список путей, в которых хранятся классы. 
Код

    /**
     * Добавляет строку в CLASSPATH
     *
     * @param path Добавляемая строка
     */
    public void addClassPath(String path) {
        paths.add(path);
    }

    /**
     * Удаляет строку из CLASSPATH
     *
     * @param path Удаляемая строка
     */
    public void removeClassPath(String path) {
        int index = paths.indexOf(path);
        paths.remove(index);
    }

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

    /**
     * Устанавливает новый загрузчик классов для текущего потока, используя 
     * данный экземпляр класса расширенного загрузчика. Перед установкой
     * выполняется проверка не был ли уже установлен такой загрузчик
     */    
    public void setCurrentThreadClassLoader() {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        if (loader instanceof ExtendableClassLoader) return;
        else {
            currentLoader = loader;
            Thread.currentThread().setContextClassLoader(this);
        }
    }

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

public class ClassLoaderTest {

    public static void main(String[] args) throws Exception {
        new ClassLoaderTest().go();
    }
    
    private void go() throws Exception {
        ExtendableClassLoader ecl = new ExtendableClassLoader();
        ecl.addClassPath("lib/commons-lang-2.0.jar");
        Class c = ecl.loadClass("org.apache.commons.lang.math.RandomUtils");
    }

}

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


--------------------
Disclaimer: this post contains explicit depictions of personal opinion. So, if it sounds sarcastic, don't take it seriously. If it sounds dangerous, do not try this at home or at all. And if it offends you, just don't read it.
PM MAIL WWW   Вверх
  
Ответ в темуСоздание новой темы Создание опроса
Правила форума "Java"
LSD   AntonSaburov
powerOn   tux
javastic
  • Прежде, чем задать вопрос, прочтите это!
  • Книги по Java собираются здесь.
  • Документация и ресурсы по Java находятся здесь.
  • Используйте теги [code=java][/code] для подсветки кода. Используйтe чекбокс "транслит", если у Вас нет русских шрифтов.
  • Помечайте свой вопрос как решённый, если на него получен ответ. Ссылка "Пометить как решённый" находится над первым постом.
  • Действия модераторов можно обсудить здесь.
  • FAQ раздела лежит здесь.

Если Вам помогли, и атмосфера форума Вам понравилась, то заходите к нам чаще! С уважением, LSD, AntonSaburov, powerOn, tux, javastic.

 
1 Пользователей читают эту тему (1 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | Java: Общие вопросы | Следующая тема »


 




[ Время генерации скрипта: 0.0654 ]   [ Использовано запросов: 22 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.