Часть первая. Подсказка Нужно ли мне читать эту статью?
Загрузчик классов Java имеет решающее значение для работы системы Java, но мы часто его игнорируем. Загрузчик классов Java загружает классы во время выполнения, находя и загружая их. Пользовательские загрузчики классов могут полностью изменить способ загрузки классов, персонализируя вашу виртуальную машину Java так, как вам нравится. В этой статье кратко описывается загрузчик классов Java, а затем он иллюстрируется на примере создания собственного загрузчика классов. Этот загрузчик классов автоматически компилирует код перед загрузкой класса. Вы узнаете, что на самом деле делает загрузчик классов и как создать свой собственный. Если у вас есть базовые знания Java, вы знаете, как создавать, компилировать и запускать программы Java с командной строкой, а также некоторые основные понятия о файлах классов Java, вы сможете понять содержание этой статьи. Прочитав эту статью, вы сможете:
* Расширьте функции виртуальной машины Java
* Создайте собственный загрузчик классов.
* Как интегрировать собственный загрузчик классов в ваше приложение.
* Измените загрузчик классов, чтобы он был совместим с Java 2.
Часть 2. Введение Что такое загрузчик классов?
Разница между Java и другими языками заключается в том, что Java работает на виртуальной машине Java (JVM). Это означает, что скомпилированный код сохраняется в формате, независимом от платформы, а не в формате, который работает на конкретной машине. Этот формат имеет множество важных отличий от традиционного формата исполняемого кода. В частности, в отличие от программы C или C++, программа Java не является независимым исполняемым файлом, а состоит из множества отдельных файлов классов, каждый файл класса соответствует классу Java. Кроме того, эти файлы классов не загружаются в память сразу, а загружаются тогда, когда они нужны программе. Загрузчик классов — это инструмент, используемый в виртуальной машине Java для загрузки классов в память. Более того, загрузчик классов Java также реализован на Java. Таким образом, вы можете легко создать свой собственный загрузчик классов, не имея глубокого понимания виртуальной машины Java.
Зачем создавать загрузчик классов?
Теперь, когда в Java Virtual Gold уже есть загрузчик классов, нужно ли нам самим создавать другие? Хороший вопрос. Загрузчик классов по умолчанию знает, как загружать классы только из локальной системы. Когда ваша программа скомпилирована полностью в собственном коде, загрузчик классов по умолчанию обычно работает хорошо. Но одна из самых интересных особенностей Java — это то, насколько легко загружать классы из сети, а не просто локально.
Например, браузер может загружать классы через собственный загрузчик классов. Есть также много способов загрузки классов. Одна из самых интересных особенностей Java заключается в том, что ее можно настраивать не только локально, но и по сети:
* Автоматически проверять цифровые подписи перед выполнением ненадежного кода.
* Расшифровать код на основе пароля, предоставленного пользователем.
* Динамически создавайте классы в соответствии с потребностями пользователя. Все, что вам нужно, можно легко интегрировать в ваше приложение в виде байт-кода. Примеры пользовательских загрузчиков классов, если вы использовали JDK (Java Software Development Kit), appletviewer (браузер небольших приложений) или другой.
Для браузеров со встроенным Java вы уже используете собственный загрузчик классов. Когда Sun впервые выпустила язык Java, одним из самых интересных моментов было наблюдение за тем, как Java выполняет код, загруженный с удаленного веб-сайта. Выполнить с удаленного сайта через HTTP
Байт-код, передаваемый через P-соединение, выглядит немного странно. Это работает, потому что Java имеет возможность устанавливать собственные загрузчики классов. Браузер апплетов содержит загрузчик классов. Этот загрузчик классов не находит классы Java локально. Вместо этого он обращается к удаленному серверу, загружает исходный файл байт-кода через HTTP, а затем преобразует его в класс Java на виртуальной машине Java. Конечно, загрузчики классов делают много других вещей: они блокируют небезопасные классы Java и не позволяют различным апплетам на разных страницах мешать друг другу. Echidna, пакет, написанный Люком Горри, представляет собой открытый программный пакет Java, который позволяет безопасно запускать несколько приложений Java на виртуальной машине Java. Он предотвращает взаимодействие между приложениями за счет использования специального загрузчика классов, предоставляющего каждому приложению копию файла класса.
Пример нашего загрузчика классов Теперь, когда мы знаем, как работает загрузчик классов и как определить наш собственный загрузчик классов, мы создаем собственный загрузчик классов с именем CompilingClassLoader (CCL). CCL выполняет всю работу по компиляции за нас, поэтому нам не придется компилировать ее вручную. По сути, это эквивалентно наличию программы make, которая встраивается в нашу среду выполнения.
Примечание. Прежде чем мы перейдем к следующему шагу, необходимо понять некоторые связанные концепции.
Система была значительно улучшена в версии JDK 1.2 (это то, что мы называем платформой Java 2). Эта статья написана под JDK 1.0 и 1.1, но все будет работать и в более поздних версиях. ClassLoader также был улучшен в Java2.
Подробное введение представлено в пятой части.
Часть 3. Обзор структуры ClassLoader Основная цель загрузчика классов — обслуживать запросы к классам Java. Когда виртуальной машине Java нужен класс, она передает имя класса загрузчику классов, а затем загрузчик классов пытается вернуть соответствующий экземпляр класса. Пользовательские загрузчики классов можно создавать путем переопределения соответствующих методов на разных этапах. Далее мы узнаем о некоторых основных методах загрузчика классов. Вы поймете, что делают эти методы и как они работают при загрузке файлов классов. Вы также узнаете, какой код вам нужно написать при создании собственного загрузчика классов. В следующей части вы воспользуетесь этими знаниями и нашим пользовательским CompilingCl.
assLoader работает вместе.
Метод loadClass
ClassLoader.loadClass() — это точка входа ClassLoader. Сигнатура метода следующая:
Класс loadClass (имя строки, логическое разрешение);
Имя параметра указывает полное имя класса (включая имя пакета), требуемого виртуальной машиной Java, например Foo или java.lang.Object.
Параметр разрешения указывает, необходимо ли разрешить класс. Разрешение класса можно понимать как полную готовность к запуску. Разбор, как правило, не требуется. Если виртуальная машина Java хочет только знать, существует ли этот класс, или хочет знать его родительский класс, синтаксический анализ совершенно не нужен. В Java 1.1 и ее предыдущих версиях, если вы хотите настроить загрузчик классов, метод loadClass — единственный метод, который необходимо переопределить в подклассе.
(ClassLoader изменился в Java1.2 и предоставил метод findClass()).
методопределитькласс
defineClass — очень загадочный метод в ClassLoader. Этот метод создает экземпляр класса из массива байтов. Этот необработанный массив байтов, содержащий данные, может поступать из файловой системы или из сети. defineClass иллюстрирует сложность, загадочность и зависимость виртуальной машины Java от платформы — он интерпретирует байт-код, чтобы превратить его в структуры данных времени выполнения, проверяет достоверность и многое другое. Но не волнуйтесь, вам не нужно ничего этого делать. На самом деле, вы не можете отменить это вообще,
Потому что метод модифицируется ключевым словом Final.
Методфиндсистемкласс
Метод findSystemClass загружает файлы из локальной системы. Он ищет файлы классов в локальной системе и, если находит, вызывает
defineClass преобразует исходный массив байтов в объект класса. Это механизм по умолчанию для виртуальной машины Java для загрузки классов при запуске приложений Java. Для пользовательских загрузчиков классов нам нужно использовать findSystemClass только после того, как нам не удалось загрузить. Причина проста: наш загрузчик классов отвечает за выполнение определенных шагов при загрузке классов, но не за все классы. например,
Даже если наш загрузчик классов загружает некоторые классы с удаленного сайта, все равно остается много базовых классов, которые необходимо загрузить из локальной системы.
Эти классы нас не интересуют, поэтому мы позволяем виртуальной машине Java загружать их способом по умолчанию: из локальной системы. Это то, что делает findSystemClass. Весь процесс примерно следующий:
* Виртуальная машина Java запрашивает наш специальный загрузчик классов для загрузки класса.
* Проверяем, есть ли на удаленном сайте класс, который необходимо загрузить.
* Если есть, получаем этот класс.
* Если нет, мы думаем, что этот класс находится в базовой библиотеке классов, и вызываем findSystemClass, чтобы загрузить его из файловой системы.
В большинстве пользовательских загрузчиков классов вам следует сначала вызвать findSystemClass, чтобы сэкономить время при поиске с удаленного устройства.
Фактически, как мы увидим в следующем разделе, виртуальной машине Java разрешено загружать классы из локальной файловой системы только в том случае, если мы уверены, что автоматически скомпилировали наш код.
Метод разрешениякласса
Как уже говорилось выше, записи классов можно разделить на частичную загрузку (без разбора) и полную загрузку (включая разбор). Когда мы создаем собственный загрузчик классов, нам может потребоваться вызватьsolveClass.
Методфиндлоадедкласс
findLoadedClass реализует кеш: когда для загрузки класса требуется loadClass, вы можете сначала вызвать этот метод, чтобы проверить, был ли загружен класс, чтобы предотвратить перезагрузку уже загруженного класса. Этот метод должен быть вызван первым. Давайте посмотрим, как эти методы организованы вместе.
Наш пример реализации loadClass выполняет следующие шаги. (Мы не указываем конкретную технологию получения файла класса — он может быть из сети, из сжатого пакета или динамически скомпилирован. В любом случае мы получаем исходный файл байт-кода)
* Вызовите findLoadedClass, чтобы проверить, загружен ли этот класс.
* Если он не загружен, мы каким-то образом получаем исходный массив байтов.
* Если массив получен, вызовите defineClass, чтобы преобразовать его в объект класса.
* Если исходный массив байтов получить невозможно, вызовите findSystemClass, чтобы проверить, можно ли его записать из локальной файловой системы.
* Если разрешение параметра истинно, вызовитеsolveClass, чтобы разрешить объект класса.
* Если класс не найден, выдайте исключение ClassNotFoundException.
* В противном случае верните этот класс.
Теперь, когда у нас есть более полное понимание принципов работы загрузчиков классов, мы можем создать собственный загрузчик классов. В следующем разделе мы обсудим CCL.
Часть 4. Компиляция ClassLoader
CCL показывает нам функцию загрузчика классов. Цель CCL — обеспечить автоматическую компиляцию и обновление нашего кода. Вот как это работает:
* При запросе класса сначала проверьте, существует ли файл класса в текущем каталоге и подкаталогах диска.
* Если файла класса нет, но есть файл исходного кода, вызовите компилятор Java для компиляции и создания файла класса.
* Если файл класса уже существует, проверьте, не старше ли файл класса, чем файл исходного кода. Если файл класса старше файла исходного кода, вызовите компилятор Java для повторного создания файла класса.
* Если компиляция не удалась или файл класса не может быть сгенерирован из исходного файла по другим причинам, выдайте исключение ClassNotFou.
ндИсключение.
* Если вы еще не получили этот класс, он может существовать в других библиотеках классов. Вызовите findSystemClass, чтобы узнать, можно ли его найти.
* Если не найдено, выдать исключение ClassNotFoundException.
* В противном случае верните этот класс.
Как реализована компиляция Java?
Прежде чем идти дальше, нам нужно понять процесс компиляции Java. Обычно компилятор Java компилирует только указанные классы. Он также скомпилирует другие связанные классы, если этого требуют указанные классы. CCL скомпилирует классы, которые нам нужно скомпилировать в приложении, один за другим. Однако, вообще говоря, после того, как компилятор скомпилирует первый класс,
CCL обнаружит, что другие необходимые связанные классы действительно скомпилированы. Почему? Компилятор Java использует те же правила, что и мы: если класс не существует или исходный файл был обновлен, класс будет скомпилирован. Компилятор Java, по сути, на шаг впереди CCL, и большая часть работы выполняется компилятором Java. Похоже, что CCL компилирует эти классы.
В большинстве случаев вы обнаружите, что он вызывает компилятор в основном классе функций, и все — простого вызова достаточно. Однако есть особый случай, когда эти классы не компилируются при первом появлении. Если вы загружаете класс по его имени, используя метод Class.forName, компилятор Java не знает, нужен ли этот класс. в этом случае,
Вы обнаружите, что CCL снова вызывает компилятор для компиляции класса. Код в разделе 6 иллюстрирует этот процесс.
Использование CompilationClassLoader
Чтобы использовать CCL, мы не можем запустить нашу программу напрямую, ее необходимо запустить особым образом, например так:
% Java Foo arg1 arg2
Мы запускаем это следующим образом:
% java CCLRun Foo arg1 arg2
CCLRun — это специальная программа-заглушка, которая создает CompilingClassLoader и использует его для загрузки нашего основного функционального класса. Это гарантирует, что вся программа загрузится с помощью CompilingClassLoader. CCLRun использует Ja
API отражения va вызывает главную функцию основного класса функций и передает параметры этой функции. Чтобы узнать больше, обратитесь к исходному коду в части 6.
Давайте запустим пример, чтобы продемонстрировать, как работает весь процесс.
Основная программа — это класс Foo, который создает экземпляр класса Bar. Этот экземпляр Bar, в свою очередь, создает экземпляр класса Baz, который существует в пакете baz. Это сделано для демонстрации того, как CCL загружает классы из подпакетов. Bar также загружает класс Boo на основе имени класса.
, это также делает CCL. Все классы загружены и готовы к работе. Используйте исходный код из главы 6 для выполнения этой программы. Скомпилируйте CCLRun и CompilingClassLoader. Убедитесь, что вы не компилируете другие классы (Foo, Bar, Baz, a
nd Boo), иначе CCL не будет работать.
% java CCLRun Foo arg1 arg2
CCL: Компиляция Foo.java...
фу! аргумент1 аргумент2
бар! arg1 arg2
баз! arg1 arg2
CCL: Компиляция Boo.java...
Бу!
Обратите внимание, что компилятор вызывается впервые для Foo.java, а Bar и baz.Baz также компилируются вместе. И как Бу
Когда канал необходимо загрузить, CCL снова вызывает компилятор для его компиляции.
Часть 5. Обзор улучшений загрузчика классов в Java 2. В Java 1.2 и более поздних версиях загрузчик классов был значительно улучшен. Старый код все еще работает, но новая система упрощает нашу реализацию. Эта новая модель представляет собой модель делегирования прокси-сервера, что означает, что если загрузчик классов не может найти класс, он попросит его родительский загрузчик классов найти его. Загрузчик системных классов является прародителем всех загрузчиков классов. Загрузчик системных классов загружает классы по умолчанию, то есть из локальной файловой системы. При переопределении метода loadClass обычно используется несколько способов загрузки класса. Если вы пишете много загрузчиков классов, вы обнаружите, что просто снова и снова вносите некоторые изменения в этот сложный метод. Реализация loadClass по умолчанию в Java 1.2 включает наиболее распространенный способ поиска класса, позволяющий переопределить методы findClass и loadClass для соответствующего вызова метода findClass. Преимущество этого в том, что вам не нужно переопределять loadClass, вам нужно только переопределить findClass, что снижает рабочую нагрузку.
Новый метод: findClass
Этот метод вызывается реализацией loadClass по умолчанию. Цель findClass — включить весь код, специфичный для загрузчика классов,
Нет необходимости повторять код (например, вызывать загрузчик системных классов в случае сбоя указанного метода).
Новый метод: getSystemClassLoader.
Независимо от того, переопределяете ли вы методы findClass и loadClass, метод getSystemClassLoader может напрямую обращаться к загрузчику системного класса (вместо косвенного доступа через findSystemClass).
Новый метод: getParent
Чтобы делегировать запрос загрузчику родительского класса, загрузчик родительского класса этого загрузчика классов можно получить с помощью этого метода. Вы можете делегировать запрос загрузчику родительского класса, если определенный метод в пользовательском загрузчике классов не может найти класс. Родительский загрузчик классов содержит код, создающий загрузчик классов.
Часть 6. Исходный код
КомпиляцияClassLoader.java
Ниже приводится содержимое файла CompilingClassLoader.java.
импортировать java.io.*;
/*
CompilingClassLoader динамически компилирует исходные файлы Java. Он проверяет, существует ли файл .class и старше ли файл .class исходного файла.
*/
публичный класс CompilingClassLoader расширяет ClassLoader
{
//Указываем имя файла, читаем все содержимое файла с диска и возвращаем массив байтов.
частный байт[] getBytes(String filename) выдает IOException {
// Получаем размер файла.
Файл файл = новый файл (имя файла);
длинный len = file.length();
//Создаем массив, достаточный для хранения содержимого файла.
байт raw[] = новый байт[(int)len];
//Открыть файл
FileInputStream fin = новый FileInputStream (файл);
// Прочитать все содержимое. Если оно не может быть прочитано, произошла ошибка.
int r = fin.read(raw);
если (г != лен)
throw new IOException( "Невозможно прочитать все, "+r+" != "+len );
// Не забудьте закрыть файл.
фин.закрыть();
// Возвращаем этот массив.
вернуть сырой;
}
// Создать процесс для компиляции указанного исходного файла Java и указать параметры файла. Если компиляция прошла успешно, вернуть true, в противном случае.
// Возвращаем ложь.
частная логическая компиляция (String javaFile) выдает IOException {
//Показать текущий прогресс
System.out.println( "CCL: Компиляция "+javaFile+"..." );
//Запускаем компилятор
Процесс p = Runtime.getRuntime().exec( "javac "+javaFile);
// Ждем завершения компиляции
пытаться {
п.waitFor();
} catch(InterruptedException ie) { System.out.println(ie });
// Проверяем код возврата на наличие ошибок компиляции.
int ret = p.exitValue();
//Возвращаем, прошла ли компиляция успешно.
вернуть вет == 0;
}
// Основной код загрузчика классов — загрузка классов автоматически компилирует исходные файлы при необходимости.
общедоступный класс loadClass (имя строки, логическое разрешение)
выдает ClassNotFoundException {
//Наша цель — получить объект класса.
Класс класса = ноль;
// Сначала проверяем, обработан ли этот класс.
класс = findLoadedClass(имя);
//System.out.println( "findLoadedClass: "+clas);
// Получаем путь через имя класса, например: java.lang.Object => java/lang/Object
String fileStub = name.replace( ''''.'''', ''''/'''' );
// Создание объектов, указывающих на исходные файлы и файлы классов.
Строка javaFilename = fileStub+".java";
Строка classFilename = fileStub+".class";
Файл javaFile = новый файл (javaFilename);
Файл classFile = новый файл (имя_класса);
//System.out.println( "j "+javaFile.lastModified()+" c "
//+classFile.lastModified() );
// Сначала определяем, требуется ли компиляция. Если исходный файл существует, но файл класса не существует, или оба существуют, но исходный файл
// Новее, что указывает на то, что его необходимо скомпилировать.
if (javaFile.exists() &&(!classFile.exists() ||
javaFile.lastModified() > classFile.lastModified())) {
пытаться {
// Компилируем, если компиляция не удалась, мы должны объявить причину сбоя (просто использовать устаревшие классы недостаточно).
if (!compile(javaFilename) || !classFile.exists()) {
throw new ClassNotFoundException("Ошибка компиляции: "+javaFilename);
}
} catch(IOException т.е.) {
// Во время компиляции может возникнуть ошибка ввода-вывода.
выдать новое ClassNotFoundException(ie.toString());
}
}
// Убедившись, что он скомпилирован правильно или не требует компиляции, начинаем загрузку сырых байтов.
пытаться {
// Чтение байтов.
byte raw[] = getBytes(имя_класса);
//Преобразуем в объект класса
класс = defineClass(имя, необработанный, 0, необработанный.длина);
} catch(IOException т.е.) {
// Это не означает неудачу, возможно, класс, с которым мы имеем дело, находится в локальной библиотеке классов, например java.lang.Object.
}
//System.out.println( "defineClass: "+clas);
//Возможно, в библиотеке классов, загруженной по умолчанию.
если (класс==ноль) {
класс = findSystemClass(имя);
}
//System.out.println( "findSystemClass: "+clas);
// Если разрешение параметра истинно, интерпретируйте класс по мере необходимости.
если (разрешить && класс!= ноль)
разрешитьКласс (класс);
// Если класс не был получен, что-то пошло не так.
если (класс == ноль)
выбросить новое ClassNotFoundException(имя);
// В противном случае верните этот объект класса.
вернуться в класс;
}
}
CCRun.java
Вот файл CCRun.java
импортировать java.lang.reflect.*;
/*
CCLRun загружает классы через CompilingClassLoader для запуска программы.
*/
открытый класс CCLRun
{
static public void main( String args[]) выдает исключение {
//Первый параметр указывает основной класс функции, который пользователь хочет запустить.
Строка progClass = args[0];
//Следующие параметры — это параметры, передаваемые в этот основной класс функции.
String progArgs[] = новая строка[args.length-1];
System.arraycopy(args, 1, progArgs, 0, progArgs.length);
// Создание загрузчика компиляционного класса
CompilingClassLoader ccl = новый CompilingClassLoader();
//Загружаем основной класс функции через CCL.
Класс class = ccl.loadClass(progClass);
// Используйте отражение для вызова его основной функции и передачи параметров.
// Генерируем объект класса, представляющий тип параметра основной функции.
Класс mainArgType[] = { (new String[0]).getClass() };
// Находим стандартную функцию main в классе.
Метод main = clas.getMethod("main", mainArgType);
// Создаем список параметров — в данном случае массив строк.
Объект argsArray[] = { progArgs };
// Вызов основной функции.
main.invoke(ноль, argsArray);
}
}
Фу.java
Ниже приводится содержимое файла Foo.java.
публичный класс Foo
{
static public void main( String args[]) выдает исключение {
System.out.println( "foo! "+args[0]+" "+args[1] );
новый бар (args[0], args[1]);
}
}
Бар.java
Ниже приводится содержимое файла Bar.java.
импортная баз.*;
общественный класс бар
{
общественный бар (строка a, строка b) {
System.out.println( "bar! "+a+" "+b );
новый Баз(а,б);
пытаться {
Класс booClass = Class.forName("Бу");
Объект boo = booClass.newInstance();
} catch(Исключение е) {
е.printStackTrace();
}
}
}
baz/Baz.java
Ниже приводится содержимое файла baz/Baz.java.
пакет баз;
общественный класс Баз
{
public Baz(Строка a, Строка b) {
System.out.println("baz! "+a+" "+b );
}
}
Бу.java
Ниже приводится содержимое файла Boo.java.
общественный класс Бу
{
общественныйБу () {
System.out.println("Фу!");
}
}
Часть 7. Резюме Резюме Прочитав эту статью, поняли ли вы, что создание собственного загрузчика классов позволяет вам углубиться во внутренности виртуальной машины Java. Вы можете загрузить файл класса из любого ресурса или сгенерировать его динамически, чтобы вы могли делать много вещей, которые вас интересуют, расширяя эти функции, а также выполнять некоторые мощные функции.
Другие темы о ClassLoader Как упоминалось в начале этой статьи, пользовательские загрузчики классов играют важную роль во встроенных Java-браузерах и браузерах-апплетах.