Let your Ant enjoy Spring

Extend Ant using a lightweight Inversion of Control container

I needed to add a new task to an Ant-driven build and I implemented that task using Spring, a lightweight IoC (Inversion of Control) framework. I encountered virtually no issues since, because an IoC container is noninvasive, it is relatively easy to create a wrapper or just use the objects that implement the task. I then began to wonder if Ant could directly use Spring-configured objects, thus reusing the dependency graph and configuration already specified and tested. Why duplicate and introduce side effects or other problems? If IoC containers really do provide a benefit, a more direct use is warranted.

This article introduces this approach and presents a proof-of-concept implementation. Developers new to Ant extension coding should find this example interesting.

Ant extensions

To add a custom task to Ant, the Ant manual recommends using an Ant class intended for this purpose, like the Task class. But, even this recommendation is not mandatory; Ant can invoke any class that has an execute() method. (Of course, Ant can also invoke any program using the exec or java task, but that is a different type of extension.) Ant also supports aggregating these task extensions into various kinds of properties or XML files.

The best approach for adding a custom task to Ant is one that reuses the IoC framework via a Task extension. Thus, a Task that performs what a standalone application must do to set up and use the framework's hosted objects and resources builds upon the strength of Ant.

Inversion of control

The Inversion of Control (IoC) design pattern, also called Dependency Injection (DI), in the framework context, is concerned with the componentization of Java objects. The growing interest in IoC frameworks is due in large part to the Spring framework's developers showing the synergy available in an IoC/AOP/XML/JavaBeans lightweight framework, which offers capabilities beyond DI by allowing the creation of a powerful abstraction layer to other APIs or components. Spring is itself an example of the use of IoC. Also note that Ant seems conformant with this IoC container since it too is XML/JavaBeans-based and, in some ways, already uses IoC.

Requirements

Our Ant IoC task extension requirements can be specified in an actor/goal/requirements format (the requirements are not ordered):

  • Actor: Developer
  • Goal: Modify the IoC task
  • Requirements:
    • Run a regression test following any code changes or build
    • Easily add new tests to the regression set
    • Support a different IoC framework
    • Debug by changing Ant log level and/or IoC log configuration, and getting useful output
  • Actor: Build creator
  • Goal: Edit an Ant target and use the task to specify beans that are in or out of an IoC container
  • Requirements:
    • Set the IoC descriptor path
    • Optionally, if no container is required, specify FQCN (fully qualified class name) as target
    • If using IoC, set the POJO (plain old Java object) bean name, default is antBean
    • Specify the target method name, default is execute
    • Specify a method call expression that may have arguments
    • Specify properties to insert into the target bean, overriding container properties
    • Specify element text to push to target
    • No need to specify new classes to handle the Ant/IoC combination
    • Reuse existing property files for variable expansion
  • Actor: Task extension object
  • Goal: Invoke an object method
  • Requirements:
    • Invoke a POJO specified in the IoC bean definition
    • Invoke a specified class outside a container
    • Use default bean name antBean if none specified
    • Invoke a simple method, default is execute()
    • Invoke a method expression with optional arguments
    • If target is Ant-aware, insert project
    • Insert dynamic properties

Task

The task specification that supports these requirements is SpringContextTask.

Description

This task invokes a method on an object either managed by a Spring container or unmanaged and specified as a FQCN. Classpath Spring bean definition references are not supported yet.

SpringContextTask's parameters are shown in the table below.

Parameters

Attribute DescriptionRequired
beanLocations Path to IoC descriptorsOptional, or classname, not both.
className FQCN of target classOptional, or beanLocations.
beanName The bean id in the IoCOptional. Default: antBean.
methodName Method to invoke on beanOptional. Default: execute.
call Method call expressionOptional, or methodCall, not both.
   
*Dynamic attributes0 or more.
methodCall  Optional. Expression. A nested element. Alternative to the call attribute.

Examples

The simplest example of applying our new Ant task extension is:

 

<!-- create the task definition --> <taskdef name="runBean" classpathref="testpath" classname="jbetancourt.ant.task.spring.SpringContextTask"/>

<target name="simpleAppContextUseWithDefaults"> <runBean beanLocations="applicationContext.xml"></runBean> </target>

The simpleAppContextUseWithDefaults target invokes execute() on the bean named antBean in the bean definition applicationContext.xml found on the file path. The path attribute name is plural to support future use of multiple bean definition files.

This bean invocation resembles how Ant already invokes an object; however, this time, an IoC container manages the bean. The container could have added transaction dependencies, wired in datastores, set up a Web services proxy, used remoting, or even supplied an AOP (aspect-oriented programming) proxy instead of an actual target bean. Our approach simplifies configuration since the Ant script does not need to know how to configure an object, especially a complex one. But, what if the Ant script must set some required properties for a service call on a bean:

  <target name="publish">
      <spring 
         beanLocations="applicationContext.xml"
         beanName="siteGenerator"            
         methodName="generateSite"
         host="${host.site.url}" 
         port="${site.port}">
         Made a few tweaks.  Removed some sentence fragments.           
      </spring>                    
   </target> 

Note that since the task name is defined in a taskdef, the name used depends on the Ant taskdef definition (not shown); here, the task name is spring. Now we specify the bean name and a method to call. The element text is also pushed to the target bean. In this example, the text is a publish comment.

