Skip to content

Latest commit

 

History

History
505 lines (404 loc) · 11.3 KB

File metadata and controls

505 lines (404 loc) · 11.3 KB
name groovy-developer
description Expert guide for Groovy 4 development, covering concise syntax, closures, DSLs, metaprogramming, static compilation, and integration with Java 17 and Grails
license Apache-2.0

What I Do

  • Provide deep expertise in Groovy 4.0, including core language features: syntax, closures, traits, GStrings, operators, and Groovy Truth.
  • Guide metaprogramming techniques: runtime (categories, ExpandoMetaClass, extension modules) and compile-time (AST transformations like @Immutable, @Builder, @Delegate, @Singleton, @Log, @Memoized).
  • Assist with DSL creation and usage (MarkupBuilder, JsonBuilder, builders for configuration).
  • Support testing with Spock Framework (BDD, data-driven tests, mocking) and built-in power assertions.
  • Help integrate Groovy with Java 17: joint compilation, Gradle builds, Grails, and Spring Boot.
  • Cover data handling: JSON/XML parsing and building, Groovy SQL, collection processing.
  • Advise on performance: @CompileStatic, @TypeChecked, @GrailsCompileStatic for optimal execution.

When to Use Me

Activate this skill for any Groovy-related task, including:

  • Writing or debugging Groovy classes in Grails domain, controllers, services, or taglibs.
  • Building or enhancing Gradle build scripts (build.gradle).
  • Creating DSLs for configuration, testing, or business rules.
  • Writing Spock specifications for unit and integration tests.
  • Migrating from older Groovy versions (2.x/3.x) to 4.0.
  • Performance optimization with static compilation.
  • Working with JSON/XML data, GORM criteria queries, or builders.

Groovy 4 Key Features

Concise Syntax

// No semicolons required
def name = 'Grails'

// Optional parentheses for method calls
println 'Hello World'
list.each { println it }

// Optional return keyword
String greet(String name) {
    "Hello, $name"  // Last expression is returned (GString needs double quotes)
}

GStrings (String Interpolation)

def user = "Alice"
def count = 42

// Simple interpolation
println "User: $user"

// Expression interpolation
println "Items: ${count * 2}"

// Lazy GStrings (evaluated when accessed)
def lazy = "Count is ${ -> counter }"

// Multi-line strings
def sql = """
    SELECT * FROM users
    WHERE name = '$user'
    ORDER BY created_at
"""

Closures

// Basic closure
def greet = { name -> "Hello, $name" }
println greet("World")

// Implicit 'it' parameter
def twice = { it * 2 }
println [1, 2, 3].collect(twice)

// Closure with delegate (for DSLs)
def configure = {
    name = "MyApp"
    version = "1.0"
}
configure.delegate = new Config()
configure.resolveStrategy = Closure.DELEGATE_FIRST
configure()

// Method references
def upper = String.&toUpperCase
println ["a", "b"].collect(upper)

Safe Navigation and Elvis Operator

// Safe navigation - returns null if any part is null
def city = user?.address?.city

// Elvis operator - default value if null
def name = user?.name ?: "Anonymous"

// Safe method calls
def length = text?.length() ?: 0

// Combine with collections
def firstTitle = books?.first()?.title ?: "No books"

Spread Operator

// Spread operator for collections
def names = users*.name  // Same as users.collect { it.name }

// Spread for method arguments
def args = [1, 2, 3]
someMethod(*args)

// Spread in list literals
def combined = [*list1, *list2]

Collection Literals and Methods

// List literal
def list = [1, 2, 3, 4, 5]

// Map literal
def map = [name: "Grails", version: 7]

// Range
def range = 1..10
def exclusive = 1..<10

// Powerful collection methods
list.find { it > 3 }           // First match: 4
list.findAll { it > 3 }        // All matches: [4, 5]
list.any { it > 3 }            // true
list.every { it > 0 }          // true
list.groupBy { it % 2 }        // [0:[2,4], 1:[1,3,5]]
list.collectEntries { [it, it * 2] }  // [1:2, 2:4, ...]

// Sorting
list.sort { a, b -> b <=> a }  // Descending
list.sort { -it }              // Descending (numeric)

Groovy Truth

// Falsy values: null, empty string, empty collection, 0, false
if (list) { }      // True if not null and not empty
if (string) { }    // True if not null and not blank
if (number) { }    // True if not 0

// Useful in conditionals
def items = getItems() ?: []  // Default to empty list

Records

// Immutable data class
record Point(int x, int y) {}

def p = new Point(10, 20)
println p.x()  // 10
println p      // Point[x=10, y=20]

Traits

trait Auditable {
    Date dateCreated
    Date lastUpdated

    def beforeInsert() {
        dateCreated = new Date()
    }

    def beforeUpdate() {
        lastUpdated = new Date()
    }
}

class Book implements Auditable {
    String title
}

Static Compilation

@CompileStatic

Enables static type checking and compilation for better performance:

import groovy.transform.CompileStatic

@CompileStatic
class BookService {
    List<Book> findByAuthor(String author) {
        Book.findAllByAuthor(author)
    }

    int calculateTotal(List<Integer> prices) {
        prices.sum() as int
    }
}

@TypeChecked

Type checking without full static compilation:

import groovy.transform.TypeChecked

@TypeChecked
void process(String input) {
    println input.toUppercase()  // Compile error: method not found
}

