java 模块化
在Modular Java系列的第四篇文章中,我们将介绍声明式模块化 。 我们将描述如何定义组件,然后将它们连接在一起,而又不依赖于OSGi API的程序设计。
上一期“ 模块化Java:动态模块化 ”描述了如何通过使用服务为应用程序带来动态模块化。 这些是导出一个(或多个)可以在运行时动态发现的接口的实现。 尽管这允许客户端和服务器之间完全解除耦合,但是这引发了服务如何(以及何时)启动的问题。
开始订购
在一个完全动态的系统中,服务不仅可以随着系统的运行来来去去,而且还可以按不同的顺序启动。 有时候,这不是一个大问题。 无论A和B之间的启动顺序如何,如果直到系统处于稳定状态并准备好接受事件之前才真正发生任何事件(或线程),那么先启动哪个服务都没有关系。
但是,有许多方法可以违反此简单假设。 典型的例子是日志记录。 通常,在启动和其他操作期间,服务将连接到日志服务并开始写入日志服务。 如果日志服务不可用,会发生什么?
鉴于服务可以在运行时动态地来来去去,因此当服务不存在时,客户端应该能够应对。 在那种情况下,明智的做法是退回到另一种机制(例如将输出打印到stdout)或阻塞以等待服务变得可用(这不太可能是日志系统的正确答案)。 但是,在启动之前确保有可用的服务将是理想的解决方案。
起始水平
OSGi提供了一种机制,可通过使用启动级别来控制启动时束的顺序。 这些基于UNIX运行级别的概念。 系统从级别1开始,然后单调递增,直到达到目标开始级别。 每个OSGi容器提供一个不同的默认目标启动级别。 对于Equinox,默认值为6,而对于Felix,默认值为1。
因此,可以通过将关键捆绑包服务(例如日志记录)置于比需要它的启动级别低的启动级别中,使用启动级别在捆绑包之间创建排序。 但是,由于只能使用有限数量的启动级别,并且安装程序倾向于选择单位数字作为启动级别,因此不能保证仅通过启动顺序即可解决问题。
值得观察的另一点是,同一启动级别的捆绑包是独立启动的(并且可能同时启动),因此,如果您的捆绑包具有与日志服务相同的启动级别,则无法保证它将在预期的情况下进行连接。 换句话说,开始级别适合解决大问题,但不一定适合所有问题。
声明式服务
解决此问题的一种方法是OSGi的声明服务 ,以下称为DS。 在这种方法中,组件在可用时通过外部捆绑线连接在一起。 声明式服务按照在单个XML配置文件中定义的方式连接在一起,该文件声明了需要(消耗)和提供的服务。
在我们的最后一个示例中 ,我们使用ServiceTracker
来获取,并在必要时等待服务可用。 如果我们延迟创建shorten
命令直到缩短服务可用,它将更加有用。
DS定义了component
的概念,该component
粒度比捆绑软件的粒度更细,但粒度比服务的粒度更大(因为组件可能消耗/提供多种服务)。 每个组件都有一个名称,对应于Java类,并且可以通过调用该类的方法来激活或停用。 与OSGi Java API不同,DS允许将组件开发为纯Java POJO ,而完全不依赖于OSGi。 这具有使DS易于测试/模拟的附带优势。
为了演示该方法,我们将在前面使用示例。 我们需要两个组件; 其中之一将是起酥油服务本身,而另一个将是调用它的ShortenComand
。
第一项任务是在DS中配置和注册缩短服务。 除了通过Bundle-Activator
注册服务外,我们还可以要求DS在组件启动时对其进行注册。
那么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 );
大致相同context.registerService( com.infoq.shorten.IShorten.class.getName(), new com.infoq.shorten.tinyurl.TinyURL(), null );
。 Trim()
服务将需要类似的声明,并且该声明包含在下面的源代码中。
如果需要,单个组件可以在不同的接口下提供多种服务。 捆绑包还可以包括使用相同或不同类的多个组件,每个组件都提供不同的服务。
消费服务
要使用该服务,我们需要修改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 voidsetShorten (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无关。 并且模拟实现以验证其正确工作将是微不足道的。 synchronized
修饰符可确保在设置服务时不存在争用条件。
要告诉DS我们需要绑定到EquinoxShortenCommand
组件的IShorten
服务的实例,我们需要定义它需要的服务。 当DS实例化您的组件(使用默认构造函数)时,它将通过调用bind
属性中定义的方法来连接IShorten
服务。 换句话说, setShorten()
。
<?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>
一旦IShorten
服务可用,该组件将被实例化并连接到该服务,而不管此捆绑包与其他捆绑包之间的开始顺序如何。 下一节将介绍有关策略,基数和服务提供的说明。
政策和基数
该策略可以是static
也可以是dynamic
。 static
策略意味着一旦设置,服务就不会更改。 如果服务消失,则该组件被停用; 如果有新服务到达,则创建一个新实例并重新绑定该服务。 与我们可以就地更新服务相比,这显然是沉重的负担。
随着dynamic
策略,当IShorten
服务改变时,DS将调用setShorten()
与新服务,随后unsetShorten()
与旧的。
DS在unset
set
之前调用set
的原因是为了保持服务的连续性。 如果在替换服务时打进来的电话,并且首先调用了未unset
的服务,则shorten
服务可能会暂时null
。 这也是为什么unset
方法采用参数而不是仅将服务设置为null
。
服务的基数(默认为1..1
)是以下之一:
- 0..1可选,最多一个
- 1..1强制性,最多1个
- 0..n可选,很多
- 1..n强制性,很多
如果无法满足基数(例如,强制性的,则没有缩短服务),则该组件将被停用。 如果需要许多服务,则将对每个服务调用一次setShorten()
。 相反,将为每个消失的服务调用unsetShorten()
。
此处未显示组件在联机时按实例进行自定义的能力。
在DS 1.1中,
component
元素还可以具有activate
和deactivate
属性,该属性与在激活(启动)和停用(停止)组件时调用的方法相对应。
最后,此组件还提供CommandProvider
服务的实例。 这是Equinox特定的服务,它允许提供控制台命令,并且以前是在捆绑软件的Activator
完成的。 该模型的优势在于,只要依赖服务可用, CommandProvider
服务将自动发布; 另外,代码本身不需要依赖任何OSGi API。
对于Felix特定的实现,需要实现类似的解决方案。 到目前为止,OSGi命令外壳还没有标准。 OSGi RFC 147正在进行中,以允许在不同的控制台中使用命令。 所包含的源代码具有short shorten-command-felix
组件定义,以实现完整性。
启动服务
上面的内容使我们可以启动捆绑包,以任何顺序提供(和使用)起酥油服务。 一旦命令服务启动,它将绑定到可用的最高优先级的缩短服务; 或者,如果未指定,则是服务排名最低的那个。 如果随后要启动更高优先级的服务,我们目前不会考虑并继续使用我们当前绑定的服务。 但是,如果一项服务消失了,那么那时我们将重新绑定到剩下的最高优先级的缩短服务,而不会受到客户端的干扰。
为了运行示例,您需要为每个平台下载并安装一些额外的捆绑软件:
- 费利克斯
- 配置管理员(
org.apache.felix.configadmin-1.2.4.jar
) - SCR声明式服务(
org.apache.felix.scr-1.2.0.jar
)
- 配置管理员(
- 春分点 :
到目前为止,您应该已经熟悉了安装和启动捆绑软件的过程。 但如果不是, 请参阅“静态模块”文章 。 我们需要安装上述捆绑包以及我们的缩短服务。 这是在Equinox中的样子,其中包在/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
命令服务将自动取消注册。
笔记
声明式服务使OSGi服务的连接变得容易。 但是,有几点需要注意。
- DS捆绑包需要安装和启动才能连接组件。 因此,它通常是作为OSGi框架启动的一部分安装的,例如Equinox的osgi.bundles或Felix的felix.auto.start属性。
- DS通常还有其他需要安装的依赖项。 对于Equinox,它包括
equinox.util
包。 - 声明式服务是OSGi纲要规范的一部分,而不是核心规范的一部分,因此,通常需要为服务接口提供单独的捆绑包。 在Equinox的情况下,它是由
osgi.services
提供的,但是在Felix中,该接口是由SCR(服务组件注册表,又名DS)捆绑包本身导出的。 - 声明式服务可以使用属性进行配置。 它通常利用OSGi Config Admin服务。 尽管可以选择绑定/访问。 因此,DS的某些部分需要Config Admin才能运行。 实际上,Equinox 3.5有一个错误 ,要求使用声明式服务之前必须先启动Config Admin。 这通常需要使用上面的启动属性,以确保满足正确的依赖性。
- 捆绑包中必须包含OSGI-INF目录(以及XML文件),否则DS将无法看到它。 您还需要确保捆绑清单中存在
Service-Component
标头。 - 也可以使用
Service-Component: OSGI-INF/*.xml
包含所有组件,而不必按名称单独列出。 这还允许片段将新组件添加到捆绑包中。 - 尽管在
AtomicReference
上使用compareAndSet()也可以用作单个服务的非同步占位符,但是必须synchronized
bind和unbind方法以避免潜在的竞争情况。 - 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服务层进行通信。 因此,一个捆绑包可以通过手动方法提供服务,而另一个捆绑包可以使用声明性服务使用(反之亦然)。 我们应该能够混合和匹配1.0.0
和1.1.0
实现,并且它们应该透明地工作。
可安装的捆绑软件(也包含源代码):
- com.infoq.shorten-1.0.0.jar
- com.infoq.shorten.command-1.1.0.jar
- com.infoq.shorten.tinyurl-1.1.0.jar
- com.infoq.shorten.trim-1.1.0.jar
java 模块化