你是否曾经编写了一个程序,却在复检的时候发现它的配置过程很不合理?你是否曾经使用过配置文件,却发现它们不能满足描述应用程序的需要?你是否为了解决几个特殊问题而创建过临时配置补丁,却把花费更多时间、开发普遍适用方案的希望寄托到了未来?
如果你的回答是肯定的,那么,你和大多数其他java程序员一样幸运,有一些工具能够帮助你解决这些问题。如果你的回答是否定的,关于属性文件局限的讨论也许能够让你信服——还有更好的方法可供使用。
属性文件是java编程和运行环境的一个重要组成部分。然而,当一个程序员需要的功能远远超过properties类提供的简单名字-值对时,他需要有更丰富的表现手法。通常,java程序员扩展属性文件的方法是为属性本身的名字或值(或两者同时)增加额外的语义信息。很多时候,这种看来有效的方法会使问题越来越复杂。
为说明问题,请利用属性把一系列的值赋给单个名字。让我们假定你想要管理一组名称服务器,可能采用的属性文件内容如下:
hosts_1=ns.foo.com
hosts_2=ns.bar.com
hosts_3=ns.acme.com
代码很简单。改变名字-值对中名字的含义之后,你可以轻松地编写出把“hosts_”开头的名字当成“hosts”列表中一个元素的程序。
下面,我们来看看一个更复杂的例子。假设你有同一bean类internethost的两个不同实例:实例a关联到一个web服务器的列表;实例b关联到一个名称服务器的列表。要从同一个文件配置这两个实例,一种可能的方案如下:
name_hosts_1=ns.foo.com
name_hosts_2=ns.bar.com
name_hosts_3=ns.acme.com
web_hosts_1=www.foo.com
web_hosts_2=www.bar.com
web_hosts_3=www.acme.com
这种方法行得通,但总是给人以拼拼凑凑的感觉。如果你还不相信的话,稍微增加一点问题的复杂性:让这些列表中的某个元素自己也成为一个列表;或者,使得下划线字符(“_”)在名字-值对中合法。在这些情况下,简单的属性文件变得非常复杂。
作为一个细心的读者,你可能已经发现,internethost各个实例的命名方式逐渐模糊。为了把前三个属性赋值给实例a,把后三个属性赋值给实例b,你必须用某种与具体实例无关的方法告诉实例它们该用哪一组属性值。如果用直接编码的方式,让实例a寻找以“name_”开头的属性,让实例b寻找以“web_”开头的属性,那么,这两个实例将不再属于同一对象类。
最后的例子还显示出另外一个问题。这就是,如何来调用实例a?简单地叫它“a”?到哪里去寻找它?它是本地实例还是远程接口?是否存在指向它的全局静态引用?如是,如何访问实例b(或者,那是否是“b”)?
解决这些问题的方案是使用一个组件配置和命名框架。有许多工具能够帮助你完成这个任务,其中之一就是pasx。pasx是一个源代码开放的java工具,它通过xml进行配置,通过jndi实现命名。pasx框架用xml配置用户定义的服务、jndi名称空间、jdbc连接池、事件树、工作队列和系统日志。
pasx利用xml进行配置,因为xml比简单的属性列表具有更丰富的描述能力。为了理解为何xml更适合完成这类任务,请再次考虑第一个例子。如果用pasx定义的标记重新描述,则结果应该如下:
<list>
<string>ns.foo.com</string>
<string>ns.bar.com</string>
<string>ns.acme.com</string>
</list>
虽然代码更加冗长,但它的含义比原来要清楚得多。由于一些列表可能被排序,元素在xml文档中出现的次序决定了它们在最终数据结构中的次序。属性文件最终用来构造properties对象,它的名字必须指示出元素的索引,因为properties对象直接从hashtable派生得到。
用xml描述时,第二个属性示例如下所示:
<list name="name-servers">
<string>ns.foo.com</string>
<string>ns.bar.com</string>
<string>ns.acme.com</string>
</list>
<list name="web-servers">
<string>www.foo.com</string>
<string>www.bar.com</string>
<string>www.acme.com</string>
</list>
还记得示例二之后提出的难题吗?让列表中的某个元素成为一个子列表,让其中一个列表成为元素名字可以包含下划线的映射结构。下面是它的答案:
<list name="name-servers">
<string>ns.foo.com<string>
<list>
<string>ns1.bar.com</string>
<string>ns2.bar.com</string>
</list>
<string>ns.acme.com</string>
</list>
<map name="web-servers">
<string name="most_visited">www.foo.com<string>
<string name="most_bytes">www.bar.com</string>
</map>
在pasx中,组件(一个类或者一组有着密切关系的类)是实现pasxservice接口的java bean。它们由xml <service>标记定义,这个标记用来命名组件的单个实例。赋予实例“a”名称服务器列表以及赋予实例“b”web服务器列表的xml代码如下所示:
<service name="a" class="my.internethost">
<list name="hosts">
<string>ns.foo.com</string>
<string<ns.bar.com</string>
<string<ns.acme.com</string>
</list>
</service>
<service name="b" class="my.internethost">
<list name="hosts">
<string>web.foo.com</string>
<string>web.bar.com</string>
<string>web.acme.com</string>
</list>
</service>
pasx定义了一系列的标准xml标记,用来声明list、map、integer、string、boolean等类型的属性。然而,pasxservice类还可以经由名称空间和xml模式使用它自己的xml标记。xml模式允许组件开发者定义自己的标记,允许xml解析器验证pasx所定义标记和组件开发者所定义标记的合法性。下面的示例模式定义了一个<server>标记,它必须有hostname和portnumber属性。<server>标记必须作为<cluster>标记的子元素至少出现一次,但可以出现多次。<cluster>标记必须作为<serverfarm>标记的子元素出现至少一次,但可以出现多次。
<?xml version="1.0"?>
<schema xmlns="http://www.w3.org/2000/10/xmlschema"
xmlns:pce="http://pasx.org/pasx/custom-example"
targetnamespace="http://pasx.org/pasx/custom-example" elementformdefault="qualified" >
<annotation>
<documentation>
a custom schema example to be using with pasx (pce)
</documentation>
</annotation>
<element name="server">
<complextype content="empty">
<attribute
name="hostname"
use="required"
type="string"/>
<attribute
name="portnumber"
use="required"
type="positiveinteger"/>
</complextype>
</element>
<element name="cluster">
<complextype>
<sequence>
<element
ref="pce:server" minoccurs="1" maxoccurs="unbounded"/>
</sequence>
<attribute
name="name"
use="required"
type="string"/>
</complextype>
</element>
<element name="serverfarm">
<complextype>
<sequence>
<element
ref="pce:cluster" minoccurs="1" maxoccurs="unbounded"/>
</sequence>
<attribute
name="name"
use="required"
type="string"/>
</complextype>
</element>
<element name="pce">
<complextype>
<sequence>
<element
ref="pce:serverfarm" minoccurs="1" maxoccurs="unbounded"/>
</sequence>
</complextype>
</element>
</schema>
详细介绍xml模式文档(xsd)的构造方法已经超出了本文的范围。重要的是必须认识到,pasxservice组件的开发者可以使用一组定制标记。更妙的是,开发者无需编写任何验证代码,就可以确保xml不仅格式良好而且合法(这一切由解析器完成)。下面声明的<service>标记用到了前面的模式:
<service
class="org.pasx.examples.customconfigexample"
name="examples.customconfigexample" >
<pce:pce xmlns="http://pasx.org/pasx/custom-example"
xmlns:pce="http://pasx.org/pasx/custom-example"
xsi:schemalocation="http://pasx.org/pasx/custom-example /org/pasx/examples/custom-example.xsd" >
<serverfarm name="farm0">
<cluster name="cluster0">
<server hostname="app0.foo.com" portnumber="8080" />
<server hostname="app1.foo.com" portnumber="8080" />
</cluster>
<cluster name="cluster1">
<server hostname="app2.foo.com" portnumber="8080" />
<server hostname="app3.foo.com" portnumber="8080" />
</cluster>
</serverfarm>
<serverfarm name="farm1">
<cluster name="cluster0">
<server hostname="app4.foo.com" portnumber="8080" />
<server hostname="app5.foo.com" portnumber="8080" />
</cluster>
<cluster name="cluster1">
<server hostname="app6.foo.com" portnumber="8080" />
<server hostname="app7.foo.com" portnumber="8080" />
</cluster>
</serverfarm>
</pce:pce>
</service>
pasxservice类如何使用xml配置信息实际上由类的开发者决定。在配置类的时候,它通过configure方法处理声明它的xml元素(<service>)。configure方法的特征如下:
public void configure(org.jdom.element config,
context context, servicemanager caller)
注意element参数是一个对jdom element的引用,而不是一个dom element的引用。jdom比dom更适合java处理。但是,如果你需要dom版本,jdom包提供了转换它们的方法。使用定制元素时,类的开发者必须使用jdom api访问解析后的xml。然而,由于使用了xml模式,类开发者要关心的只是如何使用jdom数据结构中的信息,但无需编写代码去验证数据结构的合法性(例如,<cluster>元素只包含<server>元素)。
如果pasxservice的开发者决定只用pasx定义的xml标记,且遵从java bean获取和设置bean属性的模式,那么,他可以使用一个称为xmlbeanutil的工具类。这个类利用bean的“内省”机制,实现bean属性和pasx所定义xml标记之间的匹配和赋值。它能够让代码编写变得非常轻松。例如:
public void configure( element config,
context context, servicemanager caller )
{ xbu = new xmlbeanutil( context ); xbu.populate( config, this );
}
如果某个pasxservice组件由一组关系密切的小型java类构成,populate方法可能被多次调用。请考虑下面这个属性:
<string name="lastname">bushaw</string>
对于这个xml片断,为了把lastname属性赋给两个不同的bean,populate方法可以被多次调用:
person father = new person();
person mother = new person(); xbu.populate( config, father ); xbu.populate( config, mother );
使用xml模式和定制标记的一个优点是对预计配置的验证。换句话说,你可以编写一个xml模式,使得对于给定的<host>标记,portnumber和ipaddress属性也必须同时指定。定义pasx标记集的模式不能这么做,因为它不了解各个pasxservice组件所需的语义信息。然而,xmlbeanutil能够跟踪哪些bean属性已经设置、哪些还没有设置,而且允许在配置的时候指定一系列对属性的约束。pasxservice类的编写者可以根据爱好、需要和方便程度,选用任意一种方法。
xmlbeanutil把bean属性分成三类:property(普通属性),dependent(依赖),compliment(遵从)。如果这三类属性的java类型是list或map,则它们都可以作为一个集合体设置。如果属性已经有值且被视为一个集合体,配置中的list或map被加入到现有的属性;如果它不被视为一个集合体,则配置中的list或map将取代属性值。下面的configure方法实例示范了它的用法。
public void configure( element config,
context context, servicemanager caller )
{ xbu = new xmlbeanutil( context ); xbu.addproperty( "hostname",
"host name of mythical tcp service",
true, false ); xbu.adddependent( "portnumber",
"port number of mythical tcp service",
"hostname", false ); xbu.addcompliment( "serverfarm",
"back-end server farm",
"nameservers", true ); xbu.addcompliment( "nameservers",
"name servers to resolve against",
"serverfarm", true ); xbu.addproperty( "person",
"person", true, false ); xbu.addproperty( "binarything",
"the binary input stream",
true, false ); xbu.addproperty( "props",
"example protomatter properties",
true, false ); xbu.addproperty( "xml",
"an example jdom xml document",
true, false ); xbu.populate( config, this );
}
传递给addproperty、adddependent、addcompliment方法的属性描述用于错误信息。xmlbeanutil类的checkservice方法将检查属性是否已经正确设置。如果还没有,它将抛出一个异常,在错误信息中利用属性的描述。要查看完整的实例,请参考beanserviceexample类。
作为一个细心的读者,你可能已经发现,此前的所有例子都没有提到jndi。即使是<service>标记的name属性也没有提到任何有关jndi的内容。因此,你也许会疑惑pasx的命名部分如何使用jndi。
默认情况下,pasx使用内存中的称为pas的扁平jndi服务提供者(它来自底层的protomatter包)。这个服务提供者仅仅是一个带有jndi接口的hashtable。其他jndi服务提供者可以通过url命名指定甚至一起使用。因此,从一个rmi注册器或者一个ldap服务器(也就是rmi://localhost/creditcardauthorizer或ldap://foo.com/uid=littlek,dc=foo,dc=com)指定一个url是可能的。
在服务中使用jndi的默认jndi动作是bind。然而,通过可选的action属性,动作也可以指定为rebind或lookup。bind动作尝试把服务放入jndi目录,但如果目录中已经包含具有指定名字的服务,则操作失败。rebind动作和bind动作基本相同,例外之处在于,rebind动作将覆盖已经存在的服务入口。无论是bind还是rebind动作,pasx都会实例化pasxservice对象。但对于lookup动作,对象从jndi目录提取得到。
<service>标记中还有两个属性可能被用到:prerebind和postrebind。prerebind属性指定一个名字或一个url,它被用于在配置生效之前把对象重新绑定到jndi目录。类似地,postrebind属性具有同样的功能,但不同的是,它在配置之后出现。
结合运用这些功能使得配置文件能够轻松地完成一些有用的任务。它如,你可以从ldap服务器“反串行化”一个对象,通过xml配置它,然后把它放入内存,使得其他对象能够访问它。考虑这样一个例子:某些业务过程的终止日期驻留在ldap服务器上。下面的标记代码从ldap服务器获取终止日期,根据当前的时区设置它,然后把它放入内存:
<service
name="ldap://foo.com/cn=expirationdate,o=foo.com"
class="my.utilitydate"
action="lookup" postrebind="pas:expirationdate">
<integer name="timezone" value="-6"/>
</service>
现在,其他服务可以通过<namedservice>标记引用这个经过配置的服务了。例如,考虑一个降价销售广告服务,它必须知道降价销售活动何时终止:
<service
name="pas:oriellysale"
class="my.salesadbanner">
<namedservice name="enddate" servicename="pas:expirationdate"/>
</service>
这并不是什么魔术。<namedservice>标记只不过是根据servicename属性,让pasx执行一个jndi查找,然后把结果放入名为enddate的bean属性。尽管如此,这种在配置时把一个对象连接到另一个对象的方法很有用。如果你开始使用这种方式,它可能改变你设计类的习惯。
pasx包含了各种各样用于jndi的工具。其中包括状态和对象工厂,用于在目录中串行化和保存对象状态;还有两个服务提供者。请参考javadoc了解有关它们的更多信息。
另外,pasx还包含一个完整的实例包,包括源代码、javadoc和伴随pasx的配置文件。这些实例从简单的配置示例开始,涵盖所有范围,直至多个jndi目录的混合应用。请参考javadoc org.pasx.examples。
我希望,本文所介绍概念的优点已经让你确信:下一次坐下来编写程序时,不管其规模大小,你有必要考虑一下配置的需求和方法问题。本文所介绍的概念属于中心主题,但pasx还包含本文没有介绍的许多其他功能。pasx为完成许多任务提供了大量标准的服务和管理器,甚至还有一个servlet框架,用户可以通过这个servlet框架管理服务和管理器。
最后,有一个称为potomac的工程。potomac是一个基于pasx的源代码开放java软件的集合。有了potomac,所有你必须做的就是解开压缩文件,这样你就有了使用pasx时所有必须的java组件,比如protomatter和jdom,还有一些快速启动脚本和入门学习用的配置文件。