Zum Inhalt springen

Groovy: Skriptobjekte

Aus Wikibooks

Unter einem Groovy-Skript verstehen wir ein Stück Groovy-Quellcode, das keine Klasse und keine Methode eingebunden ist. In Java gibt es so etwas nicht, auch das einfachste und kürzeste Programm muss in Klassen und Methoden organisiert sein. In Groovy können Sie ein ganzes lauffähiges Programm in eine einzige Zeile schreiben; das folgende Beispiel kennen Sie schon aus dem ersten Kapitel:

println "Hallo Welt!"

Wenn Sie diese Zeile in eine Datei HalloWelt.java speichern und dann mit groovyc kompilieren, erhalten Sie eine Java-Klassendatei namens HalloWelt.class, die Sie mit java ausführen können. Aber auch wenn Sie dieses Datei mit groovy direkt ausführen oder mit groovysh oder in der GroovyConsole interaktiv ausführen, übersetzt Groovy diese Zeile in eine normale Java-Klasse und führt diese anschließend aus.

> groovy HalloWelt
Hallo Welt!

Groovy-Skripte kommen nicht nur in der Form von Skript-Dateien ins Spiel, sondern auch bei der Integration dynamischer Programme in Java-Anwendungen, ein Beispiel dafür sind Groovlets, mit denen wir uns an anderer Stelle beschäftigen (siehe Groovy und Java integrieren).

Für Skripte gelten einige Besonderheiten gegenüber normalen, in Klassen organisierten Programmen.

Aufgelockerte Form von Groovy-Skripten[Bearbeiten]

Ein Groovy-Skript kann ungebundene Anweisungen und Methodendefinitionen[1] in beliebiger Mischung enthalten. import-Anweisungen müssen nicht unbedingt am Anfang stehen; die Definitionen von Funktionen brauchen sich nicht vor der Stelle befinden, wo erstmalig auf sie zugegriffen wird. Eine Skriptdatei kann auch zusätzliche Klassendefinitionen enthalten, diese sind dann aber kein Teil des Skripts, sondern werden getrennt übersetzt.

Sehen wir uns ein kurzes Beispielskript, das alle diese Elemente in einfacher Form enthält.

//Das Skript BeispielSkript.groovy

fenstertext = 'Das ist der Beispieltext'
zeigeFenster(fenstertext)

def zeigeFenster(text) {
  def bf = new BeispielFrame(text)
  bf.visible=true
}

class BeispielFrame extends JFrame {
  def BeispielFrame(text) {
    title = "Ein Beispiel-Frame"
    defaultCloseOperation = DISPOSE_ON_CLOSE
    contentPane.add(new JLabel(text.toString()))
    pack()
  }
}

import javax.swing.*

Wenn Sie dieses Skript in eine Textdatei namens BeispielSkript.groovy schreiben und mit groovy ausführen, erscheint links oben auf dem Bildschirm ein kleines Fenster mit dem Text „Dies ist ein Beispieltext“. Das eigentliche Skript besteht aus zwei Zeilen, die erst den Text definieren und dann eine eingebettete Methode zeigeFenster() aufruft. Diese wiederum instanziiert eine in derselben Datei definierte Klasse BeispielFrame. Sowohl die Methode als auch die Klasse werden vor ihrer Definition benutzt und die für JFrame und JLabel erforderliche import-Anweisung steht ganz am Ende – ein deutliches Zeichen dafür, dass wir es hier mit einem Compiler zu tun haben, der das Programm erst als Ganzes übersetzt und dann ausführt, und keinem zeilenweise arbeitenden Skript-Interpreter.

Tipp: Es fällt Ihnen vielleicht auf, dass wir den Konstruktor von JLabel mit text.toString() aufrufen. Das ist eine Sicherheitsmaßnahme, da das Argument text hier nicht typisiert ist, und wir möchten nicht, dass eine ClassCastException ausgelöst wird, falls jemand unseren BeispielFrame mit irgendetwas anderem als einem String aufruft. Innerhalb von Groovy kann man sehr gut mit untypisierten Variablen arbeiten, aber sobald Sie typisierte Java-APIs aufrufen, sorgen Sie besser dafür, dass die übergebenen Argumente auch den richtigen Typ haben. Groovy nimmt zwar einfache Typkonvertierungen für Sie vor, wird aber beispielsweise kein Date-Objekt in einen String umwandeln, wenn eine API-Methode einen String erwartet.

Sobald der Groovy-Compiler (egal, ob in einem groovy-Aufruf oder beim expliziten Kompilieren mit groovyc) auf einzeln stehende Anweisungen trifft, generiert er eine Klasse, deren Name dem Namen der Skriptdatei entspricht, und in ihr eine Methode mit dem Namen run(), die die einzeln stehenden Anweisungen enthält. Innerhalb des Skriptes definierten Methoden werden zu Methoden dieser Klasse. Außerdem erhält die Klasse eine main()-Methode, die (auf Umwegen) im Wesentlichen nichts weiter macht als die Skriptklasse zu instanziieren und run() aufzurufen; dadurch wird aus dem zu einer .class-Datei kompiliertes Skript ein mit dem java-Befehl ausführbares Programm. Alle innerhalb des Skripts definierten Klassen werden zu getrennten Java-Klassen; beim expliziten Kompilieren entstehen auch die entsprechenden .class-Dateien.

Das Skript als Klasse[Bearbeiten]

