Let your Ant enjoy Spring

Extend Ant using a lightweight Inversion of Control container

1 2 3 Page 2
Page 2 of 3

Spring support

Listing 2 shows the SpringContextTask. This class is an example of how to target a specific IoC container. The Template Method pattern allowed this class to be simple; all the details of adapting the actual target object to support the requirements are in the parent class. Essentially, using any IoC container is a matter of how to find the container, get an object from it, and insert property values.

Listing 2. SpringContextTask class

 

/* * SpringContextTask.java * Created on Jan 9, 2005 * Creator: Josef Betancourt * Project: SpringContextTask * ----------------------------------------------------------------------------- * * Copyright 2005 by Josef Betancourt * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * ----------------------------------------------------------------------------- * * */

package jbetancourt.ant.task.spring;

import java.util.Enumeration; import java.util.Properties;

import jbetancourt.ant.task.AbstractContextTask;

import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.PropertyOverrideConfigurer; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext;

/** * * Ant task extension that invokes a POJO within a Spring Application * Context, a "springdef". * * Tested with Ant 1.6.2, JDK 1.4.2, Spring 1.1.4, and OGNL 2.6.7. * * This task is added to Ant with a taskdef or one of the new Ant 1.6+ * approaches. * * * * Example taskdef: * <taskdef name="springTask" * classname="jbetancourt.SpringContextTask" * classpathref="taskdefclasspath"> * * It is then used simply by accepting the defaults as: * <target name="test1"> * <springTask beanLocations="applicationContext.xml"> * </springTask></target> * * A more complex use is: * <target name="publishBean"> * <springTask * beanLocations="applicationContext.xml" * beanName="siteGenerator"

* host="${host.site.url}" * port="${site.port}" * methodName="generateSite"> * In-line site post change text * </springTask> * </target> * * * Use of methodCall element with CDATA expression. * * <target name="test8"> * <springTask beanLocations="applicationContext.xml" * beanName="antBean1"> * <methodCall><![CDATA[execute("Goodbye")]]> * </methodCall> * </springTask> * </target> * * * * * * @author JBETANCOURT * @since Jan 9, 2005 * */ public class SpringContextTask extends AbstractContextTask {

/** runtime Spring context that that will be set or created */ private ApplicationContext applicationContext;

/** * * Get the container managed bean. * * @param beanName * @return the pojo, singleton or non-singleton. */ public Object getBeanFromContainer(String beanName) throws BuildException { try { if(applicationContext == null){ applicationContext = new FileSystemXmlApplicationContext( getBeanLocations().list()); }

return applicationContext.getBean(getBeanName()); } catch (BeansException e) { throw new BuildException("Failure: could not get bean '" + getBeanName() + "' from context",e); } } /** * Invoke target bean setters with Ant specified dynamic properties. * */ public void insertManagedBeanProperties(){ // How to programmatically post process a Spring bean? // See Spring Forum thread for source of this approach. // http://forum.springframework.org/viewtopic.php?p=11833#11833 PropertyOverrideConfigurer poc = new PropertyOverrideConfigurer(); // The keys must be of form 'beanName.key'. // Create a new Properties with this format. Properties props = addKeyPrefix(getDynamicProperties(), getBeanName());

poc.setProperties(props);

((ConfigurableApplicationContext)applicationContext). addBeanFactoryPostProcessor(poc); ((ConfigurableApplicationContext)applicationContext).refresh(); } /** * For each key in props, convert to 'beanname.key' format. * * @return with keys modified * @throws BuildException */ private Properties addKeyPrefix(Properties initProps, String prefix) throws BuildException { // TODO: This seems like a wrong approach here. // Can the passed in props be manipulated instead?

Properties props = new Properties(); for (Enumeration en = initProps.propertyNames(); en.hasMoreElements();) { String key = (String) en.nextElement(); // Let Ant resolve any property replacements. String resolvedText = getProject().replaceProperties(initProps.getProperty(key)); props.put(prefix + "." + key, resolvedText); } return props; }

/** * Get the Spring context that was created or set. * @return could be null */ public ApplicationContext getApplicationContext() { return applicationContext; }

/** * * * @param applicationContext non-null * The applicationContext to set. */ public void setApplicationContext(ApplicationContext applicationContext) { log("setting applicationContext: " + applicationContext, Project.MSG_DEBUG); this.applicationContext = applicationContext; }

/* (non-Javadoc) * @see jbetancourt.ant.task.ExternalContainer#createContainer() */ public void createContainer() throws BuildException { try { if(applicationContext == null){ applicationContext = new FileSystemXmlApplicationContext( getBeanLocations().list()); } } catch (BeansException e) { throw new BuildException("Failure: could not create Spring container.",e); } } }

Note how the Spring implementation is very straightforward. Two of Ant's features allowed this simplicity. First, any exceptions are wrapped by Ant's unchecked BuildException, thereby simplifying further extensions. Second, Ant's log system is being used, hiding logger implementation details.

The main complication is the method insertManagedBeanProperties(). Setting properties on an object is straightforward, however setting the properties within the container framework may be critical. The container semantics must be followed: the container may have or require the ability to act upon property-setting actions, such as event handling, auto-wiring, and transformations.

AOP implementation

Since most IoC frameworks support AOP, an alternative or supportive approach could leverage AOP on the IoC container side to allow even more flexibility. In Spring, for example, you could create a dedicated Ant factory bean and provide means to wire in the Ant properties and meet the other requirements. I did not pursue this approach for my task.

Testing

Ant provides support for unit testing with a JUnit Test subclass that makes Task testing more manageable. The source accompanying this article includes these tests. When run (using Maven or Ant), part of the output is:

 test:test:
    [junit] Running jbetancourt.ant.task.AbstractContextTaskTest
    [junit] Tests run: 4, Failures: 0, Errors: 0, Time elapsed: 1.752 sec
    [junit] Running jbetancourt.ant.task.spring.SpringContextTaskTest
    [junit] Tests run: 21, Failures: 0, Errors: 0, Time elapsed: 4.256 sec
BUILD SUCCESSFUL
Total time: 11 seconds 

The SpringContextTaskTest class has Ant-style task tests. In these tests, the JUnit subclass's tests invoke targets in an Ant file using executeTarget() or similar methods:

  public void test2(){ 
      executeTarget("test2"); 
  }       
  public void test3(){
      expectBuildException("test3", "beanLocation or class must be specified");
  }    

The task tests are in the Ant file, SpringContextTaskTest.xml, which has the actual test targets, such as:

  <target name="test3">  <!-- No context path or class specified; should fail -->
     <springTask methodName="length"/></target> 

And, in our extension testing, this target references the Spring bean definitions in applicationContext.xml. These bean definitions consist mostly of one actual Java class being reused as different beans (Very low coupling!). Some examples:

  <bean id="antBean1" class="jbetancourt.TestBean1"/>     
   <bean id="beanWithProps" class="jbetancourt.TestBean1"/>
   <bean id="main" class="org.springframework.beans.factory.
     config.MethodInvokingFactoryBean">
      <property name="targetObject"><ref local="antBean"/></property>
      <property name="targetMethod"><value>execute<
        /value></property>
   </bean>                    

An interesting test is number 18, which uses a bean returned by an AOP proxy:

  <target name="test18">
      <springTask beanLocations="etc/contexts/applicationContext.xml" 
        beanName="postBugReport" methodName="doService"></springTask> 
    </target>        

And the bean definitions are:

 

<bean id="securityInterceptor" class="jbetancourt.SecurityInterceptor"> </bean> <bean id="postBugReport" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="target"><ref local="antBean"/></property> <property name="proxyInterfaces"> <list> <value>jbetancourt.ISecurity</value> </list> </property> <property name="interceptorNames"> <list> <value>securityInterceptor</value> </list>

</property> </bean>

The

SecurityInterceptor

could be a security advise for the build, such as a login filter (analogous to a servlet filter). The

postBugReport

targets the same

antBean

used before.

Deployment

Optimally, the IoC target beans and definition files or classes (or only classes if using programmatic IoC configurations) can be supplied in a deployment JAR. We can call these, when using Spring IoC, "springlets" (see Figure 2).

Figure 2. Springlet usage. Click on thumbnail to view full-sized image.

Using a hierarchy, externals can override defaults found in the deployment archive. Thus, the user is required to only edit an external property file and any child bean definition files to customize and provide default values. Of course, the externals can specify other collaborating IoC JARs.

Conclusions

In this article, I presented an Ant extension to allow the invocation of an IoC container-managed object, thereby increasing loose coupling. Alternatively, the same task can invoke any method on a nonmanaged object. By employing OGNL, method invocation using Java expressions was initiated, bypassing the need for more complex XML declarations. Optimization and leveraging advanced Ant and Spring features were not addressed.

Though presented as an Ant extension, the idea may be applicable to Maven and other build systems. Similarly, though the Spring framework was used, the approach is applicable to the use of other IoC frameworks, such as HiveMind, where the Ant task would have to specify a module deployment descriptor, or PicoContainer, even though it seems to favor programmatic configuration.

Josef Betancourt is a senior software engineer living in Rhode Island.
1 2 3 Page 2
Page 2 of 3