Groovy
From Evgeny Goldin
Contents |
Multiple assignment
def (a, b) = [a, b] def (int a, String b) = [3, "Opa"] def a, b (a, b) = {[ 'aaa', 'bbb' ]}() (a, b) = [b, a]
'for' loops
for (i in 0..9) println i for (int i = 0; i < 10; i++) println i
Operators
Builders
def builder = new groovy.xml.MarkupBuilder() def bobsFriend = { person { name "fred" age 40 } } builder.person { name "bob" age 30 friend bobsFriend }
Categories
- Groovy Categories
- Groovy Categories vs ExpandoMetaClass
- Type coercion with
CoercionCategory -
groovy.xml.dom.DOMCategory -
groovy.time.TimeCategory -
@Category, @Mixin
class StringCategory { static String lower(String self) { self.toLowerCase() }} class NumberCategory { static Distance getMeters(Number self) { new Distance(number: self) }} // getMeters() => "meters" use ( StringCategory ) { assert "test" == "TeSt".lower() } use ( NumberCategory ) { def dist = 300.meters } use ( groovy.xml.dom.DOMCategory ) { assert html.head.title.text() == 'Test' assert html.body.p.text() == 'This is a test.' assert html.find{ it.tagName == 'body' }.tagName == 'body' assert html.getElementsByTagName('*').grep{ it.'@class' }.size() == 2 } use ( groovy.time.TimeCategory ) { println 1.minute.from.now println 10.hours.ago println new Date() - 3.months }
Groovy JDK
-
org.codehaus.groovy.tools.DgmConverter-
org.codehaus.groovy.runtime.dgm$832.class -
META-INF/dgminfo
-
-
org.codehaus.groovy.runtime.DefaultGroovyMethods (DGM) -
org.codehaus.groovy.runtime.DefaultGroovyStaticMethods -
org.codehaus.groovy.runtime.DateGroovyMethods -
org.codehaus.groovy.runtime.EncodingGroovyMethods -
org.codehaus.groovy.runtime.ProcessGroovyMethods(new in1.7.5) -
org.codehaus.groovy.runtime.SqlGroovyMethods -
org.codehaus.groovy.runtime.SwingGroovyMethods -
org.codehaus.groovy.runtime.XmlGroovyMethods
MOP
- Practically Groovy: Metaprogramming with closures, ExpandoMetaClass, and categories
-
pinboard.in/u:evgenyg/t:groovy mop
Dynamic Property access / Method Invocation
/** * Dynamic property access */ obj."$property" obj[(property)] obj."$property" = value /** * Dynamic method invocation */ object."$methodName"( *args ) object.invokeMethod( methodName, *args ) /** * Dynamic object creation */ def <T> T construct(Class<T> clazz, Map args) { clazz.newInstance(args) } assert 'groovy.util.Expando' == construct(Expando, [a:'aa']).class.name assert 'aa' == construct(Expando, [a:'aa']).a
MOP Classes
-
groovy.lang.GroovyObject -
groovy.lang.MetaObjectProtocol-
groovy.lang.MetaClass-
groovy.runtime.HandleMetaClass -
groovy.lang.MetaClassImpl: Allows methods to be dynamically added to existing classes at runtime-
groovy.lang.ExpandoMetaClass: Allows to dynamically add or replace methods, constructors, and properties using a closure syntax.
-
-
-
MetaProgramming Hooks
- Using invokeMethod and getProperty
- Using methodMissing and propertyMissing
- Implement
groovy.lang.GroovyInterceptableto intercept invocation of existing methods withinvokeMethod (String name, args)
| Method | Description | Usages |
|---|---|---|
getProperty (String property)
| Intercepts every property read access | |
setProperty (String property, Object newValue)
| Intercepts every property write access | |
invokeMethod (String name, args)
| Intercepts every method access |
|
propertyMissing (String name[, value])
| Intercepts failed property access. Maximum performance penalty! |
|
methodMissing (String name, args)
| Intercepts failed method dispatch. Maximum performance penalty! |
|
Inspecting MetaClass
String.metaClass.methods.each { println it.name } String.metaClass.properties.each { println it.name } println Foo.metaClass.getMetaMethods() // List<MetaMethod> println Foo.metaClass.getProperties() // List<MetaProperty> println Foo.metaClass.getMetaMethod( '..' ) // MetaMethod println Foo.metaClass.getMetaProperty( '..' ) // MetaProperty println Foo.metaClass.respondsTo ( new Foo(), "bar" ) println Foo.metaClass.hasProperty( new Foo(), "prop" )
-
respondsToonly works for "real" methods and those added viaExpandoMetaClassand not for cases whereinvokeMethodormethodMissingare overridden. It is impossible in these cases to tell if an object responds to a method without actually invoking the method.
groovy.util.Expando
def ex = new Expando() ex.propertyName = 'Some Value' ex.anotherPropName = 'Another Value' ex.printDate = { String header -> println "[$header]: [${ new Date() }]" } assert 'Some Value' == ex.propertyName assert 'Another Value' == ex.anotherPropName ex.printDate( "Date" ) def bean = new Expando( name:"James", location:"London", id:123 ) assert "James" == bean.name assert 123 == bean.id assert "aaa" == new Expando( someMethod: { "aaa" } ).someMethod()
groovy.lang.ExpandoMetaClass
- Groovydoc, src
- What's New in Groovy 1.6
-
java.lang.Class.getMetaClass() - Normally,
SomeClass.metaClassisorg.codehaus.groovy.runtime.HandleMetaClass - After
"p.metaClass.speak = { -> ... }"HandleMetaClassswitches the object's metaclass fromMetaClassImpltoExpandoMetaClass
groovy> println String.metaClass groovy> String.metaClass.aaa = { "aaa" } groovy> println String.metaClass org.codehaus.groovy.runtime.HandleMetaClass@1a503f[groovy.lang.MetaClassImpl@1a503f[class java.lang.String]] groovy.lang.ExpandoMetaClass@1205d8d[class java.lang.String]
- Using ExpandoMetaClass to add behaviour
- Behaves like an
Expando, allowing the addition or replacement of methods, properties and constructors on the fly. - By default
ExpandoMetaClassdoesn't do inheritance. To enable this you must callExpandoMetaClass.enableGlobally().
/** * Borrowing methods */ Person.metaClass.buyHouse = new MortgageLender().&borrowMoney /** * Adding methods */ Number.metaClass.multiply = { Amount amount -> amount.times(delegate) } Number.metaClass.div = { Amount amount -> amount.inverse().times(delegate) } Book.metaClass.titleInUpperCase << {-> title.toUpperCase() } // "Appends" the new method, if the method already exists an exception will be thrown Book.metaClass.titleInUpperCase = {-> title.toUpperCase() } // Replaces an instance method Person.metaClass.whatIsThis = { String arg -> .. } Person.metaClass.whatIsThis << { int arg -> .. } // Overload Person.metaClass.whatIsThis = { int arg -> .. } // Replace Number.metaClass { multiply { Amount amount -> amount.times(delegate) } div { Amount amount -> amount.inverse().times(delegate) } div << { Number factor -> delegate.divide(factor) } div << { Amount factor -> delegate.divide(factor) } method << { String s -> .. } << { Integer i -> .. } } /** * Adding static methods */ Dog.metaClass.static.create = { new Dog() } Dog.metaClass { 'static' { fqn { delegate.name } } } /** * Adding properties */ Dog.metaClass.getBreed = { 'Poodle' } // new Dog().breed Book.metaClass.author = "Stephen King" // Property is mutable and has both a setter and getter. It is stored in a ThreadLocal WeakHashMap, don't expect the value to stick around forever! Car.metaClass { lastAccessed = null invokeMethod = { String name, args -> .. delegate.lastAccessed = new Date() .. } } /** * Adding constructor */ Dog.metaClass.constructor = { String name -> new Dog ( name : name ) } Book.metaClass.constructor << { String title -> new Book( title: title ) } Book.metaClass.constructor = { new Book() } // StackOverflowError! Book.metaClass.constructor = { org.springframework.beans.BeanUtils.instantiateClass(Book) } // Bypass StackOverflowError /** * Dynamic name creation */ Person.metaClass."changeNameTo${methodName}" = {-> delegate.name = "Bob" } classes.findAll { it.name.endsWith( 'Codec' ) }.each { codec -> Object.metaClass."encodeAs${codec.name-'Codec'}" = { codec.newInstance().encode(delegate) } // encodeAsHtml, encodeAsJavaScript, etc Object.metaClass."decodeFrom${codec.name-'Codec'}" = { codec.newInstance().decode(delegate) } // decodeFromHtml, decodeFromJavaScript, etc } /** * Overriding MOP hooks */ Stuff.metaClass.invokeMethod = { String name, args -> .. } Person.metaClass.getProperty = { String name -> .. } Person.metaClass.setProperty = { String name, value -> .. } Number.metaClass.methodMissing = { String methodName, args -> .. } Dog.metaClass.static.methodMissing = { String methodName, args -> .. } Dog.metaClass.static.propertyMissing = { String propertyName -> .. } Stuff.metaClass.'static'.invokeMethod = { String name, args -> .. } /** * Adding methods to interfaces * Requires ExpandoMetaClass.enableGlobally()! */ List.metaClass.sizeDoubled = {-> delegate.size() * 2 } HttpSession.metaClass.getAt = { String key -> delegate.getAttribute(key) } HttpSession.metaClass.putAt = { String key, Object val -> delegate.setAttribute(key, val) } /** * Adding Methods to instance */ def str = "hello test" def s = "aaaaaaaaaa" str.metaClass.test = { "test" } s.metaClass { aaa { "aaa" } bbb { "bbb" } } assert "test" == str.test() assert "aaa" == s.aaa() assert "bbb" == s.bbb() x = "aaaaaaaaaa" x.metaClass.map = [:] x.map.y = 5 assert 5 == x.map.y /** * Custom casting */ String.metaClass.asType = { Long cl -> .. } /** * Undo metaprogramming */ String.metaClass = null
Intercept + Cache + Invoke
Foo.metaClass.methodMissing = { String name, args -> // Intercept def impl = { Object[] varArgs -> ... } Foo.metaClass."${name}" = impl // Cache impl( *args ) // Invoke }
Runtime mixins
class DivingAbility { def dive() { ... }} class FlyingAbility { def fly() { ... }} class JamesBondVehicle { .. } JamesBondVehicle.mixin DivingAbility, FlyingAbility new JamesBondVehicle().dive() new JamesBondVehicle().fly()
AST Transformations
- What's New in Groovy 1.6
- Compile-time Metaprogramming - AST Transformations
- Groovy Transforms - additional AST Transformations
-
org.codehaus.groovy.ast -
org.codehaus.groovy.transform
-
@Immutable -
@Delegate -
@Singleton -
@Lazy -
@Newify -
@Category -
@Mixin -
@PackageScope -
@Grab -
@Bindable -
@Vetoable -
@Typed- Groovy++
@Immutable
- Immutable AST Macro
- Class becomes final, all its fields become final, and you cannot change its state after construction
@Immutable final class Punter { String first, last } @Immutable class Coordinates { double lat, lng }
@Delegate
/** * The Groovy compiler adds all of Date's methods to the Event class, and those methods simply delegate the call to the Date field. */ class Event { @Delegate Date when String title, url } def df = new SimpleDateFormat("yyyy/MM/dd") def gr8conf = new Event(title: "GR8 Conference", url: "http://www.gr8conf.org", when: df.parse("2009/05/18")) def javaOne = new Event(title: "JavaOne", url: "http://java.sun.com/javaone/", when: df.parse("2009/06/02")) assert gr8conf.before(javaOne.when) class Employee { def doTheWork() { "done" }} class Manager { @Delegate Employee slave = new Employee()} assert new Manager().doTheWork() == "done" /** * If the delegate is not a final class, it is even possible to make the Event class a subclass of Date simply by extending Date. */ class Event extends Date { @Delegate Date when String title, url } /** * In the case you are delegating to an interface you don't need to explictely say you implement the interface of the delegate. * The @Delegate annotation fully supports polymorphism by promoting not only the delegate class's methods to the outer class, but the delegate's interfaces as well. */ class LockableList { @Delegate private List list = [] // @Delegate(interfaces = false) private List list = [] @Delegate private Lock lock = new ReentrantLock() } assert new LockableList() instanceof Lock assert new LockableList() instanceof List
@Singleton
public class T { public static final T instance = new T(); private T() {} } @Singleton class T {} @Singleton(lazy = true) class T {} T.instance
@Lazy
class Person { @Lazy List pets = ['Cat', 'Dog', 'Bird'] @Lazy(soft = true) List pets = ['Cat', 'Dog', 'Bird'] } class Dude { @Lazy List pets = retrieveFromSlowDB() }
@Newify
def buildTree() { new Tree(new Tree(new Leaf(1), new Leaf(2)), new Leaf(3)) } @Newify([Tree, Leaf]) buildTree() { Tree(Tree(Leaf(1), Leaf(2)), Leaf(3)) } @Newify([Coordinates, Path]) def build() { Path( Coordinates(48.824068, 2.531733), Coordinates(48.857840, 2.347212), Coordinates(48.858429, 2.342622) ) }
@Category, @Mixin
- Category and Mixin transformations
-
groovy.lang.Category - Classes conforming to the conventional Groovy category conventions can be used within
usestatements or mixed in at compile time with the@Mixintransformation or at runtime with themixinmethod on classes. -
@Categoryonly applies to one single type at a time, unlike classical categories which can be applied to any number of types.
@Category(Integer) class IntegerOps { def triple() { this * 3 }} use ( IntegerOps ) { assert 25.triple() == 75 } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @Category( A ) class AUtils1 { def triple() { "[$this]".multiply(3) }} @Category( A ) class AUtils2 { def duplicate() { "[$this]".multiply(2) }} // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @Mixin([ AUtils1, AUtils2 ]) @Mixin( AUtils1 ) class A { String toString(){ this.class.name }} // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ class A { String toString(){ this.class.name }} A.mixin ([ AUtils1, AUtils2 ]) A.mixin AUtils1 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ assert "[A][A][A]" == new A().triple() assert "[A][A]" == new A().duplicate()
@PackageScope
- PackageScope transformation
- To be able to expose a field with package-scope visibility, you can annotate your field with the
@PackageScopeannotation.
@Grab
- Groovy 1.7 release notes
- Grape
- Using Hibernate with Groovy
- Applies to imports, packages, variable declaration
@GrabResolver( name='neo4j-public-repo', root='http://m2.neo4j.org' ) @Grab( 'org.neo4j:neo4j-kernel:1.1.1' ) import org.neo4j.kernel.EmbeddedGraphDatabase @Grab( group='org.ccil.cowan.tagsoup', module='tagsoup', version='0.9.7' ) def getHtml() { ... } @Grab( 'net.sf.json-lib:json-lib:2.3:jdk15' ) def builder = new net.sf.json.groovy.JsonGroovyBuilder()
@Bindable, @Vetoable
AstBuilder
- Global AST Transformations
- Local AST Transformations
- Building AST Guide
- Compiler Phase Guide
- Groovy AST Browser
-
Groovy Console -> Script -> Inspect Ast (Ctrl+T)
@WithLogging def greet() { println "Hello World" } greet() // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @Retention(RetentionPolicy.SOURCE) @Target([ElementType.METHOD]) @GroovyASTTransformationClass(["gep.LoggingASTTransformation"]) public @interface WithLogging {} // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS) public class LoggingASTTransformation implements ASTTransformation { public void visit(ASTNode[] nodes, SourceUnit sourceUnit) { List methods = sourceUnit.getAST()?.getMethods() // find all methods annotated with @WithLogging methods.findAll { MethodNode method -> method.getAnnotations(new ClassNode(WithLogging)) }.each { MethodNode method -> ... } } } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ new AstBuilder().buildFromString(''' "Hello" ''') new AstBuilder().buildFromCode { "Hello" } List<ASTNode> nodes = new AstBuilder().buildFromSpec { block { returnStatement { constant "Hello" } } }
Testing
- Mocks Aren't Stubs
- CUT = Class Under Test
Unit Tests
import static groovy.util.GroovyTestCase.* class MyClass { @Test void getAnything() { ... shouldFail( UnsupportedOperationException ){ badList << "will this work?" } ... } }
Stubs
- Intercepts all method calls to instances of a given class and returns a predefined result
-
groovy.mock.interceptor.StubFor
/** * Stub Person calls */ class Person { String first, last } def stub = new StubFor(Person) stub.demand.with { getFirst{ 'dummy' } getLast { 'name' } } stub.use { def john = new Person(first:'John', last:'Smith') assert john.first == 'dummy' assert john.last == 'name' } /** * Expect demanded call cardinalities to match demanded ranges. * Assert that all demanded method calls happened. */ stub.expect.verify() /** * Demand calls to different methods */ stub.demand.getFirst{ 'dummy' } stub.demand.getLast { 'name' } /** * When calls to the stubbed method should yield different results per call, add the respective demands in sequence */ stub.demand.getFirst { 'dummy' } stub.demand.getFirst { 'ddddd' } /** * Provide a range to specify how often the demanded closure should apply; the default is (1..1) */ stub.demand.getFirst(0..35) { 'dummy' } /** * It is possible to react to the method arguments that CUT passes */ stub.demand.methodOne { number -> ( assert 0 == number % 2 ) return 1 }
Mocks
- Strict expectation of a mock verifies that all the demanded method calls happen in exactly the sequence of the specification.
- The first method call that breaks this sequence causes the test to fail immediately.
- There is no need to explicitly call the verify method, because that happens by default when the
use{ .. }closure ends. - What gets asserted is whether the CUT follows a specified protocol when talking with the outside world. A protocol defines the rules that the CUT has to obey when calling the collaborator.
-
groovy.mock.interceptor.MockFor
def relay(request) { def farm = new SortableFarm() farm.sort() farm.getMachines()[0].send(request) } def farmMock = new MockFor(SortableFarm) farmMock.demand.sort(){} farmMock.demand.getMachines { [new Expando( send: {} )] } farmMock.use { relay(null) }
Exceptions Handling
try { throw new Exception("bang!") } catch (any) { println "caught" }
Groovy Truth
Boolean
| Corresponding Boolean value is true |
Matcher
| The matcher has a match |
Collection
| The collection is non-empty |
Map
| The map is non-empty |
String, GString
| The string is non-empty |
Number, Character
| The value is nonzero |
| None of the above | Object.asBoolean() - object reference is non-null
|
Closures
Closure c = Collections.&binarySearch
DSL
- Implementing Groovy Domain-Specific Languages
- Practical Groovy Domain-Specific Languages
-
pinboard.in/u:evgenyg/t:groovy dsl - Amazon: "Groovy for Domain-Specific Languages"
