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