Das Java-Modell

Aus Eclipse
Wechseln zu: Navigation, Suche


Drei Säulen tragen den Kern der Java Development Tools (JDT) der Eclipse-Entwicklungsumgebung: Das Java-Modell, die Search-Engine und der abstrakte Syntaxbaum (abstract syntax tree, AST). Das Java-Modell ist eine Darstellung von Java-Programmen, die Navigieren, Analysieren und einfaches Modifizieren der Programme ermöglicht. Die kleinsten Einheiten, bis zu denen das Modell auflöst, sind innere Klassen, Methoden und Felder. Dieser Artikel beschreibt das Java-Modell und seine Elemente, erklärt Teile der Funktionsweise und zeigt, wozu und wie es genutzt werden kann.


Java-Modell-Elemente

Das Java-Modell ist die Interpretation des Inhalts eines Eclipse-Workspaces mit Projekten, Verzeichnissen, JAR-Archiven, .java- und .class-Files aus dem Blickwinkel Javas. Das Plug-in org.eclipse.jdt.core realisiert das Java-Modell auf Grundlage des Eclipse-Ressource-Modells, das die Verzeichnis- und Datei-Struktur eines Workspaces abbildet. Java-Programmiererinnen kennen das hierarchisch strukturierte Java-Modell und seine Elemente von den Baum-Darstellungen des Package-Explorers oder des Outline-Views mit unterschiedlichen Symbolen für verschiedene Element-Typen.