Sie können das Skript auch mit groovyc kompilieren und erhalten dann zwei Klassendateien BeispielSkript.class und BeispielFrame.class. Die erste von beiden ist folgendermaßen definiert. Sie können das mit einem Java-Decompiler, z.B. JAD (http://www.kpdus.com/jad.html), leicht nachprüfen.

public class BeispielSkript extends groovy.lang.Script {
  static {} {...}
  public BeispielSkript() {...}
  public BeispielSkript(groovy.lang.Binding context) {...}
  public static void main(java.lang.String[] args) {...}
  public java.lang.Object run(){...}
  public java.lang.Object zeigeFenster(java.lang.Object text) {...}
  public static java.lang.Long __timeStamp = ...;
  ...
}

Unter anderem finden Sie dort die generierte main()-Methode, die selbst geschriebene Methode zeigeFenster() und das eigentliche Skript als nicht-statische Methode run() wieder. Wenn Sie ein Skript erstellen, schreiben Sie also im Grunde diese run()-Methode einer von Script abgeleiteten Klasse. Das heißt, Sie befinden sich mit dem Skript in einer vollkommen objektorientierten Umgebung, auch wenn man es dem Skript selbst nicht unbedingt ansieht.

Die Variable __timeStamp dient übrigens dem Compiler zur Prüfung, ob die Klasse nach einer Änderung des Quellcodes neu übersetzt werden muss.

Skripte und das Binding[Bearbeiten]

Vielleicht ist Ihnen aufgefallen, dass wir in dem obigen Beispiel eine Variable namens fenstertext verwenden, der ein String mit dem anzuzeigenden Text zugewiesen wird, obwohl sie nirgendwo deklariert ist. Genau wie in Java müssen in Groovy Variablen, egal ob es Felder einer Klasse oder nur innerhalb einer Methode benutzte lokale Variablen sind, genau immer deklariert werden, bevor sie verwendet werden können. In Skripten gibt es aber insofern eine Ausnahme, als jeder Aufruf einer unbekannten Variablen gegen das so genannte Binding aufgelöst wird. Das Binding ist ein Objekt der Klasse groovy.lang.Binding, das dem Skript-Objekt als Property zugeordnet ist und dazu dient, Daten mit zwischen dem Skript und der Außenwelt auszutauschen.

Das Binding ist ein Behälterobjekt ähnlich einer Map (implementiert allerdings nicht das Interface java.util.Map), in dem beliebige Objekte unter einem String-Namen abgelegt werden können. Wenn Sie also in einem Skript versuchen, einen Wert einer Variablen zuzuweisen, die weder im Script-Objekt noch innerhalb des Skripts selbst angelegt worden ist, trägt Groovy einfach diesen Wert unter dem angegebenen Variablennamen in das Binding ein.

Ein im Binding eingetragener Wert kann überall im Skript, also auch in Methoden, die innerhalb des Skripts definiert sind, wie eine normale untypisierte Variable verwendet werden. Für normale Klassen (wie BeispielFrame im obigen Beispiel), die innerhalb derselben Skriptdatei definiert sind, sind die Binding-Variablen jedoch nicht sichtbar. Solche eingebetteten Klassen sind logisch vollständig vom Skript getrennt, da Groovy keine inneren Klassen kennt.

Lesend auf eine Binding-Variable zugreifen können Sie nur, wenn zuvor ein Wert unter dem entsprechenden Namen im Binding abgelegt worden ist. Dies muss nicht unbedingt innerhalb des Skripts geschehen sein, da das Binding dem Skript in der Regel mit irgendwelchen vorbelegten Werten übergeben wird. Wenn Sie versuchen, im Skript auf einen Wert zuzugreifen, der nicht als Member-Variable, lokale Variable oder Binding-Variable bekannt ist, erhalten Sie wie in jeder anderen Methode auch eine MissingPropertyException.

Tipp: Eine im Binding gespeicherter Wert verhält sich im Wesentlichen wie eine Property der Skriptklasse. So können Sie den Wert beispielsweise auch mit getProperty() abrufen. Die Methode getProperties() listet ihn allerdings nicht mit auf.

Sie können sich den Inhalt des Bindings leicht ansehen, indem Sie die Property gleichen Namens abrufen. Tragen Sie beispielsweise folgende Ausgabeanweisung in das obige Skriptbeispiel ein:

beispieltext = 'Das ist der Beispieltext'
println binding.variables

Sie erhalten dann so etwas wie die folgende Ausgabe im Konsolenfenster:

{beispieltext=Das ist der Beispieltext,  args=[Ljava.lang.String;@162dbb6}

Daran können Sie sehen, dass zwei Binding-Variablen definiert sind, und zwar der von uns stammende beispieltext sowie eine Variable args, mit der wir uns gleich beschäftigen werden.

Die wichtigsten Methoden des Binding-Objekts sind setVariable(String,Object) zum Setzen einer Binding-Variablen, getVariable(String) zum Lesen einer Variablen und getVariables(), die wir eben angewendet haben und ein Map-Objekt mit allen im Binding gehaltenen Werten liefert. Eine vollständige Auflistung enthält der Anhang Wichtige Klassen und Interfaces.

Die Aufrufargumente eines Skripts[Bearbeiten]

Groovy übergibt dem Skript die Aufrufargumente aus der Befehlszeile als String-Array in einer Binding-Variablen mit dem Namen args. Folgendes kleine Skript tut nichts weiter, als alle übergebenen Argumente aufzulisten.

for (arg in args) println arg

Speichern wir es als ZeigeArgumente.groovy ab und rufen wir es aus dem Konsolenfenster – im selben Verzeichnis – auf:

> groovy ZeigeArgumente Das sind "3 Argumente"
Das
sind
3 Argumente

  1. Streng genommen definieren wir hier keine Methoden sondern Funktionen. Da diese von Groovy aber in Methoden übersetzt werden und sie auch dieselbe Form wie Methoden haben, betrachten wir hier die Funktionen wie Methoden.