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

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> Разработка собственного загрузчика классов, Статья для FAQ 
:(
    Опции темы
tux
Дата 18.9.2005, 09:14 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Летатель
***


Профиль
Группа: Участник Клуба
Сообщений: 1853
Регистрация: 10.2.2005
Где: msk.ru

Репутация: 31
Всего: 132



 Механизм динамической загрузки классов – одна из основных причин по которой платформа 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.  

Это сообщение отредактировал(а) tux - 21.7.2006, 08:24
PM MAIL Skype GTalk Jabber YIM   Вверх
batigoal
Дата 18.9.2005, 09:35 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Нелетучий Мыш
****


Профиль
Группа: Участник Клуба
Сообщений: 6423
Регистрация: 28.12.2004
Где: Санктъ-Петербургъ

Репутация: 24
Всего: 151



Отличная статья! Достаточно нетривиальная задача, описание очень доходчивое.

Через пару дней заношу в FAQ.


--------------------
"Чтобы правильно задать вопрос, нужно знать большую часть ответа" (Р. Шекли)
ЖоржЖЖ
PM WWW   Вверх
AntonSaburov
Дата 19.9.2005, 11:58 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Штурман
****


Профиль
Группа: Модератор
Сообщений: 5658
Регистрация: 2.7.2002
Где: Санкт-Петербург

Репутация: 51
Всего: 118



Занесено в статьи.
PM MAIL WWW ICQ   Вверх
Ortega
Дата 19.7.2006, 11:41 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 115
Регистрация: 5.11.2005
Где: Харьков, Украина

Репутация: 4
Всего: 6



Под Windows он не работает по причине того, что File.separator возвращает "\", а разделитель в середине jar-файла всегда "/". Для исправления этого нужно модифицировать методы getClassFromAddedClassPaths(String className) и findResource(String name), например, следующим образом:

Код

 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;
  }

  /** 
      * По аналогии с classNameToFileName() 
  */
  private String classNameToZipEntryName(String className) {
    return className.replace('.', '/')
        + ".class";
  }

  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)
            return null;
          String url = createJarResourceURL(pathFile, name, "/");
          return new URL(url);
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
    return null;
  }


   
--------------------
Всему свое время (с) ЧайфНе парься, будь счастлив (с) Пеппи Длинный Чулок
PM MAIL WWW ICQ Skype GTalk   Вверх
tux
Дата 19.7.2006, 11:47 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Летатель
***


Профиль
Группа: Участник Клуба
Сообщений: 1853
Регистрация: 10.2.2005
Где: msk.ru

Репутация: 31
Всего: 132



Эт точно, в Windows я так и не тестил. Спасибо. 
PM MAIL Skype GTalk Jabber YIM   Вверх
Ortega
Дата 19.7.2006, 16:36 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 115
Регистрация: 5.11.2005
Где: Харьков, Украина

Репутация: 4
Всего: 6



Еще одна неприятная ошибка в findResource().
Код

  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) return null; // поиск ресурса происходит только в первом архиве в classpath
          String url = createJarResourceURL(pathFile, name, "/");
          return new URL(url);
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
    return null;
  }

17-ю строчку нужно заменить на 
Код

if (entry == null) continue;
 
--------------------
Всему свое время (с) ЧайфНе парься, будь счастлив (с) Пеппи Длинный Чулок
PM MAIL WWW ICQ Skype GTalk   Вверх
LSD
Дата 20.7.2006, 11:53 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Leprechaun Software Developer
****


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

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




Модератор: Сообщение скрыто.



--------------------
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   Вверх
Ortega
Дата 13.3.2007, 10:55 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Шустрый
*


Профиль
Группа: Участник
Сообщений: 115
Регистрация: 5.11.2005
Где: Харьков, Украина

Репутация: 4
Всего: 6



Здесь наткнулся на еще одно элегантное решение. 
Код

public class ClassPathHacker {

 

private static final Class[] parameters = new Class[]{URL.class};

 

public static void addFile(String s) throws IOException {

    File f = new File(s);

    addFile(f);

}//end method

 

public static void addFile(File f) throws IOException {

    addURL(f.toURL());

}//end method

 

 

public static void addURL(URL u) throws IOException {


URLClassLoader sysloader = (URLClassLoader)ClassLoader.getSystemClassLoader();
Class sysclass = URLClassLoader.class;
 
try {
Method method = sysclass.getDeclaredMethod("addURL",parameters);
method.setAccessible(true);
method.invoke(sysloader,new Object[]{ u });
} catch (Throwable t) {
t.printStackTrace();
throw new IOException("Error, could not add URL to system classloader");
}//end try catch



}//end method
 
}//end class


Оригинал
--------------------
Всему свое время (с) ЧайфНе парься, будь счастлив (с) Пеппи Длинный Чулок
PM MAIL WWW ICQ Skype GTalk   Вверх
  
Ответ в темуСоздание новой темы Создание опроса
Правила форума "Java"
LSD   AntonSaburov
powerOn   tux
javastic
  • Прежде, чем задать вопрос, прочтите это!
  • Книги по Java собираются здесь.
  • Документация и ресурсы по Java находятся здесь.
  • Используйте теги [code=java][/code] для подсветки кода. Используйтe чекбокс "транслит", если у Вас нет русских шрифтов.
  • Помечайте свой вопрос как решённый, если на него получен ответ. Ссылка "Пометить как решённый" находится над первым постом.
  • Действия модераторов можно обсудить здесь.
  • FAQ раздела лежит здесь.

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

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


 




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


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

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