Groovy
Eine Übersicht über die Skriptsprache Groovy.
Einführung
Groovy ist eine Skriptsprache, die es recht gut schafft, die Vorteile von Java und von Skriptsprachen zu vereinen.
Groovy wird nicht interpretiert, sondern bei der Ausführung in eine Java-Klasse übersetzt. D.h. jede Groovy-Klasse ist eigentlich eine ganz normale Java-Klassen. Dadurch hat Groovy eine sehr gute Performance - fast so gut wie nativer Java-Code. "Fast" deshalb, weil ein kleiner Overhead dazukommt: Zum einen die Übersetzung des Skripts und zum andern die zusätzliche Softwareschicht, die für Groovy-Features nötig ist, welche kein direktes Java-Äquivalent haben.
Durch diese enge Verwandtschaft mit Java, kann man in Groovy alle Vorzüge von Java ausnutzen: Die Plattformunabhängigkeit, die mächtige Standardbibliothek und natürlich die riesige Auswahl von Third-Party-Bibliotheken.
Links
- Die Groovy API
Sprachbesonderheiten
Groovy bietet die typischen Sprachfeatures einer Skriptsprache. Das fängt damit an, dass man das Semikolon, das ein Statement abschließt, weglassen kann, wenn man will.
Auch die in Java recht strenge Typisierung kann in Groovy aufgeweicht werden. D.h. man muss Variablen keinen Typ geben und man kann per "Duck-typing" auf Bean-Properties oder Methoden zugreifen, ohne dass vorher ein Cast nötig ist.
int typedVar1 = 15;
File typedVar2 = new File("myfile.txt")
def untypedVar = new File("myfile.txt")
// Methodenzugriff auf ein Objekt in einer untypisierten Variable per Duck-typing
println untypedVar.exists()
Der Zugriff auf Bean-Properties ist vereinfacht: Man muss nicht explizit den Getter bzw. Setter einer Bean aufrufen, sondern kann statt dessen mit der kompakteren Property-Notation darauf zugreifen:
// Getter / Setter style
File file = new File("myfile.txt")
println file.getAbsolutePath()
file.setLastModified(new Date().getTime());
// Property style
File file = new File("myfile.txt")
println file.absolutePath
file.lastModified = new Date().time;
Bei Verwendung der Property-Notation prüft Groovy zunächst, ob es ein public-Field mit diesem Namen gibt. Wenn nein, dann wird der passende Getter bzw. Setter aufgerufen.
Das Beispiel zeigt auch noch zwei weitere Erleichterungen, die - typisch Groovy - eigentlich nur eine Kleinigkeit sind, einem aber so oft über den Weg laufen, dass sie eine echte Erleichterung sind.
Zum einen wird nicht wie bei Java per Default nur das Package java.lang
eingebunden, sondern die Packages java.lang
, java.util
, java.io
, java.net
, groovy.lang
und groovy.util
sowie die Klassen java.math.BigInteger
und java.math.BigDecimal
.
Zum anderen gibt es für den Ausdruck System.out.println
die Abkürzung println
. Damit kann man sehr schnell mal eine kleine Debug-Ausgabe machen, ohne dass man immer so einen riesigen Schlonz hinschreiben muss.
Literal-Schreibweise für Arrays und Maps
Für Arrays und Maps gibt es eine Literalschreibweise. Damit lassen sich diese wesentlich kompakter angeben. Arrays werden dabei als List angelegt, d.h. ihre Größe kann dynamisch verändert werden.
FIXME Welchen Typ hat so ein Array genau? FIXME Umwandeln in echtes Array: [1, 2, 5, 7] as int[]
// Literalschreibweise von Maps
def map = [:]
map.key = "value";
println map["key"]
println map.key
def filledMap = [ a:15, b:"some string" ]
// Literalschreibweise von Arrays
def array = []
array.add(2);
array.add(5);
println array[0]
println array[1]
def filledArray = [ 2, 5, 15, "some string" ]
Groovy-Beans
In Java sind Beans einfach nur eine Konvention. In Groovy wird diese Konvention auch im Sprachumfang berücksichtigt. Zum einen durch die Property-Notation beim Zugriff auf Bean-Properties (siehe oben). Zum andern jedoch auch bei der Definition von Bean-Properties.
So muss man in Groovy nicht umständlich für jede Property ein privates Feld anlegen und den Zugriff darauf mit Gettern und Settern ermöglichen. Statt dessen gibt man die Properties einfach an - in einem Einzeiler. Groovy generiert dann automatisch die entsprechenden Getter und Setter.
Beispiel (Groovy-Code):
class Customer {
// properties
Integer id
String name
Date dob
// sample usage code
static void main(args) {
def customer = new Customer(id:1, name:"Gromit", dob:new Date())
println("Hello ${customer.name}")
}
}
(Die Original-Version dieses Codes kommt aus der Groovy-Dokumentation)
Äquivalenter Java-Code:
import java.util.Date;
public class Customer {
// properties
private Integer id;
private String name;
private Date dob;
public Integer getId() {
return this.id;
}
public String getName() {
return this.name;
}
public Date getDob() {
return this.dob;
}
public void setId(Integer id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setDob(Date dob) {
this.dob = dob;
}
// sample usage code
public static void main(String[] args) {
Customer customer = new Customer();
customer.setId(1);
customer.setName("Gromit");
customer.setDob(new Date());
System.out.println("Hello " + customer.getName());
}
}
(Die Original-Version dieses Codes kommt aus der Groovy-Dokumentation)
Wenn man will, kann man trotzdem einen Getter oder Setter anlegen - entweder um darin noch weiteren Code auszuführen, oder aber um die Sichtbarkeit der Property zu steuern.
Beispiele für verschiedene Sichtbarkeiten von Gettern und Settern:
class Foo {
// read only property
final String name = "John"
// read only property with public getter and protected setter
Integer amount
protected void setAmount(Integer amount) { this.amount = amount }
// dynamically typed public property
def cheese
}
Closures
FIXME Closures
def closure = { ... }
def closureWithParams = { a, b -> ... }
// Explizite closure
def handler = { key, value -> println key + " = " + value }
[ a:"b", "Hallo":"Otto" ].each(handler)
// Inline closure
[ a:"b", "Hallo":"Otto" ].each { key, value -> println key + " = " + value }
// Mit Parameter-Namen
def sum = 0;
[ 1, 5, 7 ].each { value -> sum += value }
println "Sum: " + sum
// Nutzung des Default-Parameter-Namens "it"
def sum = 0;
[ 1, 5, 7 ].each { sum += it }
println "Sum: " + sum
Kontext-Beispiel:
def text = ""
[ "a", "b", "c" ].each { value -> text += value + ", " }
println text
deviceProc.controllerUpdate = { ce -> println "I was just called with event $ce" }
Processor deviceProc = ...
deviceProc.addControllerListener(new ControllerListener() {
public void controllerUpdate(ControllerEvent ce) {
...
}
}
Anonyme Klassen
FIXME Anonyme Klassen über Maps definieren
Strings
Details zu Strings siehe: Groovy Strings (mit vielen API-Beispielen)
In Groovy gibt es vier verschiedene Arten, einen String anzugeben:
Beispiel - die verschiedenen String-Typen:
def str1 = "string"
def str2 = 'string'
def str3 = /string/
def str4 = """multiline
string"""
Strings können mit ==
verglichen werden. Es muss nicht zwingend equals
oder equalsIgnoreCase
verwendet werden:
assert "Karl" == /Karl/;
assert "Hallo\nWelt" == """Hallo
Welt""";
Substrings:
assert 'abcdefg'[ 3..5 ] == 'def'
assert 'abcdefg'[ 1, 3..5 ] == 'bdef'
assert 'abcdefg'[-5..-2] == 'cdef'
Variablen in Strings
In Strings mit einfachen oder doppelten Anführungszeichen können Variablen direkt in den Strings verwendet werden. Dazu schreibt man ein $
und anschließend den Variablennamen.
Man kann auch einen komplexeren Ausdruck angeben, muss diesen dann aber in geschweite Klammern setzen.
Beispiel - Variablen in Strings:
def bla = 5;
def blubb = "Karl"
def file = new File("myfile.txt")
println "$blubb has $bla new entries in ${file.absolutePath}"
Erweiterte API
Strings haben in Groovy eine erweiterte API gegenüber dem normalen Java-String.
FIXME Beispiele für nützliche String-Funktionen: String.split, Array.join
Weitere Details siehe API der String-Klasse
reguläre Ausdrücke
Außerdem bietet Groovy native Unterstützung für reguläre Ausdrücke:
def str = "KSC gegen Bayern München: 10:2"
def regex = "(.*) gegen (.*): (\\d+):(\\d+)"
def matcher = (str =~ regex)
def data = [
club1: matcher[0][1],
club2: matcher[0][2],
score1: matcher[0][3],
score2: matcher[0][4]
];
Hinweis: matcher
ist ein java.util.regex.Matcher.
So kann man auch einfache Regex-Tests machen (Wenn der Regex-Tests fehl schlägt, ist der Ausdruck null
, was als false
interpretiert wird):
if (file.name =~ /\.txt$/) {
...
}
Verarbeitung von Datenstrukturen
FIXME Collection-Klassen können einfach iteriert werden.
FIXME Chaining von sort, einfache Comparator
Groovy als Shell-Skript verwenden
Um Groovy direkt als Shell-Skript zu verwenden, muss man nur einen Execution-Header hinzufügen. So kann man ein Groovy-Skript z.B. auch als Cron-Skript ausführen lassen, indem man es einfach beispielsweise im Verzeichnis /etc/cron.daily/
ablegt - ein Editieren der crontab bleibt einem dabei erspart.
Beispiel /etc/cron.daily/myscript.groovy
:
#!/usr/bin/groovy
println "Hello!"
Integration von ant in Groovy
Für viele Jobs gibt es sehr praktische Ant-Tasks, mit deren Hilfe man in einem Ant-Skript in wenigen Zeilen z.B. ein Verzeichnis kopieren kann. All diese Ant-Tasks kann man auch direkt in einem Groovy-Skript verwenden.
Beispiel:
def ant = new AntBuilder()
ant.copy(todir: targetDir, preservelastmodified:true) {
fileset(dir: basedir) {
include(name: "source/**")
}
}
Siehe auch:
Integration von Groovy in ant
Ant-Hello-World
Hier ein kleines Hello-World-Beispiel für die Integration von Groovy in ant.
Dateistruktur:
+- build.xml
+- groovy
| +- Main.groovy
+- lib
+- antlr-2.7.7.jar (from groovy distribution)
+- asm-3.2.jar (from groovy distribution)
+- groovy-1.7.0.jar (from groovy distribution)
Haupt-Groovy-Klasse groovy/Main.groovy
:
class Main {
static void hello(String who) {
println "Hello $who"
}
}
Ant-Build-Datei (build.xml
) - Hello World:
<?xml version="1.0" encoding="UTF-8"?>
<project basedir=".">
<path id="groovy.classpath">
<fileset dir="lib">
<include name="*.jar" />
</fileset>
</path>
<taskdef name="groovy"
classname="org.codehaus.groovy.ant.Groovy"
classpathref="groovy.classpath"/>
<target name="my-target" description="TODO">
<groovy>
<classpath>
<pathelement location="groovy"/>
</classpath>
Main.hello("world");
</groovy>
</target>
</project>
Im groovy
-Tag kann man beliebigen Groovy-Code unterbringen. Allerdings stimmen die Zeilennummern bei Stack-Traces nicht mit den Zeilennummern im Ant-Skript überein. Daher bietet es sich an, den eigentlichen Code in eine extra .groovy
-Datei auszulagern und sich im Ant-Skript auf ein Bißchen Glue-Code zu beschränken.
Interaktion Groovy <-> Ant
Ant-Build-Datei (build.xml
) - komplexere Interaktion zwischen Groovy und Ant:
<?xml version="1.0" encoding="UTF-8"?>
<project basedir=".">
<property name="my-ant-property" location="some/path"/>
<path id="groovy.classpath">
<fileset dir="lib">
<include name="*.jar" />
</fileset>
</path>
<taskdef name="groovy"
classname="org.codehaus.groovy.ant.Groovy"
classpathref="groovy.classpath"/>
<target name="my-target" description="TODO">
<fileset id="txtfiles" dir="." includes="**/*.txt"/>
<groovy>
<classpath>
<pathelement location="groovy"/>
</classpath>
// Reading ant properties
def antProp = properties['my-ant-property']
// Reading ant filesets
def txtFileArr = project.references.txtfiles
txtFileArr.each{ fileRes ->
File file = fileRes.file
println "Processing ${file.absolutePath} ..."
}
// Failing the ant task
throw new Exception("Task failed")
</groovy>
</target>
</project>
Weblinks:
Externe Prozesse aufrufen
Siehe auch:
// Kommando einfach als String definieren
def cmd = [ "java", "-jar", "build/lib/css-validator/css-validator.jar", "file://my.css" ]
// Kommando ausführen und warten, bis es beendet ist
def proc = cmd.execute()
proc.waitFor()
// Output als String abgreifen
def output = proc.in.text
output.split("\n").each { line ->
...
}
Output direkt ausgeben (siehe Quelle):
class StreamGobbler extends Thread {
InputStream is
StreamGobbler(InputStream is) {
this.is = is
}
public void run() {
try {
InputStreamReader isr = new InputStreamReader(is)
BufferedReader br = new BufferedReader(isr)
String line = null
while ( (line = br.readLine()) != null) {
System.out.println(line)
}
} catch (IOException exc) {
exc.printStackTrace()
}
}
}
void execCmd(cmd) {
def proc = cmd.execute()
new StreamGobbler(proc.getErrorStream()).start()
new StreamGobbler(proc.getInputStream()).start()
proc.waitFor()
def exitCode = proc.exitValue()
if (exitCode != 0) {
fail("Executing ${cmd[0]} failed with exit code ${exitCode}");
}
}
// Usage examples
execCmd("some-command-needing-user-input.sh") // Command is a String
execCmd([ "java", "-jar", "myjar.jar" ]) // Command is an array of Strings
Dateien lesen und schreiben
Details siehe API der File-Klasse
def file = new File('sample.txt')
// Read complete File
String content = file.getText('utf-8')
// Read file line by line
file.withReader('utf-8') { reader ->
String line;
while ((line = reader.readLine()) != null) {
...
}
}
// Write complete file
file.withWriter('utf-8') { writer ->
writer << content
}
// Write file line by line
file.withWriter('utf-8') { writer ->
writer.writeLine 'Adding this text to the file.'
}
XML / XPath
Hinweise:
- Der Return-Wert von
xpath.evaluate
ist eine org.w3c.dom.NodeList. elem
ist eine org.w3c.dom.Node (meist ein org.w3c.dom.Element)
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.xpath.*
private void parseXml() {
def builder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
def xpath = XPathFactory.newInstance().newXPath()
File xmlFile = new File("example.xml")
def rootElem = builder.parse(xmlFile).documentElement
xpath.evaluate('/some/xpath', rootElem, XPathConstants.NODESET).each { elem ->
println elem.getAttribute("name") + " has text: " + elem.textContent
}
}
import javax.xml.transform.*
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
private void writeXml() {
File xmlFile = new File("example.xml")
def rootElem = ...
Transformer xmlTransformer = TransformerFactory.newInstance().newTransformer();
xmlTransformer.setOutputProperty(OutputKeys.INDENT, "yes"); // Pretty-print
xmlFile.withWriter('utf-8') { writer ->
xmlTransformer.transform(new DOMSource(rootElem), new StreamResult(writer))
}
}
DirectoryScanner von Ant verwenden
Siehe auch: DirectoryScanner-API
import org.apache.tools.ant.DirectoryScanner
...
long classLastModified = getMaxLastModified(new File(...), [ "class/**" ] )
...
private long getMaxLastModified(File basedir, def includes) {
DirectoryScanner scanner = new DirectoryScanner()
scanner.addDefaultExcludes()
scanner.basedir = basedir
scanner.includes = includes
scanner.scan()
long maxLastModified = -1
scanner.includedFiles.each { fileName ->
maxLastModified = Math.max(maxLastModified, new File(basedir, fileName).lastModified())
}
return maxLastModified;
}