|
用ANT来享受SPRING 通过轻量级的IoC容器来扩展ANT 作者:Josef Betancourt 2005年2月14日 翻译:xMatrix
版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明 作者: Josef Betancourt ;xMatrix 原文地址: http://www.javaworld.com/javaworld/jw-02-2005/jw-0214-antspring.html 中文地址: http://www.matrix.org.cn/resource/article/43/43845_Ant_Spring.html 关键词: Ant Spring
摘要 这篇文章介绍了通过ANT任务的扩展来实现IoC管理对象或非管理对象的执行。同时也介绍了OGNL(对象图形导航语言)如何被用来使ANT执行任何方法表达式,包括带有运行时参数的。也介绍了如何使用JUNIT来测试ANT的扩展。此外,还包含一个使用SPRING框架的实现。Ant-IoC的组合为创建松耦合的软件开发支持任务开创了新的天地。
我需要增加一个基于ANT驱动的新任务,并且我用SPRING(一个轻量级的IoC框架)来实现这个任务。我几乎没有碰到什么问题,因为IoC容器是非侵入式的,这很容易创建一个包装或者直接使用对象来实现任务。于是我开始想知道是否ANT可以直接使用SPRING配置的对象,然后重用已经定义和测试的依赖图和配置。那为什么还要重复和引入波纹效应或其他问题呢?如果IoC容器果真能提供这样的便利,就可以保证更直接的使用。
这篇文章介绍了这种方法并且演示了一种概念上的实现。刚接触ANT扩展的开发者会发现这个例子十分有趣。
ANT扩展 为了给ANT增加自定义任务,ANT手册建议使用为这个目的而提供的类,如Task类。但是这个建议不是强制的,ANT可以执行任何拥有execute()方法的类(当然ANT也可以通过使用exec或java任务来执行任何程序,但那是另一种扩展方式)。ANT也支持集成这些任务扩展到各种类型的属性或XML文件中。
给ANT增加一个自定义任务的最佳方法是通过Task扩展来重用IoC框架。因此,执行独立应用的Task必须设置和使用建立在ANT基础上的框架内置的对象和资源。
控制反转 IoC设计模式,也称作DI(依赖注射)。在框架的上下文中,这与JAVA对象的组成有关。在IoC框架上增加的投资很大一部分是由于SPRING框架的开发人员演示了在一个IoC/AOP/XML/JavaBeans轻量框架中的协同作用,而这正是通过允许为其他API或组件创建强大的抽象层来提供超越DI能力的原因。SPRING本身就是一个使用IoC的例子。ANT看起来与IoC容器相适应,因为他也是基于XML或者JavaBean的,从某方面来说,他也使用了IoC。
需求 我们的ANT IoC任务扩展需求可以通过角色/目标/需求的格式来定义(这里的需求不分顺序):
●角色:开发人员 ●目标:修改IoC任务 ●需求: 在任何代码改变或构建后执行回归测试 很容易在回归测试中增加新的测试用例 支持不同的IoC框架 通过修改ANT日志的级别或IoC日志的配置使调试时可以得到更有效的输出
●角色:构建创建人 ●目标:编辑ANT目标并使用任务来定义IoC容器的输入或输出Bean ●需求: 设置IoC描述符的位置 在不需要容器时,定义FQCN(完全限定类名)作为目标 使用IoC时,设置POJO(普通JAVA对象)Bean名,缺省为antBean 定义目标方法名,缺省为execute 定义一个调用可以带参数的表达式的方法 定义可以插入目标Bean的属性,用来复写容器属性 定义目标的元素文本 没有必要定义用来处理Ant/IoC组合的新类 为了各种扩展需要重用现存的属性文件
●角色:任务扩展对象 ●目标:执行对象方法 ●需求: 执行在IoC Bean定义中定义的POJO 执行容器外的定义类 如果没有定义使用缺省的Bean名antBean 执行简单的方法,缺省为execute() 执行带可选参数的方法表达式 如果目标是ANT相关的则插入工程 插入动态属性
任务 支持这些需求的任务定义是SpringContextTask
描述 这个任务执行由SPRING容器管理的或者是未管理的FQCN的对象的方法。目前还不支持SRPING Bean定义引用的Classpath。 SpringContextTask的参数如下表所示:

例子 最简单的应用我们的ANT任务扩展的例子如下:
<!-- create the task definition --> <taskdef name="runBean" classpathref="testpath" classname="jbetancourt.ant.task.spring.SpringContextTask"/>
<target name="simpleAppContextUseWithDefaults"> <runBean beanLocations="applicationContext.xml"></runBean> </target>
simpleAppContextUseWithDefaults目标执行在文件路径中找到的Bean定义文件applicationContext.xml中的Bean名为antBean的execute()方法。路径属性名是复数的以便将来支持多个Bean定义文件。
Bean的执行类似ANT执行对象的方法;然而,这里是IoC容器来管理Bean。容器可以增加事务依赖,包装数据库,设置网络服务代理,使用远程甚至提供AOP代理来代替实际目标Bean。我们的方法简化了配置,因为ANT脚本不再需要知道如何配置对象,特别是复杂的对象。但是如果ANT脚本确实需要为服务调用设置特定的属性时会怎么样呢:
<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>
注意因为任务名已经在taskdef中定义了,使用的名字将依赖于ANT的taskdef定义。这儿任务名是spring。现在我们定义Bean名字和调用的方法。元素文本也会被放到目标Bena中。在这个例子中,文本是一个发布的注释。
通过使用ANT的动态属性功能,我们也可以将需要的属性放到目标对象中。通常在ANT文件中一个属性被解析时,对应的set方法会被调用。使用动态属性,非对象属性或字段会通过setDynamicAttribute()方法被增加到对象中。通常因为容器已经包装了其中的Bean的属性,这种属性注入提供了一种重写的能力。但是,是否这样会将配置复杂化?我们将不得不维护ANT任务使用的属性及管理对象所需要的属性。
当然这不是必须的;如例子中的SPRING用法,相同的属性文件被ANT和SPRING同时使用— 即使使用了ANT的占位符语法(${...})。SPRING提供了这种目的的类,如PropertyPlaceHolderConfigurer。因此,这种方法不会引入新的配置恶梦。可参考旁注“属性中的属性”获得更多的帮助。
另一种放置属性的方法是通过使用call属性来调用带运行时参数的目标方法或者嵌套的methodCall元素,他的内容是java表达式。这个元素很容易使用因为XML需要的符号如实体转义符可以用CDATA来避免:
call="generateSite("${host.site.url}","${site.port}")" Or better: <methodCall><![CDATA[ generateSite("${host.site.url}","${site.port}") ]]></methodCall>
因此先前的例子可以如下写法:
<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>
当然,目标对象必须包含需要的方法和参数标识符。
上面的例子简单介绍了SpringContextTask方法。可能他们可以有其他或更好的实现。 有人可能会对这个Task扩展的特性有疑问,如调用任何方法的功能。这个功能甚至可以被移除,因为任何不包含execute()方法的目标Bean可以被包装,一个任务在IoC框架中可能更容易完成。但既然通过OGNL(后面会讨论)支持方法表达式很容易,那么方法参数的支持也不是个问题了。 有趣的是,既然任何方法可以被调用,那么同一对象可以在同一个构建文件中被重用来提供不同的服务,这样就可以在执行需要很多属性的任务中减少过度的ANT脚本混乱了。如果任务实例可以通过ID来引用的话这个功能就会有实际意义了。我们可以象下面这样写:
<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>
现在我们拥有更易读的格式而隐藏了更多的信息。我们不再关心容器中有什么,只要那儿有一个入口点—main.那个Bean可以是实际的Bean或者通过依赖注射代理给其他工具如PMD, JavaNCSS, 或者FindBugs。
我没有选择通过ID引用重用SpringContextTask的开发方式。另一种完成重用的方式是在上下文中使用不同的Bean,如:
<target name="ComputeMetrics"> <spring beanLocations="metricsContext.xml" beanName="computeNCSS"/> <spring beanLocations="metricsContext.xml" beanName="computeCCM"/> <spring beanLocations="metricsContext.xml" beanName="findBugs"/> </target>
但在这个例子中的每一个Bean必须有一个execute()方法来启动服务。而且每一个Bean实际上只是引用同样的类或对象。 现在需求已经确定而且用例也定义好了,那就让我们来看看细节吧。
实现 这是一个简单实现,并没有提供什么技巧,如ANT的IO子系统或者重用IoC容器来执行多任务。 UML图显示如下:
 图1. UML 类图. 点击查看大图
