写点什么

模块化 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:366568
用户头像

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

关注

评论

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

十万高层齐卸甲,竟无一人有慈心。前阿里员工看阿里高管不法侵害女员工事件

刘悦的技术博客

阿里巴巴 面试 职场 职场 PUA

AI + K8S 驱动存储技术变革

焱融科技

人工智能 Kubernetes 云原生 高性能 存储

The Data Way Vol.1|风口下的开源市场:如何看待开源与商业的关系?

SphereEx

数据库 开源

Compose 编程思想

Changing Lin

8月日更

Python代码阅读(第7篇):列表分组计数

Felix

Python 编程 Code Programing 阅读代码

孩子排斥写作业 VS 员工不接活儿——项目管理来帮忙

Ian哥

我看 JAVA 之 并发编程【三】java.util.concurrent.atomic

awen

Java 并发编程 Atomic 原子操作

工作多年,分享16条职场经验给新人朋友

架构精进之路

职场 成长 经验分享 8月日更

万字长文讲透低代码

百度开发者中心

最佳实践 开发者 方法论 低代码 语言 & 开发

从技术到文案,还回技术么?

escray

学习 极客时间 朱赟的技术管理课 8月日更

oeasy教您玩转vim - 11 - # 向前向后

o

TCP如何进行拥塞控制

W🌥

计算机网络 TCP/IP 8月日更

如何用Camtasia添加视频水印?

淋雨

视频剪辑 Camtasia 录屏软件

【“互联网+”大赛华为云赛道】IoT命题攻略:仅需四步,轻松实现场景智能化设计

华为云开发者联盟

IoT 华为云 LiteOS 互联网+ IoT边缘

vivo 全球商城:优惠券系统架构设计与实践

vivo互联网技术

服务器 架构设计

带你看论文丨全局信息对于图网络文档解析的影响

华为云开发者联盟

文档 CNN网络 图网络 非结构化文档 全局信息

iPhone Shortcuts 使用与场景

TroyLiu

iphone 效率工具 快捷指令 shortcuts nfc

这波性能优化,太炸裂了!

why技术

Java 性能优化 JVM

收获颇丰!这份阿里架构师纯手敲JDK源码全彩小册可以打满分

Java架构追梦

Java 阿里巴巴 架构 面试 jdk源码

MySQL中的DEFINER(定义者)是什么

Simon

MySQL

十大排序算法--桶排序

Ayue、

排序算法 8月日更

简单的Postman,还能玩出花?

码农参上

8月日更

你真的懂语音特征吗?

华为云开发者联盟

语音 音频 声学 时域图 时域

Java测试框架九大法宝

FunTester

自动化测试 JUnit 测试框架 selenium testNG

【“互联网+”大赛华为云赛道】EI命题攻略:华为云EI的能力超丰富,助你实现AI梦想

华为云开发者联盟

大数据 modelarts 大赛 互联网+ 华为云EI

浅谈BU安全建设

I

项目管理 企业安全 BU安全 安全建设

使用Grafana显示Prometheu监控

Rubble

Grafana Prometheus 8月日更

接口返回值一定不允许使用枚举类型吗?

skow

Java 面试 后端 开发规范

台达AS228T_CanOpen_VFD_X

林建

台达 AS228T Canopen 功能块 E变址

Apache之道在腾讯的探索与实践

腾源会

Apache 开源 腾源会 腾讯开源

C++ Vector

若尘

c++ vector 8月日更

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