Программа просмотра Java-классов (Java Class Viewer)

ОГЛАВЛЕНИЕ

Введение

Для начинающего не так легко понять спецификацию VM; программа просмотра Java-классов - это мощное приложение, которое может показать значение каждого байта в файле класса.

Основы

Вам стоит опустить данную часть статьи если вас не интересует история данного просмотрщика классов Java.

1. Причина создания программы просмотра Java-классов (jCV)

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

Итак, мы решили написать программу по просмотру Java-классов, которая может отображать визуально файл класса, а также отображать значение каждого байта данного файла класса. Это и является назначением приложения по просмотру Java-классов (Java Class Viewer - jCV).  

2. Библиотека файлов Java-классов (jCFL)  

При создании графического приложения было замечено, что данное приложение может быть разделено на две части:  

  1. Библиотеку файлов Java-классов (jCFL). Она анализирует файл массива байтов класса, предоставляет различные классы-помощники, которые могут помочь в получении информации обрабатываемого класса, а также предоставляет некоторые полезные элементы управления графическим интерфейсом UI для программы анализатора Java-классов. Она может предоставить больше полезной информации чем сообщения об ошибках Sun JDK javac в случае, если файл класса будет проблематичным.
  2. Программу просмотра Java-классов (Java Class Viewer). Это приложение, которое использует библиотеку jCFL для предоставления графического представления файла класса.   

Итак, на данный момент у нас две компоненты - библиотека jCFL и программа jCV.

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

Связанные библиотеки 

Существует еще одна библиотека, применяемая для проверки и сборки файла класса, такая как Byte Code Engineering Library (BCEL) из Apache. Основы разработки jCFL отличаются от BCEL. BCEL - это мощная библиотека для редактирования/изменения файлов Java-классов, а также в BCEL доступен просмотрщик файлов класса.   

Практически невозможно использовать BCEL для написания программы просмотра файлов класса, которая смогла бы показывать также значения каждого байта, поскольку она не записывает расположение при анализе файла класса. BCEL был разработан согласно авторской идее, при этом идея и соглашение об именах не следуют спецификации JVM, в частности структуре файлов класса (Class File Structure).  

jCFL может редактировать файлы классов, записывая смещение файла при анализе, при этом следуя структуре ClassFile согласно спецификации JVM.

Если вы будете использовать jCV при чтении спецификации JVM Spec, то вам будет значительно легче понять файл класса.

 

Формат классового файла

Файл класса (ClassFile) имеет следующую структуру :  

