Custom task with fields - assign directly or via 'conventionMapping'?

I have a build file with a simple custom task like this:

class MyTask extends DefaultTask {
  def foo
  def bar
  @TaskAction
  def doIt() {
    println "foo: $foo, getFoo(): ${getFoo()}"
    println "bar: $bar, getBar(): ${getBar()}"
  }
}
  def task = project.tasks.add('abc', MyTask)
//println task.class
  task.foo = 'foo'
task.conventionMapping.bar = { 'bar' }

When I execute the build with ‘gradle abc’ I get the following output:

foo: foo, getFoo(): foo bar: null, getBar(): bar

I have a few questions about this:

  1. what is the difference between setting a field directly for a task and doing it via this conventionMapping? 2. where does this ‘conventionMapping’ come from? I can’t find it in the documentation; is this the same as Task.convention? 3. why does the conventionMapping only accept closures (or convention values), not simple values? 4. why do ‘bar’ and ‘getBar’ give different results? are these different things?

I guess this has something to do with lazy evaluation and so on, but I am not really sure how this works. When would I prefer using direct task field assignment, and when via conventionMapping?

wujek

I haven’t tracked down where convention mappings come from either, but from my experience using them here’s make take on the answers:

  1. Convention mappings are evaluated each time you call the getter for the property (unless the real property has a value). This gives you the lazy evaluation you mention. 2. No idea… 3. There’s no benefit to using the convention mapping for simple values.

  2. Calling ‘bar’ gives you the value of the instance variable. Groovy can’t (as far as I know) intercept that type of reference. The ‘getBar()’ call goes through the logic of returning the instance variable (if set) otherwise it evaluates the convention mapping.

My rule of thumb on convention mapping vs direct value is:

  1. If it’s a literal value (boolean, int, String) I’ll set it directly on the task. If it should be the task’s default I prefer initializing it that way in the Task class. 2. If it’s just a new Object, same rules apply as for 1. 3. If the object being returned depends on or is a property of something else (e.g. using the archive name off a JAR task or a property from the extension) I use convention mapping.

The benefit here is that if the object being referenced by that property changes (via assignment), the Closure will stay with the property rather than direct assignment staying with the original object being referenced.

ext.printText = 'This is the old text'
  task directAssignment(type: PrintTask) {
  text = printText
}
  task conventionMapping(type: PrintTask) {
  conventionMapping.text = { printText }
}
  printText = 'This is the new text'
  class PrintTask extends DefaultTask {
  String text
    @TaskAction
  void print() { println getText() }
}

Calling ‘gradle directAssignment conventionMapping’ should print this:

:directAssignment
This is the old text.
:conventionMapping
This is the new text.

Thanks for the write up Andrew, some good points in there.

The reason that you can’t find convention mapping in the documentation is that it’s an internal feature. It, or something like it, will be made public at some point.

Gradle uses runtime class decoration to add the convention mapping ability to most objects (e.g. tasks, configurations, dependencies, extensions, project).

Some clarifications:

My rule of thumb on convention mapping vs direct value is: > > If it’s a literal value (boolean, int, String) I’ll set it directly on the task. If it should be the task’s default I prefer initializing it that way in the Task class.

Primitive vs. complex value not really the issue. You should use a convention mapping when the value is derived based on some external value.

1 Like