模块化Java:声明式模块化
副标题#e#
前一篇文章,《模块化Java: 动态模块化》描写了如何通过利用处事 (service)给应用措施带来动态模块化特性。它们是通过输出的一个(或多个 )可以在运行时被动态发明的接口而实现的。尽量这种方法使得client和server 完全解耦,可是又带来一个如何(何时)启动处事的问题。
启动顺序
在彻头彻尾的动态系统里,处事不只可以在系统运行的时候装卸,还可以以 差异的顺序启动。有时,这是个大问题:无论A和B的启动顺序如何,在系统到达 停当状态并筹备好吸收事件之前,假如没有事件(或线程)呈现,那么哪个处事 先启动都无大碍。
但是,有许多环境都不切合这一简朴假设。经典的例子就是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 也可以包括多个组件,利用沟通或差异的类,每个都提供差异的处事。
#p#副标题#e#
消费处事
要消费该处事,我们需要修改ShortenCommand,这样它就绑定到IShorten服 务的一个实例上:
#p#分页标题#e#
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:
Felix
Config Admin (org.apache.felix.configadmin-1.2.4.jar)
SCR Declarative Services (org.apache.felix.scr-1.2.0.jar)
Equinox:
org.eclipse.equinox.ds
org.eclipse.equinox.util
org.eclipse.osgi.services
#p#分页标题#e#
截至今朝,你应该已经熟悉安装和启动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实现,而且它们应能 透明地事情。