写点什么

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

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

关注

评论

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

无监控,不运维!深入浅出介绍ChengYing监控设计和使用

袋鼠云数栈

学术加油站|机器学习应用在数据库调优领域的前沿工作解读

OceanBase 数据库

数据库 oceanbase

火山引擎 DataLeap 通过中国信通院测评,数据管理能力获官方认可!

字节跳动数据平台

大数据 数据治理

硬实力,高潜力!旺链科技当选“2022德勤中国高科技高成长50强”

旺链科技

区块链 区块链技术

精准测试之覆盖

京东科技开发者

数据库 测试 代码 覆盖 企业号 1 月 PK 榜

【Redis 技术探索】「数据迁移实战」手把手教你如何实现在线 + 离线模式进行迁移 Redis 数据实战指南(数据检查对比)

洛神灬殇

redis 1月日更 数据对比 Redis-full-check

开源共建|旺链科技与FISCO BCOS达成产业应用合作

旺链科技

区块链 开源 区块链技术

软件测试/测试开发 | AppCrawler 自动遍历测试工具实践(一)

测试人

软件测试 自动化测试 测试开发 自动遍历 AppCrawler

【等保小知识】等保四级是最高级别吗?等保四级适用于哪些领域?

行云管家

等保 等保测评 等保四级

react源码中的hooks

flyzz177

React

创建好的小程序如何正式发布?

Towify

GitHub上标星79K的LeetCode算法小抄开放下载了

小小怪下士

程序员 面试 算法 LeetCode

华为云Stack新版发布:构筑行业云底座,共创行业新价值

华为云开发者联盟

云计算 后端 华为云 企业号 1 月 PK 榜

要避免的自动化实践

FunTester

网页抓取中最常用的三大 Python 库

Geek_2d6073

Diffie-Hellman密钥协商算法探究

百度Geek说

数据安全 企业号 1 月 PK 榜

大咖说·对话开源|与Tapdata论道数据技术开放生态

大咖说

开源

react hook 源码完全解读

flyzz177

React

总有一个你能用上的29个IDEA小技巧

风铃架构日知录

Java 程序员 IDEA IT 开发工具

如何使用 Towify 在小程序中创建关联表?

Towify

3700万人在线见证梅西“圆梦”!火山引擎实力护航世界杯

火山引擎边缘云

云计算 云原生 CDN 边缘计算 火山引擎边缘计算

一文总结ACE代码框架

OpenHarmony开发者

OpenHarmony

HDI压合设计准则作业规范

华秋PCB

PCB PCB设计 HDI

数字图像处理笔记

嵌入式视觉

数字图像处理 几何变换 低通滤波 图像锐化 高通滤波

牛啊!长这么大还是头一次见24W字的SpringBoot从入门到实战文档

程序知音

Java spring 微服务 springboot 后端技术

编程的终结;展望2023年AI系统方向;AI的下一个阶段

OneFlow

人工智能 深度学习

文末领资料 | 研发效能领域的三个年度关键词

思码逸研发效能

研发管理 研发效能

TDengine 公布 2022 年度中国时序数据应用创新奖,33 个科技企业创新应用脱颖而出

TDengine

数据库 tdengine 时序数据库

【等保小知识】等保3.0就是等保三级吗?

行云管家

等保 等保2.0 等保3.0

react源码中的fiber架构

flyzz177

React

ElasticSearch必知必会-基础篇

京东科技开发者

大数据 搜索引擎 数据分析 技术分享 Elastic Search

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