列表1 显示了Task扩展的抽象父类。在运行时,ANT调用xecute()。如果任务中的beanLocations属性被定义,一个IoC应用上下文被创建并且Bean变成可访问的了。如果一个类被定义则执行普通的对象实例化。
下面是Project属性,文本被插入到对象中,只依赖是否存在相应的设置方法或者对象是否是ANT相关的。因此在非IoC的情况下,这个任务是一种设置注射的方式。
OGNL 当我实现我的任务扩展时,对于调用运行时参数的方法的支持还是一个问题,虽然不是很严重,但这是一个设计上的机会。一个方法是调用增加一个包含参数的XML段给任务。ANT支持动态元素,网上也有一些给这种任务使用的XML段的例子。但这种方式看起来过于复杂而且容易出错。
想起我先前在OGNL上的研究,我决定在这儿使用他。根据OGNL网站所说,ONGL是“对象图形导航语言;他是一种表达式语言用来获取和设置JAVA对象的属性。你可以使用相同的表达式获取和设置属性的值”。OGNL的一个优秀的特点是支持方法调用,这正是我所需要的。
OGNL支持很多表达式语言。例如,下面的代码计算一个目标对象的索引表达式作为调用的一部分:
<methodCall> <![CDATA[ notifyDeveloper(names(${dev}) ]]> </methodCall>
另一个OGNL表达式计算的例子可以在包含的单元测试中找到,那里一个方法调用表达式被如下使用: convertToString(employees[getNum()]).
虽然OGNL解决了我遇到的问题并且表达式提供了一种新的强大的ANT功能,但是定义属性的使用才是推荐的方法而且与IoC模式兼容。 为了演示抽象类如何使用,下一节我们会用SPRING来实现一个实际的Task扩展。
SPRING支持
列表2显示了SpringContextTask。这个类是如何定位特定IoC容器的例子。模板方法模式使这个类简单化;所有调整实际目标对象来支持需求的细节都被放在了父类中。如何找到容器在使用任何IoC容器中都是必要的,从容器中得到对象,并且给属性赋值。
列表 2. SpringContextTask类
/* * 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); } } }
注意现在的SPRING实现是非常直接的。ANT的两个特性保证了这种简单性。首先,任何异常都被ANT的非强制异常BuildException所封装,这样就简化了更多的扩展。其次,使用ANT的日志系统可以隐藏记录实现的细节。
主要的复杂性在于方法insertManagedBeanProperties()。设置对象的属性是直接的,然而在容器框架中设置属性可能是很关键的。容器的语法必须被遵守:容器可能拥有或需要作用于属性设置操作的功能,如事件处理,自动绑定及转换。
AOP实现
既然大多数IoC框架都支持AOP,另一个可选的方法是利用IoC容器中的AOP使他更加灵活。例如,在SPRING中,你可以创建一个专一的ANT工厂Bean来提供在ANT属性中的绑定并且满足其他需求。但我没有在我的任务中使用他。
测试
ANT提供对使用JUnit Test子类的单元测试的支持,这使得Task的测试更易管理。这篇文章对应的源程序中包含这些测试。当他运行时(用Maven或Ant),部分输出如下:
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
SpringContextTaskTest类已经有ANT风格的任务测试。在这些测试中,JUnit子类的测试通过executeTarget()或类似的方法执行ANT文件中的目标:
public void test2(){ executeTarget("test2"); } public void test3(){ expectBuildException("test3", "beanLocation or class must be specified"); }
任务测试在ANT文件SpringContextTaskTest.xml中,包含了实际的测试目标,如:
<target name="test3"> <!-- No context path or class specified; should fail --> <springTask methodName="length"/></target>
并且在我们的扩展测试中,这个目标引用在applicationContext.xml中的SPRING Bean定义。这些Bean定义由一个重用为不同的Bean的JAVA类来组成。例如:
<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>
一个有趣的测试是test18,他使用一个AOP代理返回的Bean:
<target name="test18"> <springTask beanLocations="etc/contexts/applicationContext.xml" beanName="postBugReport" methodName="doService"></springTask> </target> Bean定义如下: <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>
SecurityInterceptor可以是一个用于编译的安全advise,如登录过滤器(类似于servlet过滤器)。postBugReport目标与先前的antBean一致。
部署 IoC目标Bean及定义文件或者类可以通过发布的JAR文件来提供。我们可以在使用SRPING IoC时称这些为SPRING件。(如图2)
 Figure 2. Springlet的用法。
使用有层次的外部文件可以重写在部署文档中的缺省值。因此用户只需要编辑外部的属性文件及任何子Bean定义文件来自定义缺省值。外部文件可以定义其他协作的IoC JAR。
总结 在这篇文章中,我介绍了一种ANT扩展来执行IoC容器管理的对象,从而增加程序的松耦合度。相同的任务也可以执行在非管理对象的方法上。通过使用OGNL,可以通过JAVA表达式来执行方法,而不需要更复杂的XML定义。这里并没有讨论优化和利用ANT及SPRING的高级特性。 虽然这里是通过ANT扩展来表现这个观点,但同样可以用Maven或其他构建系统来实现。同样的,这里使用的SRPING也可以用其他IoC框架来代替,如HiveMind—需要ANT任务必须定义一个模块发布描述文档,或者PicoContainer—虽然他更偏好通过编程来配置。
关于作者 Josef Betancourt,生活在罗得岛的一个高级软件工程师。
资源 ●下载与这篇文章对应的源程序: http://www.javaworld.com/javaworld/jw-02-2005/antspring/jw-0214-antspring.zip ●OGNL的好的介绍:“OGNL表达式及绑定语言”,Drew Davidson (2004年3月) http://www.ognl.org/resources/OGNL_Presentation_20040309.pdf ●为什么及如何使用轻量级容器:精通不使用EJB的J2EE开, Rod Johnson, Juergen Hoeller (Wrox, 2004年6月; ISBN: 0764558315) http://www.amazon.com/exec/obidos/ASIN/0764558315/javaworld ●“IoC简介,Sam Newman (java.net, 2004年2月) http://today.java.net/pub/a/today/2004/02/10/ioc.html ●IoC分析:“控制反转容器和依赖注射模式”,Martin Fowler (martinfowler.com): http://martinfowler.com/articles/injection.html ●IoC对设计的影响:“依赖注射和开放闭合设计” http://jroller.com/page/rickard/20040814#dependency_injection_and_open_vs ●Spring主页 http://www.springframework.org ●Ant主页 http://ant.apache.org/ ●OGNL主页 http://www.ognl.org/ ●HiveMind主页 http://jakarta.apache.org/hivemind/index.html ●PicoContainer主页 http://picocontainer.org/ ●PMD主页 http://pmd.sourceforge.net/ ●JavaNCSS主页 http://www.kclee.de/clemens/java/javancss/ ●FindBugs主页 http://findbugs.sourceforge.net/ ●Maven主页 http://maven.apache.org/ ●更多关于SPRING的知识,可阅读“Pro Spring: Spring and EJB”,(JavaWorld, 2005年2月): http://www.javaworld.com/javaworld/jw-02-2005/jw-0214-springejb.html ●学习如何使用Spring和JSF,可阅读“让JSF开始工作”,Derek Yang Shen (JavaWorld, 2004年7月) http://www.javaworld.com/javaworld/jw-07-2004/jw-0719-jsf.html ●更多关于设计模式的知识,可浏览设计JavaWorld的主题索引的模式部分 http://www.javaworld.com/channel_content/jw-patterns-index.shtml ●更多JAVA开发工具的文章,可以游览JavaWorld的主题索引的开发工具部分 http://www.javaworld.com/channel_content/jw-tools-index.shtml
|