ClassFile {
        u4 magic;
        u2 minor_version;
        u2 major_version;
        u2 constant_pool_count;
        cp_info constant_pool[constant_pool_count-1];
        u2 access_flags;
        u2 this_class;
        u2 super_class;
        u2 interfaces_count;
        u2 interfaces[interfaces_count];
        u2 fields_count;
        field_info fields[fields_count];
        u2 methods_count;
        method_info methods[methods_count];
        u2 attributes_count;
        attribute_info attributes[attributes_count];

Вот краткое описание структуры ClassFile из раздела формата файла класса (The class File Format) в спецификации JVM: 

magic - 0xCAFEBABE, магический номер файла класса. Если первые 4 байта не являются 0xCAFEBABE, то файл не будет распознан как файл класса. 

minor_version, major_version - major version и minor version вместе определяют версию класса.  

constant_pool_count, cp_info constant_pool[constant_pool_count-1] - набор констант (Constant pool) файла класса. Он может содержать 11 типов констант:

01. class/interface info
02. field reference info
03. method reference info
04. interface method reference info
05. String
06. Integer
07. Float
08. Long
09. Double
10. NameAndType
11. Utf8

access_flags - флаг доступа к классу

this_class, super_class - информация о текущем классе и суперклассе. Только java.lang.Object суперкласс класса равен null; если суперкласс не указан для этого класса, то суперклассом будет java.lang.Object. 

interfaces_count, interfaces[interfaces_count] - прямые супер интерфейсы. 

fields_count, field_info fields[fields_count] - поля данного класса, если таковые существуют.

methods_count, method_info methods[methods_count] - методы данного класса. Java-компилятор сгенерирует конструктор по умолчанию для класса (исключая внутренний класс inner class) если такового нет. Потому будет хотя бы один метод в данном классе.

attributes_count, attribute_info attributes[attributes_count] - атрибуты данного класса. Существует хотя бы один атрибут с названием "SourceFile" для названия файла исходного кода.

 

Обработка файла класса используя библиотеку файлов Java-классов

1. Обработка файла класса

Класс org.freeinternals.classfile.core.ClassFile является анализатором файла класса. Он принимает байтовый массив в качестве входного параметра - байтовый массив содержит файл класса. Массив происходит от файла .class, файла .jar, файла .war и т.д. В качестве альтернативы, байтовый массив может быть построен библиотеками как BCEL.

// ArticleCodeDemo.src.zip - org.freeinternals.demo.jCFL_CodeDemo.extractClassFile()
File file = new File("C:/Temp/File.class");
byte[] classByteArray = Tool.readClassFile(file);
ClassFile classfile = new ClassFile(classByteArray);
// ArticleCodeDemo.src.zip - org.freeinternals.demo.jCFL_CodeDemo.extractJarFile()
File file = new File("C:/Temp/tools.jar");
JarFile jarFile = new JarFile(file, false, JarFile.OPEN_READ);
ZipFile zipFile = jarFile;
 final Enumeration zipEntries = zipFile.entries();
while (zipEntries.hasMoreElements()) {
    ZipEntry zipEntry = (ZipEntry) zipEntries.nextElement();
    if (!zipEntry.getName().endsWith(".class")) {
        continue;
    }
     byte[] classByteArray = Tool.readClassFile(zipFile, zipEntry);
    ClassFile classfile = new ClassFile(classByteArray);
     System.out.println();
    System.out.println(zipEntry.getName());
    jCFL_CodeDemo.printClassFile(classfile);
}

Существует класс инструмента org.freeinternals.classfile.ui.Tool который может помочь нам считать данные из файла или zip-архива. Мы знаем, что файлы .jar и .war оба являются zip-форматом файла.

Конструктор ClassFile выбрасывает исключение org.freeinternals.classfile.core.ClassFormatException в случае, если байтовый массив не является верным файлом класса, или java.io.IOException в том случае, если произойдет какая-либо ошибка ввода-вывода (IO-error). Вы можете обрамить выражение блоком try...catch или добавить оператор throws в объявлении метода. 

 

2. Получение информации о файле класса

Как только мы успешно получим экземпляр ClassFile, мы можем получить информацию о файле класса посредством методов getXxxxx.

Вот пример вывода информации всех компонент файла класса:

// ArticleCodeDemo.src.zip - org.freeinternals.demo.jCFL_CodeDemo.printClassFile()
 // Версии Minor и Major
MinorVersion minorVersion = classfile.getMinorVersion();
System.out.println("Class File Minor Version: " + minorVersion.getValue());
 MajorVersion majorVersion = classfile.getMajorVersion();
System.out.println("Class File Major Version: " + majorVersion.getValue());
 // Набор констант
CPCount cpCount = classfile.getCPCount();
System.out.println("Constant Pool size: " + cpCount.getValue());
 AbstractCPInfo[] cpArray = classfile.getConstantPool();
for (int i = 1; i < cpCount.getValue(); i++) {
    System.out.println(
            String.format("Constant Pool [%d]: %s", i, classfile.getCPDescription(i)));
    short tag = cpArray[i].getTag();
    if ((tag == AbstractCPInfo.CONSTANT_Double) ||
            (tag == AbstractCPInfo.CONSTANT_Long)) {
        i++;
    }
}
 // Флаг доступа, this и супер-класс
AccessFlags accessFlags = classfile.getAccessFlags();
System.out.println("Class Modifier: " + accessFlags.getModifiers());
 ThisClass thisClass = classfile.getThisClass();
System.out.println("This Class Name Index: " + thisClass.getValue());
System.out.println("This Class Name: " +
    classfile.getCPDescription(thisClass.getValue()));
 SuperClass superClass = classfile.getSuperClass();
System.out.println("Super Class Name Index: " + superClass.getValue());
if (superClass.getValue() == 0) {
    System.out.println("Super Class Name: java.lang.Object");
} else {
    System.out.println("Super Class Name: " +
        classfile.getCPDescription(superClass.getValue()));
}
 // Интерфейсы
InterfaceCount interfactCount = classfile.getInterfacesCount();
System.out.println("Interface Count: " + interfactCount.getValue());
 if (interfactCount.getValue() > 0) {
    Interface[] interfaceArray = classfile.getInterfaces();
    for (int i = 0; i < interfaceArray.length; i++) {
        System.out.println(
                String.format("Interface [%d] Name Index: %d", i,
                interfaceArray[i].getValue()));
        System.out.println(
                String.format("Interface [%d] Name: %s", i,
        classfile.getCPDescription(interfaceArray[i].getValue())));
    }
}
 // Поля
FieldCount fieldCount = classfile.getFieldCount();
System.out.println("Field count: " + fieldCount.getValue());
 if (fieldCount.getValue() > 0) {
    FieldInfo[] fieldArray = classfile.getFields();
    for (int i = 0; i < fieldArray.length; i++) {
        System.out.println(String.format("Field [%d]: %s", i,
                fieldArray[i].getDeclaration()));
    }
}
 // Методы
MethodCount methodCount = classfile.getMethodCount();
System.out.println("Method count: " + methodCount.getValue());
 if (methodCount.getValue() > 0) {
    MethodInfo[] methodArray = classfile.getMethods();
    for (int i = 0; i < methodArray.length; i++) {
        System.out.println(String.format("Method [%d]: %s", i,
                methodArray[i].getDeclaration()));
    }
}
 // Атрибуты
AttributeCount attributeCount = classfile.getAttributeCount();
System.out.println("Attribute count: " + attributeCount.getValue());
 AttributeInfo[] attributeArray = classfile.getAttributes();
for (int i = 0; i < attributeArray.length; i++) {
    System.out.println(String.format("Attribute [%d]: %s", i,
                attributeArray[i].getName()));
}

Вот некоторые заметки по поводу указанного выше кода. 

Набор констант: это набор констант (Сonstant pool) начиная с индекса 1 до (constant_pool_count-1); также CONSTANT_Long_info и CONSTANT_Double_info получат две позиции индексов, в то время как другие типы всего лишь получат одну позицию. 

Суперкласс: индекс суперкласса будет равен нулю только тогда, когда текущим классом будет java.lang.Object. В противном случае, он должен быть элементом в наборе констант. 

Интерфейсы и поля: класс может не иметь ни интерфейсов, ни полей, потому нам необходимо проверить переменные InterfaceCount и FieldCount до того, как мы получим массив интерфейса/поля.

Методы: если класс не является внутренним, то он должен иметь хотя бы один метод, который будет являться конструктором экземпляров по умолчанию, созданным javac; но для внутреннего класса методы нет возможности создать. Потому нам необходимо проверить равенство переменной MethodCount нулю. 

Атрибуты: один класс должен иметь хотя бы один атрибут - SourceFile; нам не нужно добавлять логику для него.

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


  

Добавление обработанного класса к элементу управления Swing 

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

1. Элемент управления Tree для создания иерархии компонентов файла класса

Класс org.freeinternals.classfile.ui.JTreeClassFile является подклассом JTree, который принимает объект ClassFile в конструкторе. Он добавить все компоненты класса в элемент управления tree.  

2. Контейнер для интерактивной бинарной программы просмотра файлов классов

Класс org.freeinternals.classfile.ui.JSplitPaneClassFile является подклассом JSplitPane, который разделен на две панели: левая панель является JTreeClassFile, а правая - это двоичный просмотрщик файла класса.  

В то время как мы выбираем каждую компоненту в дереве, соответствующие байты будут подсвечены - потому у нас все интерактивно. 

// JavaClassViewer.src.zip - org.freeinternals.javaclassviewer.Main.open_ClassFile()
private JSplitPaneClassFile cfPane;

private void open_ClassFile(final File file) {
    this.cfPane = new JSplitPaneClassFile(Tool.readClassFile(file));
    this.add(this.cfPane, BorderLayout.CENTER);
    this.resizeForContent();
}

К примеру, после открытия класса File.class (java.io.File), при нашем выборе узла для метода getName(), соответствующие байты будут подсвечены.

Если мы выберем узел name_index, descriptor_index для данного метода, то только секция индексов (для значения 119, 24) будет подсвечена.

Если мы выберем узел code в атрибуте Code, и откроем закладку Opcode, то все машинные коды данного метода будут извлечены. 

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

  

3. Элемент управления Tree для ZipFile (jar, war, etc.)

Класс org.freeinternals.classfile.ui.JTreeZipFile является подклассом JTree, который принимает ZipFile в конструкторе. Он построит дерево для всех записей в zip-файла. Файл .jar/.war сам по себе также является zip-файлом. 

// JavaClassViewer.src.zip - org.freeinternals.javaclassviewer.Main.open_JarFile()
// Здесь осталась только ключевая логика

private JTreeZipFile zftree;

private void open_JarFile(final File file) {
    this.zftree = new JTreeZipFile(new JarFile(file, false, JarFile.OPEN_READ));
    this.zftreeContainer = new JPanelForTree(this.zftree);
    this.add(this.zftreeContainer, BorderLayout.CENTER);
    this.resizeForContent();
}

А вот как выглядит элемент управления Tree ZipFile.

Также, если мы дважды щелкнем по узлу xxxxx.class, то будет открыто новое окно для файла класса.


 

Построение просмотрщика Java-классов

Используя элементы управления, предоставленные библиотекой jCFL, вы с легкостью сможете написать программу просмотра файлов классов. Все что вам необходимо добавить, так это меню/панель инструментов, а также расположить элементы по JFrame.

Вам может понадобиться ознакомиться с исходным кодом, предоставленным в самом начале данной статьи.

Интересные факты

Данная программа является всего лишь отправным пунктом в понимании Java. Вы можете использовать ее в качестве инструмента для изучения новых возможностей, предоставленных новыми версиями Java, в качестве инструмента для изучения АОП, изучения кода структур/платформ и т.д. 

Sun (а может, и Oracle) пытаются сохранить совместимость формата файлов класса. К примеру, аннотация добавляет некоторые атрибуты к файлу класса и использует механизм отражения для считывания и анализа аннотации - тем самым, производительность аннотации невысока, но таким образом мы можем сохранить совместимость.

Автор: Amos Shi

Загрузить демо пример  - 87.51 KB
Загрузить двоичный код - 73.71 KB
Загрузить исходный код - 44.04 KB

Вам стоит использовать NetBeans версии 6.5 или выше для того, чтобы открыть предлагаемый исходный код. Вам стоит обновить библиотечную ссылку jCFL проекта NetBeans поскольку ваш путь к каталогу может отличаться.  

Последнюю версию библиотеки вы можете загрузить со страницы загрузок jCFL .