在 OSGi 中,实现 bundle 间交互和扩展性有两种常见的方式,也就是服务模型(service model)和扩展者模型(extender model)。
服务模型较为容易理解,在这个模型中包含了服务的提供者、使用者以及注册中心。服务接口构成了提供者和使用者之间的契约,提供者将希望别人调用的服务发布到注册中心,使用者到注册中心查找符合需求的服务。在传统的服务模型中,都是通过查找的方式使用服务,但是随着 Declarative Service 以及 Blueprint 的流行,服务可以在运行时注入,简化了开发人员的编码过程。
图 1:OSGi 中的服务模型(图片来源于《OSGi 实战》)
扩展者模式更为灵活和抽象,它是与 OSGi 的生命周期相关联的。在《OSGi 实战》一书中是这样描述扩展者模式的:扩展者模式背后的主要思想是,在其他bundle 的生命周期事件(安装、解析、启动、停止等)上构建动态扩展性。通常,应用中的某个bundle 扮演扩展者的角色:它负责监听bundle 的启动、停止。当一个bundle 启动时,扩展者会对其进行检测并判断它是否是一个扩展bundle。扩展者检查bundle 的清单文件(使用bundle.getHeaders())或bundle 的内容(使用bundle.getEntry()),以此来寻找它能够识别的特定元数据。如果这个bundle 确实包含一个扩展,那么该扩展会通过元数据来描述。扩展者读取元数据并执行必要的任务,这有助于将扩展bundle 集成到应用中。如果扩展bundle 被停止,扩展者同样会监听到,此时扩展者会将相关的扩展从应用中移除。(《OSGi 实战》一书由人民邮电出版社翻译出版,对应的文字来源于第89 页)。在Eclipse 插件开发中最常用的扩展点模型就是一种典型的扩展者模式,不过这是Equinox 的特有实现,并不是OSGi 规范的一部分。
图2:《OSGi 实战》一书中图形样例所对应的扩展者模式
近日,针对这两种模型的优劣和使用场景,有人阅读在完 Apache Felix 样例文档后产生了疑问,并在Stackoverflow 进行了咨询。OSGi 领域中的资深专家、 Bndtools 作者 Neil Bartlett 对此进行了细致的回复,而 OSGi 联盟技术总监 Peter Kriens 更是专门撰写博文来解释这一问题。
Neil Bartlett 认为服务更为常用,应该作为第一选择,除非有明确的理由才要设计新的扩展者。扩展者模式更为灵活和抽象,但是理解和使用起来会更加复杂。他以在 OSGi 应用中实现帮助系统作为例子,其他的 bundle 只需在约定的目录下包含一个 HTML 文档,核心的帮助系统 bundle 会扫描这些 bundle 以查找 HTML 文档并将其添加到主页的帮助索引中。这个特性如果使用服务来实现的话就会比较繁琐:假设遵循“白板(whiteboard)”模式,你需要定义一个带有 getHelpDocuments() 方法的 HelpProvider 接口,任何想提供帮助内容的 bundle 都需要实现这个接口并将其注册为服务。而如果采用扩展者模式的话会更加简便,因为能够跟踪到 bundle 的到来和消失。在现实世界中,扩展者模型用到了如下的地方:
- Declarative Services 是一个扩展者,它会查找其他 bundle 中的 Service-Component 声明,并且会执行初始化组件、注入服务等操作。
- Blueprint 也是一个扩展者,所做的事情很类似,它会查找 Bundle-Blueprint 声明或约定路径下的 XML 文件。
- OSGi 企业级规范所定义的 JPA 扩展者会查找 Meta-Persistence 头信息中所声明的 persistence.xml。如果找到的话,将会为该 bundle 创建持久化单元。
- Eclipse 也包含了扩展者(所起的名字有点令人感到迷惑,叫做 Extension Registry)。Eclipse 使用它来创建视图、透视图以及菜单等,它们通过位于 bundle 根目录中的 plugin.xml 文件进行声明。
总而言之,服务用来基于契约注册和查找对象,而扩展者用来扩展 bundle 的功能,通常会基于某种类型的资源声明或头信息,而不是可执行的代码。
Christian Schneider 是 Talend 的开源软件开发人员,主要工作内容包括 Apache Camel、Apache Karaf 以及 Apache CXF,针对这个问题,他的看法是扩展者主要用于框架之中,它需要了解 bundle 的内部实现,因此不利于松耦合,但是其优势在于能够定义全新的抽象功能,如 Blueprint、Declarative Services 以及 CDI,这些框架都是使用扩展者模型将 bundle 按照一定的规范装配在一起的。而服务对于应用本身来说是正确的选择,它能够隐藏实现的细节,同时能够避免服务的使用者实例化类,因此有利于实现松耦合。在实际中,我们经常会组合使用它们,如可以使用 CDI 注解来装配服务,在内部 CDI 会使用扩展者模型,而对于应用本身来说则使用服务来将 bundle 装配在一起。
在 Peter Kriens 的博文中,他将bundle、服务以及扩展者类比为日常的社会交流。在社会交流场景中,我们会扮演不同的角色,如商业交易中的销售方和购买方。在OSGi 中,µservice 模型就是这样的参与者。µservice 比其他的服务模型更适合这样的场景,因为它是基于动态化的。
在社会化的类比中,bundle 就是具体的人,它会控制行为并选择要参与什么样的场景,以及要使用或提供什么样的服务。但是有一些场景是重复性的、无聊的并且不是我们主要的兴趣所在,这时候使用服务就不太合适了。假设我有两辆汽车,其中一辆在探测到我有车钥匙时会自动打开车门,而另一辆做不到这一点,因此我会更喜欢前一辆。
我们可以使用OSGi 模拟这样的场景,因为能够知道已经安装了什么bundle 以及何时有新的bundle 安装进来。就像那辆聪明的汽车能够探测到我口袋中的钥匙一样,bundle 也可以查看其他bundle 中的内容并作出相应的反应。Peter 还讨论了数据库迁移的例子,同样使用了扩展者模式。数据库模式的扩展者bundle 会根据其他bundle 的模式描述文件,对数据库作出相应的变更,这样的话,一些凌乱的代码就能统一抽取到扩展者bundle 中了,另一个类似的例子是在Declarative Services 的实现中,会有一个专门的扩展者bundle 来负责解析XML、处理依赖等繁琐的事情。Paremus 的packager packs 也使用这种模式来统一管理应用的生命周期。
那么究竟该何时使用服务模式,何时使用扩展者模式呢?
Peter 认为,服务模型是关于 bundle 中核心领域内容的,它应该是 bundle 中主要的关注点所在,代码应该按照这种模式来进行编写。扩展者模式主要用来将杂乱和样板式的代码从领域 bundle 转移到一个特定的 bundle 之中。了解服务和 bundle 的动态性会极大地帮助我们理解这两种模型。
服务和扩展者都是在 OSGi 中十分重要和常见的模型,希望本篇文章中介绍的专家观点能够帮助您厘清它们的概念以及适用场景。
评论