BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

模块化Java:声明式模块化

| 作者 Alex Blewitt 关注 4 他的粉丝 ,译者 宋玮 关注 0 他的粉丝 发布于 2010年2月10日. 估计阅读时间: 23 分钟 | QCon上海2018 关注大数据平台技术选型、搭建、系统迁移和优化的经验。

在模块化Java系列文章的第4篇里,我们将介绍声明式模块化,描述如何定义组件并将它们组织在一起,而无需依赖于OSGi API进行编程。

前一篇文章,《模块化Java: 动态模块化》描述了如何通过使用服务(service)给应用程序带来动态模块化特性。它们是通过输出的一个(或多个)可以在运行时被动态发现的接口而实现的。尽管这种方式使得client和server完全解耦,但是又带来一个如何(何时)启动服务的问题。

启动顺序

在彻头彻尾的动态系统里,服务不仅可以在系统运行的时候装卸,还可以以不同的顺序启动。有时,这是个大问题:无论AB的启动顺序如何,在系统达到就绪状态并准备好接收事件之前,如果没有事件(或线程)出现,那么哪个服务先启动都无大碍。

可是,有很多情况都不符合这一简单假设。经典的例子就是logging: 通常,服务在启动和做其他操作的时候,就要连接并开始写日志了。如果日志服务此时还不可用,那会有什么后果?

假定服务在运行时能够动态装卸,client应该能够应对服务不存在时的情况。在这种情况下,它也许能聪明地转移到另一种机制(如输出到标准输出),或者处于阻塞状态等待服务可用(对logging系统来说不是好的答案)。可是,让服务启动之前就可用是不切实际的。

启动级别

OSGi提供了一种机制来控制bundle启动时的顺序,即使用启动级别(start levels)。这一概念是基于UNIX运行级别的概念:系统以级别1启动,然后单调递增,直到达到目标启动级别。每个OSGi容器都提供了不同的默认目标级别:Equinox默认值是6;而Felix是1。

启动级别可被用来创建bundle间的启动顺序,让关键bundle服务(比如logging)的启动级别比那些需要用它的bundle更低。可是因为可 用的启动级别值是有限的,而且安装程序倾向于选择单一数字作为启动级别,因此它并不能确保你仅通过启动顺序就能解决问题。

另一点值得注意的是,具有相同启动级别的bundle是各自独立启动的(可能并行),因此,如果你有一个与log服务具有相同启动级别的bundle,谁也不能保证log服务能够在需要的时候已经就绪。换句话说,启动级别可以解决大部分问题,但不能解决所有问题。

声明式服务

解决这一问题的一个方案是OSGi的声明式服务(以下称为DS——declarative services)。用这一方法,各个组件是由外部bundle将他们组织在一起并决定他们什么时候可用。声明式服务是通过在一个XML配置文件组织在一起的,文件中描述了需要(消费)或提供什么服务。

上篇文章最后一个例子中,我们使用ServiceTracker去获得服务,如果必要则需等待服务可用。如果我们把创建shorten命令延迟到shortening服务可用之后会很有用。

DS定义了一个组件(component)概念,其是比bundle更细粒度的概念,但是比服务的概念粒度更大一些(因为一个组件可以消费/提供多个服务)。每个组件都有一个名字,对应一个Java类,并可以通过调用该类的方法使其激活或失效。与OSGi Java API不同,DS允许用纯Java POJO来开发组件,根本不需要从程序上依赖OSGi。其附带的好处是让DS更加易于测试和模拟(test/mock)。

为了说明这一方法,我们将继续使用前面的例子。我们需要两个组件:一个是shortening服务本身,另一个是调用它的ShortenComand。

第一项任务是用DS配置并注册shorten服务。我们可以让DS在服务启动时注册它,而不是通过Bundle-Activator注册该服务。

那么DS怎么知道要激活并连接谁呢?我们需要给Bundle的Manifest头增加一个条目,其指示了一个(或多个)XML组件定义文件。

Bundle-ManifestVersion: 2
...
Service-Component: OSGI-INF/shorten-tinyurl.xml [, ...]*

这个 OSGI-INF/shorten-tinyurl.xml组件定义文件内容如下:

<?xml version="1.0" encoding="UTF-8"?> 
<scr:component name="shorten-tinyurl" xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"> 
	<implementation class="com.infoq.shorten.tinyurl.TinyURL"/>
	<service> 
		<provide interface="com.infoq.shorten.IShorten"/> 
	</service> 
</scr:component>

当DS处理这一组件时,其效果与代码context.registerService( com.infoq.shorten.IShorten.class.getName(), new com.infoq.shorten.tinyurl.TinyURL(), null );基本一样。Trim()服务需要类似的声明,在下面的源代码中包含着这部分内容。

