I have been doing a lot of digging into Groovy and DSLs lately, getting ready for my OSCON talk next week.
The Groovy language has great built-in support for creating DSLs. Along with its dynamic nature, Groovy has the “use” keyword for working with categories, and coming in 1.1, the ExpandoMetaClass if you want to complete.
“Use”ing Categories
Categories help you do scoped “extension” to normal every day objects. An example is in order:
So, inside the use block, there is now (effectively) a new property on the Integer class, called tacos, that will return a string describing your delectable entree. This is a very powerful technique, and is very useful for developing your own expressive syntax.
One drawback to this technique is that you have to define where you are using it. Use can be extended into closures, but does not always extend into predefined methods.
-
-
}
-
-
-
// However, you can do this from Java (or Groovy):
-
-
-
// calling methods on a class:
-
class A {
-
4.tacos // has not been evaluated yet
-
}
-
}
-
-
-
// but you can do this:
-
}
Groovy + Annotations + Aspects = annotated “use” clause
I was talking about Groovy Categories with Blaine Buxton, and was telling him that I did not like having the “use” block inside my DSL code, and wanted a more transparent approach. He suggested that perhaps an Annotation and an Aspect would do what I was looking for.
With the 1.1-Beta1 Release, Groovy has built in support for Java Annotations, and since Groovy compiles down to byte code, there is implicit support for Aspects. Putting these two things together allows us to create an annotation for a more powerful use:
With a simple annotation, @Uses(category=[list of category classes]), we now can define them for any method we want, without having to put them into the method body.
Here is the Annotation code:
-
package com.nimblelogic.groovy.dsl;
-
-
import java.lang.annotation.Inherited;
-
import java.lang.annotation.Retention;
-
import java.lang.annotation.RetentionPolicy;
-
-
@Inherited
-
@Retention(RetentionPolicy.RUNTIME)
-
public @interface Uses {
-
public Class[] category();
-
}
And here is the Aspect code:
-
package com.nimblelogic.groovy.dsl;
-
-
import groovy.lang.Closure;
-
-
import java.util.ArrayList;
-
import java.util.List;
-
-
import org.aspectj.lang.JoinPoint;
-
import org.aspectj.lang.annotation.SuppressAjWarnings;
-
import org.aspectj.lang.reflect.MethodSignature;
-
import org.codehaus.groovy.runtime.GroovyCategorySupport;
-
-
public aspect UsesAspect {
-
-
pointcut allMethods(): call(@Uses * *(..));
-
-
@SuppressAjWarnings
-
List< class > classes = getCategories(thisJoinPoint);
-
-
Object result = null;
-
if ( classes.isEmpty() ) {
-
result = proceed();
-
} else {
-
result =
-
GroovyCategorySupport.use(classes,
-
new Closure(thisJoinPoint.getTarget()) {
-
@Override
-
return proceed();
-
}
-
});
-
}
-
-
return result;
-
}
-
-
@SuppressWarnings(“unchecked”)
-
private List< class > getCategories(JoinPoint thisJoinPoint) {
-
List< class > classes = new ArrayList< class >();
-
-
// get class level categories (if any)
-
Class cl = thisJoinPoint.getTarget().getClass();
-
-
addCategories((Uses) cl.getAnnotation(Uses.class), classes);
-
-
// get method level categories (if any)
-
if ( thisJoinPoint.getKind().equals(JoinPoint.METHOD_CALL)) {
-
MethodSignature sig = (MethodSignature) thisJoinPoint.getSignature();
-
addCategories((Uses) sig.getMethod().getAnnotation(Uses.class), classes);
-
}
-
-
return classes;
-
}
-
-
-
private void addCategories(Uses uses, List< class > classList) {
-
if ( uses != null ) {
-
Class[] classes = uses.category();
-
for(Class cl : classes)
-
classList.add(cl);
-
}
-
}
-
}
One thing to note when working with Annotations / Aspects in Groovy: you have to keep track of the build order. I struggled for a while to figure out why I was not seeing the annotation in my Groovy classes. It was because it had not been built yet! I ended up having to split my compilation into 4 parts:
- Build the Annotation
- Compile the Groovy code
- Compile my other Java code (just test code)
- Weave the Aspects
John Wilson | 20-Jul-07 at 3:28 am | Permalink
Seems a huge amount of trouble to avoid typing:
use(MeasurementCategory) {
…
}
Your annotation takes more keystrokes than that!
If you don’t want to put the use in your function you can just put it in your main function
static main(args) {
use(MeasurementCategory) {
new MyClass().start()
}
}
categories are inherited by methods called from within the closure.
Matt | 20-Jul-07 at 6:09 am | Permalink
Yes, it does take more code overall… there are times where I will take that hit versus readability.
I thought about putting the use code in a surrounding method, and it works ok. That is until some other client tries to call the method containing the category code without a use block… I wanted a way to guarantee that the method was inside a use block.
And, if nothing else, this can be treated as an example of how to use Groovy, annotations and aspects all together.
Andres Almiray | 20-Jul-07 at 6:27 pm | Permalink
Nice example Matt, but you also have to consider that multiple use() calls have a performance hit, specially if the code is run in more than one thread, as use() binds the new methods to a ThreadLocal variable.
If you suspect that a lot of calls to use() will be ensued the n it would be better to ‘bootstrap’ all your categories at once and enclose your code, this is the same approach groovlets have, as John previously commented too. Remember that you can declare more than one category with just one call to use() =)
John Wilson | 21-Jul-07 at 5:18 am | Permalink
Andres,
The performance hit is not in executing the use method.
The hit comes form the fact the whist you are executing the closure the way in which the MetaClass resolves calls is changed. So whist you have a category applies all your calls will be slower (this includes calls to objects which the Category has not touched).
In addition this performance hit is taken by all the threads in the JVM not just the thread which is applying the Category.
The current implementation makes the method calls about three times slower when a Category is applied.
There is no intrinsic reason why the run time system should impose this overhead and I would expect future versions of Groovy to reduce this overhead to virtually nothing (I have written a working MetaClass implementation for another dynamic language for the JVM which supports Categories and imposes a call overhead of less than 1% and that overhead is only incurred by the thread which uses the Category).