With the use of Ant's dynamic attributes feature, we also push required properties to the target object. Usually when an attribute is parsed in an Ant file, a setter is called on the task class. With dynamic attributes, a non-object property or field is added to the object with the setDynamicAttribute() method. In essence, this property injection provides an override capability, since the container could have already wired in the properties in the hosted bean. But, won't this complicate configuration? Will we have to maintain properties for use by Ant tasks and also properties needed by the managed objects?

Not necessarily; in Spring usage for example, the same property files used for Ant can also be used by Spring—even Ant's placeholder syntax (${...}) is used. Spring has classes for this purpose, such as the PropertyPlaceHolderConfigurer. Hence, this approach will not introduce new configuration nightmares. See the sidebar "A Property of Properties" for more on this topic.

An alternative to pushing these properties by use of attributes is to call the target method with runtime arguments by using a call attribute or a nested methodCall element, the contents of which are Java expressions. The element is easier to use since XML-required noise, such as the entity escapes, can be avoided using CDATA:

 call="generateSite(&quot;${host.site.url}&quot;,&quot;${site.port}&quot;)" 

Or better:

 <methodCall><![CDATA[    generateSite("${host.site.url}","${site.port}")   ]]></methodCall> 

So the previous example could be written as:

  <target name="publish">
      <spring beanLocations="applicationContext.xml" beanName="siteGenerator">
         <methodCall>  generateSite("${host.site.url}","${site.port}")  </methodCall>
         Made a few tweaks.  Removed some sentence fragments.   
      </spring>                    
   </target> 

Of course, the target object must have the required method and argument signature.

The above examples simply introduce the SpringContextTask approach. Perhaps they can lead to alternatives or better implementations.

So one may question some of this Task extension's features, such as the ability to call any method. This ability could even be removed, since any target bean that does not have an execute() method can be wrapped by one that does, a task even easier to complete within an IoC framework. But since supporting method expressions via OGNL (Object Graph Navigation Language, detailed later) is easy enough, method argument support is not an issue.

Interestingly, since any method can be called, the same object is reusable in the same build file to provide different services, thereby reducing excess Ant script clutter if each task invocation requires many attributes. This functionality would prove practical if the task instance could be referenced by an ID reference. Then we could write something like:

 

<spring id="metrics" beanLocations="metricsContext.xml" beanName="main" exampleAttribute="a value" and so forth . . ./>

<target name="ComputeMetrics"> <spring refid="metrics" call="computeNCSS"/> <spring refid="metrics" call="computeCCM"/> <spring refid="metrics" call="findBugs"/> </target>

<target name="genDocs"> <!- here are calls to other types of docs '/> <!- now call the metric docs '/> <spring refid="metrics" call="createDocs"/> </target>

Now we have a more readable format and more information hiding. We don't care what is in the container, just that there is only one entry point, main. That bean could be an actual bean that does it all or, via dependency injection, delegate to other tools such as PMD, JavaNCSS, or FindBugs.

I chose not to develop the approach of reusing a SpringContextTask by a reference ID. Another way to accomplish this reuse is just to use different beans in the context, such as:

 <target name="ComputeMetrics">
   <spring beanLocations="metricsContext.xml" beanName="computeNCSS"/>
   <spring beanLocations="metricsContext.xml" beanName="computeCCM"/>
   <spring beanLocations="metricsContext.xml" beanName="findBugs"/>
</target> 

But each bean in this example must have an execute() method that kicks off the service. And, of course, each bean could actually just be referring to the same class or object.

Now that the requirements are known and use cases specified, let's look at the prototype details.

Implementation

The implementation is simple and does not address many details, such as tapping into the Ant I/O subsystem or reusing the IoC container for multiple task invocations.

The UML diagram is shown below.

Figure 1. UML class diagram. Click on thumbnail to view full-sized image.

Listing 1 shows the abstract superclass of the Task extension. At runtime, Ant calls the execute() method. If a beanLocations attribute is specified in the task, an IoC application context is created (if not set) and the bean is accessed. Otherwise, if a class is specified, normal object instantiation is performed.

Next the Project, properties, and text are inserted into the object, depending on the presence of an appropriate setter method and whether the object is Ant-aware. So, in the non-IoC case, this task is a form of setter injection.

OGNL

When I implemented my task extension, the support for runtime arguments to the method call was problematic and, though not critical, a design challenge. One design called for adding an XML fragment to the task that would contain the arguments. Ant supports dynamic elements and a few examples of an XML fragment being used for this task are available on the Web. But this approach seemed overly complex and error prone.

Remembering prior research I did on OGNL, I decided to use it for this purpose. According to the OGNL Website, OGNL is "... Object-Graph Navigation Language; it is an expression language for getting and setting properties of Java objects. You use the same expression for both getting and setting the value of a property." One great feature of OGNL is support for method calls, which I required.

OGNL supports a rich expression language. For example, the code below evaluates an indexed expression on the target object as part of the call:

  <methodCall>  <![CDATA[    notifyDeveloper(names(${dev}) ]]>       </methodCall>   

Another example of OGNL expression evaluation is found in one of the included unit tests, where a method-call expression used is: convertToString(employees[getNum()]). Though OGNL solved the problem I encountered and expressions offer a powerful new Ant capability, the use of attributes to define properties is the recommended approach and is compatible with the IoC pattern.

To show how the abstract class is used, the next section uses Spring to implement the actual Task extension.

1 2 3 Page 1