【IT168 专稿】
在我们的程序中,经常会使用XML来进数据交换、保存和获取配置信息等工作。尤其是最近几年,XML的使用已经到了高峰,几乎所有的软件系统都将配置文件改成了XML格式,以适应通用的需求。
为了满足使用XML的需要,大多数程序设计语言都提供了基于DOM和SAX的两种访问XML的方式。Java作为现在最流行的程序设计语言之一当然也不例外。在Java中可以很方便地通过DOM和SAX包中的类来处理XML。但问题随之而来。由于这些API来底层。XML对于它们来说就是简单的字符串。而开发人员必须使用各种方法(如使用循环来查找某一个结点)来对付这些字符串。这就象直接调用Windows API来写程序,虽然几乎拥有无限的灵活性,但也会给开发人员的工作量带来指数级的增长。
问量产生了,当然会有解决它的方法。apache组织就为我们提供了一个更完美的解决方案。它就是Digester。
为了满足使用XML的需要,大多数程序设计语言都提供了基于DOM和SAX的两种访问XML的方式。Java作为现在最流行的程序设计语言之一当然也不例外。在Java中可以很方便地通过DOM和SAX包中的类来处理XML。但问题随之而来。由于这些API来底层。XML对于它们来说就是简单的字符串。而开发人员必须使用各种方法(如使用循环来查找某一个结点)来对付这些字符串。这就象直接调用Windows API来写程序,虽然几乎拥有无限的灵活性,但也会给开发人员的工作量带来指数级的增长。
问量产生了,当然会有解决它的方法。apache组织就为我们提供了一个更完美的解决方案。它就是Digester。
一、什么是Digester
对于Apache的工程,也许每个使用Java语言的开发人员都会说出几样,如地球人都知道的Tomcat、当然还有最近火的不能再火的MVC框架Struts。哈哈,对,有人也许会抢着说,还有大名鼎鼎的Apache HTTP Server。这些都是Apache系列软件中的耀眼明星。但我们却忽略了Apache中一个很少有人问津的角落。这就是commons库。我们可以通过http://commons.apache.org来访问它。commons库有几十个子库组成。也许有人会说,这些库只是一些单一功能的jar包,没什么了不起的。但当我们看一下Apache的这些著名工程后,就会改变这种看法了。
如在Tomcat中的lib目录就有很多commons库的jar包,如digester等。我们看看其他的工程,如Struts也是如此。因此,我们可以得出一个结论。commons库就是组成apache工程的基石,也可以将其称为是镶嵌在Apache王冠上的宝石。
由于本文的内容和XML有关,因此,本文将主要介绍commons库中和XML有关的digester包,如果读者对其他的commons包感兴趣,可以查看相关文档,如http://commons.apache.org。
Digester包最初是和Struts一起发布的。并随着Struts一起成长。这个包最初是为了处理struts-config.xml配置文件而设计的。但在不久就被很多人意识到这个包非常有用,于是就将其从Struts中分离出来,放到了commons库中。这个包的目标就是用最快速的方法将XML映射成Java类,并且拥有更大的灵活生。现在最新的Digester版本是1.8。本文将采用这个版本来编写程序。
Digester类可以使开发人员指定一系列的动作(如为每个XML结点建立一个对象实例等),在任何时候来分析XML时都可以执行这些动作。Digester框架由10的“规则”组成。这些规则可以满足大多数的处理XML的需求(如建立一个bean或设置bean的属性),但用户可以任意定义自己的规则来满足自己特殊的需求。
二、定义用于映射XML的Java类
在这个例子中,将通过Digester将一个XML文档映射成响应的Java Beans类。这个XML文档如下:
下面让我们来定义这个XML要映射成的Java类。首先让我们从XML的根结点开始。虽然Digester并没有限制映射类的名字,但习惯上把映射类名和XML的结点名一致。而且这些类需要被声明为public。因此,根据上述规则,对应于整个XML的类是Catalog。在根结点下,有两个相同的结点:book和magazine。这两个结点可作为addBook和addMagazine方法将这两个结点的子信息加到Catalog类中。并为这两个子结点编写两个类Book和Magazine。Catalog类的代码如下:<?xml version="1.0"?> <catalog library="somewhere"> <book> <author>Author 1</author> <title>Title 1</title> </book> <book> <author>Author 2</author> <title>His One Book</title> </book> <magazine> <name>Mag Title 1</name> <article page="5"> <headline>Some Headline</headline> </article> <article page="9"> <headline>Another Headline</headline> </article> </magazine> <book> <author>Author 2</author> <title>His Other Book</title> </book> <magazine> <name>Mag Title 2</name> <article page="17"> <headline>Second Headline</headline> </article> </magazine> </catalog>
在上面的Catalog类中加入了一个toString方法,用于将Book和Magazine的信息显示出来。下面我们来设计Book和Magazine类。我们从book结点可以看出,在book结点中只包含了两个子结点:author和title。而且这两个结点是最终的结点,也就是说它们除了文本信息,并不包含其他的子结点。因此我们可以将title和author设为两个属性。Book类的代码如下:import java.util.Vector; public class Catalog { private Vector books; private Vector magazines; public Catalog() { books = new Vector(); magazines = new Vector(); } public void addBook( Book rhs ) { books.addElement( rhs ); } public void addMagazine( Magazine rhs ) { magazines.addElement( rhs ); } public String toString() { String newline = System.getProperty( "line.separator" ); StringBuffer buf = new StringBuffer(); buf.append( "--- Books ---" ).append( newline ); for( int i=0; i<books.size(); i++ ){ buf.append( books.elementAt(i) ).append( newline ); } buf.append( "--- Magazines ---" ).append( newline ); for( int i=0; i<magazines.size(); i++ ){ buf.append( magazines.elementAt(i) ).append( newline ); } return buf.toString(); } }
public class Book { private String author; private String title; public Book() {} public void setAuthor( String rhs ) { author = rhs; } public String getAuthor{return author;} public void setTitle( String rhs ) { title = rhs; } public String getTitle{return title;} public String toString() { return "Book: Author='" + author + "' Title='" + title + "'"; } }
让我们再看看magazine结点。在magazine结点中,也有两个子结点:name和article。name结点和book结点的两个子结点一样,也没有子结点。因此,name结点可以作为Magazine类的一个属性存在。但article结点仍然有一个handline结点,因此,这个Article仍然需要以一个类存在。Magazine类的定义如下:
import java.util.Vector; public class Magazine { private String name; private Vector articles; public Magazine() { articles = new Vector(); } public void setName( String rhs ) { name = rhs; } public String getName(){ return name;} public void addArticle( Article a ) { articles.addElement( a ); } public String toString() { StringBuffer buf = new StringBuffer( "Magazine: Name='" + name + "' "); for( int i=0; i<articles.size(); i++ ){ buf.append( articles.elementAt(i).toString() ); } return buf.toString(); } }
下面是Article类的定义,代码如下:
public class Article { private String headline; private String page; public Article() {} public void setHeadline( String rhs ) { headline = rhs; } public String getHeadline(){return headline;} public void setPage( String rhs ) { page = rhs; } public String toString() { return "Article: Headline='" + headline + "' on page='" + page + "' "; } }
三、将XML映射成Java类
Digester类通过模式(patterns)和规则来处理XML文档。其中的模式必须匹配XML元素,也就是在XML文档树中的结点名和位置。Digester中的匹配模式的语法有些类似XPath的匹配模式。如模式catalog匹配最顶层结点<catalog>,模式catalog/book匹配<catalog>结点中的子结点<book>,其它的匹配类似。
所以的匹配模式都是绝对路径,也就是说所有的路径都是从根元素开始向下被指定的。但有一个例外,就是包含有通配符的"*"的匹配模式。如模式*/name将匹配XML文档中的任何<name>结点。还要注意一点,没有必要为根结点特殊做标记,因此所有的结点都是绝对的。
无认何时,当Digester遭遇到指定的匹配模式时,就会执行和这些模式相关的动作。从底层上来看,Digester框架是基于SAX分析器实现的(事实上,Digester类就实现了org.xml.sax.ContentHandler接口,并维护了相关的分析栈)。所有被Digester使用的规则(Rule)必须继承于org.apache.commons.digester.Rule,在这个类中提供了一些类似SAX的CantentHandler的回调方法。当打开和关闭XML结点时就会调用begin()和end()方法。
在匹配内容的过程中body()方法被调用,最后是finish()方法,这个方法在完成一次tag处理后调用,为了使开发工作更容易,大多数应用程序开发人员并不用关心这些方法的调用,而这一切都由Digester来为我们处理。
为了处理XML文档,我们需要建立一个org.apache.commons.digester.Digester类的实例,并配置它。如果必要,可以指定相关的模式和规则,最后,将XML文件的引用传入parse()方法。下面将给出一个DigesterDriver类,它将通过对上述的XML文档的处理来演示Digester的使用。XML文件将通过命令行方法传入程序。DigesterDriver的代码如下:
import org.apache.commons.digester.*; import java.io.*; import java.util.*; public class DigesterDriver { public static void main( String[] args ) { try { Digester digester = new Digester(); digester.setValidating( false ); digester.addObjectCreate( "catalog", Catalog.class ); digester.addObjectCreate( "catalog/book", Book.class ); digester.addBeanPropertySetter( "catalog/book/author", "author" ); digester.addBeanPropertySetter( "catalog/book/title", "title" ); digester.addSetNext( "catalog/book", "addBook" ); digester.addObjectCreate( "catalog/magazine", Magazine.class ); digester.addBeanPropertySetter( "catalog/magazine/name", "name" ); digester.addObjectCreate( "catalog/magazine/article", Article.class ); digester.addSetProperties( "catalog/magazine/article", "page", "page" ); digester.addBeanPropertySetter( "catalog/magazine/article/headline" ); digester.addSetNext( "catalog/magazine/article", "addArticle" ); digester.addSetNext( "catalog/magazine", "addMagazine" ); File input = new File( args[0] ); Catalog c = (Catalog)digester.parse( input ); System.out.println( c.toString() ); } catch( Exception exc ) { exc.printStackTrace(); } } }
在实例化Digester对象后。我们将不需要用DTD来验证我们的XML文档。这是因为在Catalog文档中并没有指定DTD。我们要做的只是指定模式和相关的规则。如首先使用addObjectCreate()方法将catalog结点和Catalog类绑定。如果XML的结点有属性的话,我们还可以通过的SetProperties方法将结点和Java类的属性对应起来。这个方法的第一个参数是XML结点属性名,第二个参数是类的属性名。
在上面代码中SetProperties方法仅仅是设置从一个属性取值,而SetBeanPropertySetter方法才真正将当前的结点和Java类对应。在使用setBeanPropertySetting方法时,并不一定要指写Java类的属性名,属性名在默认情况下和当前XML结点名相同。在上面的代码中,在设置catalog/magazine/article/headline模式时就使用了默认值。最后,我们使用addSetNext方法设置了分析栈的最顶层元素,也就是说article有多个,就象链表一样,一个接着一个排列。在本例子中使用了三次adSetNext方法来分别设置catalog/book、catalog/magazine/article和catalog/magazine。
要注意的是,我们可以为同一种模式注册更多的规则。如果我们这样做。这个规则将以它们被加入Digester的顺序来执行。例如,为了处理<article>元素,这个元素在catalog/magazine/article处被发现,我们首先会建立相应的Java类,然后设置page属性,最后弹出这个article类,并将其插入magzine结点。
四、调用任意的函数
Digester的功能还不仅仅是设置bean的属性,它还可以调用在Digester栈中对象的任意方法。这个功能可以由CallMethod方法来完成。我们首先要为这个方法传入方法名,然后可以选择传入参数个数和参数类型。在Digester的说明文档中描述了CallParam方法定义的传入invoked函数的参数值。这个值可以使用当前XML结点的属性值,也可以使用当前结点包含的字符串。例如,在上面DigesterDriver的实现中使用BeanPropertySetter方法时,我们可以通过显式地调用setter属性,并将数据作为参数来达到同样的效果。修改后的实现代码如下:
digester.addCallMethod( "catalog/book/author", "setAuthor", 1 );
digester.addCallParam( "catalog/book/author", 0 );
在上面代码中,首先将方法名(setAuthor())作为addCallMethod方法的第二个参数传入。第三个参数是setAuthor方法的参数个数。第二行的代码的作用是将当前结点<author>包含的字符串值作为setAuthor方法的参数值,并将这个值作为参数数组的第1个值。我们还可以指定一个属性名来作为参数的值,代码如下:
digester.addCallParam( "catalog/book/author", 0, "author" );
上面的代码将catalog/book/author结点中的author的值作为setAuthor的参数。
在上面代码中,首先将方法名(setAuthor())作为addCallMethod方法的第二个参数传入。第三个参数是setAuthor方法的参数个数。第二行的代码的作用是将当前结点<author>包含的字符串值作为setAuthor方法的参数值,并将这个值作为参数数组的第1个值。我们还可以指定一个属性名来作为参数的值,代码如下:
digester.addCallParam( "catalog/book/author", 0, "author" );
上面的代码将catalog/book/author结点中的author的值作为setAuthor的参数。
上面的代码有一个地方容易犯错误,或是容易搞糊涂。digester.addCallMethod( "pattern", "methodName", 0 )并不是指定一个无参数方法的调用,而是指定有一个参数的方法。这个值就是当前XML元素的值。如果要想指定无参数的调用方法,可以使用如下的代码:
五、Digester的标准规则
1. 创建规则(Creational)
1. 创建规则(Creational)
对象创建规则(ObjectCreateRule):
根据指定类和它的默认构造方法创建一个对象,并将其压栈。当这个结点处理完后,并这个对象弹出。这个类可以通过类对象或类的全名进实例化。
工厂创建规则(FactoryCreateRule):
使用指定的工厂类创建一个对象,并将其压栈。这个类没有默认的构造方法的工厂类是非常有用的。这个工厂类必须实现org.apache.commons.digester.ObjectCreationFactory接口。
2. 属性设置规则(Property Setters)
SetPropertiesRule:
使用指定的XML结点属性来设置栈的顶层bean的一个或多个属性。属性名通过String[]数组传递,如处理象<article page="30">的XML指令。
BeanPropertySetterRule:
通过当前XML结点的字符串值设置栈顶对象的属性,如<page>30</page>。
SetPropertyRule:
这个规则设置一个最顶层的对象的属性,需要有两个值,key和value。如<article key="page" value="10" />。
这个规则设置一个最顶层的对象的属性,需要有两个值,key和value。如<article key="page" value="10" />。
3. 父 / 子管理规则(Parent/Child Management)
SetNextRule:
弹出最顶层的对象,并将它传递给这个对象下面的指定的方法。类似将子结点插入到父结点的下面。
弹出最顶层的对象,并将它传递给这个对象下面的指定的方法。类似将子结点插入到父结点的下面。
SetTopRule:
将栈的第二个对象传给顶层对象。这对于当子对象有一个setParent方法而不是其他方法时非常有用。
SetRootRule:
调用栈底部对象的一个方法,并将顶层对象作为一个参数传入。
调用栈底部对象的一个方法,并将顶层对象作为一个参数传入。
4. 调用任意方法
CallMethodRule:
调用最顶层bean的指定的任意方法。这个方法可以有任意个参数。参数值通过CallParamRule给出。
调用最顶层bean的指定的任意方法。这个方法可以有任意个参数。参数值通过CallParamRule给出。
CallParamRule:
描述方法参数的值。参数值或者是XML结点的属性值,或是XML结点的字符串内容值。这个规则要求参数的位置必须由一个整数索引指定。
描述方法参数的值。参数值或者是XML结点的属性值,或是XML结点的字符串内容值。这个规则要求参数的位置必须由一个整数索引指定。
六、使用xmlrules包指定XML规则
到现在为止,我们已经学习了如何在编译时指定模式和规则。这些方法更简单和直接,但感觉有点单一。在本节中,我们将介绍如何在运行时来组织和处理XML指令和数据。在这让我们先来修理一下编译时的行为。我们从代码中可以看出,在源代码中有很多固定的东西(这些东西是不可编程的),而这些都可以在运行时完成。
不过不用担心,读取这些信息无须读者自己做,在Digester中提供了一个包:org.apache.commons.digester.xmlrules。这个包中的类可以从一个XML文档中读取并返回一个指定的配置信息。XML文档配置文件必须使用DTD,这是xmlrules包的一部分。
下面是一个在这们例子中使用的XML规则的配置文件rules.xml。在看这个文件之前,我将有几点说明。
模式可以由两种不同的方式指定:一种是每一个XML结点属性描述一个规则,或使用<pattern>结点。这些被定义的模式对于所有的规则结点都是可用的。这两种方式可以混合使用,并且<pattern>结点可以嵌套。在这种情况下,被子结点定义的pattern被加到<pattern>结点之后。
<alias>结点用来和<set-propeties-rule>一起使用来将XML结点的属性映射成Bean的属性。下面是rules.xml的代码:
上面的xml已经将所有的模式和规则设置完成,而调用类就变得非常简单了。为了运行它。我们需要通过命令行指定catalog和rules.xml文件名。(要注意的是,DigesterLoader不能通过rules.xml文件名去读文件或使用org.xml.sax.InputSource,而需要一个URL)。代码如下:
到现在为止Digester的主要功能已经讲完了。当然,Digester的功能还远不止这些。Digester还可以处理XML命名空间,而且还可以根据指定的命名空间来设置规则。
在本文中未详细描述如何开发定制规则,不过这个非常简单,只需要继承Rule类,并实现相应的方法即可。Digester类还提供了push()、peek()和pop()方法,这此方法可以为开发人员提供更大的弹性(可以自由地将对象从栈中弹出或压入)。
<alias>结点用来和<set-propeties-rule>一起使用来将XML结点的属性映射成Bean的属性。下面是rules.xml的代码:
<?xml version="1.0"?> <digester-rules> <object-create-rule pattern="catalog" classname="Catalog" /> <set-properties-rule pattern="catalog" > <alias attr-name="library" prop-name="library" /> </set-properties-rule> <pattern value="catalog/book"> <object-create-rule classname="Book" /> <call-method-rule pattern="author" methodname="setAuthor" paramcount="0" /> <call-method-rule pattern="title" methodname="setTitle" paramcount="0" /> <set-next-rule methodname="addBook" /> </pattern> <pattern value="catalog/magazine"> <object-create-rule classname="Magazine" /> <call-method-rule pattern="name" methodname="setName" paramcount="0" /> <pattern value="article"> <object-create-rule classname="Article" /> <set-properties-rule> <alias attr-name="page" prop-name="page" /> </set-properties-rule> <call-method-rule pattern="headline" methodname="setHeadline" paramcount="0" /> <set-next-rule methodname="addArticle" /> </pattern> <set-next-rule methodname="addMagazine" /> </pattern> </digester-rules>
上面的xml已经将所有的模式和规则设置完成,而调用类就变得非常简单了。为了运行它。我们需要通过命令行指定catalog和rules.xml文件名。(要注意的是,DigesterLoader不能通过rules.xml文件名去读文件或使用org.xml.sax.InputSource,而需要一个URL)。代码如下:
import org.apache.commons.digester.*; import org.apache.commons.digester.xmlrules.*; import java.io.*; import java.util.*; public class XmlRulesDriver { public static void main( String[] args ) { try { File input = new File( args[0] ); File rules = new File( args[1] ); Digester digester = DigesterLoader.createDigester( rules.toURL() ); Catalog catalog = (Catalog)digester.parse( input ); System.out.println( catalog.toString() ); } catch( Exception exc ) { exc.printStackTrace(); } } }
七、结论
到现在为止Digester的主要功能已经讲完了。当然,Digester的功能还远不止这些。Digester还可以处理XML命名空间,而且还可以根据指定的命名空间来设置规则。
在本文中未详细描述如何开发定制规则,不过这个非常简单,只需要继承Rule类,并实现相应的方法即可。Digester类还提供了push()、peek()和pop()方法,这此方法可以为开发人员提供更大的弹性(可以自由地将对象从栈中弹出或压入)。