Faktenextraktion aus Java-Bytecode für Refaktorisierungswerkzeuge

Ähnliche Dokumente
Java Virtual Machine (JVM) Übersicht

Inhaltsverzeichnis 1

Kapitel 1. Bytecode, JVM, Dynamische Compilierung. Am Beispiel des IBM Jalapeno-Compilers (besser als SUN!) 1

J.5 Die Java Virtual Machine

Java Virtual Machine

Verhindert, dass eine Methode überschrieben wird. public final int holekontostand() {...} public final class Girokonto extends Konto {...

Objektorientierte Programmierung. Kapitel 12: Interfaces

Objektorientierte Programmierung

Programmieren in Java

Java Virtual Machine (JVM) Bytecode

Einführung in die Java- Programmierung

Praktikum Compilerbau Sitzung 9 Java Bytecode

Datensicherung. Beschreibung der Datensicherung

Beuth Hochschule Die virtuelle Java Maschine (JVM) WS13/14

Das Typsystem von Scala. L. Piepmeyer: Funktionale Programmierung - Das Typsystem von Scala

3 Objektorientierte Konzepte in Java

Grundlagen von Python

C# im Vergleich zu Java

Vorkurs C++ Programmierung

4D Server v12 64-bit Version BETA VERSION

Java Kurs für Anfänger Einheit 4 Klassen und Objekte

Kompilieren und Linken

Objektorientierte Programmierung für Anfänger am Beispiel PHP

Übung: Verwendung von Java-Threads

Der Aufruf von DM_in_Euro 1.40 sollte die Ausgabe 1.40 DM = Euro ergeben.

Java Kurs für Anfänger Einheit 5 Methoden

Leitfaden zur Installation von Bitbyters.WinShutdown

Javadoc. Programmiermethodik. Eva Zangerle Universität Innsbruck

Einführung in Eclipse und Java

Tutorium Informatik 1. Aufgabe 2: Formatierte Ein- und Ausgabe

Wintersemester Maschinenbau und Kunststofftechnik. Informatik. Tobias Wolf Seite 1 von 22

Virtueller Seminarordner Anleitung für die Dozentinnen und Dozenten

Lineargleichungssysteme: Additions-/ Subtraktionsverfahren

Einführung in die Programmierung

In 12 Schritten zum mobilen PC mit Paragon Drive Copy 11 und Microsoft Windows Virtual PC

Ordner Berechtigung vergeben Zugriffsrechte unter Windows einrichten

Fachdidaktik der Informatik Jörg Depner, Kathrin Gaißer

Modellierung und Programmierung 1

Vererbung & Schnittstellen in C#

Compilerbau. Martin Plümicke WS 2018/19

Handbuch ECDL 2003 Basic Modul 5: Datenbank Grundlagen von relationalen Datenbanken

Folge 19 - Bäume Binärbäume - Allgemeines. Grundlagen: Ulrich Helmich: Informatik 2 mit BlueJ - Ein Kurs für die Stufe 12

VBA-Programmierung: Zusammenfassung

Erstellen einer digitalen Signatur für Adobe-Formulare

Binäre Bäume. 1. Allgemeines. 2. Funktionsweise. 2.1 Eintragen

Klausur WS 2006/07 Programmiersprache Java Objektorientierte Programmierung II 15. März 2007

1 Mathematische Grundlagen

5 DATEN Variablen. Variablen können beliebige Werte zugewiesen und im Gegensatz zu

Software Engineering Interaktionsdiagramme

Einführung in die Java- Programmierung

Das erste Programm soll einen Text zum Bildschirm schicken. Es kann mit jedem beliebigen Texteditor erstellt werden.

Programmieren I. Strategie zum Entwurf von Klassen. Beispiele. Design von Klassen. Dr. Klaus Höppner. Beispiel: Bibliothek

.procmailrc HOWTO. zur Mailfilterung und Verteilung. Stand:

Handbuch Fischertechnik-Einzelteiltabelle V3.7.3

Typumwandlungen bei Referenztypen

Einrichtung des Cisco VPN Clients (IPSEC) in Windows7

Übungen zur Softwaretechnik

Einführung in Java. PING e.v. Weiterbildung Andreas Rossbacher 24. März 2005

13 OOP MIT DELPHI. Records und Klassen Ein Vergleich

SafeRun-Modus: Die Sichere Umgebung für die Ausführung von Programmen

Innere Klassen in Java

Speicher in der Cloud

5. Abstrakte Klassen. Beispiel (3) Abstrakte Klasse. Beispiel (2) Angenommen, wir wollen die folgende Klassenhierarchie implementieren:

Kommunikations-Parameter

DOKUMENTATION VOGELZUCHT 2015 PLUS

Die Bedeutung abstrakter Datentypen in der objektorientierten Programmierung. Klaus Kusche, September 2014

Kommunikations-Management

schnell und portofrei erhältlich bei beck-shop.de DIE FACHBUCHHANDLUNG mitp/bhv

Informatik 1 Tutorial

.NET Code schützen. Projekt.NET. Version 1.0

Java Reflection. Meta-Programmierung mit der java.lang.reflection API. Prof. Dr. Nikolaus Wulff

! " # $ " % & Nicki Wruck worldwidewruck

EasyWk DAS Schwimmwettkampfprogramm

Prinzipien Objektorientierter Programmierung

Computeranwendung und Programmierung (CuP)

Software Engineering Klassendiagramme Assoziationen

Um zu prüfen welche Version auf dem betroffenen Client enthalten ist, gehen Sie bitte wie folgt vor:

Willkommen zur Vorlesung. Objektorientierte Programmierung Vertiefung - Java

Snippets - das Erstellen von "Code- Fragmenten" - 1

Objektorientierte Programmierung

Klassenentwurf. Wie schreiben wir Klassen, die leicht zu verstehen, wartbar und wiederverwendbar sind? Objektorientierte Programmierung mit Java

Artikel Schnittstelle über CSV

Mit der Maus im Menü links auf den Menüpunkt 'Seiten' gehen und auf 'Erstellen klicken.

In 15 einfachen Schritten zum mobilen PC mit Paragon Drive Copy 10 und Microsoft Windows Virtual PC

Objektorientierte Programmierung OOP

Javakurs zu Informatik I. Henning Heitkötter

Programmierkurs Java

Installation der SAS Foundation Software auf Windows

Diplomarbeit. Konzeption und Implementierung einer automatisierten Testumgebung. Thomas Wehrspann. 10. Dezember 2008

CMS.R. Bedienungsanleitung. Modul Cron. Copyright CMS.R Revision 1

Upgrade auf die Standalone Editionen von Acronis Backup & Recovery 10. Technische Informationen (White Paper)

Zugriff auf die Modul-EEPROMs

Suche schlecht beschriftete Bilder mit Eigenen Abfragen

M. Graefenhan Übungen zu C. Blatt 3. Musterlösung

In 15 Schritten zum mobilen PC mit Paragon Drive Copy 11 und VMware Player

Numerische Datentypen. Simon Weidmann

Zahlensysteme: Oktal- und Hexadezimalsystem

CADEMIA: Einrichtung Ihres Computers unter Windows

4 Aufzählungen und Listen erstellen

Transkript:

Fern-Universität Hagen Fachbereich Mathematik und Informatik Lehrstuhl Programmiersysteme Abschlussarbeit im Studiengang Master of Computer Science Wintersemester 2010/2011 Faktenextraktion aus Java-Bytecode für Refaktorisierungswerkzeuge Autor: Dipl.-Ing. (FH) Gabriel Wetzler Matrikel-Nr.: 7285167 Adresse: Auf den Brechen 35 52372 Kreuzau E-Mail: gabriel.wetzler@online.de Betreuer: Dipl.-Inf. Andreas Thies Stand: 10. April 2011 1

Danksagung Herrn Prof. Dr. Steimann danke ich für die Ermöglichung, meine Masterarbeit im Bereich Programmiersysteme der FernUni Hagen anfertigen zu können. Insbesondere möchte ich mich bei meinem Betreuer Dipl.-Inf. Andreas Thies für die interessante Aufgabenstellung, seine Unterstützung, seine hilfreichen Ratschläge sowie die fachlichen Diskussionen und die Durchsicht der Arbeit bedanken. Ebenso möchte ich mich an dieser Stelle ganz herzlich bei meiner Frau Petra für ihr entgegengebrachtes Verständnis, ihre stetige Unterstützung sowie das Korrekturlesen der Arbeit bedanken. Außerdem geht ein großes Dankeschön an meine Eltern für deren Unterstützung in jeglicher Hinsicht, ganz besonders während der Anfertigung der Masterarbeit. 2

Inhaltsverzeichnis 1 Einleitung...5 1.1 Motivation...5 1.2 Aufgabenstellung...6 1.3 Aufbau der Arbeit...6 2 Erläuterungen zur Aufgabenstellung...7 3 Der JVM Bytecode...9 3.1 Das Class-File Format...9 3.2 Class-File Strukturen...10 3.2.1 Die Hauptstruktur (Class-File Struktur)...10 3.2.2 Die Constant-Pool Tabelle...12 3.2.3 Deskriptoren...14 3.2.3.1 Feld-Deskriptoren...14 3.2.3.2 Methoden-Deskriptoren...15 3.2.4 Tabellen mit Bezug zum Constant-Pool...15 3.2.4.1 Felder...15 3.2.4.2 Methoden...16 3.2.4.3 Attribute / Annotations...17 3.2.4.4 Code Attribut...19 3.3 Die Java-Virtual-Machine (JVM)...20 3.3.1 Einführung...21 3.3.2 Strukturen der JVM...21 3.3.3 Die Method Area...22 3.3.4 JVM Stack und Frames...23 3.3.5 Die Execution Engine...24 3.3.6 Typen der JVM...25 3.3.6.1 Die Primitiven Typen...25 3.3.6.2 Die Referenz-Typen...26 3.3.7 Befehlssatz der JVM...27 4 Bytecode Framework Bibliotheken...29 4.1 BCEL...29 5 Der FaktenExtraktor...33 5.1 Der Programmablauf...34 5.2 Deklarations-ID...39 5.3 Interface zu Referenzen und Deklarationen...41 5.4 Das FaktenExtraktor Interface...45 6 Eine Anwendung...47 6.1 Beispiel 1: Offene Rekursion...47 6.2 Beispiel 2: Down-Cast...50 6.3 Bemerkung...52 7 Evaluierung...54 8 Installation...56 9 Was nicht analysiert werden kann...58 10 Zusammenfassung...59 11 Erklärung...60 12 Anhänge...61 A. Interface Hierarchie...61 3

B. AST Parser...63 C. Inhalt der CD...65 D. Verzeichnisse...66 4

Einleitung 1 Einleitung 1.1 Motivation Das Wort Refactoring ist eine Neuschöpfung aus dem englischen Sprachbereich. Sie leitet sich aus dem Verständnis her, dass ein Programm aus einer Menge von Faktoren besteht, es also faktorisiert ist. Die Faktorisierung offenbart dabei einer innere Struktur des Programms. Ändert man die Faktorisierung, nennt man das folgerichtig Refaktorisierung [Steimann 1853][ 5]. Ein Refactoring beschreibt den Vorgang, bei dem der Code eines Programms verändert wird, ohne jedoch seine Funktionalität, also seine Bedeutung, zu ändern. Ziel dabei ist es, den Code verständlicher, einfacher änderbar bzw. wartbarer zu machen. Das Besondere dabei ist, dass diese Designverbesserung erfolgt, nachdem ein Softwareprojekt komplett oder teilweise implementiert worden ist. Es ist damit ein wirksames Werkzeug, um der sog. Softwarefäulnis entgegenzutreten. Hierunter versteht man den Umstand, dass bei notwendigen Änderungen am Programm lediglich die Änderungen am Code vorgenommen werden, die für die Funktionalität unbedingt notwendig sind. Dabei geht jedoch schnell die Struktur des Programms verloren. Um die Struktur eines Programms zu erhalten, muss jedoch oft ein wesentlich höherer Aufwand betrieben werden, als nur seine Funktionalität zu ändern. Die Methode des Extreme Programmings verzichtet sogar komplett auf die Designphase bei der Entwicklung und setzt statt dessen auf Refactoring als strukturbildende Maßnahme in Softwareprojekten [Steimann 1853][ 5.1]. Dennoch gelten Refactorings als eher lästige Aufgabe, die zu Fuß ausgeführt zudem noch sehr fehleranfällig und zeitaufwändig ist. In der Softwareindustrie wäre daher ein solches Vorgehen nicht akzeptabel. Die Kosten und das Risiko würden nämlich aus Sicht des Unternehmens die oben genannten Vorteile sehr schnell in den Schatten stellen. Es besteht also die Notwendigkeit, für diesen Prozess ein Werkzeug zur Verfügung zu stellen, welches die meisten Aufgaben schnell und zuverlässig erledigen kann. Versteht man unter einem bestimmten Refactoring eine bestimmte Codeänderung, beispielsweise die Umbenennung einer Methode, so kann man die hierfür notwendigen Tätigkeiten als einen Algorithmus formulieren. Dieser erhält als Eingabe den verbesserungswürdigen Code (und weitere Parameter, wie z.b. den neuen Namen der Methode...) und liefert als Ausgabe den verbesserten refaktorierten Code. Ein solcher Algorithmus, als Programm implementiert, ist ein Refactoring Tool. Refactoring Tools gehören daher zu den sog. Metaprogrammen. Um Refactorings durchzuführen, muss ein solches Werkzeug zunächst bestimmte Informationen bzgl. des zu refaktorisierenden Codes ermitteln. So muss ein Refactoring Tool, welches eine Methode umbenennt, beispielsweise feststellen, an welchen Stellen im Programmcode diese Methode referenziert wird. Diese Referenzen müssen dann ebenfalls entsprechend geändert werden. Fragestellungen dieser Art sollen durch das hier entwickelte Programm beantwortet werden. Als Informationsquelle dient dabei nicht, wie man wohl vermuten würde, der AST des Eingabecodes, sondern der aus einem Kompiliervorgang resultierende Bytecode. Dies hat zum einen den Vorteil, dass gewisse Informationen, die der Compiler dem Programm hinzufügt, mit betrachtet werden. Zum anderem eignet sich dieses Programm zur Analyse von allen Codes von Programmiersprachen, aus welchem JVM basierter Bytecode generiert werden kann. Für diese Arbeit steht jedoch die Programmiersprache Java im Vordergrund. 5

Einleitung 1.2 Aufgabenstellung Refaktorisierungswerkzeuge müssen, bevor sie ihre Aufgabe korrekt durchführen können, eine möglichst genaue Analyse des Sourcecodes durchführen. Dies erfolgt normalerweise durch Betrachtung des AST der zu refaktorisierenden Klassen. Dabei müssen beispielsweise Deklarationen und Referenzen aus dem Code extrahiert werden. Problematisch ist hier, dass sich die benötigten Informationen teils nicht aus dem Sourcecode ermitteln lassen. Beispielsweise sind vorhandene Defaultkonstruktoren von Java-Klassen nicht im Code sichtbar. Durch diese Arbeit soll untersucht werden, welche Fakten bzgl. des Sourcecodes aus einer Bytecodedatei ermittelt werden können. Hierfür soll eine Anwendung entwickelt werden, die möglichst vollständig die im Sourcecode enthaltenen Referenzen und Deklarationen ermittelt. Die Elemente beider Mengen sollen dabei möglichst mit all ihren Eigenschaften aufgeführt werden. Die Mengen sind derart miteinander zu verknüpfen, dass eine Baumstruktur entsteht, die die Struktur des Programmcodes widerspiegelt. Diese soll mittels eines geeigneten Interfaces zugänglich gemacht werden. 1.3 Aufbau der Arbeit Kapitel 1 gibt einen Überblick zur Aufgabenstellung. Kapitel 2 erläutert die Aufgabenstellung und definiert einige Ziele, die zu erreichen sind. Im Kapitel 3 werden einige Grundlagen vermittelt, deren Kenntnis zur Lösung der Aufgabenstellung notwendig ist. Der Fokus liegt dabei auf dem Aufbau von Bytecodedateien für die JVM und einem Überblick in die Funktionsweise der JVM. Zum einen soll dabei klar werden, welche Informationen aus Bytecodedateien ermittelt werden können. Zum anderen soll ein Verständnis vermittelt werden, wie und wo Referenzen entstehen. Dadurch wird erklärt, an welchen Stellen innerhalb von Bytecodedateien nach Referenzen gesucht werden kann. Im Kapitel 4 wird das zur Implementierung des Programms verwendete Framework BCEL vorgestellt. Im Kapitel 5 wird die Funktion des entwickelten Programms zur Ermittlung von Fakten aus Bytecodedateien vorgestellt. Kapitel 6 gibt ein Beispiel der Anwendung des FaktenExtraktors. Dabei wird dieser zur Prüfung von Vorbedingungen des RWID Refactorings, welches am Lehrgebiet für Programmiersysteme der FernUni Hagen entwickelt wurde, eingesetzt. Kapitel 7 vergleicht die Performance des FaktenExtraktors mit einem rudimentären AST Parser bezüglich der Laufzeit anhand von drei OpenSource Projekten. Kapitel 8 gibt einige Hinweise, die bei der Installation zu beachten sind. Kapitel 9 erläutert Elemente, die vom FaktenExtraktor nicht analysiert werden können. Kapitel 10 fasst diese Arbeit zusammen. 6

Erläuterungen zur Aufgabenstellung 2 Erläuterungen zur Aufgabenstellung Zunächst stellt sich die Frage, was genau Deklarationen und was Referenzen im Programmcode sind. Deklarationen sind jeweils die Definitionen von: Interfaces Klassen Feldern Methoden lokalen Variablen Methodenparametern Es gibt allerdings auch implizite Deklarationen, die nicht aus dem Programmcode ersichtlich sind. Dies sind Defaultkonstruktoren oder implizite Initialisierungsmethoden in Klassen, z.b. zur Initialisierung von statischen Variablen. Referenzen ergeben sich aus Bezügen auf Deklarationen. Hierzu zählen : Zugriffe auf Variablen, Felder und Methodenparameter, Aufrufe von Methoden oder Konstruktoren, Referenzen auf Typen, z.b. in Type Casts, in Methodensignaturen und Deklarationen von lokalen Variablen Die Menge der Referenzen kann demnach an die Menge der Deklarationen gekoppelt werden. Dabei kann sich eine Referenz immer nur auf eine bestimmte Deklaration beziehen. Dagegen können auf eine Deklaration beliebig viele Referenzen Bezug nehmen. Es besteht eine 1:n Beziehung zwischen Deklarationen und Referenzen. Deklarationen selbst können dabei auch Referenzen bilden. Dies ist bei der Deklaration eines Feldes, einer lokalen Variablen, einer Methode oder dem Überschreiben einer Methode der Fall. Derartige Deklarationen beziehen sich dabei auf eine Typdeklaration oder, falls eine Methode überschrieben wird, auf die Deklaration einer Methode. Diese Beziehungen können mittels entsprechender Eigenschaften von Deklarationen aufgelöst werden. Für die zu erzeugenden Objekte müssen einige Eigenschaften der Deklarationen bzw. Referenzen definiert werden, damit sie die für ein Refaktorisierungswerkzeug notwendigen Informationen bereitstellen können. Im Falle von Deklarationen sind dies z.b.: der Access Modifier der Bezeichner der deklarierte Typ (eine Referenz auf eine Deklaration) 7

Erläuterungen zur Aufgabenstellung der Ort, an dem sich die Deklaration befindet die Methodenparameter (im Falle einer deklarierten Methode oder eines deklarierten Konstruktors) eine Liste der Typparameter einer Klasse oder Methode die Supertypen einer Klasse eine Liste der implementierten Interfaces einer Klasse die Art der Deklaration (Package / Compilation-Unit / Klasse / abstrakte Klasse / Feld / Methode / abstrakte Methode) Im Falle von Referenzen sind dies beispielsweise: der Name, mit dem eine Deklaration referenziert wird der Ort, an dem die Referenz steht die Deklaration, an die die Referenz vom Compiler gebunden wird der Empfänger eines Methodenaufrufs bzw. Feldzugriffs Der Zugriff auf diese Mengen erfolgt dann über ein geeignetes Interface. 8

Der JVM Bytecode 3 Der JVM Bytecode Ein syntaktisch korrektes Java-Programm, welches in einem beliebigem Editor erstellt wurde, muss, bevor es zur Ausführung kommt, zunächst von einem Compiler in Java-Bytecode umgesetzt werden. Bei den klassischen Programmiersprachen, wie beispielsweise C / C++, übersetzt der zugehörige Compiler den Quelltext direkt in Befehlszyklen, die von einem bestimmten Prozessor verarbeitet werden können. Java geht hier einen anderen Weg. Programme werden als Quelltext in Dateien gespeichert und jede für sich von einem Compiler in Bytecode übersetzt. Abbildung 1: Implementierungsmöglichkeiten der JVM Dieser Bytecode ist unabhängig von einer bestimmten CPU oder einem bestimmten Betriebssystem. Der aus dem Compiler-Vorgang resultierende Bytecode ist jedoch nicht unmittelbar durch einen Prozessor verarbeitbar, sondern muss zunächst einer Java Virtual Machine (JVM) zugeführt werden. Eine Komponente der JVM, der ClassLoader, übernimmt das Laden und Linken mehrerer Bytecodedateien und legt diese dann in einer zur Verarbeitung optimierten Form im Speicher ab. Die JVM bildet eine CPU nach, die den Bytecode ausführen kann. Sie erzeugt dabei einen Maschinencode, der spezifisch für eine bestimmte CPU ist. JVM Implementierungen sind daher abhängig von einer bestimmten Betriebssystem/CPU-Kombination. Ziel dieses Vorgehens ist es, eine größere Flexibilität bei der Entwicklung und Verteilung von Anwendungen zu erhalten. Denn der vom Compiler generierte Bytecode ist im Prinzip auf jeder Maschine und jedem Betriebssystem ausführbar, für das es eine JVM gibt [Steimann 1814][ 4.1]. Ein weiterer Vorteil dieses Konzeptes ist es, dass durch die Verwendung unterschiedlicher Compiler auch aus anderen Programmiersprachen als Java Bytecode für JVM's generiert werden kann. Heute existieren außer Java weitere Programmiersprachen, z.b. ADA, C, PHP, Ruby oder Phyton, die eine JVM als Ausführungseinheit nutzen. 3.1 Das Class-File Format Ein Class-File entsteht durch das Kompilieren des Codes durch den Compiler. Der Dateiaufbau ist so gestaltet, dass möglichst kleine Dateien produziert werden. Bytecodedateien weisen dabei eine bestimmte Struktur auf, die wiederum aus mehreren ineinander verschachtelten Strukturen aufgebaut ist. Einige davon sind von variabler Größe, wodurch es nicht möglich ist, ein bestimmtes Feld durch einen Offset vom Dateianfang aus zu finden. Aus diesem Grund sind Bytecodedateien 9

Der JVM Bytecode nicht sehr performant auswertbar. Sie werden daher beim Laden durch den ClassLoader der JVM geparst. Die JVM wandelt sie dann intern in Strukturen um, die dann in bestimmte Speicherbereiche abgelegt werden (siehe auch Kapitel 3.3.3). Diese Strukturen weisen dann eine für die maschinelle Verarbeitung optimierte Form auf. Im Folgenden werden die Strukturen eines Class-Files näher betrachtet. 3.2 Class-File Strukturen Ein Class-File bezieht sich immer auf exakt eine Klasse oder ein Interface [JVMS][ 3.1]. Dabei kann eine Klasse auch Anonyme Klassen oder Innere Klassen enthalten. Ein Class-File besteht aus einer Folge von 8 Bit Bytes. Ein Wort kann dabei auch 16, 32 oder 64 Bit lang sein. In diesem Falle ist es dann aus mehreren 8 Bit Worten zusammengesetzt. Jedes dieser Wörter stellt ein Feld einer Struktur im Class-File dar. Alle Felder werden in einer durch die JVM- Spezifikation vorgeschriebenen Reihenfolge im Class-File notiert. Die Länge der Felder ist ebenfalls durch die JVM-Spezifikation vorgeschrieben. Einige Felder, meist handelt es sich um tabellenartige Auflistungen, haben eine variable Länge. In diesem Falle ist die Anzahl der Elemente, die in dieser Struktur gespeichert wird, durch ein weiteres Feld vor der entsprechenden Struktur angegeben. Es gibt ansonsten keine Start- oder Ende-Zeichen für Felder oder Strukturen. 3.2.1 Die Hauptstruktur (Class-File Struktur) Die Hauptstruktur eines Class-Files ist dabei immer eine Struktur vom Typ Class-File. Diese ist folgendermaßen aufgebaut [JVMS][4.1]: ClassFile { magic; (2 Byte) minor_version; (2 Byte) major_version; (2 Byte) constant_pool_count; (2 Byte) cp_info constant_pool[constant_pool_count-1]; access_flags; (2 Byte) this_class; (2 Byte) super_class; (2 Byte) interfaces_count; (2 Byte) interfaces[interfaces_count]; (2 Byte pro Interface) fields_count; (2 Byte) field_info fields[fields_count]; methods_count; (2 Byte) method_info methods[methods_count]; attributes_count (2 Byte); attribute_info attributes[attributes_count]; 10

Der JVM Bytecode Die Bedeutung der Felder ist dabei wie folgt: Feld magic minor_verison, major_version constant_pool_count constant_pool[] access_flags this_class super_class interfaces_count Interfaces[] fields_count field_info fields[] methods_count method_info methods[] attributes_count attribute_info attributes[] Bedeutung Version des Class-File Formates Version dieses Class-Files Anzahl der Einträge in der Constant-Pool Tabelle die Constant-Pool Tabelle kodiert den Zugriffsmodifier der korrespondierenden Klasse oder des Interfaces Index in Constant-Pool Tabelle (zeigt auf eine CONSTANT_Class_info Struktur) Index in Constant-Pool Tabelle (zeigt auf eine CONSTANT_Class_info Struktur, bezeichnet die Superklasse dieser Klasse) Anzahl der Interfaces der Superinterfaces dieser Klasse Indices der Constant-Pool Tabelle (zeigen auf eine CONSTANT_Class_info Struktur, bezeichnet die Superinterfaces dieser Klasse) Anzahl der Felder dieser Klasse Indices der Constant-Pool Tabelle (zeigt auf eine field_info Struktur, beschreibt Felder dieser Klasse) Anzahl der Methoden dieser Klasse Indices der Constant-Pool Tabelle (zeigt auf eine method_info Struktur, beschreibt Methoden dieser Klasse) Anzahl der Attribute Indices der Constant-Pool Tabelle (zeigt auf eine attribute_info Struktur, beschreiben Eigenschaften von Elementen dieser Klasse) Tabelle 1: Elemente der ClassFile Struktur 11

Der JVM Bytecode Alle hier als Array dargestellten Strukturen sind dabei ebenfalls Tabellen bzw. Listen, die im Folgenden weiter beschrieben werden. 3.2.2 Die Constant-Pool Tabelle Die Einträge in dieser Tabelle haben wiederum eine Struktur, welche in nachstehender Tabelle beschrieben ist. Die in der Constant-Pool Tabelle enthaltenen Elemente beziehen sich auf folgende Informationen: Namen von Methoden und Feldern der korrespondierenden Klasse die Klassen, die von der korrespondierenden Klasse referenziert werden Deskriptoren (für Felder und Methoden) numerische Konstanten String Konstanten Jeder Eintrag wird dabei mit einem sog. Tag eingeleitet. Ein Tag ist eine ganze Zahl, welche den Typ des Eintrags kodiert. Es folgt eine Instanz des Typs. Insgesamt gibt es 11 verschiedene Typen, die in der Constant-Pool Tabelle vorkommen können. Einer davon, CONSTANT_Utf8, hat eine variable Länge, die anderen haben feste Längen. Folgende Typen von Constant-Pool Einträgen werden in der JVM-Spezifikation definiert [JVMS] [ 4.4]: Name Aufbau Bedeutung CONSTANT_Utf8 Tag (= 1) Länge bytes[] Name, Bezeichner CONSTANT_Integer Tag (= 3) bytes Wert einer Integer Konstanten CONSTANT_Float Tag (= 4) bytes Wert einer Float Konstanten CONSTANT_Long CONSTANT_Double Tag (= 5) high_bytes low bytes Tag (= 6) high_bytes low bytes Wert einer Long Konstanten Wert einer Double Konstanten CONSTANT_Class Tag (= 7) Name-Index Index auf einen CONSTANT_Utf8 Eintrag, der den Namen einer Klasse enthält CONSTANT_String Tag (= 8) String-Index Index auf einen CONSTANT_Utf8 Eintrag, der den Wert des Strings enthält 12

Der JVM Bytecode Name Aufbau Bedeutung CONSTANT_Fieldref Tag (= 9) Class-Index NameType-Index CONSTANT_Methodref Tag (= 10) Class-Index NameType-Index CONSTANT_InterfaceMeth odref Tag (= 11) Class-Index NameType-Index CONSTANT_NameAndType Tag (= 12) Name-Index Deskriptor-Index Tabelle 2: Typen von Constant-Pool Einträgen Index eines CONSTANT_Class Eintrags (Name der Klasse, zu der dieses Feld gehört) und Index auf einen CONSTANT_NameAndType Eintrag (Name und Deskriptor des Feldes) Index eines CONSTANT_Class Eintrags (Name der Klasse, zu der diese Methode gehört) und Index auf einen CONSTANT_NameAndType Eintrag (Name und Deskriptor der Methode) Index eines CONSTANT_Class Eintrags (Name des Interfaces, zu dem diese Methode gehört) und Index auf einen CONSTANT_NameAndType Eintrag (Name und Deskriptor der Methode) Index auf einen CONSTANT_Utf8 (Name) Eintrag und Index auf einen CONSTANT_Utf8 (Descriptor) Eintrag Die Constant-Pool Tabelle hat also einige Referenzen auf sich selbst. Aus ihr können einige der Eigenschaften von Deklarationen und Referenzen der korrespondierenden Klasse abgeleitet werden. Vier Arten von Informationen können hier gefunden werden: numerische Werte von Konstanten Strings von Namen Verweise auf andere Einträge derselben Tabelle Deskriptoren (siehe Kapitel 3.2.3) Typ von Feldern Parameter und Typ von Methoden 13

Der JVM Bytecode Beispiel: Möchte man beispielsweise alle Methoden, die von der korrespondierenden Klasse referenziert werden, ermitteln, muss man alle Einträge mit Tag 10 (CONSTANT_Methodref), deren Class- Index nicht auf den Eintrag mit dem eigenen Klassennamen zeigt, auswählen. Die zugehörigen Namen erhält man, indem aus CONSTANT_Methodref der NameAndType-Index ermittelt wird. Der so ermittelte Name-Index verweist auf einen CONSTANT_Utf8 Eintrag, der den Namen der entsprechenden Methode darstellt. 3.2.3 Deskriptoren Es gibt zwei Arten von Deskriptoren: Feld-Deskriptoren und Methoden-Deskriptoren. Sie beschreiben den Typ von Feldern bzw. die Signatur von Methoden. Methoden-Signaturen der Java Sprache setzen sich aus dem Namen einer Methode und den Methodenparametern zusammen. Sie sind entscheidend beim dynamischen Binden von Methoden. Dynamisches Binden erfolgt jedoch zur Laufzeit einer Anwendung. Daher müssen diese Informationen der JVM zugänglich gemacht werden. Deskriptoren zu einem Feld oder einer Methode werden im Constant-Pool durch Einträge vom Typ CONSTANT_NameAndType an diese gebunden. Der Deskriptor selbst ist dann in einer Konstanten vom Typ CONSTANT_Utf8 als String abgelegt (siehe auch 2.1.2). Deskriptoren gehören zu einer Struktur vom Typ Attribut, welcher noch gesondert betrachtet wird. 3.2.3.1 Feld-Deskriptoren Diese beschreiben den Typ einer Klasse, einer Instanz oder einer lokalen Variablen. Der Typ wird dabei mittels eines konstanten Literals kodiert. Folgende Literale werden von der JVM Spezifikation benannt [JVMS][ 4.3.2]: Literal Type Bedeutung B Byte Byte Wert C Char Unicode Zeichen D Double Double Wert F Float FloatingPoint Wert I Int Integer Wert J Long Long Integer Wert L<Klassenname>; Referenz Eine Instanz der Klasse Klassenname S Short Short Integer Wert Z Boolean true oder false [<Komponententyp> Referenz Referenz auf ein Array Tabelle 3: Feld-Deskriptoren 14

Der JVM Bytecode Alle Literale außer 'L' und '[' gehören zu den Base Typen. Diese stellen die Integralen Typen der JVM dar (siehe auch Kapitel 3.3.6 und [JVMS][ 3.3.1]). Der Komponententyp eines Arrays ist wiederum ein Literal aus obiger Tabelle. Beispiel: Der Deskriptor einer Variablen vom Typ Integer ist das Literal I. Der Deskriptor einer Variablen vom Typ String sieht folgendermaßen aus: Ljava/lang/String;. Der Deskriptor einer Referenz auf ein Array mit Elementen vom Typ Integer sieht wie folgt aus: [I;. Ein zweidimensionales Array mit Elementen vom Typ String sieht wie folgt aus: [[Ljava/lang/String;. 3.2.3.2 Methoden-Deskriptoren Ein Methoden-Deskriptor beschreibt die Parameter und den Rückgabewert einer Methode. Der Name ist hier nicht enthalten. Ein Methoden-Deskriptor ist dabei aus folgenden Elementen aufgebaut [JVMS][ 4.3.3]: ( Feld_Deskriptor_1,, Feld_Deskriptor_n ) Return_Deskriptor Der Return-Deskriptor ist dabei entweder ebenfalls ein Feld-Deskriptor oder ein Void-Deskriptor. Void-Deskriptoren werden durch das Literal 'V' dargestellt. Beispiel: Der Deskriptor der Methode Object method (String Name, long[][] werte) sieht wie folgt aus: (Ljava/lang/String;[[J)Ljava/lang/Object; 3.2.4 Tabellen mit Bezug zum Constant-Pool Im Class-File gibt es noch drei weitere Tabellen mit Elementen, die sich auf den Constant-Pool beziehen. Hierbei handelt es sich um die field_info fields[], method_info methods[]und attribute_info Felder des ClassFile Typs. Dies sind Listen mit den Feldern, Methoden oder Attributen der mit dem ClassFile korrespondierenden Klasse. 3.2.4.1 Felder In dieser Tabelle können alle Informationen zu den in der korrespondierenden Klasse deklarierten Feldern gefunden werden. Die field_info fields[] Tabelle enthält Elemente vom Typ field_info, die wie folgt aufgebaut sind [JVMS][ 4.5]: field_info{ access_flags; (2 Bytes) name-index; (2 bytes) descriptor_index; (2 Bytes) attribute_count; (2 Byte) attribute_info[] attributes; 15

Der JVM Bytecode Die Bedeutung der Felder ist dabei wie folgt: Feld access_flags Name-Index descriptor_index attribute_count attribute_info[] attributes Bedeutung Ein Byte, das den Zugriffsmodifier eines Feldes kodiert. Dabei hat jedes Flag des Bytes die Bedeutung eines Zugriffsmodifiers (siehe 2.1.4.2). Index auf einen CONSTANT_Utf8 Eintrag im Constant-Pool (Name des Feldes) Index auf einen CONSTANT_Utf8 Eintrag im Constant-Pool (Deskriptor des Feldes) Anzahl der Elemente vom Typ attribute_info in der folgenden Tabelle Tabelle mit Einträgen vom Typ attribute_info (Attribute des Feldes) Tabelle 4: Elemente der field_info Struktur 3.2.4.2 Methoden Das method_info methods[] Feld der Class-File Struktur enthält Einträge vom Typ method_info. Diese repräsentieren je eine Methode, die in der korrespondierenden Klasse deklariert ist. Sie ist wie folgt aufgebaut: method_info{ access_flags; (2 Bytes) name-index; (2 bytes) descriptor_index; (2 Bytes) attribute_count; (2 Byte) attribute_info[] attributes; Die Bedeutung der Felder entspricht der Bedeutung der Felder der field_info Struktur. Die Accessflags der Felder als auch der Methoden sind wie folgt, durch eine zwei Byte große Maske, kodiert [JVMS][ 4.5, 4.6]: Zugriffsmodifier public private protected Wert 0x0001 0x0002 0x0004 16

Der JVM Bytecode Zugriffsmodifier static final synchronized volatile transient native abstract strictfp Wert 0x0008 0x0010 0x0020 0x0040 0x0080 0x0100 0x0200 0x0400 Tabelle 5: Zugriffsmodifier 3.2.4.3 Attribute / Annotations Als letztes Feld der ClassFile Struktur bleibt noch die Liste der Attribute. Wie oben bereits erwähnt, sind Tabellen dieses Typs auch Bestandteil der Fields und der Method Tabelle. Tabellen dieses Typs enthalten Einträge des Typs attribute_info, welche wie folgt aufgebaut sind [JVMS][ 4.7]: attribute_info{ attribute_name_index; (2 Byte) attribute_length; (4 Byte) info[]; Die Bedeutung der Felder ist wie folgt: Feld attribute_name_index attribute_length Info[] Bedeutung Index auf einen CONSTANT_Utf8 Eintrag im Constant-Pool (Name des Attributes) Anzahl der Bytes des folgenden Info Strings Liste mit Attributen Tabelle 6: Elemente der attribute_info Struktur Attribute sind dabei Namen - Werte Paare. Sie können für Klassen, Felder und Methoden definiert werden. Sie befinden sich dann immer innerhalb der Attribut-Tabelle der entsprechenden Struktur. Die JVM Spezifikation definiert folgende Attribute für Felder Methoden oder ClassFiles [JVMS][ 4.7ff]: 17

Der JVM Bytecode Name Vorkommen in Strukturen Bedeutung Synthetic Depricated Signature RuntimeVisibleAnnotat ions RuntimeInvisibleAnnot ations SourceFile InnerClasses EnclosingMethod SourceDebugExtension Code LineNumberTable LocalVariableTable Exceptions RuntimeVisibleParamet erannotations AnnotationDefault Methoden, Felder, Class-Files (max. 1x) Methoden, Felder, Class-Files (max. 1x) Methoden, Felder, Class-Files (exakt 1x) Methoden, Felder, Class-Files (max. 1x) Methoden, Felder, Class-Files (max. 1x) Class-Files (exakt 1x) Class-Files (max. 1x) Class-Files (max. 1x) Class-Files (max. 1x) Methoden (max. 1x) Code (max. 1x) Code (max 1x) Methoden (max. 1x) Methoden (max. 1x) Methoden (max. 1x) ein vom Compiler generiertes Element Elemente, die nicht mehr verwendet werden sollen Verweis auf einen Eintrag im Constant-Pool Annotations, die zur Laufzeit über Reflections abgefragt werden können Annotations, die nicht zur Laufzeit abgefragt werden können Verweis auf einen Eintrag im Constant-Pool, der den Namen der Quellcode-Datei enthält Zugriffsrechte und Eigenschaften von inneren Klassen Zusatzinformationen zu lokalen und anonymen Klassen zusätzliche Texte für den Debugger enthält die Bytecode -Implementierung einer Methode Zeilennummern innerhalb von Methoden, die vom Debugger genutzt werden können Name und Typinformationen der lokalen Variablen einer Methode Ausnahmen, die eine Methode auslösen kann Annotations von Methoden, die zur Laufzeit mittels Reflexion abgefragt werden können Default Wert von Annotation Methoden, die per Reflection abgefragt werden können ConstenValue Felder Verweis auf eine Konstante im 18

Der JVM Bytecode Name Vorkommen in Strukturen Bedeutung (max. 1x) Tabelle 7: Attribute von der JVM Spezifikation definiert Constant-Pool, mit der ein Feld initialisiert wird Grundsätzlich haben alle Attribute die oben beschriebene Struktur. Dabei enthalten einige der Attribute weitere Informationen und daher auch größere Strukturen als oben beschrieben. Für eine genauere Beschreibung siehe [JVMS][ 4.7]. Für alle Attribute gilt, dass sie nur maximal einmal in ihrer Elternstruktur vorkommen dürfen. Es gibt aber auch Attribute, die zwingend vorhanden sein müssen. Aus dem Vorhandensein von Attributen können wichtige Informationen ermittelt werden. Beispielsweise lässt sich am Vorhandensein des Attributes InnerClass ablesen, dass die korrespondierende Klasse mindestens eine Innere Klasse besitzt. Ob weitere Innere Klassen vorhanden sind, können dem Inhalt des Attributs entnommen werden. Eines der oben aufgelisteten Attribute hat jedoch für diese Arbeit eine besonders wichtige Bedeutung. Es ist das Code Attribut, welches deshalb nun genauer betrachtet wird. 3.2.4.4 Code Attribut Alle Methoden, die nicht abstract oder native deklariert sind, müssen exakt ein Code Attribut besitzen. Code Attribute tragen u.a. Informationen über: die Größe des Stacks, der benötigt wird, um diese Methode ausführen zu können die Anzahl der lokalen Variablen die JVM Befehlsfolge, die ausgeführt werden muss, um diese Methode auszuführen die Exceptions, die ausgelöst werden können Ihre Struktur sieht wie folgt aus [JVMS][ 4.7.3]: Code_attribute { attribute_name_index; (2 Bytes) attribute_length; (4 Bytes) max_stack; (2 Bytes) max_locals; (2 Bytes) code_length; (4 Bytes) code[code_length]; exception_table_length; (2 Bytes) { start_pc; (2 Bytes) end_pc; (2 Bytes) handler_pc; (2 Bytes) catch_type; (2 Bytes) exception_table[exception_table_length]; attributes_count; (2 Bytes) attribute_info attributes[attributes_count]; 19

Der JVM Bytecode Die Bedeutung ist wie folgt: Feld attribute_name_index attribute_length max_stack max_locals code_length Code[] exception_table_length start_pc, end_pc handler_pc catch_type attributes_count attribute_info attributes[] Bedeutung Name des Attributes Länge des Attributes in Bytes maximale Größe des Stacks, der zur Ausführung dieser Methode benötigt wird Anzahl der lokalen Variablen Größe der Liste mit JVM Befehlen in Bytes Liste von JVM Befehlen; diese stellen den implementierten Code dar Anzahl der Exceptions, die für diese Methode definiert worden sind beschreiben Abschnitte, in denen ein Exceptionhandler aktiv ist der erste Befehl, der vom Exceptionhandler ausgeführt werden muss; Index auf das Code Array Index im Constant-Pool auf einen CONSTANT_Class Eintrag, der die zu fangende Klasse beschreibt Anzahl der Attribute des Code Attributes die Attribute des Code Attributes; dies können die LineNumberTable und LocalVariableTable Attribute sein Tabelle 8: Elemente der Code_attribute Struktur Vor allem die JVM Befehlsfolge, also das Code Array des Code Attributs, ist von besonderem Interesse. Hierbei handelt es sich um das, was vom Prozessor ausgeführt wird. Dies ist die Stelle, an der Referenzen entstehen können. Die JVM Befehlsfolge von Methoden muss daher zum Auffinden von Referenzen analysiert werden. Daher ist es notwendig, den Befehlssatz der JVM interpretieren zu können. Dieser wird im Kapitel 3.3.7 genauer betrachtet. 3.3 Die Java-Virtual-Machine (JVM) Bytecodedateien sind streng genommen lediglich eine Form der Persistenz von Typen. Instanzen dieser Typen entstehen erst in der JVM. Dabei wandelt die JVM Bytecodedateien in Objekte um, die für die Verarbeitung optimiert sind, und legt diese in bestimmte Speicherbereiche ab. Bei diesem Vorgang werden zusätzliche Objekte erzeugt, die so nicht in Bytecodedateien zu finden sind. Um der Aufgabenstellung dieser Arbeit zu genügen, ist es jedoch notwendig, alle Deklarationen und Referenzen eines Programms zu betrachten. Eigentlich müsste der Speicherbereich, in dem Instanzen abgelegt werden, Gegenstand der Untersuchung sein, denn hier sind alle Klassen, von denen Instanzen gebildet werden können, zu finden. Dies ist jedoch nur mit sehr hohem Aufwand 20

Der JVM Bytecode und zudem auch nur in Abhängigkeit des Betriebssystems, auf dem die JVM installiert ist, möglich. Statt dessen werden Bytecodedateien betrachtet. Diese entstehen durch den Kompilierungsprozess, also bevor Klassen (im Sinne von Templates zur Erzeugung von Instanzen) von der JVM erzeugt und im Speicher abgelegt werden. Folglich müssen weitere Operationen, die bei der von der JVM vorgenommen Umwandlung von Bytecodedateien in Klassen durchgeführt werden, berücksichtigt werden (siehe Kapitel 3.3.6.2). Ein rudimentäres Verständnis des Aufbaus und der Funktionsweise der JVM ist daher unumgänglich. Dieses Kapitel beschreibt daher die Strukturen und einige für diese Arbeit wichtige Prozesse der JVM im Überblick. Hierdurch wird ermöglicht, die Bytecodedateien derart zu interpretieren, dass alle Deklarationen und Referenzen aus ihnen abgeleitet werden können. 3.3.1 Einführung Die JVM bildet einen realen Prozessor nach. Genau wie ein Prozessor hat sie einen Befehlssatz und arbeitet nach dem Stackprinzip [JVMS][ 7.2]. Sie manipuliert verschiedene Speicherbereiche und erzeugt Befehlssätze, die von einer realen CPU verarbeitet werden können [JVMS][ 1.2]. Eine JVM muss sowohl spezifisch zu einem Betriebssystem als auch zu einer CPU implementiert werden. Als Eingabe akzeptiert eine JVM ausschließlich Bytecode und hat damit keine Berührungspunkte mit der Programmiersprache Java. Tatsächlich werden JVM s auch von ganz anderen Sprachen genutzt. Beispielsweise gibt es Compiler, die Bytecode für JVM s erzeugen und als Eingabe Textdateien der Sprachen ADA, C, PHP, Ruby, Phyton... (um nur einige zu nennen) akzeptieren. Entscheidend ist jedoch immer ein von der verwendeten CPU und dem Betriebssystem unabhängiger Bytecode, der auf virtuellen Maschinen ablaufen kann (siehe auch Abb. 1). 3.3.2 Strukturen der JVM Die JVM reserviert zur Laufzeit verschiedene Datenbereiche, in denen die zur Ausführung eines Programms benötigten Daten abgelegt werden. Einige dieser Bereiche werden reserviert, wenn die JVM gestartet wird und werden beim Beenden wieder freigegeben. Andere Bereiche werden für jeden Thread, den die JVM erzeugt, neu angelegt und beim Beenden des Threads wieder freigegeben [JVMS][ 3.5]. Die JVM ist als Stackmachine implementiert. Sie erwartet daher die Operanden eines Befehls auf einem Verarbeitungsstack, auf den die Execution Engine Zugriff hat. Das Ergebnis der Operation wird dann wieder auf den Stack gelegt. Der Operand Stack (auch JVM - Stack) ist als 32 Bit LIFO Stack ausgeführt. JVM Stacks sind zur Ausführung von Code, der in Bytecodedateien hinterlegt ist, vorgesehen. Dabei handelt es sich immer um einen für die JVM spezifischen Befehlssatz, welcher dem Befehlssatz eines Prozessors ähnelt. Die JVM kann aber optional auch sog. Native Code ausführen. Dieser Code wird in der Native Method Area abgelegt. Mittels des Native Stack, welcher ebenfalls für jeden Thread angelegt wird, kann dann dieser Code ausgeführt werden. Der Native Stack und auch die Native Area sind laut Spezifikation optional [JVMS][ 3.5.6]. 21

Der JVM Bytecode Abbildung 2: Strukturen der JVM Native Code müsste eigentlich mit berücksichtigt werden. Dies würde jedoch den Rahmen dieser Arbeit sprengen. Daher werden Strukturen, die in diesen Themenbereich fallen, nicht weiter betrachtet. Das gleiche gilt für den Heap. Zwar ist es durchaus möglich, die Method Area als Teil des Heaps zu implementieren, dies ist jedoch nicht von der JVM Spezifikation vorgeschrieben [JVMS][ 3.5.4]. Aus diesem Grunde wird die Method Area als eigenständige Struktur betrachtet, obwohl sie logisch wohl zum Heap gehört. 3.3.3 Die Method Area Die Method Area ist eine Struktur der JVM, auf die alle Threads Zugriff haben. Sie enthält alle Informationen der Class-File Struktur einer Bytecodedatei. Dies sind der Constant-Pool, die Felder und Methoden und die Attribute einer Bytecodedatei: Der Constant-Pool Numerische Konstanten String Konstanten Referenzen auf Felder 22

Der JVM Bytecode Methoden (dieser Klasse und referenzierten Klassen) Klassen (die von dieser Klasse referenziert werden) die Felder der Klasse Methoden (den Code der Methoden, also JVM Befehle) Attribute (z.b. Type Parameter, Exceptiontables ) (siehe hierzu auch Abb. 2) Dabei lädt der ClassLoader die Bytecodedatei und übergibt den Inhalt der JVM. Diese erzeugt dann eine implementationsabhängige interne Darstellung der Klasse und legt diese in der Method Area ab. Innerhalb der Method Area wird eine Substruktur erzeugt, der Runtime Constant-Pool (RCP), welcher die Informationen des Constant-Pools einer Bytecodedatei enthält. Die JVM versucht dabei, alle Referenzen des Constant-Pools aufzulösen und veranlasst den ClassLoader Bytecodedateien nachzuladen, falls einige dieser Referenzen nicht aufgelöst werden können. Dieser Vorgang wird als Resolution bezeichnet [JVMS][ 2.17.1]. Wichtig dabei ist, dass Methoden generiert werden, die nicht explizit im Programmcode bzw. in der Bytecodedatei zu finden sind. Dabei handelt es sich um zwei Methoden, die Teil von Konstruktoren sind. Genau eine Methode mit dem Namen <init>. Diese wird immer dann aufgerufen, wenn eine neue Instanz erstellt wird. Sie gehört zum Konstruktor einer Klasse und initialisiert diese, falls sie nicht initialisiert ist [JVMS][ 3.5.4, 3.9]. Sie stellt also den Default Konstruktor dar. Des weiteren genau eine Methode <client>. Diese initialisiert statische Member einer Klasse oder eines Interfaces, falls welche vorhanden sind [JVMS][ 3.5.4, 3.9]. Sie wird immer dann ausgeführt, wenn eine Klasse statische Felder besitzt und diese instantiiert wird. 3.3.4 JVM Stack und Frames Jeder Thread, der innerhalb einer JVM erzeugt wird, besitzt einen JVM Stack und ein PC Register (siehe hierzu auch Abb. 2). Das PC Register verweist dabei immer auf den nächsten in diesem Thread auszuführenden Befehl. Der Stack eines Threads enthält eine Liste mit Frames, die abzuarbeiten sind. Dabei erzeugt der JVM Stack eines Threads für jede Methode, die aufgerufen wird, einen Frame und zerstört diesen wieder, wenn die Methode abgearbeitet wurde. Ein Frame ist eine Struktur, die die folgenden Komponenten enthält: die lokalen Variablen einer Methode einen Operandstack eine Referenz auf den Runtime Constant-Pool der Klasse, zu der die Methode dieses Frames gehört 23

Der JVM Bytecode Abbildung 3: JVM Stack Komponenten Dabei wird ein Frame n erzeugt, wenn die Methode, die mit Frame n-1 korrespondiert, eine weitere Methode aufruft. Folglich wird Frame 1 gelöscht, wenn alle folgenden Frames abgearbeitet wurden. Das Ergebnis einer Methode, falls eines existiert, wird auf den Operandstack des aufrufenden Frames abgelegt [JVMS][ 3.6]. Die Befehle der JVM werden auf dem Operandstack eines Frames ausgeführt. Dabei handelt es sich um Befehle aus dem Befehlssatz der JVM. JVM-Befehle entstehen beim Kompilieren von Methoden des Quellcodes. Sie sind Bestandteil von Bytecodedateien. Im Rahmen dieser Arbeit ist es wichtig zu verstehen, dass Referenzen innerhalb von Frames der JVM entstehen. Es kann gefolgert werden, dass Referenzen, die in einem Frame entstehen, sich lediglich auf Elemente der Menge der Lokalen Variablen oder des Constant-Pools beziehen können. Es ist vom ausgeführten Befehl abhängig, worauf sich eine Referenz bezieht. Daher ist es notwendig, den Befehlssatz der JVM genauer zu betrachten (siehe Kapitel 3.3.7). 3.3.5 Die Execution Engine Die Execution Engine stellt den eigentlichen virtuellen Prozessor der JVM dar. Sie führt die Befehle des Bytecodes aus und bedient sich dabei des JVM Stacks. Neben dem Laden der Operanden eines Befehls und der Steuerung des PC-Registers gehört es auch noch zu ihren Aufgaben, den Befehl in nativen Maschinencode umzuwandeln und auf dem physikalischen Prozessor auszuführen. Die Ausführung eines Bytecode Befehls durch die Execution Engine ist in folgender Abbildung dargestellt. 24

Der JVM Bytecode Abbildung 4: Execution Engine Zyklus Zu beachten ist dabei, dass die Execution Engine innerhalb eines Frames arbeitet. Hier hat sie Zugriff auf die lokalen Variablen und die Parameter der Methode. Über eine Referenz auf die Method Area der korrespondierenden Klasse kann auf den Bytecode der Methode und auf weitere Referenzen der korrespondierenden Klasse bzw. über den Runtime Constant-Pool auf andere Klassen zugegriffen werden. 3.3.6 Typen der JVM Die JVM unterscheidet bei den Datentypen zwischen zwei verschiedenen Grundtypen, den Primitiven Typen und den Referenztypen. 3.3.6.1 Die Primitiven Typen Wie aus der nachstehenden Tabelle hervorgeht, werden Primitive Typen in weitere Subtypen unterteilt [JVMS][ 3.3 ff.]. Dabei entsprechen die Numerischen Typen den entsprechenden Basis Typen des Class-Files (siehe auch 2.1.3.1). Die Integral-Typen werden dabei intern als Integer abgelegt. Der Boolean Typ wird ebenfalls innerhalb der JVM wie ein Integer behandelt. Dabei wird das Literal true durch den Wert 1 und das Literal false durch den Wert 0 repräsentiert. Bei diesen Typen handelt es sich um automatisch in die JVM integrierte Typen. Sie werden nicht in irgendeiner Form über den Bytecode deklariert sonder sind schon da. Für diese Arbeit bedeutet das, dass es Referenzen auf Typen geben kann, die nicht explizit deklariert worden sind. Daher werden diese in dem zu implementierenden Programm so behandelt, als wären sie im Bytecode deklariert worden. Sie werden standardmäßig den vorhandenen Typen eines Programms hinzugefügt. 25

Der JVM Bytecode Primitive Typen numerische Typen Integral-Typen Byte (8 Bit, vorzeichenbehaftete Integer) Boolean Tabelle 9: Primitive Typen der JVM 3.3.6.2 Die Referenz-Typen FloatingPoint-Typen Referenz-Typen unterteilen sich in folgende Subtypen [JVMS][ 3.4]: Referenztypen Klassen Typen Interface Typen Tabelle 10: Referenztypen der JVM Array Typen (diese Klassen werden dynamisch von der JVM erzeugt) Aufzählungstypen (diese Klassen werden vom Compiler erzeugt) Char (werden automatisch zu Integer erweitert) Short (16 Bit, vorzeichenbehaftete Integer) Int (32 Bit, vorzeichenbehaftete Integer) Long (64 Bit, vorzeichenbehaftete Integer, 2 JVM Worte) Float (32 Bit) Double (64 Bit, 2 JVM Worte) Die Referenz null ist eine spezielle Referenz, die keinen Laufzeittyp hat. Diese kann jedoch zu allen Referenztypen gecastet werden. Auch hier muss wiederum unterschieden werden, welche Deklarationen aus dem Bytecode ablesbar sind und welche implizit schon vorhanden sind. Die Deklarationen von Klassen- und Interface- Typen können natürlich in Class-Files gefunden werden. Aufzählungstypen (in Java Enum`s) werden vom Compiler in Bytecodedateien als Klasse deklariert. Sie können also wie Klassen und Interfaces in Bytecodedateien gefunden werden. Anders verhält es sich mit Arrays. Sie werden von der JVM ebenfalls als Klasse oder Typ betrachtet. Allerdings sind solche Deklarationen nicht in der Bytecodedatei zu finden. Sie werden erst durch den ClassLoader der JVM generiert und können daher nicht im Bytecode gefunden werden [JVMS][ 5.3]. Genau wie bei den impliziten Typen werden deklarierte Arrays daher wie deklarierte Typen betrachtet. 26

Der JVM Bytecode 3.3.7 Befehlssatz der JVM Dieses Kapitel gibt einen Überblick über die Befehle der JVM. Diese werden in Gruppen geordnet und hinsichtlich der Referenzbildung bewertet. Detaillierte Informationen zu einzelnen Befehlen können der JVM Spezifikation im Kapitel 3.11ff entnommen werden. Wie schon weiter oben thematisiert, arbeitet die JVM intern wie eine Stackmaschine. Ihre Befehle werden auf dem Operandstack eines Frames ausgeführt. Ein Frame wird wiederum für jeden Methodenaufruf vom JVM-Stack erzeugt (s. Kapitel 3.3.4). Wird ein JVM-Befehl ausgeführt, werden zunächst die hierfür notwendigen Operanden auf den Operandstack des aktiven Frames geladen. Danach wird der Befehl ausgeführt und das Ergebnis auf den Operandstack zurückgeschrieben. Betrachtet man einen Frame zunächst als ein geschlossenes System, so kann man Schnittstellen mit der Außenwelt erkennen. Dies sind [JVMS][ 3.6]: eine Referenz auf den Runtime Constant-Pool ein Array namens LocalVariables. Hierzu gehören: eine Referenz auf die Instanz, zu der die Methode, für die ein Frame erzeugt wurde, gehört (also der this Zeiger) die Parameter der Methode, für die ein Frame erzeugt wurde die lokalen Variablen der Methode der Rückgabewert einer Methode. Dies ist auch genau die Menge von Objekten, auf die Referenzen gebildet werden können. Das Erzeugen einer Referenz stellt sich dabei als ein lesender oder schreibender Bezug auf ein Element der obigen Menge dar. Zu beachten ist, dass der Runtime Constant-Pool selbst eine Menge von Objekten darstellt, auf die nur lesend zugegriffen werden kann. Hierbei handelt es sich ja ausschließlich um Konstanten. Bezüge auf den Constant-Pool sind dabei Referenzen auf Konstanten, Typen, Methoden oder Feldern von Klassen. Auf die Elemente des LocalVariable Arrays kann hingegen lesend und schreibend zugegriffen werden. Einige Befehle des JVM Befehlssatzes bilden keine Referenz im Sinne dieser Arbeit. Sie nehmen lediglich Bezug auf den Stack. Dies an sich hat noch keine Auswirkung auf die Außenwelt eines Frames. Lediglich JVM Befehle, die eine Lade- oder Speicher-Operation ausführen, nehmen Bezug auf die Außenwelt eines Frames und bilden daher Referenzen. Die folgende Tabelle listet alle JVM Befehle auf und ordnet diese in Gruppen ein. Dabei werden die Gruppen dahingehend geordnet, ob sie Referenzen bilden oder nicht. 27

Der JVM Bytecode bilden Referenzen ja nein Gruppe Operation Befehl Laden und Speichern Zugriff auf Objekte Zugriff auf Typen Arithmetik Flusskontrolle Konstante Stack LocalVariable Stack Stack LocalVariable Arraywert Stack Stack Arraywert Variablen Arrays Objekte Exceptions Methoden Typumwandlung Stack Stack Ganzzahlen Gleitkomma Bitweise Sprünge Unterprogramme Tabellen Threads aconst_null, iconst_<n> (iconst_m1, iconst_0, iconst_1, iconst_2, iconst_3, iconst_4, iconst_5), lconst_<n> (lconst_0, lconst_1), fconst_<n> (fconst_0, fconst_1, fconst_2), dconst_<n> (dconst_0, dconst_1) bipush, sipush, ldc, ldc_w, ldc2_w, iload, iload_<n> (iload_0, iload_1, iload_2, iload_3) lload, lload_<n> (lload_0, lload_1, lload_2, lload_3) fload, fload_<n> (fload_0, fload_1, fload_2, fload_3) dload, dload_<n> (dload_0, dload_1, dload_2, dload_3) aload, aload_<n> (aload_0, aload_1, aload_2, aload_3) istore, istore_<n> (istore_0, istore_1, istore_2, istore_3) lstore, lstore_<n> (lstore_0, lstore_1, lstore_2, lstore_3) fstore, fstore_<n> (fstore_0, fstore_1, fstore_2, fstore_3) dstore, dstore_<n> (dstore_0, dstore_1, dstore_2, dstore_3) astore, astore_<n> (astore_0, astore_1, astore_2, astore_3) iaload, laload, faload, daload, aaload, baload, caload, saload iastore, lastore, fastore, dastore, aastore, bastore, castore, sastore iinc, wide newarray, anewarray, arraylength, multianewarray getstatic, putstatic, getfield, putfield, new, checkcast, instanceof athrow invokevirtual, invokespecial, invokestatic, invokeinterface, ireturn, lreturn, freturn, dreturn, areturn, return i2l, i2f, i2d, i2b, i2c, i2s, l2i, l2f, l2d, f2i, f2l, f2d, d2i, d2l, d2f nop, pop, pop2, dup, dup_x1, dup_x2, dup2, dup2_x1, dup2_x2, swap iadd, isub, imul, idiv, irem, ineg, ladd, lsub, lmul, ldiv, lrem, lneg fadd, fsub, fmul, fdiv, frem, fneg, dadd, dsub, dmul, ddiv, drem, dneg ishl, ishr, iushr, iand, ior, ixor, lshl, lshr, lushr, land, lor, lxor lcmp, fcmpl, fcmpg, dcmpl, dcmpg, ifeq, ifne, iflt, ifge, ifgt, ifle, if_icmpeq, if_icmpne, if_icmplt, if_icmpge, if_icmpgt, if_icmple, if_acmpeq, if_acmpne, ifnull, ifnonnull, goto, goto_w jsr, ret, jsr_w tableswitch, lookupswitch monitorenter, monitorexit Tabelle 11: JVM Befehle 28

Bytecode Framework Bibliotheken 4 Bytecode Framework Bibliotheken In vielen Forschungsprojekten werden durch Erweiterung der JVM mit Metaprogrammen Verbesserungen erzielt, so z.b. in der Programmiersprache Java. Die aspektorientierte Programmierung ist beispielsweise eine solche Entwicklung. Hierbei handelt es sich um Metaprogramme, die eine Bytecodedatei manipulieren, bevor diese von der JVM geladen wird. Hierdurch können einem Programm zusätzliche Aspekte hinzugefügt werden. Um dies zu erreichen, muss ein Programm nicht neu kompiliert werden. Die Programmerweiterung erfolgt genau dann, wenn die Bytecodedatei geladen wird, also nach dem Kompiliervorgang. Für solche Zwecke, aber auch zur Analyse von Bytecodedateien, wurden eine Reihe von Frameworks entwickelt. Eines dieser Frameworks ist ASM [ASM] vom OW2 Consortium. Hierbei handelt es sich um ein Framework, welches vor allem für die Manipulation von Bytecodedateien vorgesehen ist. Es wurde besonderer Wert auf ein schlankes Design und eine performance-optimierte Ausführung gelegt. Aber auch die Untersuchung von Bytecodedateien wird durch den Einsatz des Vistitor-Patterns gut unterstützt. Dieses Framework ist sehr gut dokumentiert, allerdings weist es eine geringe Abstraktion zum Bytecode auf, was die Einarbeitung ein wenig erschwert. Javassist ist ein Framework, welches als Unterprojekt des JBoss open Source SOA Projektes entwickelt wurde [Javassist]. Es handelt sich um eine recht große Bibliothek mit einem sehr hohen Abstraktionsgrad zum Bytecode. Integriert ist ein eigener Compiler, welcher es ermöglicht, kleinere Snippets anstatt ganzer Dateien zu verarbeiten. Der Fokus bei der Entwicklung dieses Frameworks lag in der Manipulation des Bytecodes und der leichten Erlernbarkeit desselben. Dagegen stand die Untersuchung von Bytecodedateien wohl nicht im Vordergrund. BCEL stellt einen Kompromiss zwischen den oben genannten Frameworks dar. Auf der einen Seite bietet es einen sehr hohen Abstraktionsgrad zum Bytecode. Im Besonderen können die verwendeten Klassen intuitiv den entsprechenden Elementen einer Bytecodedatei zugeordnet werden. Dies macht das Erlernen dieses Frameworks sehr einfach. Auf der anderen Seite wird die Untersuchung von Bytecodedateien durch intensiven Einsatz des Vistitor-Patterns sehr gut unterstützt. Die Dokumentation des Frameworks ist ausreichend gut. BCEL ist Bestandteil jedes JRE s, welches von SUN ausgeliefert wird. Zur Lösung der Aufgabenstellung habe ich daher das BCEL Framework als Basis gewählt. 4.1 BCEL BCEL besteht hauptsächlich aus drei Teilen, einem Package, welches die statischen Abhängigkeiten eines Class-Files repräsentiert, einem Package, welches für Codemanipulationen genutzt werden kann und verschiedenen Werkzeugen und Codebeispielen. Die statischen Abhängigkeiten einer Bytecodedatei werden durch Objekte im Package org.apache.bcel.classfile repräsentiert. Hier sind alle in der JVM Spezifikation [JVMS] beschriebenen Komponenten und Datenstrukturen auf Klassen gemappt. Unter Voraussetzung der Kenntnis des Aufbaus von Bytecodedateien ist die Klassenhierarchie des 29

Bytecode Framework Bibliotheken BCEL Frameworks daher sehr leicht verständlich. Ein Objekt namens JavaClass ist die Wurzel der Klassenhierarchie von BCEL. Hierbei handelt es sich um ein Kompositum aus Feld- und Methodenobjekten, Referenzen auf die Superklasse und den von einer Klasse implementierten Interfaces. Der interne Aufbau der Klasse entspricht der unter Kapitel 3.2.1 beschriebenen Class-File Struktur. Eigenschaften von Klassenobjekten manifestieren sich in Objekten vom Typ Attribut. Sie können Objekten des Typs JavaClass, Field, Method oder Code zugeordnet sein. Enthält eine Java-Klasse beispielsweise innere Klassen, dann besitzt diese auch genau ein Attribut vom Typ InnerClass. Dieses enthält dann Verweise auf Elemente des Constant-Pools, in welchen die Eigenschaften der Inneren Klasse(n) beschrieben werden. Abbildung 5: Bcel JavaClass Objektdiagramm (aus [BCEL]) Der ConstantPool wird durch ein eigenes Objekt repräsentiert. Wie im Kapitel 3.2.2 beschrieben, handelt es sich hierbei um eine Liste aus Konstanten, die auf Objekte vom Typ Constant gemappt werden. Der Typ Constant ist die Oberklasse aller Objekte, die einen Konstantentyp repräsentieren. Jedes JavaClass Objekt hat einen Member vom Typ ConstantPool. 30

Bytecode Framework Bibliotheken Abbildung 6: Bcel Constant-Pool Objektdiagramm (aus [BCEL]) Methoden, sofern sie implementiert sind, besitzen ein Attribut vom Typ Code. Dieses enthält unter anderem einen Member namens Code vom Typ byte[]. Dieser Member repräsentiert eine Liste der in dieser Methode auszuführenden JVM Befehle. Hierbei handelt es sich allerdings nur um den Opcode der JVM Befehle und nicht um Klassen, die z.b. mittels eines Visitors untersucht werden könnten. Dieser Nachteil wird jedoch durch die Klassen des Package org.apache.bcel.generic behoben. Die Klassen in diesem Package sind dafür vorgesehen, Bytecodedateien dynamisch zu erzeugen und zu manipulieren. Wurzel der Objekthierarchie der generic Klassen ist eine Klasse vom Typ ClassGen. Sie basiert auf einem JavaClass Objekt aus dem Package org.apache.bcel.classfile. Eine Instanz von JavaClass muss dem Konstruktor von ClassGen als Parameter übergeben werden. Das ClassGen Objekt besitzt im Gegensatz zum JavaClass Objekt Getter und Setter, mittels derer Klassenelemente hinzugefügt, verändert oder gelöscht werden können. Dasselbe gilt für alle anderen Objekte in diesem Package, die eine Entsprechung zu einer Klasse im Package org.apache.bcel.classfile haben. Die oben erwähnte Code Liste von Methoden wird in einem MethodGen Objekt durch eine Liste vom Typ InstructionList repräsentiert. Diese enthält Instanzen von Klassen, die die entsprechenden JVM Befehle abstrahieren. Jeder JVM Befehl ist auf eine gleichnamige Klasse gemappt. JVM Befehle sind per Subtyping mittels Interfaces gruppiert. Die Gruppen entsprechen dabei größtenteils der Einteilung der Befehlsgruppen in der JVM Spezifikation [JVMS][ 3.11ff]. Die Interfaces enthalten Funktionen, durch welche Informationen zum entsprechenden JVM Befehl ermittelt werden können. Alle Instruktionen können mittels eines Visitors traversiert werden. Dabei rufen die accept Methoden der einzelnen Instruktionen implizit die accept Methoden ihrer Superklassen auf. Dies erlaubt es, im Visitor vistit Methoden zu implementieren, die Gruppen von Instruktionen gleich behandeln. 31

Bytecode Framework Bibliotheken Abbildung 7: Bcel ClassGen Objektdiagramm (aus [BCEL]) Während - wie oben beschrieben - die ClassGen Objekte mittels der korrespondierenden JavaClass Instanz erzeugt werden, übernimmt diese Aufgabe für JavaClass Objekte ein Class Parser. Dieser gehört ebenfalls zum BCEL Framework. Im Package org.apache.bcel.generic existiert darüber hinaus eine kleine Objekthierarchie, die die möglichen Datentypen der JVM Spezifikation abstrahiert [JVMS][ 3.2ff]. Es wird zwischen Basic Types (JVM Spec Primitive Types) und Reference Types unterschieden. Die Signaturen von Typen können dabei mittels dieser Objekte ermittelt werden. Diese Klassen und eine Hilfsklasse namens Utilitys erleichtern den Umgang mit Deskriptoren, falls diese aus dem Constant-Pool gelesen werden müssen. 32

Der FaktenExtraktor 5 Der FaktenExtraktor Mittels des Bcel Frameworks ist es nun möglich, die gewünschten Mengen der Deklarationen und der Referenzen aus Bytecodedateien zu generieren. Hierzu wird zunächst eine Instanz des Objektes vom Typ FaktenExtraktor erzeugt. Instanzen von diesem Typ besitzen je einen Member vom Typ DeclarationFactory und ReferenceFactory. Diese Member werden im Konstruktor vom FaktenExtraktor initialisiert. Abbildung 8: FaktenExtraktor Objekte Die DeclarationFactory ist dabei für die Bildung von Objekten vom Typ Declaration und die ReferenceFactory für die Bildung von Objekten vom Typ Reference zuständig. Instanzen vom Typ Declaration bzw. Reference repräsentieren dabei jeweils eine Deklaration bzw. Referenz in einer Bytecodedatei. Die wichtigsten Member einer Referenz sind die Deklaration, die referenziert wird, der Receiver, dem die Referenz zugewiesen wird und der Ort, an dem sich die Referenz befindet. Orte an denen Referenzen und Deklarationen auftreten, werden durch eine Deklaration beschrieben. Daher ist die Eigenschaft location einer Referenz oder einer Deklaration ein Objekt vom Typ IDeclaration. Dabei entstehen die meisten Referenzen innerhalb von Methoden (siehe auch Kapitel 2). 33

Der FaktenExtraktor Der Receiver einer Referenz ist in einem eigenen Objekt vom Typ Receiver gekapselt. Es handelt sich dabei im Prinzip um einen Wrapper eines Objektes vom Typ ITypeDeclaration. Dieses besitzt zusätzlich noch weitere Eigenschaften, die eine TypeDeklaration weiter spezifizieren. Sowohl Referenzen als auch Deklarationen müssen innerhalb der entsprechenden Mengen eindeutig sein. Für Referenzen wird dies erreicht, indem verhindert wird, dass eine Bytecodedatei mehrmals eingelesen wird. Deklarationen dagegen werden in Strukturen vom Typ Map gespeichert. Als Schlüssel der Map dient die Eigenschaft Id von Deklarationen. Deklarationen können daher nur einmal in dieser abgelegt werden. Die Id von Deklarationen ist das Element, mit dem Deklarationen und Referenzen miteinander verknüpft werden können. Sie sind ähnlich dem Prinzip des FullQualifiedNames aufgebaut (siehe Kapitel 5.2). Die Informationen, die zur Verfügung stehen, wenn eine Referenz in einer Methode erzeugt wird, erlauben es, die Id einer Deklaration aufzubauen und das entsprechende ITypeDeclaration Objekt in der Menge der Deklarationen zu finden (siehe Kapitel 5.1). 5.1 Der Programmablauf Ausgehend von einer Bytecodedatei werden die Mengen aller Referenzen und aller Deklarationen gebildet. Sollten sich in dieser Bytecodedatei Bezüge zu Typen befinden, die nicht Bestandteil dieser Bytecodedatei sind, werden die entsprechenden Dateien ebenfalls geladen. Derart nachgeladene Dateien werden jedoch nur hinsichtlich der enthaltenen Deklarationen untersucht. Referenzen, die in diesen Klassen gebildet werden, werden nicht betrachtet. Zunächst werden die Deklarationen in einer Bytecodedatei ermittelt. Dies ist notwendig, da sich Referenzen auf Deklarationen beziehen. Diese müssen daher abrufbar sein. Diese Aufgabe wird von der DeclarationFactory durchgeführt. Deklarationen der Datentypen, die nicht explizit deklariert wurden (siehe Kapitel 3.3.6), werden ebenfalls durch die DeclarationFactory behandelt. Sie erzeugt beim Programmstart Deklarationsinstanzen mit Bezug auf die Basistypen und fügt diese der Menge der Deklarationen hinzu. Arrays werden innerhalb der JVM ebenfalls wie ein eigener Typ behandelt (siehe Kapitel 3.3.6). Daher wird eine Deklarationsinstanz mit Bezug auf einen Array-Typ genau dann generiert, wenn bei der Untersuchung der Bytecodedatei ein Array-Typ festgestellt wird. Im nächsten Schritt werden dann die Referenzen ermittelt. Hierfür ist die ReferenceFactory zuständig. Ihre Aufgabe besteht darin, die in den Methoden der zu untersuchenden Bytecodedateien enthaltenen JVM Befehle hinsichtlich Referenzen zu analysieren. Die nachfolgenden Diagramme stellen die Prozesse schematisch dar. Zunächst ein Überblick des Gesamtprozesses der Bildung von Referenzen und Deklarationen. 34

Der FaktenExtraktor Abbildung 9: Deklarations- und Referenz-Mengen bilden Im folgenden Diagramm wird die Ermittlung von Deklarationen schematisch dargestellt. 35

Der FaktenExtraktor Abbildung 10: Deklarationen ermitteln Die Deklarationen für Basistypen werden in einer Funktion namens initbasictypedeklarations generiert und der Menge der Deklarationen hinzugefügt. Diese Funktion wird nur einmal im Konstruktor der DeclarationFactory aufgerufen. 36

Der FaktenExtraktor Die Funktion builddeklarations dagegen kann beliebig oft aufgerufen werden. Als Parameter erhält diese ein JavaClass Objekt, welches die zu untersuchende Bytecodedatei repräsentiert (siehe auch Abbildung 9). Das Hinzufügen der Deklarationen einer Bytecodedatei erfolgt aus der Klasse FaktenExtraktor heraus. Diese ist für das Parsen der Bytecodedatei und das Erzeugen einer Instanz von JavaClass zuständig. Wie schon erwähnt, werden Deklarationen in einer Sammlung vom Typ Map abgelegt. Diese wird von der DeclarationFactory verwaltet (siehe auch Abbildung 8). Werden bei der Ermittlung von Deklarationen mittels einer der Suchfunktionen der Klasse DeclarationFactory Typen erkannt, die noch nicht im Deklarationspool enthalten sind, laden diese Funktionen die entsprechende Bytecodedatei nach und rufen ebenfalls builddeklarations auf. Dies kann während der Aktionen Methoden des Typs ermitteln, Lokale Variablen und Methodenparameter ermitteln und/oder Felder der Klasse ermitteln der Fall sein. Denn diese Elemente können sich auf einen Typ beziehen, der noch nicht der Menge der Deklarationen hinzugefügt wurde. Im folgenden Diagramm wird schematisch beschrieben, wie Referenzen ermittelt werden. Dieser Prozess wird immer von einer Instanz von FaktenExtraktor aus initiiert. Dabei wird die Methode buildreferences der Klasse ReferenceFactory aufgerufen. Als Parameter wird eine Instanz von Typ JavaClass übergeben. Diese Instanz repräsentiert die Klasse, deren Referenzen ermittelt werden. 37

Der FaktenExtraktor Abbildung 11: Referenzen ermitteln 38

Der FaktenExtraktor Die ReferenceFactory kontrolliert dabei nicht nur den Ablauf des Prozesses zur Ermittlung der Referenzen sondern ermittelt diese auch. Zu diesem Zweck ist ReferenceFactory von EmptyVisitor abgeleitet. EmptyVisitor implementiert zu jedem JVM Befehl eine leere Visitor-Methode mittels derer alle JVM Befehle traversiert werden können. Die Klasse EmptyVisitor wird vom BCEL Framework bereitgestellt. Visitor-Methoden, die zu einem JVM Befehl gehören, der zur Laufzeit Referenzen erzeugt, werden in ReferenceFactory überschrieben. Ihre Aufgabe besteht darin, ID s für Referenzen zu den jeweils referenzierten Deklarationen zu erzeugen. Die so erzeugten ID s werden durch Aufruf der Funktion findoraddtypedeclarationbyid der DeclarationFactory in entsprechende Deklarationsobjekte aufgelöst. Falls dabei festgestellt wird, dass die zugehörige Bytecodedatei noch nicht geparst worden ist, wird diese nachgeladen (siehe auch Abb. 10). Die Funktion addnewreferene der ReferenceFactory hat die Aufgabe, ein Objekt vom Typ IReference zu erzeugen und zu initialisieren. Dabei wird anhand des Typs der Deklaration, auf welche sich die Referenz bezieht, der Receiver und der Typ der Referenz abgeleitet. Diese Referenz wird anschließend der Menge der Referenzen hinzugefügt. 5.2 Deklarations-ID Um die Bezüge von Referenzen auf eine Deklaration ermitteln zu können, müssen diese aus der Menge der Deklarationen ausgewählt werden können. Wichtig dabei ist, dass Deklarationen eindeutig in dieser Menge identifizierbar sind. Bei der Verknüpfung von mehreren Referenzen mit einer Deklaration muss daher eine 1:n Beziehung zwischen der Deklaration und der Menge der Referenzen entstehen. Um Deklarationen effektiv innerhalb einer Menge finden zu können, werden diese in einer HashTable abgelegt. Als Key wird hierzu eine ID zu jeder Deklaration generiert. Die ID besteht in den meisten Fällen aus dem Full-Qualified-Name des entsprechenden Elements. Im Falle von lokalen Variablen, zu denen ja auch Methodenparameter gehören, wird der Variablenname durch den Index der Variablen in der LocalVariableTable ersetzt. Die LocalVariableTable enthält alle Objekte, die die Methodenparameter und die lokalen Variablen einer Methode repräsentieren. Sie ist zur Laufzeit des Programms Bestandteil eines jeden Frames (siehe Kapitel 3.3.4). JVM Befehle können Parameter nur durch Bezüge auf die Einträge der LocalVariableTable ermitteln. Alle Referenzen, die innerhalb einer Methode erzeugt werden können, werden also über die LocalVariableTable bezogen. Der Zugriff auf einen Eintrag in der LocalVariableTable durch einen JVM Befehl erfolgt über den Index des Eintrags. Die LocalVariableTable ist jedoch nicht immer Bestandteil einer Bytecodedatei. Dies hängt von den Einstellungen des Compilers ab. Meist besitzen Klassen, die per.jar in eine Anwendung importiert werden, keine LocalVariableTable. Hierdurch kann die Dateigröße von Bytecodedateien erheblich reduziert werden. Die JVM generiert dann zur Laufzeit die entsprechende LocalVariableTable. Die Namen der lokalen Variablen befinden sich in der LocalVariableTable, falls diese vorhanden ist. Daher kann für lokale Variablen der Full-Qualified-Name nicht immer gebildet 39

Der FaktenExtraktor werden. Der Index aus der LocalVariableTable, auf den ein JVM Befehl zugreift, kann jedoch immer ermittelt werden. Daher enthalten ID s von Deklarationen, die auf lokale Variablen oder Methodenparameter beruhen, statt ihren Namen den Index innerhalb der LocalVariableTable, zu der sie gehören. Der restliche Teil der Id ist nach den Regeln des Full-Qualified-Names aufgebaut. Für den Fall einer Bytecodedatei, die keine LocalVariableTable enthält, können die Deklarationen der Methodenparameter nur aus der Methodensignatur ermittelt werden. Um in diesen Fällen die ID mit dem richtigen Index aufzubauen, ist die Kenntnis des Aufbaus der entsprechenden LocalVariableTable einer Methode notwendig. Dieser ist abhängig von der Methode, für die sie erzeugt wurde. Hier ergeben sich Unterschiede zwischen statischen Methoden, Klassen-Methoden und Methoden, die zu Inneren oder Anonymen Klassen gehören. Die Einträge der LocalVariable Tabellen enthalten alle Elemente in der Reihenfolge, wie sie in den Methoden auftreten. Für die einzelnen Methoden-Typen sieht das wie folgt aus: statische Methoden Methodenparameter lokale Variablen Klassen-Methoden Referenz auf die Klasse, zu der die Methode gehört (this Zeiger) Methodenparameter lokale Variablen Methoden von Inneren/Anonymen Klassen Referenz auf die äußere Klasse Referenz auf die Klasse, zu der die Methode gehört (this Zeiger) Methodenparameter lokale Variablen Entsprechend wird der Index für die Einträge vergeben. Die lokalen Variablen, die innerhalb eines Methodenrumpfes definiert sind, können in diesen Fällen ignoriert werden. Denn Bytecodedateien, deren Methoden keine LocalVariableTable enthalten, sind immer Bestandteil eines.jars. Diese Klassen können daher nicht refaktorisiert werden, und auf eine lokale Methoden-Variable kann außerhalb der Methode keine Referenz existieren. Für Klassen, die Gegenstand einer Analyse der Referenzen und Deklarationen sind, ist jedoch die richtige Einstellung des Compilers essentiell. Für die Eclipse Entwicklungsplattform lassen sich die notwendigen Einstellungen folgendermaßen vornehmen: 40

Der FaktenExtraktor Abbildung 12: Compiler Einstellung Diese Einstellung entspricht der Compiler-Option -g:{vars. 5.3 Interface zu Referenzen und Deklarationen Die ermittelten Mengen der Referenzen und der Deklarationen können über mehrere Interfaces abgefragt werden. Alle Interfaces zusammen sind hierarchisch aufgebaut. Die Wurzel des Baumes wird durch ein Interface namens ISourceCodeElement gebildet. Dieses Interface wird von allen Referenzen und allen Deklarationen implementiert. Abbildung 13: Interface Hierarchie Wurzel 41

Der FaktenExtraktor Folgende Elemente können deklariert werden: Packages Typen (Klassen) Felder Methoden Methodenparameter Lokale Methodenparameter Bis auf Packages werden alle oben aufgeführten Elemente einer Bytecodedatei weiter unterteilt. Typen können Interfaces oder Klassen sein. Klassen wiederum können Enums, Innere Klassen oder Anonyme Klassen sein. Tager-Interfaces markieren die Deklarationen entsprechend. Bei den Methoden wird zusätzlich unterschieden, ob es sich um einen Konstruktor handelt oder um eine Klassenmethode. Methodenparameter sind lokale Methoden-Variablen. Solche Deklarationen werden mittels des IMethodParameter Interface gekennzeichnet. Dieses ist von IlocalVariableDeklaration abgeleitet. Felder werden in statische und nicht statische Felder eingeteilt. Gemeinsame Eigenschaften von Typen, Methoden und Feldern werden in ein Interface namens IDeclarationWithAccessibility ausgelagert. Das nachfolgende Diagramm gibt einen Überblick über die Interface Hierarchie von Deklarationen. 42

Der FaktenExtraktor Abbildung 14: Interface Hierarchie Deklarationen 43

Der FaktenExtraktor Die Hierarchie der Referenzen ist etwas einfacher aufgebaut. Referenzen leiten sich aus dem Zugriff auf Deklarationen ab. Dementsprechend gibt es für Referenzen die Interfaces für Typen, Methoden, Konstruktoren, Lokale Variablen und Felder. Mittels der Methode getdeclaration() kann von einer Referenz aus auf die entsprechende Deklaration navigiert werden. Abbildung 15: Interface Hierarchie Referenzen 44

Der FaktenExtraktor 5.4 Das FaktenExtraktor Interface Der FaktenExtraktor ist dazu vorgesehen, als Bibliothek in Projekten verwendet zu werden. Daher wird dieser zu diesem Zweck als Jar-Datei bereitgestellt. Innerhalb eines Projektes muss dann eine Instanz des FakenExtraktors erzeugt werden. Dies erfolgt mit: IFaktenExtraktor faktenextraktor = new FaktenExtraktor(); Der weitere Zugriff erfolgt dann über das Interface IFaktenExtrator. Die hierin enthaltenen Methoden werden im Folgenden kurz beschrieben. Zu analysierende Bytecodedateien können dem Faktenanalysator auf folgende Weise zugeführt werden: mit einem Classpath als Argument, der nach Bytecodedateien durchsucht wird: faktenxtraktor.addalldeclarationsinfolder(string classpath); mit einem Clathpath und einer Auflistung von zu untersuchenden Verzeichnissen als Argumente: faktenextraktor.addalldeclarationsinclasspath(string classpath, String... foldernames); mit einem Classpath als Argument, der nach Bytecodedateien durchsucht wird: faktenextraktor.addallreferencesinclasspath(string classpath); mit einem Clathpath und einer Auflistung von zu untersuchenden Verzeichnissen als Argumente: faktenextraktor.addallreferencesinclasspath(string classpath, String... foldernames); mit einem Objekt vom Typ class als Argument: faktenextraktor.adddeklarations(classfile); mit einem Objekt vom Typ JavaClass (BECEL Typ) als Argument: faktenextraktor.adddeklarations(javaclass); mit einem Klassennamen als Argument, falls die Klasse im Classpath der Anwendung liegt: faktenextraktor.adddeklarations(classname); mit einem Objekt vom Typ class als Argument: faktenextraktor.addreferences(classfile); mit einem Objekt vom Typ JavaClass (BECEL Typ) als Argument: faktenextraktor.addreferences(javaclass); mit einem Klassennamen als Argument, falls die Klasse im Classpath der Anwendung liegt: faktenextraktor.addreferences(classname); Auf die dann entstandene Menge der Deklarationen bzw. Referenzen kann folgendermaßen zugegriffen werden: auf die Deklarationen mit: Collection<IDeclaration> decl = faktenextraktor.getdeclarations(); 45

Der FaktenExtraktor auf die Referenzen mit: Collection<IReference> ref = faktenextraktor.getreferences(); auf alle Elemente in einer Liste vereint mit: Collection<ISourceCodeElement> allelements = faktenextraktor.getallelements(); Die Elemente, welche in den so entstanden Listen enthalten sind, implementieren dann die passenden Interfaces. Um eine bestimmte Deklaration in der Menge der Deklarationen zu suchen, kann sich folgender Methoden bedient werden: falls ein class-objekt der gesuchten Deklaration vorliegt: faktenextraktor.finddeclarationbyclass(class); falls die Id der gesuchten Deklaration bekannt ist (siehe auch Kapitel 5.2): faktenextraktor.finddeclarationbyid(string id); falls das BECEL-JavaClass-Objekt der gesuchten Klasse vorliegt: faktenextraktor.finddeclarationbyjavaclass(javaclass jc); 46

Eine Anwendung 6 Eine Anwendung Der FaktenExtraktor lässt sich zur Prüfung von Vorbedingungen für Refactoring Werkzeuge einsetzen. Hierfür müssen für das jeweilige Refactoring geeignete Vorbedingungen formuliert werden und die Mengen der Referenzen und Deklarationen auf geeignete Weise untersucht werden. Am Beispiel des recht komplexen Replace Inheritence with Delegation Refactorings wird in diesem Kapitel demonstriert, wie Vorbedingungen für Refactorings überprüft werden können. Das RIWD- Refactoring ist das Ergebnis einer Entwicklung, die am Lehrgebiet für Programmiersysteme der FernUni Hagen durchgeführt wurde [RIWD]. Für dieses Refactoring existiert eine Liste mit bekannten Fehlern [riwd Bugs]. Es handelt sich um Spezialfälle, für welche keine geeignete Vorbedingung des Refactorings formuliert wurde. Anhand der aufgeführten Fehler werden Vorbedingungen formuliert und mittels FaktenExtraktors geprüft. 6.1 Beispiel 1: Offene Rekursion Folgende Klassen stellen die Ausgangssituation dar: class Super { Super() { m(); void m() { void n() { class Sub extends Super { Sub() {super(); void m() { n(); Mittels eines JUnit Tests kann überprüft werden, ob eine Instanz der Klasse Sub erzeugt werden kann. @Test public void createsub() throws Exception { Sub sub = new Sub(); Bevor das Refactoring ausgeführt wird, ist der JUnit Test erfolgreich. Nach der Ausführung des Refactorings stellt sich der Sourcecode wie folgt dar: public class Sub { protected final Delegatee delegatee; public Sub() { delegatee = new Delegatee(); void m() { n(); void n() { delegatee.n(); protected class Delegatee extends Super { Delegatee() { super(); void m() { Sub.this.m(); protected void n() { super.n(); 47

Eine Anwendung public class Super { protected Super() { m(); void m() { protected void n() { Die Vererbungsbeziehung wurde also an eine Innere Klasse delegiert. Wird der oben angegebene JUnit Test erneut ausgeführt, schlägt dieser nun fehl. Es kommt zu einer java.lang.nullpointerexception in der Methode createsub(). Der Grund hierfür ist eine implizite offene Rekursion, die erst bei genauerer Analyse der Klassen klar wird. Beim Erzeugen einer Instanz der refaktorierten Klasse Sub wird vom Konstruktor folgender Aufrufstack erzeugt: Sub() Delegatee() Super() Delegatee.m() Da der Konstruktor von Delegatee jedoch noch nicht komplett abgearbeitet wurde, existiert noch keine gültige Instanz von Delegatee, womit der Aufruf von Delegatee.m() im Konstruktor von Super zu einer java.lang.nullpointerexception führt. Denn hierbei handelt es sich um eine offene Rekursion, die eben Delegatee.m() aufruft. Die Vorbedingung, die daraus abgeleitet werden kann, lautet: Im Aufrufstack der Konstruktoren der zu refaktorisierenden Klasse dürfen keine Funktionen von Superklassen aufgerufen werden, die sich per offener Rekursion auf Funktionen der zu refaktorisierenden Klasse beziehen. Um dies zu prüfen, wird ein JUnit Test implementiert, der erfolgreich sein soll, falls keine derartige Rekursion besteht und nicht erfolgreich sein soll, falls das Refactoring nicht angewendet werden darf. Die Prüfung dieser Bedingung ist mit Hilfe des FaktenExtraktors leicht zu bewerkstelligen. Zunächst werden zwei Mengen gebildet: die Menge der Methoden der zu refaktorisierenden Klasse, die Methoden der Superklasse überschreiben rekursiv, die Menge der Methoden, die durch die Konstruktoren der zu refaktorisierenden Klasse aufgerufen werden Danach werden die ermittelten Mengen miteinander verglichen. Gibt es eine Entsprechung, so existiert eine schädliche offene Rekursion. In diesem Falle ist die Vorbedingung nicht erfüllt. Folgender Junit Testcase prüft die Vorbedingung: public class riwdcheckopenrecursion extends TestCase { Collection<IMethodAccess> byconstruktorreferencedmethods; Collection<IMethodDeclaration> byclassmethodsoverriddenmethods; public void testcheckopenrecursion() throws Exception { // die zu refaktorisierende Klasse 48

Eine Anwendung Class cl = Sub.class; FaktenExtraktor prg = new FaktenExtraktor(cl); byconstruktorreferencedmethods = new Vector<IMethodAccess>(); byclassmethodsoverriddenmethods = new Vector<IMethodDeclaration>(); // ermittle die Methoden, die Methoden der Ausgangsklasse überschreiben for (IDeclaration declaration : prg.getdeclarations()) { if (declaration.isfromsource() && declaration.getid().contains(cl.getname())&& declaration.getdeclarationkind().equals(declarationkind.classdeclaration)) { IClassDeclaration cd = (IClassDeclaration)declaration; Collection<IMethodDeclaration> methodes = cd.getmethods(); for (Iterator<IMethodDeclaration> iterator = methodes.iterator(); terator.hasnext();) { IMethodDeclaration imethoddeclaration = iterator.next(); byclassmethodsoverriddenmethods.addall(imethoddeclaration.getoverriddenmethods()); // falls es keine überschriebenen Methoden gibt, ist die Vorbedingung erfüllt if (byclassmethodsoverriddenmethods.size() == 0) { asserttrue("no methods are overridden",byclassmethodsoverriddenmethods.size() == 0); // ermittle für jeden Konstruktor rekursiv alle Methodenreferenzen for (IDeclaration declaration : prg.getdeclarations()) { if (declaration.isfromsource() && declaration.getid().contains(cl.getsimplename())&& declaration.getdeclarationkind().equals(declarationkind.constructordeclaration)) { IConstructorDeclaration cd = (IConstructorDeclaration) declaration; addmethodreferences(cd.getuses()); if (byconstruktorreferencedmethods.size() == 0) { asserttrue("no Constructor uses a method",byconstruktorreferencedmethods.size() == 0); // vergleiche die Mengen der referenzierten Methoden mit der Menge der überschreibenden Methoden for (Iterator<IMethodAccess> iterator = byconstruktorreferencedmethods.iterator(); iterator.hasnext();) { IMethodAccess ma = iterator.next(); Collection<IMethodDeclaration> overriddenmethods = ma.getdeclaration().getoverriddenmethods(); for (Iterator<IMethodDeclaration> iterator2 = overriddenmethods.iterator(); iterator2.hasnext();) { IMethodDeclaration imethoddeclaration = iterator2.next(); assertfalse("open recrution, activated in a construktor, detected",overriddenmethods.contains(imethoddeclaration)); private void addmethodreferences(collection<imethodaccess> mrefs){ // alle von diesen Methoden referenzierten Methoden // der byconstruktorreferencedmethods Collection hinzufügen 49

Eine Anwendung byconstruktorreferencedmethods.addall(mrefs); // für jede Referenz prüfen ob weitere Methoden // byconstruktorreferencedmethods hinzuzufügen sind... for (Iterator<IMethodAccess> iterator = mrefs.iterator(); iterator.hasnext();) { IMethodAccess imethodaccess = iterator.next(); addmethodreferences(imethodaccess.getdeclaration().getuses()); Wie gewünscht, scheitert der JUnit Test, wenn dieser auf die Ausgangssituation dieses Beispiels angewendet wird. 6.2 Beispiel 2: Down-Cast Die Ausgangssituation stellt sich wie folgt dar: public interface DownCastInterface { public class DownCastSub extends DownCastSuper { DownCastInterface make() { return new DownCastSub(); public void cast() { DownCastSuper o = (DownCastSuper) make(); public static void main(string[] args) { new DownCastSub().cast(); public class DownCastSuper implements DownCastInterface { Mittels eines JUnit-Tests testen wir die Funktionalität der cast Funktion: @Test public void createandcast() throws Exception { new DownCastSub().cast(); Vor dem Refactoring wird der JUnit Test erfolgreich absolviert. Nach dem Refactoring stellt sich der Sourcecode wie folgt dar: public interface DownCastInterface { public class DownCastSub { protected final Delegatee delegatee; public DownCastSub() { delegatee = new Delegatee(); 50

Eine Anwendung DownCastInterface make() { return new DownCastSub(); public void cast() { DownCastSuper o = (DownCastSuper) make(); public static void main(string[] args) { new DownCastSub().cast(); protected class Delegatee extends DownCastSuper { public class DownCastSuper implements DownCastInterface { Nach dem Refactoring schlägt der JUnit-Test fehl. An der Stelle DownCastSuper o = (DownCastSuper) make(); kommt es nun zu einer java.lang.classcastexception. Der Grund hierfür ist der Down-Cast, der nun nicht mehr funktionieren kann, da die Vererbungsbeziehung entfernt wurde. Hieraus kann folgende Vorbedingung abgeleitet werden: Es dürfen keine Down-Casts von Instanzen vom Typ der zu refaktorisierenden Klasse vorgenommen werden. Diese Regel gilt natürlich für jedes Element, welches den zu refaktorisierenden Typ referenziert. Um die Regel zu prüfen, wird in der Menge der Referenzen nach einer Referenz gesucht, die: 1. vom Typ CAST_ACCESS ist, 2. in einen Typ vom Typ eines Supertypen der zu refaktorisierenden Klasse casten möchte 3. das zu castende Element vom Typ eines Supertypen des Carstparameters (also des 'Typs zu dem gecastet werden soll) ist. Sind alle drei Bedingungen erfüllt, liegt ein DownCast des zu refaktorisierenden Typs vor. Die Vorbedingung wäre dann nicht erfüllt. Folglich müsste ein entsprechender JUnit Test scheitern. Folgender JUnit Test überprüft, ob Down-Casts der zu refaktorisierenden Klasse durchgeführt werden. public void testcheckdowncasts() throws Exception { Class cl = DownCastSub.class; FaktenExtraktor faktenextraktor = new FaktenExtraktor(cl); IClassDeclaration torefactorclass = null; // ermittle die Deklaration der Klasse, die refaktorisiert werden soll 51

Eine Anwendung torefactorclass = (IClassDeclaration) faktenextraktor.finddeclarationbyclass(cl); // ermittle alle Referenzen vom Typ CAST_ACCESS, die // auf eine Superklasse der Ausgangsklasse casten, // wobei die zu castende Klasse eine Superklasse des Castarguments sein soll for (IReference reference : faktenextraktor.getreferences()) { if ( reference.getreferencekind() == ReferenceKind.CAST_ACCESS && (torefactorclass.getsupertypes().contains(reference.getreferenceddeclaration()))&& (((ItypeDeclaration)reference.getReferencedDeclaration()). getsupertypes().contains(((castoperation)reference).gettobecastedtype()))){ assertfalse("downcast occured", true); Wie gewünscht, scheitert der JUnit Test, wenn dieser auf die Ausgangssituation dieses Beispiels angewendet wird. 6.3 Bemerkung Um diesen Test implementieren zu können, wurde das ursprüngliche Framework um das Interface ICastOperation und die Klasse CastOperation erweitert. Beide liegen in der Hierarchie unterhalb von ITypeAccess bzw. TypeAccess. Ein Referenz vom Typ CastOperation entsteht dabei immer dann, wenn der JVM Befehl CHECKCAST von der ReferenzFactory innerhalb einer Methode gefunden wird. Der Typ, in den gecastet werden soll, kann dabei dem Objekt CHECKCAST entnommen werden. Der zu castende Typ kann nicht direkt ermittelt werden. Allerdings können folgende Fakten dennoch dazu genutzt werden, um diesen Typ zu ermitteln: 1. Die JVM Cast-Operation konsumiert genau eine Referenz vom aktuellem Operand-Stack. 2. Eine entsprechende JVM-Operation, die eine solche Referenz erzeugt, taucht unmittelbar vor der Cast-Operation in der fraglichen Methode auf. 3. Der Typ, den diese Reference repräsentiert, ist der Typ, der gecastet werden soll. Man kann also davon ausgehen, dass die gesuchte Referenz unmittelbar vor der Typ-Referenz, die durch die Cast-Operation selbst entstanden ist, der Liste der Referenzen hinzugefügt wurde. Dazwischen liegen lediglich die Referenzen einer Reihe von Klassen, die build in Exceptions der Cast-Operation darstellen. Ihre Anzahl ist konstant. Es handelt sich um acht Exceptions, die durch die Cast-Operation aufgelöst werden können. Wird also der achtletzte Eintrag der Liste der Referenzen ermittelt, bevor die Referenz auf den zu Castenden Typ hinzugefügt wird, handelt es sich um die gesuchte Referenz, deren Typ den Typ darstellt, der gecastet werden soll. Gefährlich hieran ist, dass, falls zukünftige Compiler der Cast-Operation weitere Exceptions hinzufügen, dies zu einem falschem Ergebnis führen würde. Daher ist dieser Lösungsansatz als kritisch zu betrachten. Ein besser Ansatz wäre es, die gesuchte Referenz anhand der LineNumberTable innerhalb der Menge der Referenzen zu suchen. Hierfür müssen dann jedoch gegebenenfalls bestimmte Compilereinstellungen berücksichtigt werden. Die LineNumberTable ist ansonsten nicht Teil der Bytecodedatei. 52

Eine Anwendung Eine Eigenschaft LineNumber, die den Ort einer Referenz genauer spezifizieren würde, könnte auch für andere Fälle von Bedeutung sein. Daher könnte es interessant sein, die Eigenschaft Location einer Referenz um ein Attribute LineNumber zu erweitern. 53

Evaluierung 7 Evaluierung AST Parser sind Werkzeuge der Sourcecode-Analyse, wie sie von der Eclipse- Entwicklungsumgebung genutzt werden. Sie werden von vielen Refaktorisierungs-Werkzeugen zur Überprüfung der Vorbedingungen eingesetzt. Ein Vergleich des Analyse-Ergebnisses des FaktenExtraktors mit andern Analysewerkzeugen ist ohne großen Aufwand nicht möglich. Um dennoch eine Aussage zur der Performance des Faktenanalysators zu tätigen, wurde dieser mit dem im Anhang beschriebenen rudimentären AST Parser plus Visitor für Methodendeklarationen und Felddeklarationen verglichen. Die Aufgabe des hier eingesetzten AST-Parsers besteht lediglich darin, alle CompilationUnits eines Projekts zu ermitteln und zu parsen. Aus diesen müssen dann alle AST Nodes, die eine Methodendeklaration oder eine Felddeklarationen darstellen, extrahiert werden. Im Gegensatz dazu ermittelt der FaktenExtraktor alle Deklarationen und Referenzen desselben Projektes und betrachtet dabei auch die vom Projekt referenzierten Jar Dateien. Die Evaluierung wurde auf einem PC mit i7 (8 Core) Prozessor, 6 GB RAM und Windows 7 durchgeführt. Als erstes Testprojekt wurde das OpenSource Projekt jaxen 1.1.1 von Codehaus [jaxen] gewählt. Dieses Projekt besteht aus 200 Klassen. Tool AST-Parser FaktenExtraktor Zeit [sec] 2,13 1,38 Gefundene Elemente 1658 Methoden + Felder 31984 Deklarationen 53141 Referenzen Geschwindigkeit / Element [ms] 1,5 0,02 Tabelle 12: Ergebnis Evaluierung mit Projekt jaxen 1.1.1 Das zweite Testprojekt enthält wesentlich weniger Klassen mit deutlich weniger Felder und Methoden. Es handelt sich um das OpenSource Projekt Jester 1.37b [Jester]. Tool AST-Parser FaktenExtraktor Zeit [sec] 0,49 1,12 Gefundene Elemente 291 Methoden + Felder 43071 Deklarationen 11716 Referenzen Geschwindigkeit / Element [ms] 1,6 0,02 Tabelle 13: Ergebnis Evaluierung mit Projekt Jester 1.37b 54

Evaluierung Als drittes Testprojekt wurde das Open Source Projekt Commons Codec von Apache gewählt [Commons Codec]. Tool AST-Parser FaktenExtraktor Zeit [sec] 0,92 0,87 Gefundene Elemente 825 Methoden + Felder 22944 Deklarationen 30944 Referenzen Geschwindigkeit / Element [ms] 1,1 0,02 Tabelle 14: Ergebnis Evaluierung mit Projekt Commons Codec Die Zeit zur Ermittlung einer Deklaration bzw. einer Referenz ist beim FaktanExtraktor sehr konstant. Sie liegt bei 0,02 ms. Die Zeit pro Element bei einem AST Parser liegt in etwa bei 1,4 ms. Daraus ergibt sich, dass der FaktenExtraktor ca. 70 mal schneller Elemente aus dem Bytecode extrahieren kann als ein AST Parser aus dem Sourcecode. 55

Installation 8 Installation Da im Programm einige Jars aus dem JDK genutzt werden, die als restricted deklariert sind, müssen entsprechende Fehlermeldungen unterdrückt werden. Abbildung 16: Compiler-Einstellungen für restricted API 56

Installation Damit die erzeugten Class-Files alle benötigten Informationen enthalten, werden des weiteren folgende Compilereinstellungen benötigt: Abbildung 17: Compilereinstellung für zusätzliche Informationen in Class-Files 57