写点什么

模块化 Java:声明式模块化

  • 2010-02-09
  • 本文字数:5659 字

    阅读完需:约 19 分钟

在模块化 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="<b>com.infoq.shorten.tinyurl.TinyURL</b>"/>
<service>
<provide interface="<b>com.infoq.shorten.IShorten</b>"/>
</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 <b>setShorten</b>(IShorten shorten) {
this.shorten = shorten;
}
public synchronized void <b>unsetShorten</b>(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="<b>com.infoq.shorten.command.EquinoxShortenCommand</b>"/>
<reference
interface="<b>com.infoq.shorten.IShorten</b>"
bind="<b>setShorten</b>"
unbind="<b>unsetShorten</b>"
<i>policy="dynamic"<br></br> cardinality="1..1"</i>
/>
<i><service> <br></br> <provide interface="org.eclipse.osgi.framework.console.CommandProvider"/> <br></br> </service></i>
</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 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2010-02-09 06:366536
用户头像

发布了 150 篇内容, 共 45.6 次阅读, 收获喜欢 10 次。

关注

评论

发布
暂无评论
发现更多内容

Android 开发必备知识点整理,34岁程序员年薪50w

android 程序员 移动开发

Android 技术的下半场(1),android开发书籍下载

android 程序员 移动开发

Android 组件化实战,kotlin协程实现原理

android 程序员 移动开发

Android 网络框架之Retrofit源码解析,android嵌入式开发及实训答案

android 程序员 移动开发

Android 关于CPU类型的so文件兼容问题(ABI),十年Android编程开发生涯

android 程序员 移动开发

Android 反编译利器,jadx 的高级技巧,看完这篇彻底明白了

android 程序员 移动开发

Android 开发一比一年难做!面试题都这么难了,android插件化

android 程序员 移动开发

Android 技术的下半场,kotlin开源

android 程序员 移动开发

Android 插桩入门,腾讯T2大牛手把手教你

android 程序员 移动开发

Android 约束布局(ConstraintLayout)1,最详细的解释小白也能听懂

android 程序员 移动开发

Android 自定义 View 之 LeavesLoading,移动开发工程师考试

android 程序员 移动开发

Android 组件通信中有哪些不为人知的细节?,面试题分享

android 程序员 移动开发

Android 初中级开发社招面试总结!,android自定义控件开发入门与实战

android 程序员 移动开发

Android 可能你想要的APK瘦身笔记,android2018面试题

android 程序员 移动开发

Android 性能监控系列一(原理篇),闭关60天学懂NDK+Flutter

android 程序员 移动开发

Android 无缝换肤深入了解与使用,android快速开发

android 程序员 移动开发

Android 进阶之 MVP,高级安卓工程师面试题

android 程序员 移动开发

Android 可能你想要的APK瘦身笔记(1),2021金三银四面试季

android 程序员 移动开发

Android 基础掌握好,面试基本不会倒!,面向Android开发者的复习指南

android 程序员 移动开发

Android 开发市场是盛是衰?你应该知晓,android实战项目

android 程序员 移动开发

android 快速开发(三)巧用公共标题栏(1),android开发从入门到精通

android 程序员 移动开发

Android 架构组件的最新进展 (上篇),flutter登录界面设计

android 程序员 移动开发

android 快速开发(三)巧用公共标题栏,音视频编解码开发

android 程序员 移动开发

Android 获取子 View 的位置及坐标的方式(1),Android入门视频教程

android 程序员 移动开发

Android 基础与底层机制面试题,万字解析

android 程序员 移动开发

Android 已发行多年,移动 App 已经趋近饱和,那么 Android 开发还会有那么吃香吗

android 程序员 移动开发

Android 开发之深入浅出 NavigationUI,2020-2021阿里巴巴安卓面试真题解析

android 程序员 移动开发

Android 开发经验分享:挺重要的网络基础,android实现选择题模式

android 程序员 移动开发

Android 开发行业真的不行了嘛?,深入讲解Android

android 程序员 移动开发

Android 开发面试心得:BAT大厂Android面试题整理,面试8家大厂后终于拿到Offer

android 程序员 移动开发

Android 开发必备知识点及面试题汇总(Android+Java,斩获offer

android 程序员 移动开发

模块化Java:声明式模块化_Java_Alex Blewitt_InfoQ精选文章