Groovy DSL Tricks

I’ve recently needed to support creating a Groovy DSL that can be called from Java, to allow scripting of some server activities.  The aim was to load a file, script.groovy, and run it so that it uses the server’s API.

 

Researching the subject, most articles discuss how to create nicer looking syntax when working from within Groovy. Things like being able to write 5.euro. But in our scenario there were other goals:

  • Efficiency: on the one hand being able to change the script while the server is running. On the other, not paying the parsing & compilation overhead for each call
  • Cleanliness: because the script provides lean customization for the server, we didn’t want it to be cluttered with “mechanical issues” like import statements, requiring to use ‘use’ or mixin statements etc.
  • Extensability: easily add methods to the DSL as well as allow the script to be broken to small parts

 

Here I will describe some of the techniques I’ve used to achieve these goals

DSL Class

When creating a DSL, there are functions/methods that form the DSL. A question is where to define them. All the articles I could find suggested either

  • Using the builder mechanism - where a method just creates a node with that name in the builder, and the structure created is post processed
  • Putting methods in the meta class

 

The former approach requires post processing and limits what can be written in the script, the latter means  one need to load the script through a wrapper that will put the methods in the meta class before running the script. It is also tricky to do this from Java/Scala.

 

The approach I used is to use an undocumented feature of how Groovy loads scripts.

 

Before going into details, here is how Groovy loads scripts: it reads the script file and then compiles the code to create a JVM class that extends from class Script. It then creates an instance of the class and calls the #run method.

 

The feature I used is that Groovy allows to create a class so that it extends a custom class. Then that class can contain all our DSL methods (and variables).

 

def create(root: String, parentClass: Class[_]) = {

    val config = new CompilerConfiguration()

 

    config.setScriptBaseClass(parentClass.getCanonicalName)

 

    val pcl = new GroovyClassLoader(parentClass.getClassLoader, config, false)

 

    new GroovyScriptEngine(root, pcl)

 

}

 

val dsl = create(root, classOf[DslClass]).createScript(name + ".groovy", binding).asInstanceOf[DslClass]

// dsl.afterLoad // if there's some method to call before running

dsl.run

// dsl.afterRun // if there's some post processing

 

(The methods #afterLoad and #afterRun seen above should be defined in DslClass)

 

There’s another alternative: if the bindings of the script contain a Closure, that closure is treated as a method in the script. One can create a subclass of Closure, overriding the methods #call or #call(Object[]).

 

Performance

Unfortunately, GroovyScriptEngine takes some time to create, therefore it is best to cache instances based on the root path and parent class.

 

Another thing is that in the #create method above we can set a target folder for the compiled files so they are not just in-memory. This is done with:

    config.setTargetDirectory(new File(root, "target").getPath)

 

Modularity

When using scripts for non-trivial code it is nice to allow to separate them. This can be done in Groovy, but only if the separated scripts are treated as full fledged classes. So in root.groovy one can call ‘import another’, and have class ‘another’ created from another.groovy. Then create an instance with ‘new another()’  and use its methods. This is fine of course, but not very DSLy.

 

What I wanted is for loading of other scripts to behave as the loading of the “root” script: run some code. Another thing was to allow them to modify the original script, to call methods, set variables etc.

 

The approach is simple. In the DSL, add a #load(name: String) method that creates a script using GroovyScriptEngine, passing it the root in the binding and runs it. In the loaded script, it can use ‘root’ to call DSL methods, set variables and create new methods.

 

def load(path: String) = {

    val binding = new Binding(getBinding.getVariables)

    binding.setVariable("root", this)

    gse.run(path + “.groovy”, binding)

 }

 

(where ‘gse’ is a lazy val that creates a new GroovyScriptEngine, or better yet, gets one from a cache)

 

Use as:

 

// in root.groovy

load("subdir/another")

 

// in subdir/another.groovy

root.someMethod()

root.newVariable = 1

def newMethod() {

       // blah blah

}

root.newMethod = this.&newMethod

 

Enhanced Objects

One of the issues I’ve encountered was that I wanted to add some methods to existing classes to make it easy to use them in the script. The best way is using DefaultGroovyMethods to mixin a class that adds them. My specific use case, was using XML DOM objects as ‘dom.someTag.text()’

 

The code:

 

Array(classOf[Node], classOf[NodeList], classOf[NamedNodeMap]/*, classOf[DOMCategory.NodesHolder]*/).foreach {cls =>

    DefaultGroovyMethods.mixin(cls, classOf[DOMCategory])

 }

 

This code needs to be run once.

 

You can see that I’m using DOMCategory. Which begs the question of why not use the Groovy ‘use’ mechanism. This can be done with:

 

   val closure = new Closure(gse) {

     override def call() = {

        val script = GroovyJobRunner.gse(root).createScript(params.jobName + ".groovy", binding).asInstanc-        returning(script.run){script.afterRun}

      }

    }

   GroovyCategorySupport.use(classOf[DOMCategory], closure)

 

This is of course more cumbersome, and also requires calling GroovyCategorySupport#use before every closure invocation. So I’m not sure how useful it is.

 

 

 

 

 

 

Thank you for your interest!

We will contact you as soon as possible.

Send us a message

Oops, something went wrong
Please try again or contact us by email at info@tikalk.com