Avalon的简要历史以及创建它所有的设计原则概述
事情是从Apache JServ项目开始的。Stefano Mazzocchi和其它协助开发Apache JServ的人员认识到项目中所用到的一些模式很通用,足以用于创建一个服务器框架。 在1999年1月27日,星期三(在JServ 1.0b发布大约一个月后),Stefano拿出一份建议书,建议启动一个名为Java Apache Server Framework的项目。它的目标是成为Apache所有Java服务器代码的基础。想法是通过提供一个框架,将跨项目的一些组件和重用代码集中在一起。
Stefano Mazzocchi,Federico Barbieri和Pierpaolo Fumagalli创建了最初的版本。在2000年末,Berin Loritsch和Peter Donald参加到项目中来。那时,Pierpaolo和Stefano已转向其它项目的开发,Java Apache Server Framework开始被称为Avalon。这五个开发者是框架目前版本所使用的设计和概念的主要负责人。当前版本与2000年6月发行的版本非常相似。实际上,主要的区别是对包重新组织,以及将项目划分为子项目。同样的设计模式和接口今天依然存在。
什么是Avalon?
Avalon 是五个子项目的父项目:Framework、Excalibur、LogKit、Phoenix、和Cornerstone。当听到Avalon时,大多数人会联想到Framework,但Avalon不止包括Framework。Avalon开始作为Java Apache Server Framework时就包含框架、工具、组件和一个服务器核心的实现,这些都在一个项目中。因为Avalon的不同部分具有不同的成熟程度,发布周期也不同,我们决定将Avalon划分为前面提到的小项目。这样做也便于新开发者理解和学习Avalon的不同部分——这在以前几乎无法办到。 Framework
Avalon Framework是Avalon大伞下的所有其它项目的基础。它定义了接口、契约(contracts)和Avalon的缺省实现。Framework将大部分工作置于其中,因此也是最为成熟的项目。
Excalibur
Avalon Excalibur是一组服务器端组件,您可以在自己的项目中使用它们。它包括了池(pooling)的实现、数据库连接管理和其它一些组件管理的实现。
LogKit
Avalon LogKit是一个高速日志记录工具集,Framework、Excalibur、Cornerstone和Phoenix都用到它。它的模型与JDK 1.4 Logging package采用相同的原理,但与JDK 1.2+兼容。
Phoenix
Avalon Phoenix是服务器核心,它管理服务(Service,实现为服务器端组件,称作Block)的发布和执行。
Cornerstone
Avalon Cornerstone是一组Block或服务,这些Block可以布署在Phoenix环境中。这些Block包括了socket管理和Block之间的任务调度。
Scratchpad
Scratchpad并不是一个真正的正式项目,而是那些还没准备好放入Excalibur中的组件的一个临时区域。这些组件品质差别较大,它们的API也不保证会不变,直到它们被提升到Excalibur项目为止。
本概述的重点
在这个概述中,我们把重点放在Avalon Framework上,但会介绍足够的Avalon Excalibur和Avalon LogKit的相关知识,以便让您能够起步。我们将通过一个假想的业务服务器来展示如何在实践中使用Avalon。定义一个完整全面的方法学,或介绍所有子项目的方方面面超出了本概述的范围。我们将重点放在Avalon Framework上是因为它是所有其它项目的基础。如果您能理解该框架,您就可以理解任何基于Avalon的项目。对于Avalon中常用的一些编程习惯结构(idiom),您也会逐渐熟悉。将重点放在框架上并涉及Avalon Excalibur和Avalon LogKit项目的另一个原因是它们是正式发布并被支持的。
Avalon可用在哪里?
我被问过好几次,要求阐明Avalon 适合做什么,不适合做什么。Avalon把重点放在服务器端编程和让以服务器应用为中心的项目的设计和维护变得更容易。Avalon可被描述为一个包含了实现的框架。尽管Avalon的重点是服务器端解决方案,很多人却发现对普通应用程序来说它也是有用的。Framework、Excalibur、和 LogKit中使用的概念很通用,足以在任何项目中应用。将重点更直接放在服务器上的两个项目是Cornerstone和Phoenix。 Framework
1. 一个支持性的或封闭性的结构2. 包含思想的一个基本系统或一种安排
框架这个词在应用程序中的含义很广泛。重点放在单一行业的框架被称为垂直市场框架,例如医药系统或通信系统。原因是同样的框架不能胜任其它行业。具有很好通用性,可用于多个行业的框架被称为水平市场框架。Avalon是一个水平市场框架。您可以使用Avalon的Framework构建垂直市场框架。用Avalon构建的最有说服力的垂直市场框架的例子是Apache Cocoon出版框架。Apache Cocoon第2版是使用Avalon的Framework、Excalibur和LogKit项目构建的。它利用了Framework中的接口和契约,让开发者能用更少的时间理解Cocoon是如何工作的。它也有效地利用了Excalibur提供的数据源管理和组件管理代码,这样它就不必重新发明轮子了。最后,它使用了LogKit来处理出版框架中所有的日志问题。一旦您理解了隐藏在Avalon Framework后面的原理,您就能理解基于Avalon构建的任何系统。一旦您理解了系统,您将能更快地捕获因为误用框架所引起的缺陷。不存在魔术公式
值得一提的是,任何试图使用某种工具作为成功的魔术公式的做法都是自找麻烦。Avalon也不例外。因为Avalon的Framework是为服务器端解决方案设计的,所以用它来构建图形用户界面(GUI)不是什么好主意。Java已经有了一个构建GUI的框架,称为Swing。尽管您需要考虑 Avalon是否适合您的项目,但您还是能从它的原理和设计中学到一些东西。您需要问自己的问题是:"项目将用在哪里?" 如果回答是它将运行在一个服务器环境中,那么Avalon将是一个好的选择,无论您是要创建一个Java Servlet或一个特殊用途的服务器应用。如果回答是它将运行在一个客户的机器上,并且与服务器没有交互,那么可能Avalon不太适合。即使如此,组件模型也是非常灵活,有助于在大型应用程序中对复杂性进行管理。
原理和模式
Avalon整个是基于一些特定设计原理来构建的。最重要的两个模式是反向控制(Inversion of Control) 和分离考虑(Separation of Concerns)。 Component Oriented Programming、Aspect Oriented Programming和Service Oriented Programming也对Avalon产生了影响。每种程序设计原理都可以写出数卷的书,但它们都是一些设计思维习惯。反向控制
反向控制(Inversion of Control,IOC)的概念是指组件总是由外部进行管理的。这个短语是由Brian Foote在他的一篇论文中最先使用的。组件所需的一切通过Contexts、Configurations和Loggers的方式赋予组件。实际上,组件生命周期中的每个阶段都是由创建组件的代码所控制的。当您使用这种模式时,就实现了一种组件与您的系统安全交互的方法。IOC与安全性并不等价! IOC提供了一种机制,允许你实现一个可扩展的安全模型。为了让一个系统真正做到安全,每个组件都必需安全,没有组件可以修改传递给它们的对象的内容,而且所有的交互都必须使用已知的实体。安全性是一个主要问题,IOC是程序员工具库中的一种工具,用于实现安全性的目标。
分离考虑
您应该从不同的思考方向来看待您的系统,这一思想导致了分离考虑(Separation of Concerns,SOC)模式。一个例子是从同一个问题空间的不同视角来看一个web服务器。web服务器必需安全、稳定、可管理、可配置并满足HTTP规范。每种属性都是一个单独的考虑范围。这其中的某些考虑与其它考虑相关,如安全性和稳定性(如果一个服务器不稳定,它就不可能安全)。分离考虑模式又导致了Aspect Oriented Programming (AOP) 。研究者发现许多考虑不能在类或方法的粒度上进行处理。这些考虑被称为aspect。aspect的例子包括管理对象的生命周期、记日志、处理异常和清理释放资源等。由于没有一种稳定的AOP实现,Avalon开发团队选择通过提供一些小的接口,然后由组件来实现,从而实现aspect或考虑。
面向组件的编程
面向组件的编程(Component Oriented Programming ,COP)是把系统分割成一些组件或设施的一种思想。每种设施都有一个工作接口和围绕该接口的契约。这种方式允许容易地更换组件的实例,同时不影响系统其它部分的代码。面向对象编程(Object Oriented Programming ,OOP)和COP的主要区别在于集成的层次。COP系统的复杂性更容易管理,这得益于类之间更少的相互依赖。这提高了代码重用的程度。COP的主要好处之一是修改项目代码的一些部分不会破坏整个系统。另一个好处是可以有某组件的多种实现,并可以在运行时刻进行选择。
面向服务的编程
面向服务的编程(Service Oriented Programming ,SOP)的思想是把系统划分为由系统提供的一些服务。服务
1. 为其它人执行的工作或职责2. 提供修理或维护的一种设施3. 向公众提供工具的一种设施
Avalon 的 Phoenix把每一种要提供的设施看作是一项服务,由特定接口和相关契约组成。服务的实现被称为Block。一个服务器程序是由多种服务组成的,认识这一点很重要。以邮件服务器为例,它会有协议处理服务、认证和授权服务、管理服务和核心邮件处理服务等。Avalon的 Cornerstone提供了一些低层的服务,您可以在自己的系统中加以利用。提供的服务包括连接管理、socket管理、参与者/角色管理和调度等。我们在这里介绍到服务是因为它与把我们的假定系统分解为不同设施的过程有关。
分解一个系统
您是如何决定由哪些东西组成一个组件的? 关键是定义您的解决方案所需的设施,以便能有效率地进行操作。
我们将使用一个假想的业务服务器来展示如何识别和确定服务与组件。在我们定义了一些系统用到的服务之后,我们将以这些服务中的一个为例,定义该服务所需的不同组件。我的目标是传递给您一些概念,这些概念有助于您把您的系统定义成可管理的部分。
系统分析——识别组件
尽管提供一个完整全面的方法学不是本篇的范围,我还是愿意讨论几点问题。我们将从组件和服务的面向实现的定义开始,然后提供一个实践的定义。组件
一个组件是一个工作接口和该工作接口的实现的组合。使用组件提供了对象间的松耦合,允许改变实现而不影响使用它的代码。
服务
一个服务由一个或更多的组件组成,提供了一种完整的解决方案。服务的例子包括协议处理器、任务调度器、认证和授权服务等等。
尽管这些定义提供了一个起点,但它并没有提供一个完整图景。为了把一个系统(定义为一组设施,组成一个项目)分解为必要的组成部分,我建使用自顶向下的方式。采用这种方式可以在您了解到底有哪些设施之前,避免陷入到细节的泥潭中。确定您的项目范围
您的项目预期完成什么功能,一开始您总要有个大致的想法。在商业世界里,初始工作说明(initial statement of work )就是完成这项工作的。在开放源代码的世界里,这通常是由一个想法或一个脑力激荡的过程完成的。我想不论如何强调具备一个项目的高层视图的重要性都是不过份的。很明显,一个大项目将由许多不同的服务组成,而小项目只有一两个服务。如果您开始感到有点不知所措,只需提醒自己大项目实际上是一把大伞下的许多小项目。最终,您将能理解整个系统大的图景。
工作说明:业务服务器
业务服务器(Business Server)是一个假想的项目。出于我们讨论问题的目的,它的功能是处理销售订单,自动向客户发出账单,并管理存货控制。销售订单到达时必须得到处理,通过某种类型的事务系统。在销售订单填写30天后,服务器自动向客户发出账单。库存由服务器和工厂或仓库的当前库存量同时来管理。该业务服务器将是一个分布式系统,每个服务器将通个一个消息服务来与其它服务器通信。
发现服务
我们将使用这个Business Server项目来发现服务。考虑上面的过于概括性的工作说明,我们立即可以看到项目描述中定义的一些服务。服务的清单可被分成两大类:显式的服务(可以从工作说明中直接导出的服务)和隐式的服务(根据相似的工作发现的服务,或对显示服务起支持作用的服务)。请注意,实现系统的公司不必亲自开发所有的服务 ——有一些可作为商业解决方案购买。在那些情况下,我们可能会开发一个封装层(wrapper),以便我们能够以确定的方式与商业产品实现互操作。实现系统的公司将构建大部分的服务。显式的服务
从工作说明中,我们可以快速导出一些服务。但这种初始分析并不意味我们的工作已完成,因为某些服务的定义需要其它服务的存在才能保证。事务处理服务
工作说明明确指出“销售订单到达时必须得到处理 ”。这表明我们需要有一种机制能够接受销售请求并自动地处理它们。这与web服务器工作的方式相似。它们接收到对资源的请求,进行处理并返回一个结果(如 HTML页面)。这被称作是事务处理。完整地说起来,存在不同类型的事务处理。这种一般性的事务处理服务极有可能必须分解为一些更特殊的东西,如“销售订单处理器“。具体方法取决于您的服务的通用性。在可用性和可重用性之间存在一种平衡。服务越是通用,它就越可重用。通常它也就越难于理解。
调度服务
在某些情况下,当事务完成,过了一段特定的时间之后,必须调度某个事件。而且,库存控制过程必需能周期性地开出采购订单。因为工作说明中指出“ 在销售订单填写30天后,服务器自动向客户发出账单” ,所以我们需要一个调度服务。所幸的是Avalon Cornerstone为我们提供了一个,这样我们就不必再自己写一个了。
消息服务
工作说明中指出,在我们的分布式系统中“每个服务器将通个一个消息服务来与其它服务器通信“。让我们来考虑这个问题,有时用户需要一个特定的产品或一种他们想用的方法。消息服务是利用其它公司产品的一个首选例子。极有可能,我们将采用Java Messaging Service (JMS) 来作为Messaging Service的接口。因为JMS是一个标准,它的接口不大可能短期内发生变化。从实践经验上来说,一个定义良好的面向消息的系统在可扩展性方面要强于面向对象的系统(如EJB)。可扩展性更好的一个原因是消息通常并发内存开销较小。另一个原因是它更容易把消息处理的负载分散到所有服务器上去,而不是把所有的处理集中在少量的服务器集群上(甚至是在一台服务器上)。
库存控制服务
尽管这不是一个教科书上的经典服务,但它是这个系统的一项需求。库存控制服务固定地监视工厂或仓库存货的记录,当存货开始不足时触发一些事件。
隐式的服务
运用在过去系统中获得的经验,将系统更进一步分解出其它服务,将得到没有明确指出但又是系统需要的一些服务。因为篇幅关系,我们将不做全面地分解。认证和授权服务
认证和授权服务没有在工作说明中明确地提到,但是所有的业务系统必须认真考虑安全性。这意味着系统所有的客户端都必须经过认证,用户的所有行为都必须经过授权。
工作流自动化服务
工作流自动化是企业系统中的一个热门开发领域。如果您不使用第三方的工作流管理服务器,您就需要自己写一个。通常工作流自动化所做的是使用软件系统来安排贯穿公司业务过程的任务。更多的信息请参考Workflow Management Council的网站http://www.wfmc.org/。
文档中心服务
作为一个任务的当前状态信息,"文档中心"这个词的定义很不精确。换言之,当公司接到一份购买订单时,我们的系统需要能够存储并重新调出购买订单信息。出账单和系统中其它任何过程,从库存到新的用户请求都有同样的需求。
小结
我希望Business Server项目的服务的例子可以帮助您发现更多。您会发现,当您从较高的抽象层逐渐转向较低的抽象层时,会发现需要更多类型的服务,如用于在一个打开端口上处理请求的连接服务。我们定义的某些服务将通过第三方的系统来实现,如消息服务和工作流管理服务。对这些服务来说,使用一个标准接口是最符合您的利益的,这样您可以在以后更换供应商。有些服务实际上是由多个服务组成的大服务。有些服务Avalon Excalibur或Avalon Cornerstone中已经提供了。在发现一个系统中的服务时,应该牢记的一件事是:一个服务应该是一个高层子系统。这将有助于您通过分析师团队来定义组件。因为我们已识别出了主要的服务,您可以让多个个人(或团队)并行地分解每个服务。子系统边界也定义良好,发生重叠的可能性很小。如果您决定进行并行分析,您应该回过头来识别通用的组件,以便能够尽可能地重用。
UML Diagram for the Business Server
发现组件
我们将以前面提到的文档中心服务为例来说明识别合适的组件的过程。为讨论方便起见,我们现在来列出文档中心服务的需求。文档中心将采用一个数据库来作为持久存储,对客户端授权,在内存中缓存文档。组件的实践性定义
当我们谈论组件时,您考虑问题的角度应该是:我的服务需要操作哪些设施?Avalon相信将系统投影(cast)的概念。系统的开发者会面对一个组件的职责列表,组件则被称为它的角色(role)。什么是角色?
角色的概念来自剧院。一部戏剧、音乐剧或电影片都会有一定数量的角色,由演员来扮演。尽管演员似乎从不短缺,角色的数量却是有限的。演出的脚本定义了角色的功能或行为。如同剧院里发生的一样,脚本决定了您如何与组件交互。考虑系统中的不同角色,您会将组件的投影为角色,并与之对话。一个角色是一类组件的契约。例如,我们的文档中心服务需要操作数据库。Avalon Excalibur定义了一个组件,符合"Data Source"脚色的需要。在Excalibur中有两个不同的组件,都能满足该角色的需要。具体使用哪一个取决于服务所处的环境,但是它们都满足相同的契约。大量基于Avalon的系统对每个角色将只用到一个活动的组件。脚本就是工作接口:其它组件与之交互的接口。在确定组件的接口时,必须有确定的契约并牢记在心。契约规定了组件的使用者必须提供什么,以及组件生产出什么。有时在契约中必须包括使用语义。一个例子是临时存储组件和持久存储组件之间的区别。当接口和协议定义好之后,您就可以致力于实现。
怎样算是一个好的候选组件?
在我们的文档中心服务中,我们已识别了四个可能的组件:DataSourceComponent (来自Excalibur)、 Cache、Repository、Guardian。您应该寻求那些很可能有多种实现的角色,与这些实现的交互可以无缝地进行。通个这个例子,您会发现一些情况下您需要使用可替换的设施。大多数情况下,您只会用到该设施的一种实现,但您需要能独立地升级它而不影响到系统的其它部分。其它情况下,您需要根据环境的不同使用不同的实现。例如,Excaliber定义的"Data Source"通常会自己处理所有的JDBC连接池,但有时你可能希望利用Java 2 Enterprise Edition(J2EE)中提供的设施。Excalibur解决这个问题的办法是,一个"Data Source"组件直接管理JDBC连接和池,另一个组件使用Java's Naming and Directory Interface (JNDI) 来得到特定的连接。
怎样不算是一个好的组件?
习惯于使用JavaBeans的人喜欢把所有的东西都实现为一个 JavaBean。这意味着从数据模型到事务处理的一切东西。如果您用这种方式来处理组件,您可能会得到一个过于复杂的系统。把组件视为一个服务或设施的模型,而不是数据的模型。您可以有从其它资源拉数据的组件,但数据还是应该保持为数据。在Avalon Excalibur中这种哲学的一个例子是连接(Connection)不是一个组件。另一个例子是我们前面提到的Guardian组件。可能存在的争议是,Guardian所包含的逻辑与文档中心服务太相关,不能做为一个组件用在完全不同的服务中。尽管管理复杂性有多种方式,也有多种方式让它变得灵活,但有时为它付出额外的工作并不值得。在这种情况下,您必仔细权衡您的决定。如果一个潜在组件的逻辑将被一致地应用,那么将它作为一个组件可能是有意义的。在一个系统中可以有一个组件的多个实例,它们可以在运行时进行选择。如果潜在组件的逻辑只是根据另外一个组件来确定的,也许可以把这些逻辑放到另外的那个组件中去。通过Guardian组件和Repository组件的例子,我们可以辩称Guardian太专注于Repository,不是作为一个组件来实现的。
分解文档中心服务
我们将列出将要实现的组件,以及它们的角色、根本原因和来源(如果组件已经存在的话)。DocumentRepository
DocumentRepository 是整个服务的父组件。在Avalon中,服务实现为Block,Block是一个特定类型的组件。Block必须有一个工作接口,扩展了Service marker接口。Block接口也扩展了Avalon的Component接口。请注意,Block和Service是包含在Avalon Phoenix中的接口。最后,Service从技术上说仍是一种特定类型的Component。DocumentRepository是我们从持久存储中取得Document对象的方法。它与服务中的其它组件交互,以提供安全性、功能性和速度。这个特定的DocumentRepository会与数据库连接,在内部使用数据库的逻辑来建造Document对象。
DataSourceComponent
DataSourceComponent由Avalon Excalibur提供。它是我们获得有效的JDBC连接对象的方式。
Cache
Cache是一个短期内存中的存储设施。DocumentRepository将用它来保存Document对象,并通过一个散列算法来引用。为了提高Cache组件的可重用性,存储的对象必须实现一个Cacheable接口。
Guardian
Guardian组件的作用是基于参与者管理许可。Guardian将从数据库中装入许可规则集。Guardian将使用标准Java安全模型,以保证对特定Document的访问。
小结
到目前为止,您应该对怎样才算是一个好组件有一些认识了。例子描述了在文档中心服务中的所有组件,简要介绍了它们将完成的工作。快速浏览这个列表,它体现了将设施实现为组件而不是数据的方法。到目前为止,你应该能够确定您的服务需要操作什么组件。
框架和基础
我们将描述Avalon的契约和接口,为我们实际编写组件打下基础。
Avalon Framework是整个Avalon项目的中心部分。如果您理解了框架所定义的契约和结构,您就能理解任何利用该框架的代码。请记住我们已讨论过的原理和模式。在本部分中,我们将详细解释角色的概念在实践中是怎样起作用的,组件的生命周期以及接口是如何工作的。
定义组件的角色
在Avalon中,所有的组件都扮演一个角色。原因是您通过角色来获取您的组件。在这个舞台上,我们唯一要考虑的是角色的签名。回顾一下第二部分,我们把组件定义为"一个工作接口和该工作接口的实现的组合"。工作接口就是角色。创建角色的接口
下面您将看到一个接口的例子,以及一些最佳的实践和原因。package org.apache.bizserver.docs;public interface DocumentRepository extends Component{ String ROLE = DocumentRepository.class.getName(); Document getDocument(Principal requestor, int refId);}
最佳实践
· 包含一个名为"ROLE"的字符串,这是角色的正式名字。该名字与该工作接口的完全限定名称是一样的。这在今后我们需要得到一个组件的实例时会有帮助。· 如果有可能,请扩展组件接口。这会使您在发布组件时变得更容易。如果您不负责控制该工作接口,那么这点对你无用。问题也不会太大,因为您在发布时总可以将其强制转换为Component 的实例。· 做一件事并把它做好。组件的接口应该尽可能地简单。如果您的工作接口扩展了其它一些接口,就会把组件的契约给搞得难以理解。一个老的美国首字母缩写对这一点表述得很好:Keep It Simple, Stupid (KISS)。比自己更聪明(犯傻)并不难,我自己就干过几次。· 只确定您需要的方法。客户程序应该不知道任何实现细节,太多的可替换方法只会带来不必要的复杂性。换言之,选择一种方式并坚持不变。· 别让您的角色接口扩展任何生命周期或生存方式的接口。如果实现任何一个这样的类或接口,您就是在试图实现规范。这不是一个好模式,只会在将来带来调试和实现问题。
选择角色名称
在Avalon中,每个角色都有一个名称。它是您取得系统中其它组件引用的方式。Avalon开发团队已经概括了一些对角色命名的习惯方式。命名习惯方式
· 工作接口的完整限定名通常就是角色名。例外情况列在本通用规则的下面。在这个例子里,我们理论上的组件名称应该是 "org.apache.bizserver.docs.DocumentRepository"。这就是应该包含在您的接口的"ROLE"属性里的名字。· 如果我们通过一个组件选择器得到了该组件的引用,我们通常使用从第一条规则推导出的角色名称,在末尾加上单词"Selector"。这条命名规则的结果将是"org.apache.bizserver.docs.DocumentRepositorySelector"。您可以通过 DocumentRepository.ROLE + "Selector"来得到这个名称。· 如果我们有多个组件实现相同的工作接口,但用于不同目的,我们将分离角色。一个角色是组件在一个系统中的目的。每个角色名都将以最初的角色名开头,但表示角色目的的名字会以/${purpose}的形式附在后面。例如,对DocumentRePository 我们可以有如下的目的: PurchaseOrder(购买订单)和Bill(账单)。这两个角色可被分别表述为DocumentRepository.ROLE + "/PurchaseOrder"和DocuementRepository.ROLE + "/Bill"。
Framework接口概述
整个Avalon Framework可以被分成七个主要类别(根据API): Activity, Component, Configuration, Context, Logger, Parameters, Thread, and Miscellany。每一类(Miscellany除外)表示了一个考虑方向(concern area)。一个组件通常实现几个接口来标明它所关心的考虑方向。这使组件的容器能以一致的方式来管理每个组件。Avalon接口的生命周期
当一个框架实现了多个接口以分开考虑组件的各个方面时,存在搞不清方法调用次序的潜在可能性。Avalon Framework意识到了这一点,因此我们开发了事件生命周期次序的协定。如果您的组件不实现相关的接口,就简单跳到下一个要处理的事件。因为存在一个正确的创建和准备组件的方法,您可以在接收到事件时设置好组件。组件的生命周期被分成三个阶段:初始化阶段、活动服务阶段和销毁阶段。因为这些阶段是依次发生的,我们将依次讨论这些事件。另个,因为Java语言的原因,Construction和Finalization的行为是隐含进行的,所以跳过不谈。我们将列出方法名和所需的接口。在每个阶段中,会有一些由方法名标识的步骤。如果组件扩展了括号中指定的接口,这些步骤会依次执行。初始化阶段
以下的步骤依次发生,在组件生存期中只发生一次。1. enableLogging() [LogEnabled] 2. contextualize() [Contextualizable] 3. compose() [Composable] 4. configure() [Configurable] or parameterize() [Parameterizable] 5. initialize() [Initializable] 6. start() [Startable]
活动服务阶段
以下的步骤依次发生,但在组件的生存期中可能发生多次。请注意,如果您选择不实现Suspendable接口,那么您的组件有责任在执行任何re开头的步骤时保证正确的功能。1. suspend() [Suspendable] 2. recontextualize() [Recontextualizable] 3. recompose() [Recomposable] 4. reconfigure() [Reconfigurable] 5. resume() [Suspendable]
销毁阶段
以下的步骤依次发生,在组件生存期中只发生一次。 1. stop() [Startable] 2. dispose() [Disposable]
Avalon Framework契约
在本部分中,我们将按字母次序介绍所有内容,除了最重要的部分:Component,我们把它放在最前面。当我使用"容器"或"容纳"来描述组件时,我是有特殊含义的。我是指那些已经由父组件实例化并控制的子组件。我不是指通过ComponentManager或ComponentSelector得到的组件。更进一步,容器组件所接收到的一些Avalon步骤执行命令必须传播到它的所有子组件,只要这些子组件实现了相应的接口。特定的接口是指 Initializable、Startable、Suspendable和Disposable。这样安排契约的原因是这些接口有特别的执行约定。Component
这是Avalon Framework的核心。这个考虑方向所定义的接口会抛出ComponentException异常。Component
每个Avalon组件必须 实现Component接口。Component Manager和Component Selector只处理Component。这个接口没有定义方法。它只是作为一个标记性接口。任何组件必须使用不带参数的缺省构造方法。所有配置都是通过Configurable或Parameterizable接口来完成的。
Composable
一个用到其它组件的组件需要实现这个接口。这个接口只有一个方法compose(),该方法带唯一一个ComponentManager 类型的参数。围绕该接口的契约是:在组件的生存期中,compose()被调用一次且仅被调用一次。这个接口与其它任何定义了方法的接口一样,都使用的是反向控制模式。它由组件的容器调用,只有该组件需要的那些组件会出现在ComponentManager中。
Recomposable
在少数的情况下,组件会需要一个新的ComponentManager和新的组件-角色映射关系。在这些情况下,需要实现recomposable接口。它的方法也与Composable的方法名称不一样,是recompose()。围绕该接口的契约是:recompose() 方法可以被调用任意多次,但不能在组件完全初始化好之前调用。当该方法被调用时,组件必须以一种安全和一致的方式更新它自己。通常这意味着组件进行的所有操作必需在更新之间停下来,在更新之后再继续。
Activity
这组接口与组件生命周期的契约相关。如果在这组接口调用过程中出现了错误,您可以抛出一个通用的Exception。Disposable
如果一个组件需要以一种结构化的方式知道自己不在需要了,它可以使用Disposable接口。一旦一个组件被释放掉,它就不能再用了。实际上,它就等待着被垃圾收集。该接口只有一个方法dispose(),该方法没有参数。围绕该接口的契约是:dispose()方法被调用一次,也是组件生存期中调用的最后一个方法。同时也表明该组件将不再使用,必须释放该组件所占用的资源。
Initializable
任何组件如果需要创建其它组件,或需要执行初始化操作从其它的初始化步骤中获取信息,就要用到Initializable接口。该接口只有一个initialize()方法,该方法没有参数。围绕该接口的契约是:initialize()方法被调用一次,它是初始化过程中最后被调用的方法。同时也表明,该组件已处于活动状态,可以被系统中其它组件使用。
Startable
任何组件如果在其生存期中持续运行,就要用到Startable 接口。该接口定义了两个方法:start() 和stop()。这两个方法都没有参数。围绕该接口的契约是:start()方法在组件完全初始化之后被调用一次。stop() 方法在组件被销毁之前被调用一次。它们都不会被调用两次。start() 总在stop()之前被调用。对该接口的实现要求安全地执行start() 和stop() 方法 (不像Thread.stop()方法) 并且不会导致系统不稳定。
Suspendable
任何组件如果在其生命期中允许自己被挂起,就要用到Suspendable 接口。虽然它通常总是和Startable 接口在一起使用,但这不是必需的。该接口有两个方法:suspend() 和resume()。这两个方法都没有参数。围绕该接口的契约是:suspend() and resume() 可以被调用任意多次,但在组件初始化并启动之前,或在组件停止并销毁之后不能调用。 对已挂起的组件调用suspend() 方法,或对已在运行的组件调用resume() 将没有任何效果。
Configuration
这一组接口描述了配置方面的考虑。如果发生任何问题,例如没有所需的Configuration 元素,那么可以抛出ConfigurationException异常。Configurable
那些需要根据配置确定其行为的组件必须实现这个接口以得到Configuration 对象的实例。该接口有一个configure() 方法,只有一个Configuration 类型的参数。围绕该接口的契约是:configure() 方法在组件的生存期中被调用一次。传入的Configuration 对象一定不能为null。
Configuration
Configuration 对象是一由配置元素组成的树,配置元素拥有一些属性。在某种程度上,您可以将配置对象看作一个大大简化的DOM。Configuration类的方法太多,不便在本文中介绍,请看JavaDoc文档。您可以从Configuration 对象中取得String, int, long, float, or boolean类型的值,如果配置没有将提供缺省值。对属性也是一样的。您也可以取得子Configuration 对象。契约规定,具有值的Configuration 对象不应有任何子对象,对子对象也是这样的。你会注意到你无法得到父Configuration 对象。设计就是这样做的。为了减少配置系统的复杂性,大多数情况下容器会将子配置对象传给子组件。子组件不应该有权访问父配置的值。这种方式可能带来一些不便,但是Avalon团队在需要做折衷的情况下总是选择把安全性放在第一。
Reconfigurable
实现该接口的组件行为与 Recomposable 组件非常相似。它只有一个reconfigure()方法。这样的设计决策是为了降低Re开头的那些接口的学习难度。Reconfigurable 对Configurable 来说就象Recomposable 对Composable一样。
Context
Avalon中Context 的概念起源于需要一种机制来实现从容器向组件传递简单对象。确切的协议和名字绑定有意没有定义,以便给开者提供最大的灵活性。围绕Context 对象的使用的契约由您在您的系统中定义,尽管机制是一样的。Context
Context 接口只定义了一个get()方法。它有一个Object 类型的参数,返回以参数对象为键值的一个对象。Context 对象由容器组装,然后传递给子组件,子组件对Context只有读权限。除了Context 对子组件总是只读的之外,没有别的契约。如果您扩展了Avalon的Context,请注意遵守该契约。它是反向控制模式的一部分,也是安全性设计的一部分。另外,在Contex中传一个引用 给容器的做法也是不好的,原因和Context应该是只读的相同。
Contextualizable
希望接收来自于容器的Context对象的组件应该实现该接口。它有一个名为contextualize() 的方法,参数是容器组装的Context 对象。围绕这个接口的契约是:contextualize() 在组件的生存期中被调用一次,在LogEnabled 之后,但在其它初始化方法之前。
Recontextualizable
实现该接口的组件行为与Recomposable 组件非常相似。它只有一个名为recontextualize()的方法。这样的设计决策是为了降低Re开头的接口的学习难度。 Recontextualizable 对Contextualizable 就如同Recomposable 对Composable。
Resolvable
Resolvable 接口用于标识一些对象,这些对象在某些特定上下文中需要分离(need to be resolved)。一个例子是:某对象被多个Context 对象共享,并根据特定的Context改变其自身的行为。在对象被返回之前Context会调用 resolve() 方法。
Logger
每个系统都需要具备对事件记录日志的能力。Avalon内部使用了它的LogKit项目。尽管LogKit有一些方法可以静态地访问一个Logger实例,但Framework希望使用反向控制模式。LogEnabled
每个需要Logger实例的组件都要实现该接口。该接口有一个名为enableLogging() 的方法,将Avalon Framework的Logger 实例传递给组件。围绕该接口的契约是:在组件的生存期中只调用一次,在任何其它初始化步骤之前。
Logger
Logger 接口用于对不同的日志库进行抽象。它提供一个唯一的客户端API。Avalon Framework提供了三个实现该接口的封装类:针对LogKit的LogKitLogger 、针对Log4J的Log4jLogger 、和针对JDK1.4日志机制的Jdk14Logger 。
Parameters
Avalon认识到Configuration对象层次结构在许多场合下太重量级了。因此,我们提出了一个Parameters对象来提供Configuration 对象的替代,使用名称-值对的方法。Parameterizable
任何希望用Parameters 来取代Configuration 对象的组件将实现该接口。Parameterizable 只有一个名为parameterize()的方法,with the parameter being the Parameters object. 围绕该接口的契约是:它在组件的生存期中被调用一次。该接口与Configurable接口不兼容。
Parameters
Parameters 对象提供了一种通过一个String 类型的名字取得值的机制。如果值不存在,有方便的方法允许你使用缺省值,也可以得到Configurable 接口中任何相同格式的值。尽管Parameters 对象与java.util.Property 对象之间存在相似性,但它们还是存在重要的语义差别。首先,Parameters 是只读的。 其次,Parameters 总是很容易从Configuration 对象中导出。最后,Parameters 对象是从XML 片断中导出的,看上去是这样的:
Thread
线程标记器(marker)用于根据组件的使用发出容器的基本语义信息信号。它们考虑到线程安全,对组件的实现提供标记。最佳实践是把这些接口的实现推迟到最终实现该组件的类。这样做避免了一些复杂情况,有时某个组件被标记为ThreadSafe,但从它派生出来的组件实现去不是线程安全的。这个包中定义的接口组成了我们称之为LifeStyle系列接口的一部分。另一个LifeStyle接口是Excalibur包的一部分(所以它是这个核心接口集的扩展),Poolable定义在Excalibur的池实现中。 SingleThreaded
围绕SingleThreaded组件的契约是实现该接口的组件不允许被多个线程同时访问。每个线程需要有该组件的自己的实例。另一种做法是使用一个组件池,而不是每次请求该组件时都创建一个新的实例。为了使用池,您需要实现Avalon Excalibur的Poolable接口,而不是这个接口。
ThreadSafe
围绕ThreadSafe组件的契约是:不管有多少线程同时访问该组件,它们的接口和实现都能够正常工作。尽管这是一个有弹性的设计目标,但有时候由您所使用的技术,它就是不能实现。实现了这个接口的组件通常在系统中只有一个实例,其它的组件将使用该实例。
其它
在Avalon Framework的根包(root package)中的这些类和接口包括了Exception层次和一些通用工具类。但是有一个类值得一提。Version
JavaTM 版本技术是在jar包中的manifest文件中有一项指定。问题是,当jar被解包后您就失去了版本信息,并且版本信息放在易于修改的文本文件中。当您把这些问题与一个较陡的学习曲线放在一起考虑时,检查组件和接口的版本就比较困难。Avalon开发小组设计了Version对象,让您可以容易地检查版本和比较版本。您可以在您的组件中实现Version对象,测试合适的组件或最低版本号也会更容易。
实现梦想
我们将向你展示怎样使用Avalon Framework和Avalon Excalibur来实现您的服务应用程序。我们将向您展示Avalon有多么易于使用。
在您完成分析以后,您需要创建组成您的系统的组件与服务。如果Avalon只是描述了一些您可以使用的编程习惯,那么它的用处就不大。但即使是这样,运用这些编程习惯和模式也会对理解整个系统有所帮助。Avalon Excalibur提供了一些有用的组件和工具,您可以在您自己的系统中使用它们,这可以让您的日子过得更轻松些。作为我们的演示,我们把定义一个组件从一个repository中取出一个文档实现的全过程走一遍。如果您还记得我们关于理论上的业务服务器的讨论,我们曾确定将这个组件作为一个服务。在实际情况中,一个组件就是一个服务的情况是很多的。
实现该组件
这里,我们定义如何实现我们的组件。我们会把实现前面提到的DocumentRepository组件的过程整个走一遍。我们需要弄清楚的第一件事就是我们的组件所关注的领域。然后我们需要弄清楚怎样创建和管理我们的组件。选择关注的领域
我们在前面已经为DocumentRepository组件定义了角色和接口,我们已准备好来创建实现。因为DocumentRepository的接口只定义了一个方法,我们有机会创建一个线程安全的组件。这是最受欢迎的一类组件,因为它允许只消耗最少的资源。为了让我们的实现是线程安全的,我们确实需要仔细考虑如何实现该组件。既然我们所有的文档都存放在数据库中,而且我们希望使用一个外部的Guardian 组件,我们将需要访问其它组件。作为一个负责任的开发者,我们希望对有助于调试组件的信息记录日志,追踪内部发生了什么。Avalon框架的优美之处在于,您只需实现您需要的接口,可以忽略不需要的那些。这就是Separation of Concerns带来的好处。当您发现需要考虑一个新的方面时,您只需实现相关的接口就为组件加入了新的功能。对于使用您的组件的部分来说,不需要作任何改动。既然线程安全是一个设计目标,我们就已经知道了需要实现ThreadSafe接口。 DocumentRepository接口只有一个方法,所以对于该组件的工作界面的使用是满足该需求的。而且我们知道,Component在完全初始化之前是不会被使用的,在它被销毁之后也不会被使用。为了完成设计,我们需要实现一些隐含的接口。我们希望解决方案足够安全,让我们可能显式地得知组件是否已经完全初始化。为了达到这个目标,我们将实现Initializable和Disposable接口。由于关于环境方面的信息可能发生改变,或者可能需要能定制,我们需要让DocumentRepository实现Configurable接口,Avalon提供的取得所需组件的实例的方法是利用一个 ComponentManager。我们需要实现Composable 接口来从ComponentManager取得组件实例。因为DocumentRepository访问数据库中的文档,我们需要做一个决定。我们是要利用Avalon Excalibur DataSourceComponent呢,还是希望自己实现数据库连接管理的代码。在本文中,我们将利用DataSourceComponent。此时,我们的类骨架看起来是这样的:public class DatabaseDocumentRepositoryextends AbstractLogEnabledimplements DocumentRepository , Configurable, Composable, Initializable, Disposable, Component, ThreadSafe{ private boolean initialized = false; private boolean disposed = false; private ComponentManager manager = null; private String dbResource = null; /** * Constructor. All Components need a public no argument constructor * to be a legal Component. */ public DatabaseDocumentRepository() {} /** * Configuration. Notice that I check to see if the Component has * already been configured? This is done to enforce the policy of * only calling Configure once. */ public final void configure(Configuration conf) throws ConfigurationException { if (initialized || disposed) { throw new IllegalStateException ("Illegal call"); } if (null == this.dbResource) { this.dbResource = conf.getChild("dbpool").getValue(); getLogger().debug("Using database pool: " + this.dbResource); // Notice the getLogger()? This is from AbstractLogEnabled // which I extend for just about all my components. } } /** * Composition. Notice that I check to see if the Component has * already been initialized or disposed? This is done to enforce * the policy of proper lifecycle management. */ public final void compose(ComponentManager cmanager) throws ComponentException { if (initialized || disposed) { throw new IllegalStateException ("Illegal call"); } if (null == this.manager) { this.manager = cmanager; } } public final void initialize() throws Exception { if (null == this.manager) { throw new IllegalStateException("Not Composed"); } if (null == this.dbResource) { throw new IllegalStateException("Not Configured"); } if (disposed) { throw new IllegalStateException("Already disposed"); } this.initialized = true; } public final void dispose() { this.disposed = true; this.manager = null; this.dbResource = null; } public final Document getDocument(Principal requestor, int refId) { if (!initialized || disposed) { throw new IllegalStateException("Illegal call"); } // TODO: FILL IN LOGIC }}
您在以上代码中可以发现一些结构模式。当您在设计时考虑到安全性时,您应该在组件中显式地强制实现每个契约。安全强度总是取决于最弱的那一环。只有当您肯定一个组件已经完全初始化以后才能使用它,在它被销毁后,就再也不要用它了。我在这里放上这些逻辑是因为您在编写自己的类时也会采用相同的方式。
组件实例化和管理组件
为了让您能理解容器/组件的关系是如何工作的,我们将先讨论管理组件的手工方式。接下来我们将讨论Avalon's Excalibur组件体系结构是如何为您隐藏复杂性的。您仍会发现有些时候宁愿自己管理组件。但在大多数时候,Excalibur的强大能力和灵活性就能满足您的需要。The Manual Method
所有Avalon的组件都是在某处创建的。创建该组件的代码就是该组件的容器。容器负责管理组件从构造到析构的生命周期。容器可以有一个静态的"main"方法,让它能从命令行调用,或者它也可以是另一个容器。在您设计容器时,请记得反向控制的模式。信息和方法调用将只从容器流向组件。颠覆控制(Subversion of Control)
颠覆控制是反向控制的反模式。当您把容器的引用传递给组件时,就实现了颠覆控制。当您让一个组件管理它自己的生命周期时,也属于这种情况。以这种方式操作的代码应该被视为是有缺陷的。当您将容器/组件关系混在一起时,它们的交互将使系统难于调试,并难以审计安全性。
为了管理子组件,您需要在它们整个生命同期都保存对它们的引用。在容器和其它组件可以使用该子组件之前,它必须完成初始化。对我们的 DocumentRepository来说,代码看起来可能象下面的样子:class ContainerComponent implements Component, Initializable, Disposable{ DocumentRepository docs = new DatabaseDocumentRepository(); GuardianComponent guard = new DocumentGuardianComponent(); DefaultComponentManager manager = new DefaultComponentManager(); public void initialize() throws Exception { Logger docLogger = new LogKitLogger( Hierarchy.defaultHierarchy() .getLoggerFor( "document" ) ); this.docs.enableLogging( docLogger.childLogger( "repository" ) ); this.guard.enableLogging( docLogger.childLogger( "security" ) ); DefaultConfiguration pool = new DefaultConfiguration("dbpool"); pool.setValue("main-pool"); DefaultConfiguration conf = new DefaultConfiguration(""); conf.addChild(pool); this.manager.addComponent( DocumentRepository.ROLE, this.docs ); this.manager.addComponent( GuardianComponent.ROLE, this.guard ); this.docs.compose( this.manager ); this.guard.compose( this.manager ); this.docs.configure(conf); this.guard.initialize(); this.docs.initialize(); } public void dispose() { this.docs.dispose(); this.guard.dispose(); }}
为了简洁,我把显式地检查从以上代码中移去了。您可以看到手工地创建和管理组件是很细节化的工作。如果您忘记做了组件生命周期中的某一步,您就会发现bug。这也需要对您正在实例化的组件有一些深入的了解。另一种做法是给上面的ContainerComponent增加一些方法,来动态地处理组件的初始化。
Automated Autonomy
开发者的本性是懒惰的,所以他们会花时间写一个特别的ComponentManager 作为系统中所有组件的容器。通过这种方式,他们就不必深入地了解系统中所有组件的接口。这可能是个令人沮丧的任务。Avalon的开发者已经创建了这样一个怪兽。Avalon Excalibur的组件体系结构中包括了一个ComponentManager,通过XML的配置文件来控制。当您把管理组件的责任交给 Excalibur的ComponentManager时,存在一个折衷。您放弃了对CompomentManager中包含哪些组件的精细控制。但是,如果您的系统相当大,您会发现手工控制是一项令人沮丧的工作。在这种情况下,出于对系统稳定性的考虑,最好由一个地方集中式地管理系统中所有的组件。既然可以与Excalibur的组件体系结构有不同中层次的集成程度,我们将从最低层次开始。Excalibur有一组ComponentHandler对象,它们作为每类组件独立的容器。它们管理您的组件的整个生命周期。让我们引入生存方式(lifestyle)接口的概念。一个生存方式接口描述了系统是怎样对待一个组件的。既然组件的生存方式对系统运行会产生影响,我们就需要讨论当前的一些生存方式所暗含的意义: · org.apache.avalon.framework.thread.SingleThreadedo 不是线程安全的或可重用的。o 如果没有指定其它生存方式方式接口,系统就认为是这个。o 在每次请求组件时,都会创建一个全新的实例。o 实例的创建和初始化被推迟到请求组件时进行。· org.apache.avalon.framework.thread.Threadsafeo 组件是完全可重入的,并符合所有的线程安全的原则。 o 系统创建一个实例,所有Composable组件对它的访问是共享的。o 实例的创建和初始化是在ComponentHandler创建时完成的。· org.apache.avalon.excalibur.pool.Poolableo 不是线程安全的,但是是完全可重用的。o 创建一组实例放在池中,当Composable组件请求时,系统提供一个可用的。o 实例的创建和初始化是在ComponentHandler创建时完成的。ComponentHandler接口处理起来是很简单的。你通过Java类、Configuration对象、ComponentManager对象、Context对象和RoleManager对象来初始化构造方法。如果您知道您的组件将不需要上述的某一项,您可以在它的位置上传一个null。在这之后,当您需要对该组件的引用时,您就调用"get"方法。当您用完之后,您调用"put"方法将组件归还给ComponentHandler。以下的代码便于我们理解这一点。class ContainerComponent implements Component, Initializable, Disposable{ ComponentHandler docs = null; ComponentHandler guard = null; DefaultComponentManager manager = new DefaultComponentManager(); public void initialize() throws Exception { DefaultConfiguration pool = new DefaultConfiguration("dbpool"); pool.setValue("main-pool"); DefaultConfiguration conf = new DefaultConfiguration(""); conf.addChild(pool); this.docs.configure(conf); this.docs = ComponentHandler.getComponentHandler( DatabaseDocumentRepository.class, conf, this.manager, null, null); this.guard = ComponentHandler.getComponentHandler( DocumentGuardianComponent.class, null, this.manager, null, null); Logger docLogger = new LogKitLogger( Hierarchy.defaultHierarchy() .getLoggerFor( "document" ) ); this.docs.enableLogging( docLogger.childLogger( "repository" ) ); this.guard.enableLogging( docLogger.childLogger( "security" ) ); this.manager.addComponent(DocumentRepository.ROLE, this.docs); this.manager.addComponent(GuardianComponent.ROLE, this.guard); this.guard.initialize(); this.docs.initialize(); } public void dispose() { this.docs.dispose(); this.guard.dispose(); }}
这里,我们只少写了几行代码。我们还是手工地创建了Configuration对象,还是设置了Logger,还是不得不对ComponentHandler对象进行初始化和销毁。这里我们所做的只是防止受到接口变化的影响。您会发现用这种方式对代码有好处。Excalibur所做的更进了一步。大多数复杂的系统都有一些配置文件。它们允许管理员调整关键的配置信息。 Excalibur可以用以下的格式读取配置文件,并从中创建系统的组件。
根元素可以由您任意指定。您会注意到我们已经定义了一些组件。我们有了熟悉的DocumentRepository类和GuardianComponent类,以及一些Excalibur DataSourceComponent类。 而且,现在我们对Guardian组件有了一些特定的配置信息。为了把这些系统读入您的系统,Avalon框架为您提供了一些方便:DefaultConfigurationBuilder builder = new DefaultConfigurationBuilder();Configuration systemConf = builder.buildFromFile("/path/to/file.xconf");
这确实对我们前面手工构建配置元素的代码起到了简化作用,而且它限制了我们在编程时需要明确了解的信息。 让我们再看一眼Container类,看看是否真的省了一些事。记住我们指定了5个组件( ComponentSelector算作是一个组件), 以及每个组件的配置信息。class ContainerComponent implements Component, Initializable, Disposable { ExcaliburComponentManager manager = new ExcaliburComponentManager(); public void initialize() throws Exception { DefaultConfigurationBuilder builder = new DefaultConfigurationBuilder(); Configuration sysConfig = builder.buildFromFile("./conf/system.xconf"); this.manager.setLogger( Hierarchy.getDefaultHierarchy() .getLoggerFor("document") ); this.manager.contextualize( new DefaultContext() ); this.manager.configure( sysConfig ); this.manager.initialize(); } public void dispose() { this.manager.dispose(); }}
难道不令人惊奇?我们对数量超过两倍的组件进行了初始化,而代码量减少了一倍多(6行代码,而不是13行)。这个配置文件有一个缺点,看起来有点疯狂,但它将需要写的代码数量降到了最低。在ExcaliburComponentManager的背后发生了很多的活动。对配置文件中的每个"component"元素,Excalibur为每个类的条目(entry)创建了一个ComponentHandler,并建立起与角色(role)的对应关系。 "component"元素和它的所有子元素是对组件的配置。当组件是一个ExcaliburComponentSelector时, Excalibur会读入每个"component-instance"元素并执行和前面同类型的操作,这次是与hint entry建立对应关系。让配置文件好看一些
我们可以使用别名来改变配置文件的外观。Excalibur使用一个RoleManager为配置系统提供别名。RoleManager可以是您专门创建的一个类,也可以使用DefaultRoleManager并传入一个Configuration对象。如果我使用DefaultRoleManager,我将把角色配置文件和系统的其它部分藏在jar文件中。这是因为角色配置文件将只由开发者改动。下面是RoleManager接口:interface RoleManager{ String getRoleForName( String shorthandName ); String getDefaultClassNameForRole( String role ); String getDefaultClassNameForHint( String hint, String shorthand );}
让我们来看一下Excalibur是如何使用我们的框架中的RoleManager的。首先,Excalibur循环读入根元素的所有子元素。这包括了所有的"component"元素,但这次Excalibur并不识别元素的名称,它询问RoleManager 对这个组件我们将使用什么角色。如果RoleManager返回null, 那么该元素和它所有的子元素都被忽略。接下来, Excalibur 从角色名称中导出类名。最后的方法是动态地将类名与ComponentSelector的子类型对应起来。Excalibur提供了一个 RoleManager的缺省实现,它使用一个XML配置文件。标记相当简单,它把所有您不希望管理员看到的附加信息都隐藏起来的。
为了使用RoleManager,您需要改变容器类中的"初始化"方法。您将使用配置构造器(configuration builder)通过这个文件来构造一个Configuration树。请记住,如果您打算使用一个RoleManager,您必须在调用"configure"方法之前调用"setRoleManager"方法。为了展示您如何从类装入器中取得这个XML文件,我将在下面展示该技巧:DefaultConfigurationBuilder builder = new DefaultConfigurationBuilder();Configuration sysConfig = builder.buildFromFile("./conf/system.xconf");Configuration roleConfig = builder.build( this.getClass().getClassLoader() .getResourceAsStream("/org/apache/bizserver/docs/document.roles"));DefaultRoleManager roles = new DefaultRoleManager();roles.enableLogging(Hierarchy.getDefaultHierarchy().getLoggerFor("document.roles"));roles.configure(roleConfig);this.manager.setLogger( Hierarchy.getDefaultHierarchy() .getLoggerFor("document") );this.manager.contextualize( new DefaultContext() );this.manager.setRoleManager( roles );this.manager.configure( sysConfig );this.manager.initialize();
既然我们增加了6行代码,就要看一下它带来了什么好处。我们最终的配置文件可以这样写:
正如您所看到的那样,与前面的文件相比,这个文件的可读性要强很多。 现在我们可以为系统增加任意数目的组件,而不需要写更多的代码来支持它们。
使用该组件
现在我们已经创建了我们的组件,我们将使用它们。不管组件是如何被初始化和管理的,您访问组件的方法都是一样的。您必须实现Composable接口,这样才能从ComponentManager得到一个引用。 ComponentManager保存着所有您需要的组件的引用。为了讨论方便起见,我们将假定我们得到的ComponentManager 是按照前一节的最终的配置文件来配置的。 这就是说我们有一个Repository, 一个Guardian, 和两个DataSources。使用组件管理基础结构的原则
组件管理基础结构要求您释放您得到引用的组件。这个限制的原因是为了能正确地管理组件资源。ComponentManager的设计考虑到您对特定的角色有不同类型的组件。 ComponentSelector的另一个独特的方面是它也被设计为一个组件。这使我们可以从一个ComponentManager取得一个 ComponentSelector。有两种合法的方式来处理对外部组件的引用。您可以在初始化过程中得到引用,在销毁时释放它们。您也可以把处理组件的代码放在try/catch/finally语句块中。两种方法各有优缺点。Initialization and Disposal Approach
class MyClass implements Component, Composable, Disposable{ ComponentManager manager; Guardian myGuard; /** * Obtain a reference to a guard and keep the reference to * the ComponentManager. */ public void compose(ComponentManager manager) throws ComponentException { if (this.manager == null) { this.manager = manager; myGuard = (Guardian) this.manager.lookup(Guardian.ROLE); } } /** * This is the method that uses the Guardian. */ public void myMethod() throws SecurityException { this.myGuard.checkPermission(new BasicPermission("test")); } /** * Get rid of our references */ public void dispose() { this.manager.release(this.myGuard); this.myGuard = null; this.manager = null; }}
从示例代码中您可以看到,照这样做很容易。当该对象第一次接收到 ComponentManager时,它取得了一个Guardian组件的引用。如果您可以保证Guardian组件是线程安全的(实现了ThreadSafe接口),那么就只需要做这些事。不幸的是,从长远来看,您不能保证这一点。为了能正确地管理资源,在用完组件之后,我们必须释放对组件的引用。这就是为什么我们保持一个对ComponentManager的引用的原因。这种方式的主要不利之处在于处理组件池中的组件时。对组件的引用维系着该组件的生命。如果该对象的生存期很短,这可能不是个问题;但是如果该对象是一个由Excalibur组件管理体系结构所管理的组件,只要有对它的引用,它的生存期就会继续。这意味着我们实际上把组件池变成了一个组件工厂。这种方式的主要好处是,得到和释放组件的代码很清楚。您不必去理解异常处理的代码。另一个细微差别是,您把Guardian的存在与初始化这个对象的能力捆绑在了一起。一旦在一个对象的初始化阶段抛出一个异常,你就只好认为该对象不是一个有效的对象。有时您希望当要求的组件不存在时就让程序失败掉,那么这就不是问题。在设计组件时,您确实需要注意到这一层隐含的意思。
Exception Handling Approach
class MyClass implements Composable, Disposable{ ComponentManager manager; /** * Obtain a reference to a guard and keep the reference to * the ComponentManager. */ public void compose(ComponentManager manager) throws ComponentException { if (this.manager == null) { this.manager = manager; } } /** * This is the method that gets the Guardian. */ public void myMethod() throws SecurityException { Guardian myGuard = null; try { myGuard = (Guardian) this.manager.lookup(Guardian.ROLE); this.criticalSection(myGuard); } catch (ComponentException ce) { throw new SecurityException(ce.getMessage()); } catch (SecurityException se) { throw se; } finally { if (myGuard != null) { this.manager.release(myGuard); } } } /** * Perform critical part of code. */ public void criticalSection(Guardian myGuard) throws SecurityException { myGuard.checkPermission(new BasicPermission("test")); }}
如您所见,这段代码有些复杂。为了能理解它,您需要理解异常处理。这可能不是问题,因为绝大多数Java开发者都知道如何处理异常。 用这种方式,您不需要太担心组件的生存方式问题,因为一旦我们不需要它时,就释放了它。这种方式的主要不利之处是增加了异常处理代码,因为较为复杂。为了能将复杂度降到最低,让代码更易于维护,我们把工作代码提取出来,放在另一个方法中。请记住在try语句块中,我们想得到多少组件的引用,就可以得到多少。这种方式的主要好处是您可以更有效率地管理组件引用。同样,如果您使用的是ThreadSafe组件,就没有实质差别,但如果您使用的是组件池里的组件时,就有差别了。在每次您使用一个组件,取得一个新的引用时,有一点轻微的开销,但是被迫创建一个新的组件实例的可能性大大降低。像初始化和销毁的方式一样,您也必须了解一个微妙的差别。 如果管理器找不到组件,异常处理的方式不会让程序在初始化时失败掉。 如前所述,这并不是完全没好处的。很多时候,您希望某个组件存在,但是如果期望的组件不存在,程序也不需要失败掉。
从ComponentSelector取得一个组件
对于大多数操作,您只需要使用ComponentManager。 既然我们决定我们需要DataSourceComponent的多个实例,我们就需要知道如何得到我们想要的那个实例。 ComponentSelector比ComponentManagers要稍复杂一些,因为处理时有暗示要取得想要的引用。 一个组件属于一个特定的角色,这我们已经说得很清楚了。 但是,有时候我们需要从一个角色的多个组件中选择一个。 ComponentSelector使用一个任意的对象来作为暗示。大多数时候,这个对象是一个String,尽管您可能会希望使用一个Locale对象来取得一个正确国际化的组件。在我们已建立起来的系统中,我们选择使用字符串来选择DataSourceComponent的正确实例。我们甚至给了自己一个Configuration元素,来指明为了得到正确的组件,需要的字符串是什么。这是一个好的实践,可以照着做,因为它使系统管理更容易。比起要系统管理员记住这些神奇的配置值来,这便他们更容易看到对其它组件的引用。从概念上来看,从ComponentSelector取得一个组件与从 ComponentManager取得组件并无差别。 你只多了一个步骤。 请记住ComponentSelector也是一个组件。当您查找ComponentSelect的角色时,ComponentManager 将准备好ComponentSelector组件并返回给您。然后您需要通过它来选择组件。 为了说明这一点,我将扩展前面讨论的异常处理方式的代码。public void myMethod() throws Exception{ ComponentSelector dbSelector = null; DataSourceComponent datasource = null; try { dbSelector = (ComponentSelector) this.manager.lookup(DataSourceComponent.ROLE + "Selector"); datasource = (DataSourceComponent) dbSelector.select(this.useDb); this.process(datasource.getConnection()); } catch (Exception e) { throw e; } finally { if (datasource != null) { dbSelector.release(datasource); } if (dbSelector != null) { this.manager.release(dbSelector); } }}
您可以看到,我们通过使用指定组件的角色得到了 ComponentSelector的引用。 我们遵守了前面提到的角色命名规范,在角色名的后名加上"Selector"作为后缀。您可以使用一个静态的接口来处理系统小所有的角色名,以减少代码中字符串连接的次数。这样做也是完全可以接受的。接下来,我们得从 ComponentSelector到了DataSource组件的引用。我们的示例代码假定我们已经从Configuration对象取得了所需的信息并把它放在一个名为"useDb"的类变量中。
Excalibur的工具类
最后这一节是向您介绍Apache Avalon Excalibur带的几类组件和工具类。 这些工具类是健壮的, 完全可以在实际生产系统中使用。我们有一个非正式的分级式项目称为"Scratchpad",在这个项目中,我们解决潜在新工具类的实现细节问题。 Scratchpad中的工具类的品质各有不同,它们的使用方式也不能保证不变,尽管您可能觉得用起来不错。 命令行接口(Command Line Interface,CLI)
CLI工具类在一些项目中使用,包括Avalon Phoenix和Apache Cocoon,用于处理命令行参数。它提供了打印帮助信息的机制,并能够以短名字或长名字的方式来处理参数选项。
集合工具类
集合工具类对JavaTM Collections API进行了一些增强。 这些增强包括在两个list中找出交叉处的功能和一个PriorityQueue ,它是对Stack 的增强,允许对象的优先级改变简单的先进后出的Stack实现。
组件管理
我们已经在前面讨论了这方面的用法。这是Excalibur中最复杂的怪兽,但是它在很少的几个类中提供了很多的功能。在简单的 SingleThreaded 或ThreadSafe两种管理类型之外,还有一种Poolable类型。如果一个组件实现了Excalibur的Poolable接口,而不是 SingleThreaded接口, 那么它将维护一个组件池并重用实例。大多数情况下这工作得很好。在少数组件不能重用的情况下,就使用SingleThreaded接口。
LogKit管理
Avalon开发团队意识到许多人需要一种简单的机制来创建复杂的日志目标层次结构。出于RoleManager类似的想法,团队开发了LogKitManager,可以被前面提到的Excalibur Component Management系统使用。基于"logger"属性,它将为不同的组件给出相应的 Logger 对象。
线程工具类
concurrent包提供了一些辅助多线程编程的类:Lock (mutex的实现), DjikstraSemaphore, ConditionalEvent, 和 ThreadBarrier.
Datasources
这是根据javax.sql.DataSource类设计的,但是作了简化。 DataSourceComponent有两个实现:一个显式地使用JDBC连接池,另一个使用J2EE 应用服务器的javax.sql.DataSource 类。
输入/输出 (IO) 工具类
IO工具类提供了一些FileFilter类以及File和IO相关的工具类。
池实现
Pool 实现在各种情况下都能使用的池。其中有一个实现非常快,但只能在一个线程中使用,用来实现FlyWeight模式是不错的。还有一个DefaultPool,它不管理池中对象的数目。 SoftResourceManagingPool在对象被归还时判断是否超过一个阈值,如果超过,就让对象“退役”。最后, HardResourceManagingPool在您达到对象的最大数目时会抛出一个异常。后面的三个池都是ThreadSafe的。
Property工具类
Property工具类与Context对象一起使用。它们使您可以扩展Resolvable对象中的"variables"。它是这样工作的:"${resource}" 将去寻找一个名为"resource"的Context的值,并用这个值来代替这个符号。
结论
Avalon经受了时间的考验,可以供您使用。本部分提供的证据可以帮助说服您自己和其它人,使用一个成熟的框架比自己创建一个要好。
您可能已经被说服了,但需要一些帮助来说服您的同事,让他们相信Avalon是正确的选择。 也许您也需要说服您自己。不管怎样,本章将帮助您整理思路,并提供一些有说服力的证据。 我们需要与对开放源代码模式的(Fear, Uncertainty, and Doubt ,FUD)作斗争。 关于开放源代码的有效性的证据,我推荐您阅读Eric S. Raymond对这一主题的优秀论述N400017. 不论您对他所持观点的看法如何,他写的文章汇编成的书The Cathedral and the Bazaar 将提供使人从总体上接受开放源代码的信息。
Avalon能工作
我们的底线是Avalon完成了它最初设计时要达到的目标。 Avalon没有引入新的概念和思想,而是使用了一些经受时间考验的概念,并将它们规范化。影响Avalon设计的最新的概念是分离考虑(Separation of Concerns)模式,它大约是在1995提出的。即使在那时候,分离考虑也是一种系统分析技术的规范化方法。Avalon的用户群数以百计。 一些项目如Apache Cocoon, Apache JAMES, 和Jesktop是建立在Avalon之上的。这些项目的开发者是Avalon Framework的用户。因为Avalon有如此众多的用户,它得到了很好的测试。由最优秀的人设计
Avalon的作者认识到,我们不是服务器端计算的唯一一群专家。我们使用了来自其它人的研究的概念和想法。我们响应来自用户的反馈。Avalon不仅仅是由前面介绍的5个开发者设计的,他们带来的是反向控制、分离考虑和面向组件编程的思想,并设计了它。开放源代码的优秀之处在于,得到的结果是最佳的思想和最佳的代码的融合。 Avalon经过一了些测试想法的阶段并拒绝了一些想法,因为还有更好的解决方案。您可以利用Avalon开发小组所获得的知识,并在您自己的系统中运用。您可以在自己的项目中使用Excalibur中预先定义好的组件,它们经过了测试,可以在重负载下运行无误。
兼容性的许可证
Apache Software License (ASL)可以兼容其它任何已知的许可证。 已知的最大例外是GNU Public License (GPL) 和Lesser GNU Public License (LGPL). 重要的是ASL对合作开发相当友好,如果您不想的话,也不会强迫您发布源代码。 Apache Software Foundation名下的德高望众的HTTP服务器用的是相同的许可证。
集群式的研发
大多数的Avalon用户以某种方式做出他们的贡献。这把开发,调试和文档的工作量分散到了一些用户身上。 这也表明Avalon的代码经过了更广泛的对等复查,这在公司开发中是不可能做到的。而且,Avalon的用户支持Avalon。 尽管开放源代码的项目通常没有一个帮助平台或电话支持热钱,但是我们有一个邮件列表。您的许多问题可以通过邮件列表得到很快的回答,比一些支持热线还要快。
简化的分析和设计
基于Avalon开发有助于开发者达到一种精神状态。 在这种精神状态下,开发者的工作集中在如何发现组件和服务上。既然关于组件和服务的生存期的细节问题都已得到分析和设计,开发者只需选择他们需要的就行了。需要重点指出的是,Avalon的开始并不是取代了传统的面向对象分析和设计,而是对它进行了增强。 您还是使用以前所用的技术,只不过现在您有了一组工具,能更快地实现您的设计。
Avalon已经就绪
Avalon Framework, Avalon Excalibur, 和 Avalon LogKit已经准备好让您使用了。它们已经成熟,只会变得越来越好。尽管Avalon Phoenix和Avalon Cornerstone正在紧锣密鼓地开发,但您在它们基础上写的代码将来只需做些不重要的改动就能工作。
Atas ialah kandungan terperinci 详细介绍 Avalonjs. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!