Hierarchie eines Java-Projekts (nach D'Anjou et al. (2005, S. 215)

Im Zusammenhang des Java-Modells sind zwei Hierarchien strikt zu unterscheiden:

  • Die im Package-Explorer bzw. Outline-View gezeigte Hierarchie der Java-Projekte im Workspace mit ihren Bibliotheken, Packages, Klassen und Interfaces bis hinunter zu deren Methoden und Feldern.
  • Die Hierarchie der Typen des Java-Modells selbst, mit Hilfe derer die vorhergehende Hierarchie der Java-Projekte beschrieben wird.

Wir stellen im Folgenden einige zentrale Typen in der Typ-Hierarchie des Java-Modells vor.

Jedes Element des Java-Modells erweitert das Interface IJavaElement, das die Funktionalitäten des Navigierens in der Baumstruktur des Java-Projekts und der Ermittlung des Typs des Java-Elements bereitstellt.

Das Interface IJavaModel des Java-Modells ist das Gegenstück zum IWorkspace des Ressource-Modells. Seine Instanz ist die Wurzel aller Java-Elemente im Workspace. IPackageFragment bildet Java-Packages ab und abstrahiert damit von der zugrunde liegenden Verzeichnisstruktur. Instanzen des Interfaces ICompilationUnit entsprechen einzelnen .java-Files (N.B. Die Klasse org.eclipse.jdt.dom.CompilationUnit repräsentiert AST-Knoten und ist damit keine Instanz von ICompilationUnit sondern die korrespondierende Klasse im AST-Modell, deren Objekte .java-Files darstellen und die Wurzeln eines AST sind). Java-Elemente, die das Interface IOpenable implementieren, werden automatisch geöffnet, bevor ihr Inhalt benutzt wird.

Auszug aus der Typhierarchie der Java-Modell-Elemente

Auch das Interface IMember erweitert IJavaElement. Zu seinen Subinterfaces gehören die für die Code-Analyse wichtigen Typen IType, IMethod und IField mit den jeweils intuitiv verständlichen Bedeutungen. Zu einem IMember ermittelt die Methode getDeclaringType() den Typen, in der das IMember deklariert ist, und getFlags() liefert die Modifier seiner Deklaration. Typen von Feldern und Signaturen von Methoden sind als Strings codiert, die mit Hilfe der Klasse Signature zerlegt oder deren Bezug mit der Methode IType#resolveType(String) aufgelöst werden können. Das Auflösen eines Bezuges ist allerdings aufwändig, da dafür der interne AST-Parser benötigt wird. Anwendungen des umfangreichen Protokolls des Interfaces IType behandelt der Abschnitt #Anwendungen.

Das Java-Modell zerlegt Java-Programme hinunter bis zu (inneren) Klassen und Interfaces, deren Methoden und Feldern einschließlich deren Modifier und Signaturen. Die Körper von Methoden werden jedoch nicht weiter in Anweisungen zerlegt. Für Analysen, die eine feinere Zerlegung erfordern, gibt es den Abstract Syntax Tree (AST) und für noch höhere Auflösung den Scanner (IScanner) der ToolFactory im Plug-in org.eclipse.jdt.core.

Eine knappe tabellarische Übersicht der Java-Modell-Elemente gibt die JDT-Dokumentation, die aus dem Eclipse-Workbench-Menü erreichbar ist:

Menu -> Help -> Help Contents -> JDT Plug-in Developer Guide ->
       Programmer's Guide -> JDT Core -> Java Model

In der Web-Version der Eclipse-Hilfe ist dieses Dokument unter folgendem Link direkt zugänglich: Java Model


Unter der Haube

Wir erläutern in diesem Abschnitt Aspekte des inneren Aufbaus des Java-Modells, mit dem Ziel, die Dokumentation der API (application program interface) lesbarer zu machen. Anwenderinnen sollen auch verstehen können, welcher Aufwand mit dem Erzeugen von Objekten und Methodenaufrufen verbunden sein kann, Referenzen auf welche Objekte gehalten werden dürfen, und welche lieber dem Garbage Collector überlassen werden.

Eine zentrale Idee des Ressource- und des Java-Modells ist, komplexe Objekte in einen einfachen Handle und einen aufwändigen Körper bzw. Body aufzuteilen. Der Body wird erst erzeugt, wenn er tatsächlich benötigt wird, und nicht länger gehalten als nützlich. Entsprechend sind alle Instanzen, die IJavaElement implementieren, lediglich Handles für die zugrunde liegenden Objekte, die erst die vollständige Funktionalität des Java-Elements implementieren. Methoden die in der API-Dokumentation mit der Anmerkung "this is a handle-only method" versehen sind, kommen ohne das korrespondierende Body-Objekt aus.

Während das Ressource-Modell die Handles (IFile, IFolder, IProject und IWorspaceRoot) komplett im Hauptspeicher hält, instanziieren die JDT auch die Java-Element-Handles erst bei Bedarf und speichern die ebenfalls lazy erzeugten Body-Objekte vom Typ JavaElementInfo in einem LRU-Cache (least recently used), den das interne Singleton JavaModelManger organisiert.

IJavaElement als Proxy und Bridge (nach Gamma und Beck (2004, S. 310)

Die JDT setzen die beschriebene Trennung von Handle und Body nach den Entwurfsmustern (virtueller) Proxy und (degenerierter) Bridge um: Ein IJavaElement ist ein Proxy, der den Zugriff auf das zugrunde liegende JavaElementInfo Objekt regelt. Das Bridge-Pattern entkoppelt die Typhierarchie der API-Interfaces von der Hierarchie der internen Klasse JavaElementInfo und ihrer Subklassen für die jeweiligen Java-Modell-Elemente.

Die Handles oder Proxys sind einfache Wertobjekte, deren Identität bedeutungslos ist. Sie müssen deshalb immer mit equals(Object) verglichen werden. Es ist möglich, auch zwischen Eclipse-Sessions, über einen Bezeichner-String, äquivalente Handles wieder zu gewinnen. IJavaElement#getHandleIdentifier() liefert einen String, mit dessen Hilfe ein Objekt mit gleichem Wert von der Plug-in-Klasse JavaCore, die hier als Façade und Factory fungiert, mit der statischen Methode JavaCore.create(String) zu erzeugen. Referenzen auf die Handle-Objekte können bedenkenlos über lange Zeit in Collections gehalten werden.

Eine Schwierigkeit ergibt sich aus der Trennung von Handle und Body allerdings. Es kann vorkommen, dass der Body, das JavaElementInfo-Objekt, gar nicht (mehr) existiert. Die Methode IJavaElement#exists() erlaubt es, das festzustellen.


Der Implementation des Java-Modells gelingt mit der Trennung von Handle- und Body-Objekt ein ausgewogener Kompromiss zwischen dem Bedürfnis nach umfassender Darstellung von Java-Projekten im Workspace einerseits und akzeptabler Skalierbarkeit auf Workspaces mit 10 000 und mehr Klassen und Interfaces andererseits. Dem Java-Modell fehlt deshalb die Auflösung von Bezeichner-Bezügen, die erst der AST mit Bindings bietet. Die Fehlertoleranz erlaubt dafür eine akkurate Darstellung der Programmstruktur auch während der Arbeit am Code und bei Compile-Fehlern.


Anwendungen

Design-Ziel und erster Verwendungszweck des Java-Modells und seines API sind die Visualisierungen in der Java-Entwicklungsumgebung, die die Navigation im Code unterstützen. In diesem Abschnitt stellen wir solche Teile des API vor, die auch die bekannten Views Package-Explorer, Outline und Hierarchy verwenden. Darüber hinaus zeigen wir, wie im Rahmen des Java-Modells Namens- oder genauer Bezeichner-Bezüge aufgelöst werden und wie Quellcode modifiziert werden kann. Besonders interessant für das Testen ist dabei die Möglichkeit, programmatisch neue Java-Projekte und Klassen zu erzeugen.


Navigieren

Vom Java-Element zur Ressource und zurück

Java-Elemente können als Adapter im Sinne des gleichnamigen Entwurfsmusters für IResource-Objekte aufgefasst werden. Liegt einem IJavaElement-Objekt eine Ressource zugrunde, kann sie mit der Methode IJavaElement#getResource() bezogen werden.

In die entgegengesetzte Richtung, von der Ressource zum Java-Element, müssen wir die statische Factory-Methode der Plug-In-Klasse JavaCore.create(IResource) bzw. ihre spezifischeren Überlastungen bemühen, die uns das entsprechende Handle-Objekt des Java-Modells liefert.


Traversieren des Java-Element-Baums

Die Teil-Ganzes-Beziehung des Java-Element-Baums drückt das IParent-Interface aus, das alle Java-Elemente mit Ausnahme von IField, IMethod und IInitializer implementieren. Letztere sind naturgemäß Blätter des Baums. IParent bietet die Methoden hasChildren() und getChildren() an, mit deren Hilfe der Elemente-Baum zu seinen Blättern hin traversiert werden kann. Ein Code-Beispiel haben wir auf die Diskussions-Seite verbannt, da es verhältnismäßig aufwändig ist, den Baum auf diese Art zu traversieren (Alle Kinder und deren Body-Objekte würden dabei erzeugt werden). Oft ist eine Suche mit der JDT-Search-Engine sehr viel günstiger. Deshalb bietet das Java-Modell, anders als das Ressource-Modell oder der AST, gar nicht erst einen Visitor für das Traversieren des Baums an.

Um den Baum schrittweise aufwärts durchgehen zu können, implementieren alle Instanzen von IJavaElement die (handle-only) Methode getParent(). Zusätzlich gibt es Methoden um markante Vorfahren einschließlich der Wurzel des Baums direkt aufzusuchen. Dazu gehören getJavaModel(), getJavaProject() und bei den Instanzen von IMember zusätzlich getCompilationUnit().


Analysieren

Die Tasten F4 oder Ctrl-T der Eclipse-JDT gehören zu den Lieblingsfunktionen vieler Java-Programmiererinnen. Sie führen zur Anzeige der Typhierarchie des selektierten Typs bzw. des Typs an der Cursor-Position des Editors. Die Entwicklungsumgebung nutzt für diese Analyse die Funktionalität des Java-Modells, die Typhierarchie zu berechnen und den Bezug von Bezeichnern im Quellcode aufzulösen.


Typ- und Supertyp-Hierarchie

Das Java-Modell delegiert die Berechnung der Typhierarchie an ein ITypeHierarchy-Objekt (Gamma und Beck 2004, S. 314, nennen das Muster Objectify Associations). Beginnen wir mit dem einfacheren Problem, der Supertyp-Hierarchie.

Die Hierarchie der Supertypen ist klar definiert und schnell mit der Methode IType#newSupertypeHierarchy(IProgressMonitor) zu ermitteln. Diese Methode gibt ein ITypeHierarchy-Objekt zurück, von dem man alles erhält was man wünscht: getAllTypes() liefert ein IType-Array aller Typen im Supertyphierarchie-Graphen, getAllClasses() gibt nur die Klassen, getAllInterfaces() nur die Interfaces zurück. Man kann auch die Wurzel-Klasse getRootClasses() oder alle Interfaces, die keine Superinterfaces erweitern bekommen: getRootInterfaces(). Das Ergebnis lässt sich auch sortiert und beschränkt auf einen Typ erfragen:getAllSuperClasses(IType) liefert eine IType-Array mit allen gefundenen Superklassen, den Baum aufwärts sortiert, getAllSupertypes(IType) liefert alle Supertypen (laut API-Doku ebenfalls von unten nach oben sortiert -- es bleibt allerdings unklar was das für Interfaces bedeuten soll).

Die Menge der Subtypen ist offen und erheblich aufwändiger zu ermitteln. Für die Nutzung des API macht das allerdings kaum einen Unterschied. Anstatt eine Supertyp-Hierarchie zu berechnen, beziehen wir einfach vom betrachteten Typ die (vermeintlich) gesamte Typhierarchie mit der Methode IType#newTypeHierarchy(IProgressMonitor). Am zurückgegebenen ITypeHierarchy-Objekt können wir mit getAllSubtypes(IType) alle Subtypen des gegebenen Typs abfragen. Entsprechend geht das für Klassen und Interfaces. Die Methode getAllTypes() gäbe nun die Vereinigung der Menge der Sub- und Supertypen zurück.

Was meinen wir mit der Aussage, die Menge der Subtypen sei offen? Ist ein Typ erst einmal aus dem Workspace entkommen, kann an beliebigem Ort ein Subtyp des entfleuchten Typs gebildet werden, der sich selbstverständlich nicht ermitteln lässt. Umgekehrt kann es sinnvoll sein, die Suche nach Subtypen auf weniger als den ganzen Workspace zu beschränken und nur im Bereich des Kontextes eines Projektes oder einer abgegrenzten Region (IRegion) zu suchen. Für diesen Zweck implementiert ein IJavaProject die Methode newTypehierarchy(IType, IRegion, IProgressMoniotr). Dabei ist IRegion eine Menge von Java-Elementen.

Ein Beispiel der Anwendung dieser Analyse-Technik im Kontext zeigt unser Artikel Alle überschreibenden Methodendeklarationen ermitteln.


Auflösen von Bezügen

Damit Ctrl-T bzw. F4 (Open Type Hierarchy), F3 (Open Declaration) oder F2 (Show Tooltip) auf einem Typ- oder Methoden-Bezeichner im Java-Editor der Entwicklungsumgebung überhaupt funktionieren können, müssen die JDT herausfinden, auf welchen Typ der Bezeichner sich bezieht oder präziser, an welchen Typ der Bezeichner gebunden ist. Da diese Funktionalität der Entwicklungsumgebung auch funktionieren soll, wenn der Code noch nicht fehlerfrei kompiliert, ist diese Funktionalität im vergleichsweise stabilen Java-Modell implementiert. Das Java-Modell ermöglicht Bezüge von Bezeichnern im Quellcode in ihrem Kontext aufzulösen (resolve in der Sprache des JDT-Kerns).

Das Protokoll zum Auflösen von Bezeichner-Bezügen ist im Interface ICodeAssist zusammengefasst. Jede ICompilationUnit, die ja einen .java-File repräsentiert, erweitert dieses Interface. Auf einer ICompilationUnit aufgerufen, gibt die Methode codeSelect(int offset, int length) die aufgelösten Java-Elemente zurück, die im angegebenen Textbereich der ICompilationUnit vorkommen. Die Parameter der Methode codeSelect(..) entsprechen ITextSelection#getOffset() und ITextSelection#getLength().


Modifizieren

Unter Modifizieren verstehen wir Operationen, die neue Java-Elemente zum Workspace hinzufügen, vorhandene löschen, verschieben oder umbenennen. Diese einfachen Änderungen sind mit dem Java-Modell effizient zu erledigen. Die nächsten Abschnitte zeigen, wie es geht.


Erzeugen neuer Projekte, neuer Klassen und Methoden

Zunächst sei daran erinnert, dass die oben erwähnten JavaCore.create(..)-Methoden nur die Handle-Objekte erzeugen, nicht aber die zugrunde liegenden JavaElementInfo-Objekte oder gar zugehörige IResource-Objekte, die Verzeichnisse und Files im Workspace repräsentieren. Der Begriff create hat in diesem Zusammenhang zwei sehr unterschiedliche Bedeutungen (vgl. Eclipse FAQ): Erzeugen der Handles und erzeugen der Handles samt zugrunde liegender Ressource. Um neue Java-Elemente im Workspace vollständig zu erzeugen, gibt es create-Methoden beim Java-Element-Typen des jeweiligen Elter-Knotens des zu erzeugenden Elements im Java-Projekt-Baum (mit Elter bezeichnen wir geschlechtslos einen unmittelbaren Vorfahren).

Ein Beispiel: Angenommen, wir haben im Workspace bereits das Java-Projekt de.feu.ps.wiki und darin das Package fp.example. Diesem Package wollen wir den neuen Klassen-File Foo.java mit der Klasse Foo hinzufügen. Wir gehen in zwei Schritten vor. Zunächst besorgen wir uns ein Handle für das existierende Package, ein IPackageFragment. Im zweiten Schritt fügen wir diesem Package die Klasse Foo samt Feld barhinzu. Anschließend ergänzen wir, zu Demonstrationszwecken separat, noch die zu oft vergessene Methode toString().


    // Step 1: Find existing project and package in workspace
    // Retrieve the resources
    IWorkspaceRoot root= ResourcesPlugin.getWorkspace().getRoot();
    IProject project= root.getProject("de.feu.ps.wiki");
    IFolder folder= project.getFolder("src");
    
    // Switch from resource to Java model
    IJavaProject javaProject= JavaCore.create(project);
    IPackageFragmentRoot src= javaProject.getPackageFragmentRoot(folder);
    IPackageFragment examplePackage= src.getPackageFragment("fp.example");
    Assert.isTrue(examplePackage.exists());
    
    
    /* Step 2 a: Create new class Foo */
    String classContents= "package fp.example;"       + "\n" +
                          "public class Foo {"        + "\n" +
                          "    String bar;"           + "\n" +
                          "}";
    ICompilationUnit icu= examplePackage.createCompilationUnit(
                "Foo.java", classContents, FORCE, monitor);
    
    // Step 2 b: Add method toString() to existing class Foo
    String methodContents= "public void toString(){"  + "\n" +
                           "    return bar;"          + "\n" +
                           "}";
    IType foo= icu.getType("Foo");
    IMethod m= foo.createMethod(methodContents, sibling, FORCE, monitor);

Wie können wir nun ein neues Projekt anlegen? In den Eclipse-FAQ gibt es ein kurzes Kochrezept dafür. Aeschlimann (2008, S. 9 ff) zeigt im JDT Tutorial detaillierter, wie man programmatisch von Grund auf ein ganzes Java-Projekt neu im Workspace anlegt -- von der Java nature und dem class path bis zum Feld einer Klasse. Eine praktisch nutzbare Implementation, die als Test-Projekt gedacht ist, stammt von Gamma und Beck (2004, S. 371-374). Mondl (2008, S. 119-123) hat die Klasse TestProject für seine Arbeit auf aktuellen Stand gebracht.


Änderungen an vorhandenen Java-Elementen

Das Interface ISourceManipulation fasst das Protokoll für einfache Änderungen am Quellcode zusammen. Subklassen von ICompilationUnit und IMember implementieren dieses Protokoll. Die Methode ISourceManipulation#delete(boolean force, IProgressMonitor m) macht genau, was sie sagt. Ebenso einfach zu benutzen sind rename(String newName, boolean replace, IProgressmonitor m) und move(..). Aus der Kombination von copy(..) und rename(..) ergibt sich schließlich eine weitere Möglichkeit, neue Java-Elemente anzulegen.


Weitergehende Modifikationen arbeiten auf einem dem Java-Element zugrunde liegenden IBuffer, der mit der Methode getBuffer() von einer ICompilationUnit bezogen werden kann, vorausgesetzt, die ICompilationUnit ist eine Arbeitskopie im Hauptspeicher (ICompilationUnit#isWorkingCopy() == true). Komplexe Code-Manipulationen erfordern einen AST und AST-Rewriting.


Nicht das Ende

Die Fähigkeiten des Java-Modells sind mit den bisher in diesem Artikel angesprochenen Anwendungen bei Weitem nicht erschöpft. Hier noch ein paar Appetizer.

Programmiererinnen von Benutzungsoberflächen oder beispielsweise von Metrik-Werkzeugen werden die Möglichkeiten interessieren, IElementChangedListener-Objekte bei der Plug-in-Klasse des JDT registrieren zu können (JavaCore.addElementChangedListener(..)). Bei jeder Änderung am Code werden die Observer mit elementChanged(ElementChangedEvent) benachrichtigt. Vom ElementChangedEvent-Objekt kann man die Änderungen mit getDelta erfragen und vom zurückgegebenen IJavaElementDelta bekommt man wiederum die betroffenen Java-Elemente (getAffectedChildren()).

Code-Vervollständigung ist für programmatische Änderungen an Java-Programmen vielleicht weniger relevant als die Möglichkeiten der automatischen Code-Formatierung. Die ToolFactory liefert ein ICodeFormatter-Objekt, das Quellcode nach vorgegebenen Regeln neu formatiert. Bei der ToolFactory findet man übrigens auch einen Byte-Code-Disassembler.

Alle weitergehenden Code-Analysen und Modifikationen, die mit dem abstrakten Syntaxbaum (AST) oder mit dem Scanner bzw. Tokenizer arbeiten, beginnen ebenfalls mit dem Java-Modell und seinen Elementen. Wie man von Java-Elementen zum AST kommt und wieder zurück beschreiben wir dort.


Quellen

Martin Aeschlimann, Dirk Bäumer und Jerome Lanneluc, 2005. Java Tool Smithing, Extending the Eclipse Java Development Tools. http://eclipsecon.org/2005/presentations/EclipseCON2005_Tutorial29.pdf

Martin Aeschlimann, 2008. JDT fundamentals - Become a JDT tool smith. http://www.eclipsecon.org/2008/sub/attachments/JDT_fundamentals.ppt

John Arthorne und Chris Laffra, 2004. Official Eclipse 3.0 FAQs. § 20 Java Development Tool API. Addison, Boston. 432 S., http://wiki.eclipse.org/index.php/Eclipse_FAQs#Java_Development_Tool_API

Jim D'Anjou, Scott Fairbrother, Dan Kehn, John Kellerman und Pat McCarthy, 2005. The Java Developer's Guide to Eclipse. § 27 Extending the Java Development Tools. 2. Aufl., Addison, Boston, S. 651-688.

Erich Gamma und Kent Beck, 2004. Contributing to Eclipse, Principles, Patterns, and Plug-Ins. § 33 Java Core. Addison, Boston, S. 307-323.

Thomas Kuhn und Oliver Thomann, 2006. Abstract Syntax Tree. http://www.eclipse.org/articles/Article-JavaCodeManipulation_AST/index.html

JDT Plug-in Developer Guide, 2008. http://help.eclipse.org/ganymede/nav/3