Monday, August 1, 2011

Grails, and seemingly dirty command objects

I love Grails, but sometimes, the magic is a little too much.

I was trying to debug a particularly strange oddity, and as far as I could tell, Grails was reusing a dirty command object.  That's not possible, right?  Further investigation showed that no, it's not using a dirty command object, well, not really.  Its only doing what it's supposed to: dependency injection and convention over configuration.
Here is the simplest form of the problem I have been able to put together.

The controller:
def debug = { DebugCommand d ->
render new JSON(d)

The command objects: I have nested commands, with DebugCommand being the outer command (used by the controller) and DebugMapCommand, a map holding some values.  I'm using a LazyMap since that's why I used in my real-life problem.

public class DebugCommand {
    int someNum
    DebugMapCommand debugMapCommand = new DebugMapCommand()

public class DebugMapCommand {
  Map things = MapUtils.lazyMap([:], FactoryUtils.instantiateFactory(String))

What happens here is that multiple calls to the controller/action result data being accumulated in the LazyMap between calls:

nancyd $ curl 'http://localhost:8080/library/rpc/debug?someNum=5\&debugMapCommand.things\[2\]=5'

nancyd $ curl 'http://localhost:8080/library/rpc/debug?someNum=5\&debugMapCommand.things\[1\]=5'

nancyd $ curl 'http://localhost:8080/library/rpc/debug?someNum=5\&debugMapCommand.things\[elephants\]=5'

If I gave a new value for a map entry, the new value was used.

So, what's happening?  The DebugCommand's reference to the DebugMapCommand is called debugMapCommand, and Grails thinks I want a DebugMapCommand injected, so it created a singleton, and passed it to all my DebugCommand instances. Oops.

Trying to prove this wasn't too easy.  It would seem that a number of factors are necessary for this particular issue to manifest:

  1. The field name must be the same as the class name with the first letter lowercased
  2. The inner/sub command, DebugMapCommand, must be annotated with @Validateable
  3. The inner/sub command must be in a package that is searched for "validateable" classes (in Config.groovy, you have to have grails.validateable.packages = ['com.example.yourpackage', ...])

So, what's the lesson here?

Don't name your fields after their class.