如果需要的话,一个单一组件可以基于不同接口提供多个服务。一个bundle也可以包含多个组件,使用相同或不同的类,每个都提供不同的服务。

消费服务

要消费该服务,我们需要修改ShortenCommand,这样它就绑定到IShorten服务的一个实例上:

package com.infoq.shorten.command;

import java.io.IOException;
import com.infoq.shorten.IShorten;

public class ShortenCommand {
	private IShorten shorten;
	protected String shorten(String url) throws IllegalArgumentException, IOException {
		return shorten.shorten(url);
	}
	public synchronized void setShorten(IShorten shorten) {
		this.shorten = shorten;
	}
	public synchronized void unsetShorten(IShorten shorten) {
		if(this.shorten == shorten)
			this.shorten = null;
	}
}
class EquinoxShortenCommand extends ShortenCommand {...}
class FelixShortenCommand extends ShortenCommand {...}

注意,不像上一次,这次没有对OSGi API产生依赖;mock一个实现来检验其是否工作正常也很轻松。那个synchronized修饰符确保了在服务get/set时不会产生竞争情况。

为了告诉DS需要把IShorten服务实例绑定到我们的EquinoxShortenCommand组件上,我们需要定义其所需的服务。当DS实例化你 的组件时(用默认构造器),它将通过调用定义在bind属性里的方法(setShorten())来设置IShorten服务。

<?xml version="1.0" encoding="UTF-8"?> 
<scr:component name="shorten-command-equinox" xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"> 
	<implementation class="com.infoq.shorten.command.EquinoxShortenCommand"/>
	<reference
		interface="com.infoq.shorten.IShorten"
		bind="setShorten" 
		unbind="unsetShorten" 
		policy="dynamic"
cardinality="1..1"
/> <service>
<provide interface="org.eclipse.osgi.framework.console.CommandProvider"/>
</service>
</scr:component>

无论bundle的启动顺序如何,一旦IShorten服务可用,该组件就将被实例化并连接到这个服务。有关策略(policy)、基数性(cardinality)和服务(service)的内容在下一节再做解释。

策略和基数性

策略(policy)可被设为static或dynamic。static策略表示一旦设置,服务不会变化。如果服务不可用了,组件也就失效了;如果一个新服务出现,那么就创建一个新的实例,并将该服务重新绑定。这显然比我们就地更新服务要费劲得多。

使用dynamic策略,当IShorten服务改变时,DS将对新服务调用setShorten(),随后对老服务调用unsetShorten()。

DS在unset之前调用set的原因是维持服务持续性。如果替换服务时先调用unset,shorten服务就有可能短暂为null。这也就是为什么unset方法还带个参数,而不是把服务设置为null的原因。

服务的基数性(cardinality)默认为1..1,其可取下列值之一:

  • 0..1 可选的,最多1个
  • 1..1 强制的,最多1个
  • 0..n 可选的,多个
  • 1..n 强制的,多个

如果不满足基数性(例如,设置为强制,但是没用shortening服务),那么组件是失效的。如果需要多个服务,那么每个服务都调用一次setShorten()。相反,对每个要卸载的服务都要调用unsetShorten()。

这里并没有展示组件在进入运行状态时对每个实例进行定制的能力。

在DS 1.1里,组件元素也有activate和deactivate属性,在组件激活(启动)和失效(停止)过程中相应方法被调用。

最后,这一组件还提供一个CommandProvider服务的实例。这是一个Equinox特定的服务,允许提供控制台命令,而这以前是在bundle的Activator中实现的。这种模式的好处是,只要依赖服务可用,CommandProvider服务将自动被发布;除此之外,代码本身不需要依赖任何OSGi API。

还需要针对Felix特定实现采用类似解决方案;因为到目前为止,OSGi command shell还没有标准。OSGi RFC 147是一个正在进行中的规范,允许命令在不同控制台执行。我们的例子源代码中包含了shorten-command-felix组件的完整定义。

启动服务

上面所述方法让我们可以以任何顺序供给(及消费)shortening服务。一旦command服务启动了,它将绑定到可用的最高优先级的 shortening服务上;或者,如果没有指定优先级,则绑定到拥有最低服务级别的服务上。我们现在不去考虑次高优先级服务随后是否应该被启动,而是继 续使用目前已绑定到的服务。可是,如果服务卸载,我们就要重新绑定,以维持最高优先级shortening服务对client不会中断。

为运行这个例子,这两个平台都需要下载并安装一些额外的bundle:

截止目前,你应该已经熟悉安装和启动bundles的过程了;如果没有,请参考静态模块化那篇文章。我们需要安装上述bundle,以及我们的shortening服务。下面是在Equinox环境下的操作过程,其中bundle放在/tmp目录下:

