Getting started
Introduction
Groovy is a Java-syntax-compatible object-oriented programming language for the JVM. It compiles to JVM bytecode and seamlessly integrates with Java code.
Key Features
- Optional typing - Dynamic or static
- Closures - First-class functions
- Operator overloading - Custom operators
- Native syntax - Lists, maps, ranges
- AST transformations - Metaprogramming
- Gradle - Build automation DSL
Installation
# SDKMAN (recommended)
sdk install groovy
# Homebrew (macOS)
brew install groovy
# Manual download
wget https://groovy.apache.org/download.html
Verify installation:
groovy -version
# Groovy Version: 4.0.x JVM: 17.0.x
Hello World
// Script (no class needed)
println "Hello, World!"
// With class
class Hello {
static void main(String[] args) {
println "Hello, World!"
}
}
Run with:
groovy hello.groovy
Quick Example
// List operations
def numbers = [1, 2, 3, 4, 5]
def doubled = numbers.collect { it * 2 }
println doubled // [2, 4, 6, 8, 10]
// Map operations
def person = [name: 'John', age: 30]
println person.name // John
// Closure
def greet = { name -> "Hello, $name!" }
println greet('Alice') // Hello, Alice!
Syntax Basics
Variables and Types
// Dynamic typing (def)
def name = "John"
def age = 30
def price = 19.99
// Static typing
String city = "New York"
int count = 10
List<String> items = ['a', 'b']
// Multiple assignment
def (a, b, c) = [1, 2, 3]
// Type coercion
def text = "123"
int number = text as int
Strings
// Single quotes (literal)
def literal = 'Hello'
// Double quotes (GString - interpolation)
def name = "World"
def greeting = "Hello, $name!"
def complex = "Result: ${1 + 1}"
// Triple quotes (multiline)
def multiline = '''
Line 1
Line 2
Line 3
'''
// Slashy strings (regex-friendly)
def pattern = /\d+\.\d+/
def path = /C:\Users\path/ // No escaping
// Dollar slashy (multiline regex)
def sql = $/
SELECT * FROM users
WHERE name = 'O''Reilly'
/$
String Methods
def str = "Groovy"
str.size() // 6
str.length() // 6
str.toUpperCase() // GROOVY
str.toLowerCase() // groovy
str.reverse() // yvoorG
str.capitalize() // Groovy
str.center(10) // " Groovy "
str.padLeft(8, '0') // "00Groovy"
// Substring
str[0] // G
str[0..2] // Gro
str[-1] // y (last char)
str[-3..-1] // ovy
// Pattern matching
str ==~ /G.*y/ // true (matches)
str =~ /o+/ // Matcher object
Numbers
// Integer types
def i = 10
def big = 10G // BigInteger
def longNum = 100L // Long
// Decimal types
def decimal = 3.14
def bigDec = 3.14G // BigDecimal
def floatNum = 3.14f // Float
// Operators
10.power(2) // 100
10 ** 2 // 100 (power)
10.div(3) // 3 (integer div)
10 / 3 // 3.333...
10.intdiv(3) // 3
10 % 3 // 1 (modulo)
// Ranges
1..10 // Inclusive range
1..<10 // Exclusive end
10..1 // Reverse range
'a'..'z' // Character range
Collections
Lists
// Creation
def list = [1, 2, 3, 4, 5]
def empty = []
def mixed = [1, 'two', 3.0, [4]]
// Access
list[0] // 1
list[-1] // 5 (last)
list[1..3] // [2, 3, 4]
// Modification
list << 6 // Append: [1,2,3,4,5,6]
list + [7, 8] // Concatenate
list - [2, 4] // Remove: [1,3,5]
list[0] = 10 // Set element
list.add(7) // Add element
list.remove(2) // Remove at index
// Operations
list.size() // 5
list.isEmpty() // false
list.contains(3) // true
list.reverse() // [5,4,3,2,1]
list.sort() // [1,2,3,4,5]
list.unique() // Remove duplicates
list.flatten() // Flatten nested lists
List Methods
def numbers = [1, 2, 3, 4, 5]
// Iteration
numbers.each { println it }
numbers.eachWithIndex { val, idx ->
println "$idx: $val"
}
// Transformation
numbers.collect { it * 2 } // [2,4,6,8,10]
numbers.findAll { it > 2 } // [3,4,5]
numbers.find { it > 2 } // 3 (first)
numbers.grep { it > 2 } // [3,4,5]
// Reduction
numbers.sum() // 15
numbers.max() // 5
numbers.min() // 1
numbers.join(', ') // "1, 2, 3, 4, 5"
numbers.inject(0) { sum, n ->
sum + n
} // 15
// Testing
numbers.any { it > 3 } // true
numbers.every { it > 0 } // true
Maps
// Creation
def map = [name: 'John', age: 30]
def empty = [:]
def nested = [
person: [name: 'Jane', age: 25]
]
// Access
map.name // John
map['name'] // John
map.get('name') // John
map.get('missing', 'default') // default
// Modification
map.city = 'NYC' // Add/update
map['country'] = 'USA'
map.put('zip', '10001')
map.remove('age')
// Operations
map.size() // Number of entries
map.isEmpty() // false
map.containsKey('name') // true
map.containsValue('John') // true
map.keySet() // [name, age]
map.values() // [John, 30]
Map Methods
def map = [a: 1, b: 2, c: 3]
// Iteration
map.each { key, value ->
println "$key: $value"
}
map.each { entry ->
println "${entry.key}: ${entry.value}"
}
// Transformation
map.collect { k, v -> v * 2 } // [2,4,6]
map.findAll { k, v -> v > 1 } // [b:2, c:3]
map.find { k, v -> v == 2 } // b=2
// Combining
map + [d: 4] // Merge
map - [b: 2] // Remove by entry
map.subMap(['a', 'c']) // [a:1, c:3]
Ranges
// Number ranges
def range = 1..10 // Inclusive
def exclusive = 1..<10 // Exclusive end
// Operations
range.size() // 10
range.contains(5) // true
range.from // 1
range.to // 10
// Iteration
range.each { println it }
range.step(2) { println it } // 1,3,5,7,9
// Character ranges
('a'..'z').each { print it }
// Reverse
(10..1).each { println it }
// In switch
def rating = 5
switch(rating) {
case 1..3: println 'Low'; break
case 4..6: println 'Medium'; break
case 7..10: println 'High'; break
}
Operators
Safe Navigation (?.)
def person = null
// Traditional null check
if (person != null) {
println person.name
}
// Safe navigation
println person?.name // null (no NPE)
// Chaining
def city = person?.address?.city
// With methods
person?.getName()?.toUpperCase()
// In assignments
def name = person?.name ?: 'Unknown'
Elvis Operator (?:)
// Default values
def name = null
def displayName = name ?: 'Anonymous'
// Chaining
def result = first ?: second ?: third ?: 'default'
// With safe navigation
def city = person?.city ?: 'Unknown'
// Method calls
def length = text?.length() ?: 0
// Assignment
name = name ?: 'Default'
// Same as: name ?: (name = 'Default')
Spaceship Operator (<=>)
// Comparison (-1, 0, 1)
1 <=> 2 // -1
2 <=> 2 // 0
3 <=> 2 // 1
// Sorting
def list = [3, 1, 2]
list.sort { a, b -> a <=> b } // [1,2,3]
// Custom comparison
class Person {
String name
int age
}
people.sort { a, b ->
a.age <=> b.age ?: a.name <=> b.name
}
Spread Operator (*.)
// Spread-dot (invoke on all)
def names = ['John', 'Jane', 'Bob']
names*.toUpperCase() // [JOHN, JANE, BOB]
// Method calls
def lengths = names*.length() // [4,4,3]
// Null-safe spread
def mixed = ['John', null, 'Jane']
mixed*.toUpperCase() // [JOHN, null, JANE]
// Spread in lists
def list = [1, 2, 3]
def combined = [0, *list, 4] // [0,1,2,3,4]
// Spread in maps
def base = [a: 1, b: 2]
def extended = [*:base, c: 3] // [a:1,b:2,c:3]
// Spread in method calls
def sum(a, b, c) { a + b + c }
def nums = [1, 2, 3]
sum(*nums) // 6
Field Access (.@)
class Person {
private String name = 'John'
String getName() { "Mr. $name" }
}
def person = new Person()
person.name // Mr. John (via getter)
person.@name // John (direct field)
Method Pointer (.&)
def str = "Hello"
// Method reference
def method = str.&toUpperCase
method() // HELLO
// As closure
def numbers = [1, 2, 3]
numbers.each(System.out.&println)
// Constructor reference
def factory = Date.&new
def now = factory()
Other Operators
// Identity (===)
def a = [1, 2]
def b = [1, 2]
def c = a
a == b // true (equals)
a === b // false (not same object)
a === c // true (same object)
// Regex (=~ and ==~)
def text = "Groovy 123"
text =~ /\d+/ // Matcher (partial match)
text ==~ /Groovy \d+/ // Boolean (full match)
// Membership (in)
3 in [1, 2, 3] // true
'key' in [key: 'val'] // true
// Coercion (as)
"123" as Integer // 123
[1, 2] as Set // [1,2] (Set)
{ it * 2 } as Runnable
Closures
Closure Basics
// Simple closure
def greet = { "Hello!" }
println greet() // Hello!
// With parameter
def greet2 = { name -> "Hello, $name!" }
println greet2('John') // Hello, John!
// Multiple parameters
def add = { a, b -> a + b }
println add(2, 3) // 5
// Implicit parameter (it)
def double = { it * 2 }
println double(5) // 10
// No parameters
def random = { -> Math.random() }
// Optional typing
def typed = { int x, int y -> x + y }
Closure Features
// Default parameters
def greet = { name = 'World' ->
"Hello, $name!"
}
greet() // Hello, World!
greet('Alice') // Hello, Alice!
// Variable arguments
def sum = { int... args ->
args.sum()
}
sum(1, 2, 3) // 6
// Return value (implicit)
def max = { a, b ->
a > b ? a : b // Last expression
}
// Explicit return
def max2 = { a, b ->
return a > b ? a : b
}
Closure Delegation
class Person {
String name
}
def closure = {
println name // Accesses delegate.name
}
def person = new Person(name: 'John')
closure.delegate = person
closure() // John
// Delegation strategy
closure.resolveStrategy =
Closure.DELEGATE_FIRST
// DELEGATE_FIRST, OWNER_FIRST,
// DELEGATE_ONLY, OWNER_ONLY
Closures in Collections
def numbers = [1, 2, 3, 4, 5]
// each
numbers.each { println it }
// collect (map)
def doubled = numbers.collect { it * 2 }
// findAll (filter)
def evens = numbers.findAll { it % 2 == 0 }
// inject (reduce)
def sum = numbers.inject(0) { acc, n ->
acc + n
}
// groupBy
def grouped = numbers.groupBy { it % 2 }
// [1:[1,3,5], 0:[2,4]]
// collectEntries
def map = numbers.collectEntries {
[(it): it * it]
} // [1:1, 2:4, 3:9, 4:16, 5:25]
Closure Composition
def add2 = { it + 2 }
def multiply3 = { it * 3 }
// Right shift (then)
def combined = add2 >> multiply3
combined(5) // 21 = (5+2)*3
// Left shift (compose)
def combined2 = add2 << multiply3
combined2(5) // 17 = (5*3)+2
// Memoization
def fib
fib = { n ->
n < 2 ? n : fib(n-1) + fib(n-2)
}.memoize()
// Currying
def multiply = { a, b -> a * b }
def double = multiply.curry(2)
double(5) // 10
Object-Oriented
Classes
// Simple class
class Person {
String name
int age
}
// With constructor
class Person {
String name
int age
Person(String name, int age) {
this.name = name
this.age = age
}
}
// Usage
def person = new Person('John', 30)
def person2 = new Person(
name: 'Jane',
age: 25
) // Named parameters
Properties
class Person {
// Public property (auto getter/setter)
String name
// Private field
private int age
// Computed property
String getFullName() {
"$firstName $lastName"
}
// Custom setter
void setAge(int age) {
if (age > 0) this.age = age
}
}
def p = new Person()
p.name = 'John' // Calls setName()
println p.name // Calls getName()
p.@name = 'Jane' // Direct field access
Methods
class Calculator {
// Instance method
def add(a, b) { a + b }
// Static method
static def multiply(a, b) { a * b }
// Default parameters
def greet(name = 'World') {
"Hello, $name!"
}
// Variable arguments
def sum(int... numbers) {
numbers.sum()
}
// Named parameters (via Map)
def create(Map params) {
println params.name
println params.age
}
}
def calc = new Calculator()
calc.add(2, 3)
Calculator.multiply(4, 5)
calc.create(name: 'John', age: 30)
Inheritance
class Animal {
String name
def speak() {
"Some sound"
}
}
class Dog extends Animal {
@Override
def speak() {
"Woof!"
}
def fetch() {
"Fetching..."
}
}
def dog = new Dog(name: 'Rex')
dog.speak() // Woof!
Interfaces and Traits
// Interface
interface Flyable {
void fly()
}
// Trait (with implementation)
trait Swimmable {
void swim() {
println "Swimming..."
}
}
class Duck implements Flyable, Swimmable {
void fly() {
println "Flying..."
}
}
def duck = new Duck()
duck.fly()
duck.swim()
// Trait with state
trait HasId {
long id
void generateId() {
id = System.currentTimeMillis()
}
}
AST Transformations
import groovy.transform.*
// Canonical (constructor, toString, etc.)
@Canonical
class Person {
String name
int age
}
// Immutable
@Immutable
class Point {
int x, y
}
// Singleton
@Singleton
class Database {
def connect() { }
}
// ToString
@ToString(includeNames=true)
class Product {
String name
BigDecimal price
}
// TupleConstructor
@TupleConstructor
class User {
String username
String email
}
// Builder
@Builder
class Config {
String host
int port
boolean ssl
}
def config = Config.builder()
.host('localhost')
.port(8080)
.ssl(true)
.build()
Metaprogramming
ExpandoMetaClass
// Add method to existing class
String.metaClass.shout = {
delegate.toUpperCase() + "!"
}
"hello".shout() // HELLO!
// Add static method
Integer.metaClass.static.isEven = { int n ->
n % 2 == 0
}
Integer.isEven(4) // true
// Add property
String.metaClass.getReversed = {
delegate.reverse()
}
"groovy".reversed // yvoorg
Missing Method/Property
class Dynamic {
def methodMissing(String name, args) {
println "Called: $name with $args"
}
def propertyMissing(String name) {
println "Get: $name"
return "Value of $name"
}
def propertyMissing(String name, value) {
println "Set: $name = $value"
}
}
def obj = new Dynamic()
obj.anything() // Called: anything...
obj.someProp // Get: someProp
obj.someProp = 123 // Set: someProp = 123
Categories
class StringExtensions {
static String twice(String self) {
self + self
}
}
use(StringExtensions) {
println "Groovy".twice() // GroovyGroovy
}
// Multiple categories
use([Category1, Category2]) {
// Both categories active here
}
@Delegate
class Worker {
def work() { "Working..." }
}
class Manager {
@Delegate Worker worker = new Worker()
def manage() { "Managing..." }
}
def mgr = new Manager()
mgr.work() // Delegated to Worker
mgr.manage() // Manager's own method
File I/O
Reading Files
// Read entire file
def text = new File('file.txt').text
// Read lines
def lines = new File('file.txt').readLines()
// Iterate lines
new File('file.txt').eachLine { line ->
println line
}
// With encoding
new File('file.txt')
.getText('UTF-8')
// Read bytes
def bytes = new File('file.bin').bytes
// Using withReader
new File('file.txt').withReader { reader ->
// Auto-closes
reader.eachLine { println it }
}
Writing Files
// Write text
new File('out.txt').text = "Hello"
// Write lines
new File('out.txt').withWriter { writer ->
writer.writeLine("Line 1")
writer.writeLine("Line 2")
}
// Append
new File('out.txt').append("More text")
// Write bytes
new File('out.bin').bytes = [1, 2, 3]
// Using << operator
new File('out.txt') << "Appended text"
File Operations
def file = new File('example.txt')
// Properties
file.exists()
file.isFile()
file.isDirectory()
file.canRead()
file.canWrite()
file.size()
file.absolutePath
file.parent
file.name
// Operations
file.mkdir() // Create directory
file.mkdirs() // Create with parents
file.delete() // Delete
file.renameTo(new File('new.txt'))
// Directory listing
new File('.').eachFile { file ->
println file.name
}
new File('.').eachFileRecurse { file ->
println file.absolutePath
}
Path Manipulation
// Join paths
def path = ['path', 'to', 'file.txt']
.join(File.separator)
// Using / operator
def file = new File('dir') / 'subdir' / 'file.txt'
// Relative paths
def file1 = new File('.')
def file2 = new File('..', 'file.txt')
// Canonical path
file.canonicalPath
Gradle DSL
Build Script Basics
// build.gradle
plugins {
id 'java'
id 'application'
}
group = 'com.example'
version = '1.0.0'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.slf4j:slf4j-api:2.0.0'
testImplementation 'junit:junit:4.13.2'
}
application {
mainClass = 'com.example.Main'
}
Task Definition
// Simple task
task hello {
doLast {
println 'Hello, Gradle!'
}
}
// Typed task
task copy(type: Copy) {
from 'src'
into 'dest'
}
// With configuration
task build {
group = 'build'
description = 'Builds the project'
doFirst {
println 'Starting build...'
}
doLast {
println 'Build complete!'
}
}
// Task dependencies
task compileJava
task test(dependsOn: compileJava)
// Multiple dependencies
task deploy(dependsOn: ['build', 'test'])
Dependencies
dependencies {
// Implementation (compile-time)
implementation 'group:artifact:version'
// API (exposed to consumers)
api 'group:artifact:version'
// Runtime only
runtimeOnly 'mysql:mysql-connector:8.0.30'
// Test dependencies
testImplementation 'junit:junit:4.13.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
// Platform/BOM
implementation platform('org.springframework.boot:spring-boot-dependencies:3.0.0')
// Local file
implementation files('libs/local.jar')
// Project dependency
implementation project(':subproject')
}
Configurations
// Custom configuration
configurations {
customConfig
}
dependencies {
customConfig 'group:artifact:version'
}
// Exclude transitive dependency
dependencies {
implementation('group:artifact:version') {
exclude group: 'unwanted', module: 'module'
}
}
// Force version
configurations.all {
resolutionStrategy {
force 'group:artifact:1.0'
}
}
Multi-Project Build
// settings.gradle
rootProject.name = 'my-project'
include 'app', 'lib', 'utils'
// Root build.gradle
subprojects {
apply plugin: 'java'
repositories {
mavenCentral()
}
dependencies {
testImplementation 'junit:junit:4.13.2'
}
}
// app/build.gradle
dependencies {
implementation project(':lib')
implementation project(':utils')
}
Custom Tasks
// Task class
class GreetingTask extends DefaultTask {
@Input
String greeting = 'Hello'
@TaskAction
void greet() {
println "$greeting, Gradle!"
}
}
task greet(type: GreetingTask) {
greeting = 'Hi'
}
// Incremental task
task processFiles {
inputs.dir('src')
outputs.dir('build/processed')
doLast {
// Processing logic
}
}
Advanced Topics
Regular Expressions
// Pattern matching
def text = "Groovy 4.0"
// Find operator (=~)
def matcher = text =~ /\d+\.\d+/
if (matcher) {
println matcher[0] // 4.0
}
// Exact match (==~)
text ==~ /Groovy \d+\.\d+/ // true
// Pattern object
def pattern = ~/\d+/
def matcher2 = pattern.matcher("123")
matcher2.matches() // true
// Replace
text.replaceAll(/\d+/, 'X') // Groovy X.X
// Split
"a,b,c".split(/,/) // [a, b, c]
// Groups
def m = "John Doe" =~ /(\w+) (\w+)/
m[0][1] // John (group 1)
m[0][2] // Doe (group 2)
Exception Handling
// Try-catch
try {
def result = 10 / 0
} catch (ArithmeticException e) {
println "Cannot divide by zero"
} catch (Exception e) {
println "Error: ${e.message}"
} finally {
println "Cleanup"
}
// Try with resources
new File('file.txt').withReader { reader ->
// Auto-closes
}
// Safe navigation
def length = text?.length() // No NPE
// Elvis with exception
def value = mayFail() ?: 'default'
Type Checking
import groovy.transform.TypeChecked
@TypeChecked
class Typed {
int add(int a, int b) {
return a + b
// Type errors caught at compile-time
}
}
// Compile-time static
import groovy.transform.CompileStatic
@CompileStatic
class Fast {
int multiply(int a, int b) {
a * b // No dynamic dispatch
}
}
JSON Processing
import groovy.json.*
// Parse JSON
def json = '{"name":"John","age":30}'
def obj = new JsonSlurper().parseText(json)
println obj.name // John
// Generate JSON
def person = [name: 'Jane', age: 25]
def jsonStr = JsonOutput.toJson(person)
// Pretty print
println JsonOutput.prettyPrint(jsonStr)
// With builder
def builder = new JsonBuilder()
builder.person {
name 'John'
age 30
address {
city 'NYC'
}
}
println builder.toString()
XML Processing
// Parse XML
def xml = '''
<person>
<name>John</name>
<age>30</age>
</person>
'''
def person = new XmlSlurper().parseText(xml)
println person.name // John
println person.age // 30
// Generate XML
def builder = new groovy.xml.MarkupBuilder()
builder.person {
name 'Jane'
age 25
address {
city 'NYC'
}
}
SQL/Database
import groovy.sql.Sql
def sql = Sql.newInstance(
'jdbc:h2:mem:test',
'org.h2.Driver'
)
// Query
sql.eachRow('SELECT * FROM users') { row ->
println "${row.name}, ${row.age}"
}
// Single result
def person = sql.firstRow(
'SELECT * FROM users WHERE id = ?',
[1]
)
// Execute
sql.execute('''
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(100)
)
''')
// Insert
sql.executeInsert(
'INSERT INTO users VALUES (?, ?)',
[1, 'John']
)
// Transaction
sql.withTransaction {
sql.execute('UPDATE ...')
sql.execute('INSERT ...')
}
Groovy vs Java
Key Differences
| Feature | Java | Groovy |
|---|---|---|
| Semicolons | Required | Optional |
| Return | Explicit | Implicit (last expr) |
| Types | Required | Optional (def) |
| Properties | Getters/setters | Auto-generated |
| Strings | String only |
GString, slashy, etc. |
| Collections | Verbose | Native syntax |
| Null safety | Manual checks | ?., ?: operators |
| Closures | Lambdas (8+) | First-class closures |
| Operator overload | No | Yes |
Syntax Comparison
// Java
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
// Groovy equivalent
class Person {
String name
}
// Java
List<String> list = Arrays.asList("a", "b", "c");
for (String item : list) {
System.out.println(item);
}
// Groovy
def list = ['a', 'b', 'c']
list.each { println it }
Interoperability
// Call Java from Groovy
import java.util.ArrayList
def list = new ArrayList()
list.add("item")
// Use Java libraries
import java.time.LocalDate
def today = LocalDate.now()
// Groovy class in Java
// Person.groovy compiled to Person.class
// Use normally in Java code
Tips & Gotchas
Best Practices
- Use
deffor local variables - Unless type is important - Leverage operators -
?.,?:,*.,<=>for cleaner code - Prefer closures - More idiomatic than traditional loops
- Use AST transformations -
@Canonical,@Immutable, etc. - Static compilation - Use
@CompileStaticfor performance-critical code - Named parameters - More readable than positional
- GStrings for interpolation - But use single quotes when no interpolation needed
Common Pitfalls
// == is equals(), not identity
def a = [1, 2]
def b = [1, 2]
a == b // true (use === for identity)
// GString vs String
def name = "World"
def map = ["Hello $name": 1]
map["Hello World"] // null! (GString key)
// Use toString() or single quotes
// Closure vs method parameter
[1,2,3].each { it * 2 } // Closure (ignored)
[1,2,3].collect { it * 2 } // Returns result
// Return in closure
def list = [1,2,3].collect {
if (it == 2) return 0 // Returns from closure
it * 2
} // [2, 0, 6]
// Power operator precedence
-2 ** 2 // -4 (not 4)
(-2) ** 2 // 4
Performance Tips
- Use
@CompileStatic- For performance-critical code - Avoid excessive metaprogramming - Runtime overhead
- Cache compiled patterns - When using regex frequently
- Use primitives - When possible in tight loops
- Profile before optimizing - Groovy is usually fast enough
Also see
- Groovy Documentation - Official docs
- Groovy API - GDK JavaDoc
- Gradle Build Tool - Build automation
- Grails Framework - Web framework
- Spock Testing - Testing framework
- CodeNarc - Static analysis
- Groovy Console - Online playground