@GrailsCompileStatic

Grails-aware static compilation that understands dynamic finders:

import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
class BookController {
    def show(Long id) {
        respond Book.findByIdAndActive(id, true)  // Dynamic finder works with @GrailsCompileStatic
    }
}

AST Transformations

@Immutable

import groovy.transform.Immutable

@Immutable
class Person {
    String name
    int age
}

def p = new Person(name: "Alice", age: 30)
// p.name = "Bob"  // Error: read-only

@Builder

import groovy.transform.builder.Builder

@Builder
class Email {
    String to
    String subject
    String body
}

def email = Email.builder()
    .to("user@example.com")
    .subject("Hello")
    .body("Content here")
    .build()

@Delegate

import groovy.lang.Delegate

class EnhancedList<T> {
    @Delegate List<T> list = []

    T secondLast() {
        list[-2]
    }
}

def enhanced = new EnhancedList<String>()
enhanced.add("a")  // Delegated to list
enhanced.add("b")
println enhanced.secondLast()  // "a"

@Singleton

import groovy.lang.Singleton

@Singleton
class ConfigManager {
    Map settings = [:]
}

ConfigManager.instance.settings['key'] = 'value'

@Memoized

import groovy.transform.Memoized

class Calculator {
    @Memoized
    BigInteger fibonacci(int n) {
        if (n <= 1) return n
        fibonacci(n - 1) + fibonacci(n - 2)
    }
}

@Log Variants

import groovy.util.logging.Slf4j

@Slf4j
class BookService {
    def save(Book book) {
        log.info "Saving book: ${book.title}"
        // ...
    }
}

Builders and DSLs

JsonBuilder

import groovy.json.JsonBuilder

def builder = new JsonBuilder()
builder {
    name "Grails"
    version 7
    features(["GORM", "GSP", "Plugins"])
    metadata {
        author "Grails Team"
        license "Apache 2.0"
    }
}
println builder.toPrettyString()

JsonSlurper

import groovy.json.JsonSlurper

def json = '{"name": "Grails", "version": 7}'
def data = new JsonSlurper().parseText(json)
println data.name  // Grails

MarkupBuilder (XML/HTML)

import groovy.xml.MarkupBuilder

def writer = new StringWriter()
def xml = new MarkupBuilder(writer)
xml.books {
    book(id: 1) {
        title "Grails Guide"
        author "Team"
    }
}
println writer.toString()

Custom DSL Example

class HtmlBuilder {
    def html(@DelegatesTo(HtmlBuilder) Closure c) {
        c.delegate = this
        c.resolveStrategy = Closure.DELEGATE_FIRST
        "<html>${c()}</html>"
    }

    def body(String content) {
        "<body>$content</body>"
    }
}

def builder = new HtmlBuilder()
def result = builder.html {
    body "Hello World"
}

Testing with Spock

Basic Specification

class BookServiceSpec extends Specification {

    def service = new BookService()

    def "should find book by title"() {
        given: "a book title"
        def title = "Grails Guide"

        when: "searching for the book"
        def book = service.findByTitle(title)

        then: "the book is found"
        book != null
        book.title == title
    }
}

Data-Driven Tests

@Unroll
def "max of #a and #b is #expected"() {
    expect:
    Math.max(a, b) == expected

    where:
    a | b || expected
    1 | 3 || 3
    7 | 4 || 7
    0 | 0 || 0
}

Mocking

def "should call repository when saving"() {
    given:
    def repo = Mock(BookRepository)
    def service = new BookService(repo)
    def book = new Book(title: "Test")

    when:
    service.save(book)

    then:
    1 * repo.save(book)
}

Exception Testing

def "should throw exception for invalid input"() {
    when:
    service.process(null)

    then:
    thrown(IllegalArgumentException)
}

Groovy SQL

import groovy.sql.Sql

def sql = Sql.newInstance(dataSource)

// Query
sql.eachRow("SELECT * FROM books WHERE author = ?", [authorName]) { row ->
    println "${row.title} by ${row.author}"
}

// Insert
sql.execute("INSERT INTO books (title, author) VALUES (?, ?)",
            [title, author])

// With GString (auto-parameterized)
sql.execute("INSERT INTO books (title) VALUES ($title)")

Best Practices

Performance

  • Use @CompileStatic or @GrailsCompileStatic in production code.
  • Avoid runtime metaprogramming in hot paths.
  • Prefer collect/findAll over manual loops.
  • Use @Memoized for expensive computations.

Code Style

  • Use safe navigation (?.) instead of null checks.
  • Prefer Elvis operator (?:) for default values.
  • Use GStrings for string interpolation.
  • Prefer traits over inheritance for mixins.
  • Use explicit types for public APIs, def for local variables.
  • Always use parentheses in method calls for clarity.

Grails-Specific

  • Use @GrailsCompileStatic instead of @CompileStatic in Grails artefact classes if needed.
  • Leverage GORM dynamic finders and where queries.
  • Use command objects for form binding and validation.
  • Follow convention over configuration.

Common Pitfalls

  • == vs is: == calls equals(), use is for identity comparison (same as Java).
  • GString in maps: GStrings don't work as map keys; use "key$idx".toString() if needed.
  • Closure scope: Understand delegate, owner, and this in closures.
  • Optional parentheses: Can cause ambiguity; always use parentheses for clarity.
  • Static imports: Always use explicit imports for statically imported methods.

Resources