$ java -jar org.eclipse.osgi_* -console
osgi> install file:///tmp/org.eclipse.osgi.services_3.2.0.v20090520-1800.jar
Bundle id is 1
osgi> install file:///tmp/org.eclipse.equinox.util_1.0.100.v20090520-1800.jar
Bundle id is 2
osgi> install file:///tmp/org.eclipse.equinox.ds_1.1.1.R35x_v20090806.jar
Bundle id is 3
osgi> install file:///tmp/com.infoq.shorten-1.0.0.jar
Bundle id is 4
osgi> install file:///tmp/com.infoq.shorten.command-1.1.0.jar
Bundle id is 5
osgi> install file:///tmp/com.infoq.shorten.tinyurl-1.1.0.jar
Bundle id is 6
osgi> install file:///tmp/com.infoq.shorten.trim-1.1.0.jar
Bundle id is 7
osgi> start 1 2 3 4 5
osgi> shorten http://www.infoq.com
...
osgi> start 6 7
osgi> shorten http://www.infoq.com
http://tinyurl.com/yr2jrn
osgi> stop 6
osgi> shorten http://www.infoq.com
http://tr.im/HCRx
osgi> stop 7
osgi> shorten http://www.infoq.com
...

当我们安装并启动我们的依赖后(包括shorten命令),shorten命令仍不能在控制台显示结果。只有当我们启动针对shorten命令所注册的shortening服务时才行。

当地一个shortening服务停止时,实现自动转移至第二个shortening服务。第二个服务也停掉的话,shorten command服务则自动清除注册。

注意

声明式服务让连接OSGi服务更加容易。可是还有几点需要注意。

  • DS bundle需要安装并启动,以把组件连接起来。这样,DS bundle作为OSGi框架启动部分的一部分来安装,比如Equinox的osgi.bundles或Felix的felix.auto.start
  • DS通常有其他依赖需要安装。以Equinox为例,要包括equinox.util bundle。
  • 声明式服务是OSGi Compendium Specification的 一部分,而不是核心规范的一部分,因此对于服务接口通常需要由一个独立的bundle提供。在Equinox环境下,是由osgi.services提 供,但在Felix环境下,接口由SCR(Service Component Registry——服务组件注册)bundle自身输出。
  • 声明式服务可以用properties来配置。通常利用OSGi Config Admin服务;尽管这是可选的。因此DS的有些部分需要运行Config Admin;实际上,Equinox 3.5有一个bug,如果要用Config Admin,它需要在DS(Declarative Services)之前启动。这往往要求使用start-up 属性,以确保满足正确的依赖。
  • OSGI-INF目录(与XML文件一起)需要被包含进bundle中,否则DS看不到它。你还需要确保Service-Component头在bundle的manifest中存在。
  • 还可能要用Service-Component: OSGI-INF/*.xml来包含所有组件而不是逐个罗列其名字。这也允许fragment给一个bundle增加新组件。
  • bind和unbind方法需要synchronized以避免潜在的竞争情况出现,尽管在AtomicReference之上使用compareAndSet()还可以被用作单个服务的non-synchronized占位符。
  • DS组件不需要OSGi接口,这样,它可以在其他控制反转模式(如Spring)里被模拟来测试或使用。可是Spring DM 和OSGi Blueprint服务都可用来组织服务,这就留作将来的话题吧。
  • DS 1.0 没有定义默认的XML命名空间;DS 1.1 增加了 http://www.osgi.org/xmlns/scr/v1.1.0命名空间。如果文件中没有出现命名空间,就认为其兼容DS 1.0。

总结

本文中,我们讨论了如何将我们的实现与OSGi API解耦,并使用哪些组件的声明式描述。声明式服务提供了组织组件和注册服务的能力,帮助避免启动顺序依赖。另外,动态本质意味着当我们的依赖服务起停时,组件/服务也随之起停。

最后,无论使用DS还是手动管理服务,都使用的是相同的OSGi服务层以便通信。因此,一个bundle可以通过手动方法提供服务,另一个可以用声明式服务来消费它(反之亦然)。我们应能够混合并匹配1.0.0和1.1.0实现,并且它们应能透明地工作。

本文所讲例子的可安装bundle罗列如下(包含源代码):

查看英文原文Modular Java: Declarative Modularity


感谢崔康对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家加入到InfoQ中文站用户讨论组中与我们的编辑和其他读者朋友交流。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

相当好 by qi qi

思路清晰 描述到位 辛苦了!

模块化、插件化的开源开发平台推荐JXADF by soft wmz

JXADF做为模块化、插件化的开源代表,做得相当不错,不论是在github中、还是OSGi社区中都相当活跃,详细参见:osgia.com

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

2 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT