ДАНИИЛ АЛИЕВСКИЙ
Некоторые недокументированные функции Java
Изучение недокументированных функций в применении к языку Java может показаться несколько странным. Java – грамотный, современный, высоконадежный объектно-ориентированный язык программирования, поставляемый фирмой Sun совместно с обширнейшими библиотеками готовых классов. Неужели в среде Java существуют задачи, которые не решаются с помощью стандартных библиотек и для решения которых имеет смысл прибегать к недокументированным функциям? И что вообще такое «недокументированная функция» в рамках Java?
Слова «недокументированная функция» в случае Java имеют самый прямой смысл. В стандартный комплект поставки языка Java (мы рассматриваем версии 1.4 и выше – Sun Java SDK) входит, помимо документированных пакетов типа java.lang.*, java.util.*, javax.swing.* и т. д. также целый ряд недокументированных пакетов, прежде всего подпакеты sun.* и com.sun.*. Фирма Sun совершенно справедливо рекомендует не пользоваться классами из этих пакетов. Фирма Sun оставляет за собой право в любой момент поменять поведение и даже сам набор этих классов, так что программа, пользующаяся ими, рискует оказаться несовместимой с будущими версиями Java.
На самом деле в подавляющем большинстве ситуаций недокументированные функции, точнее, недокументированные классы из подпакетов sun.* и com.sun.* действительно не нужны. Эти классы в основном обеспечивают низкоуровневую реализацию универсальных классов и интерфейсов, предназначенных для использования в прикладных программах, и почти ничего не добавляют к тем возможностям, которые и так предоставляются стандартными документированными прикладными пакетами.
Из этого правила иногда встречаются исключения.
Автор этой статьи участвовал в разработке сложной системы на языке Java, содержащей более 100 тысяч строк исходного кода. При этом нам лишь несколько раз потребовались недокументированные функции Java. Именно о таких исключениях мне хотелось бы рассказать в данной статье.
Встроенный компилятор JAVA
Наверное, наиболее популярная «недокументированная функция» Java – это компиляция исходных текстов Java в .class-файлы.
Стандартный компилятор javac, входящий в комплект поставки Sun Java SDK, вполне последовательно реализован фирмой Sun на том же самом языке Java в виде сложной иерархии Java-классов. Утилита javac не более чем тривиальная программа, реализованная в машинном коде для всех операционных систем, предназначенная для запуска виртуальной машины Java и обращения к Java-классу, выполняющему собственно компиляцию.
Если ваша программа нуждается в компиляции исходных текстов на языке Java, то наиболее изящное, хотя и недокументированное решение, – воспользоваться стандартным Java-классом фирмы Sun, реализующим такую компиляцию.
На самом деле тут существуют даже два решения – классы sun.tools.javac.Main и com.sun.tools.javac.Main. Оба этих класса находятся в JAR-файле tools.jar, входящем в комплект поставки Sun Java SDK. Этот класс по умолчанию не входит в так называемый Java Runtime Environment (JRE) – набор классов и подкаталогов, который разрешается свободно распространять совместно с вашим Java-приложением для обеспечения его корректного исполнения. Тем не менее лицензионное соглашение фирмы Sun специально разрешает распространять tools.jar в дополнение к JRE совместно с вашими приложениями.
Оба класса – sun.tools.javac.Main и com.sun.tools.ja-vac.Main – хотя и не документированы, но довольно активно обсуждаются на форумах сайта http://java.sun.com.
Первый класс, sun.tools.javac.Main, имеет конструктор:
public Main(
OutputStream p0,
String p1)
В качестве параметров конструктор принимает некоторый поток p0, в который будут выдаваться все сообщения компилятора, и некоторую строку неизвестного (автору статьи) назначения; известные мне примеры использования данного класса передавали в качестве p1 строку «javac».
Собственно компиляцию выполняет метод того же класса:
public synchronized boolean compile(
String[] p0)
Этому методу нужно передать в качестве аргумента массив строк-параметров, которые обычно передаются утилите javac, например:
new String[] {"-d","/путь_к_подкаталогу","myfile.java"}
О результатах компиляции можно узнать из результата метода compile() – в случае успеха он должен вернуть true – или с помощью отдельного метода:
public int getExitStatus()
который в случае успеха должен вернуть 0.
Самая ценная особенность класса sun.tools.javac.Main – возможность указать в конструкторе выходной поток, который будет использоваться для выдачи сообщений об ошибках компилятора. Это дает возможность легко преобразовать этот поток, скажем, в переменную типа String, с тем чтобы самостоятельно ее проанализировать и показать пользователю.
Начиная с версии Sun Java SDK 1.4, объявлен устаревшим класс sun.tools.javac.Main. Попытка явно обратиться к этому классу в программе выдает предупреждение «deprecation warning», а попытка им воспользоваться для компиляции любого класса выдает аналогичное предупреждение в поток ошибок (параметр конструктора p0), если только явно не подавить эти предупреждения ключом «-nowarn» среди параметров метода compile.
Для такого предупреждения есть основания. По крайней мере один из сложных классов, разработанных автором на Java версии 1.4 и прекрасно компилирующихся обычными компиляторами, оказалось невозможным скомпилировать с помощью класса sun.tools.ja-vac.Main. Компилятор вполне явно «сошел с ума» и стал «ругаться» на законные языковые конструкции.
Второй класс, предназначенный для компиляции исходных текстов на Java – единственный «неустаревший» в версии Sun Java SDK 1.4 – это com.sun.tools.ja-vac.Main. Функционально он эквивалентен классу sun.tools.ja-vac.Main, но несколько менее «многословен» в своем наборе методов. Конструктор этого класса не имеет параметров. Методов compile здесь два, причем несинхронизированных, в отличие от sun.tools.javac.Main:
public static int compile(String[] p0)
public static int compile(String[] p0,
PrintWriter p1)
Второй из этих методов позволяет указать поток вывода, который будет использоваться для вывода всех сообщений компилятора, например:
new PrintWriter(writer=new CharArrayWriter(),true)
Как и в случае sun.tools.javac.Main, второй метод compile позволяет перенаправить поток сообщений компилятора в собственный буфер, с тем чтобы впоследствии превратить его, скажем, в строку типа String для анализа и визуализации.
Класс com.sun.tools.javac.Main в версии Sun Java SDK 1.4 работает существенно быстрее предыдущего класса sun.tools.javac.Main и «справляется» со всеми корректными исходными текстами. Похоже, именно этот класс вызывается изнутри стандартной утилиты javac.
Файлы-ссылки – *.lnk в Microsoft Windows
Второй известный автору случай использования недокументированных функций – распознавание файлов-«ярлыков» (shortcuts) Microsoft Windows, обычно имеющих расширение «.lnk».
Стандартные средства работы с файлами из пакета java.io.* вообще не слишком хорошо «справляются» с файловой системой современных версий Microsoft Windows. Так, стандартный класс java.io.File «понятия не имеет» о том, что корнем файловой иерархии Windows следует считать «Рабочий стол» («Desktop»), у которого есть такие дочерние узлы, как «Мои документы» («My documents») или «Мой компьютер» («My computer»). Стандартный java.io.File по старинке считает корнем иерархии корневой каталог любого дискового устройства.
Чтобы скомпенсировать этот недостаток, не нарушая совместимости с классом File, фирма Sun разработала новый, более современный класс javax.swing.file-chooser.FileSystemView. Он активнейшим образом используется стандартным диалогом выбора файла javax.swing.JFileChooser, что отчасти объясняет несколько странный выбор пакета для FileSystemView.
К сожалению, даже класс FileSystemView не решает всех проблем, по крайней мере, в имеющейся у меня последней версии Sun Java SDK 1.4.1. Современные версии Windows, в частности Windows XP, предлагают пользователю интерфейс, существенно опирающийся на механизм файлов-«ссылок», или «ярлыков». Такими ссылками являются специальные файлы или даже подкаталоги (обычно, но не обязательно с расширением «.lnk»). Щелчок по ним в стандартном Windows Explorer приводит к перемещению в некоторый другой каталог или открытию некоторого другого файла. Именно так в Windows XP организована работа в локальной сети – компьютеры пользователей-«соседей» представлены маленькими виртуальными подкаталогами-«ссылками» в локальной файловой системе текущего пользователя.
Класс FileSystemView не содержит никаких средств для распознавания и обработки подобных ссылок. В результате стандартный диалог выбора файла javax.swing.JFileChooser при использовании в Windows XP производит довольно жалкое впечатление – попытка перейти к компьютерам локальной сети заканчивается позорной неудачей.
В действительности фирма Sun уже реализовала механизм обработки файлов-«ссылок» Microsoft Windows. К сожалению, он пока недокументирован. Это класс sun.awt.shell.ShellFolder. Среди прочих методов, имеющих документированные эквиваленты в классе FileSystemView, класс ShellFolder содержит следующие два метода:
public abstract boolean isLink();
public abstract ShellFolder getLinkLocation()
throws FileNotFoundException
Вот как можно ими пользоваться:
public static boolean isLink(File f) {
try {
return sun.awt.shell.ShellFolder
.getShellFolder(f).isLink();
} catch (FileNotFoundException e) {
return false;
}
}
public static File getLinkLocation(File f)
throws FileNotFoundException
{
File result= sun.awt.shell.ShellFolder
.getShellFolder(f).getLinkLocation();
if (result==null ||
result.getPath().trim().length()==0)
throw new FileNotFoundException(
"Incorrect link - it is empty");
return result;
}
Применяя эти методы, при желании можно «исправить» поведение стандартного диалога выбора файла javax.swing.JFileChooser, «научив» его правильно работать с современными локальными сетями Microsoft Windows.
Конечно, будет гораздо лучше, если фирма Sun в очередной версии Java SDK включит в FileSystemView документированные эквиваленты этих методов и исправит javax.swing.JFileChooser. Существование подобных методов в sun.awt.shell.ShellFolder позволяет на это надеяться. Пока же приходится пользоваться недокументированными методами.
Стек вызовов метода
Предположим, нужно получить «трассу стека» – узнать, какой метод исполняется в данный момент, какой метод вызвал этот метод, и т. д. Трудно представить, зачем это может понадобиться, за исключением отладки программы. Однако в реальной практике автору это однажды понадобилось – чтобы выяснить, какие загрузчики классов использовались для загрузки текущего исполняемого кода и всех классов «трассы стека», вызвавших этот код.
Даже для этой столь экзотической ситуации фирма Sun предусмотрела документированную технику. Достаточно возбудить фиктивное исключение, тут же «поймать» его и воспользоваться методом объекта-исключения getStackTrace().
Проблема может заключаться в том, что метод getStackTrace() возвращает исключительно «описательную» информацию о классах и методах «трассы стека» – попросту строковые имена классов и методов. Чтобы получить собственно классы, задействованные в данный момент в стеке (объекты типа Class), необходимо вызвать метод Class.forName или эквивалентный. Но что, если текущий загрузчик классов не в состоянии загрузить такие классы просто по имени? Что, если текущий исполняемый код загружен самым обычным традиционным загрузчиком классов, а класс, который его вызвал – это интернетовский class-файл, загруженный и исполняемый совершенно другим специальным загрузчиком? Тогда метод getStackTrace() никак не поможет «добраться» до этого класса (объекта Class) хотя бы для того, чтобы получить ссылку на загрузивший его загрузчик классов.
В подобной ситуации автору пришлось прибегнуть к недокументированному классу sun.reflect.Reflection, точнее, к его методу.
public static native Class getCallerClass(int p0)
Вот как выглядит использование этого метода:
public static Class[] getCurrentStackTraceClasses() {
List result= new ArrayList();
for (int count=1; count<10000
/* страховка на всякий случай */;
count++)
{
Class clazz= sun.reflect.Reflection
.getCallerClass(count);
if (clazz==null) break;
result.add(clazz);
}
return (Class[])result.toArray(
new Class[0]);
}
Провайдеры сервисов
Сервис-провайдеры (Service provider) – довольно распространенная и неплохо документированная техника среди стандартных библиотек Java. Тем более удивительно, что фирма Sun оставила недокументированным механизм перечисления сервис-провайдеров – класс sun.misc.Service.
Идея сервис-провайдеров вполне очевидна. Допустим, есть некоторый сервис (интерфейс или абстракный класс), предназначенный для использования прикладными программами. Есть также произвольное количество провайдеров – классов, реализующих этот интерфейс или абстрактный класс. Предполагается, что набор провайдеров не фиксирован и может меняться в зависимости от поставки программы. Не исключено, что пользователь может самостоятельно приобрести и инсталлировать в систему дополнительный набор провайдеров какого-либо стандартного сервиса.
Типичный пример применения сервис-провайдеров – разнообразные кодеки, позволяющие читать и писать изображения, аудио или видеозаписи различных форматов. Общая процедура чтения или записи объекта – это сервис, а конкретный кодек, рассчитанный на определенный формат – это провайдер.
Согласно документации, для поддержки сервис-провайдеров нужно положить в подкаталог META-INF/Services некоторого JAR-файла, присутствующего в путях поиска классов Java, специальный текстовый файл. Имя текстового файла должно совпадать с полным именем некоторого сервиса – интерфейса или абстрактного класса, например, «com.pupkin.vasya.My-Service». Этот файл должен содержать (в отдельных строках) список некоторых классов – провайдеров этого сервиса, присутствующих в данном JAR-файле. Тогда система Java сумеет стандартным образом получить этот список в виде набора объектов – экземпляров соответствующих провайдеров. Стандартные библиотеки фирмы Sun примерно так и поступают, когда нужно прочитать изображение или аудиозапись. Почему-то остался недокументированным лишь тот самый стандартный способ, которым извлекается список провайдеров некоторого сервиса.
Этот способ следующий:
for (Iterator iterator=
sun.misc.Service.providers(
класс_сервиса.class);
iterator.hasNext(); )
{
класс_сервиса o= (класс_сервиса)
iterator.next();
используем o - экземпляр
очередного провайдера сервиса;
}
На самом деле для использования такой техники не обязательно создавать JAR-файл – вполне достаточно разместить правильный подкаталог META-INF в одном из каталогов поиска class-файлов.