OSGI心得体会

什么是模块化

与面向对象一样,模块化的目的也是松耦合,高内聚。我们可以理解为模块化是将对象间的互访做了边界划分,即对一组业务相关的对象进行封装,并且提供可能的更高层次的代码访问隔离机制。

 

物理模块化 VS 逻辑模块化

物理模块化是指应用中的类文件被物理的分割放在不同的模块中,但是每个模块间的互访不受控制,各个模块可以访问模块间的内部对象,只要对象是可访问的。只是是对代码本身进行模块化管理。

例如 JAVA 中,应用被分为模块 A B ,模块 B 中有一个 public 对象 B.b ,该对象可以完全被模块 A 访问,因为它是 public 的。

 

逻辑模块化是指在物理模块化的基础上,对模块进行控制访问;即模块间实现了访问隔离,而这才是我们所说的真正的模块化的概念。

再看回上面的例子,如果 B.b 没有定义是其它模块可访问的,那么默认 A.a 是访问不到 B.b 这个对象的,不管这个对象的访问级别是什么。

 

OSGI 的作用

java 中, OSGI 是一个实现 java 模块化互访的平台,我们可以理解为是一个更高级的 JVM 。它提供了逻辑上的模块化控制。

 

OSGI 对模块的定义

OSGI 中,模块称之为 bundle ,一个 bundle 在物理上而言就是一个 jar 包。 Jar 包中有一个描述 jar 包的信息文件,位于 jar 内部的 META-INF 目录下的 MANIFEST.MF 文件。 OSGI 通过 MANIFEST 这个文件获取模块的定义信息,比如模块间的互访信息,模块的版本信息等。

 

Note :对于 MANIFEST.MF 文件的操作,由于这个文件有很多使用约束,比如一行不能超过 72 个字符,所以一般都是通过 IDE 工具对它进行编辑

 

l  bundle 里有什么

一个 bundle 中一般包含如下的东西:

部署描述文件( MANIFEST.MF ,必要的),各类资源文件(如 html xml 等,非必须的),还有类文件。这与一个普通的 jar 包没有任何的区别。但是,除此之外, bundle 里还可以放入其它的 jar 包,用于提供给 bundle 内部的类引用,即 bundle 内部的 lib 库。(跟一个 war 很类似)。

Note :实际上 bundle 里可以存放任何的内容,但是在 bundle 内部不会有嵌套的 bundle ,即上面提到的存放于 bundle 中的 jar 包就只会当成是一个普通的 jar 包,不管这些 jar 包中是否含有 bundle 定义的信息



l  MANIFEST.MF 的定义

MANIFEST.MF 位于 bundle 中的根目录下的 META-INF 目录下。

一个 bundle 的必要信息定义如下:


我们称这些内容为 bundle 的头信息,具备了这 4 个头信息就是一个 bundle

其中,

Bundle-ManifestVersion 表示 OSGI 的参考版本, 2 代表参考版本使用的是 OSGI R4.0+

Bundle-SymbolicName 用于作为 bundle ID 标识的前缀,一般用模块的顶级包名来命名

Bundle-Version 表示 bundle 当前的版本,并作为 bundle ID 标识的后缀

Bundle-Name 则是一个可读的 bundle 的命名定义,没有太大的作用

其它还有很多的头信息的定义,这里就不一一列举了。

 

头信息的格式:

主要由头属性名称加冒号和各个从句组成,从句间用逗号分隔

Property-Name: clause, clause, clause …

 

从句的定义:

target; param1(:)=value1; param2(:)=value2 …

target 表示头属性对应的值,后面可以带上很多不同的参数对,每个参数分为参数名和参数值,并且都用分号分隔

 

参数对的定义:

参数分为两种,属性和指令,如果是指令,需要在 = 号前加上冒号表示是一个指令

attr1=value1

dir1:=value1

 

头信息的简写形式:

如果头信息的所有 target 的参数对定义都是一样的,那么可以将 target 先定义在前面,所有的参数对定义在最后

Property-Name: target1; target2; attr1=value1; dir1:=value1 ….

 

Bundle Identifier

bundle ID 标识,这个是用来在 OSGI 中对 bundle 进行唯一性的定义的。在 Bundle OSGI 读取后, OSGI 通过对 Bundle-SymbolicName Bundle-Version 进行组合,成为一个唯一的 ID 标识。组合的方式为 <Bundle-SymbolicName>_<Bundle-Version>

 

Bundle-SymbolicName 的命名方式

Bundle-SymbolicName 没有任何的命名要求,可以是任意的字符组成,但是一般是用模块的顶级包名来作为它的名称

 

Bundle-Version 的命名方式

Bundle-Version 有自己的命名要求,格式为 [0-9].[0-9].[0-9].{ConstraintName}

前面都是数字作为版本号,最后一个小数点后的名称是经过命名约束的版本名

例如 1.2.3.alpha 1 代表大版本号, 2 代表中版本号, 3 代表小版本好,而 alpha 代表小版本中的某个阶段,比如 alpha beta snapshot qualifier 等,下图展示了版本大小的排序方式


 

 

代码访问机制设置

JVM 环境:

类之间的互访是通过设置 classpath 来查找的

 

OSGI 环境:

1) bundle 内部的 classpath

默认是 bundle 的根目录,但是可以通过头信息来指定 bundle 内的多个目录

 

如图所示,通过配置 Bundle-ClassPath 进行设置, . 代表 bundle 的根目录,是必须要有的, customPath 代表 <bundle>/customPath 目录,当然,也可以是具体到某个 jar 文件。将会按顺序查找

 

2)  Export-Package

通过设置 Export-Package 的头信息进行设置,可以设置多个包,只有在这里定义的包内的类可以被其它的模块进行访问。但是只能访问该包下的,该包的子包的类是不会被曝露的。

例如 Export-Package: net.georgezeng.test.modelb.service; version=1.0.0, net.georgezeng.test.modelb.dao; version=1.2.0

 

3)  Import-Package

当某个模块曝露了某些包,那么如果你要引用相应的那个模块下的包的类的话,就需要通知 OSGI 引入你设置的包到该模块,即在该模块的 MANIFEST 中定义 Import-Package 头信息。

例如上面的模块曝露了两个包,分别是 service dao ,我们这里假设该模块需要引入 service 的包来获取当中的类的访问,那么就需要如下设置

Import-Package: net.georgezeng.test.modelb.service; version=1.0.0

这样就能访问该 service 包下的类了。

Note :该头信息有一个有用的指令 resolution ,默认值是 mandatory ,可以设置为 optional ,当设置为 optional 时,依赖解析将对其忽略(下面将会提到)

 

4)  DynamicImport-Package

使用此头信息时, OSGI 将会扫描该头信息设置的所有包

它只能设置 target 值,可以多个,并且 target 值可以使用通配符,例如

DynamicImport-Package: net.gerogezeng.test.*, net.georgezeng.test2 …

OSGI 将会扫描 net.georgezeng.test 下所有的子包,但不包括 test 包下本身的类,如果要用到 test 包下的类,就是第二个 target 所设置的值的方式

Note :使用该头信息时,依赖解析也会对其忽略

 

5)  Require-Bundle

使用 Require-Bundle 相当于是将被 require Bundle export 出来的包全部 import

 

l  关于 Export Import version 属性的定义和匹配

在上面的描述中我们看到了 Export Import 中都有一个 version 的属性定义,该 version 采用的命名规则与 Bundle-Version 是一样的,这里主要是说明如何设置它的范围



 如图,你可以这样设置

Import-Package: net.georgezeng.test.modelb.service; version=”[1.0.0, 2.0.0)”

 

Export Import 间的匹配

1)  版本匹配

通过 version 属性进行设置,通过上图设置的版本的范围比较进行匹配

 

2)  自定义属性匹配

自定义属性的匹配必须是属性名和属性值都相同,即 Import 中的属性 export 中也是有的,且属性值相同

 

Note Import-Package 的规则也适用于 Require-Bundle

 

l  依赖和解析

何为依赖?

如果 BundleA Export 了某个包,而 BundleB Import 了这个包,那么就说 BundleB 依赖于 BundleA

或者 BundleA 中设置了 Require-Bundle 的头信息,那么 Require-Bundle 中的所有相关的 target Bundle 都被 A 依赖

 

Bundle 的依赖解析

OSGI 读取 Bundle 时, Bundle 将会被解析,只有当 Bundle 被正确解析成功,才能被其它 Bundle 使用或者运行。即 OSGI 获取 Bundle MANIFEST 里的头信息进行检查分析,而这个解析是连锁式反应的。

例如 BundleB 依赖于 BundleA ,假设 OSGI 要使用 BundleB ,它需要先解析 BundleB ,而 BundllerB 因为是依赖于 BundleA 的,所以 BundleA 会先于 BundleB 被进行解析,如果 BundleA 解析成功,才会继续解析 BundleB ,如果 BundleB 也成功解析,那么 OSGI 就可以正常使用 BundleB 了,这个过程由于会引起关联的 Bundle 进行解析,所以是依赖解析。

 

l  DynamicImport vs optional

上面提到了可以通过 Import-Package 添加 resolution 指令设为 optional 或通过 DynamicImport-Package 进行配置令使得 OSGI 在进行依赖解析时对其进行忽略。

他们的相同之处都是在运行期间当某个 Bundle 用到了他们 export 的包时才会去解析,如果 exported Bundle 之前已经解析成功,那么就直接搜索 class ,如果还未解析过,那么 optional 只会解析一次,如果失败了,就不会再尝试解析它,直到它已被重新解析过;而 Dynamic 的方式则是每次都尝试重新解析

 

l  依赖匹配

Bundle 间的依赖需要进行匹配校验,比如 bundleA 需要某个包,而 BundleB 曝露了这个包,但是 BundleC 也曝露了相同的包,那么这时 BundleA 到底是要依赖哪个呢?这个时候就是通过上面提到的 export import 间的匹配来进行校验的。默认会对比 version 属性,如果 import 中有自定义属性,那么 export 中也必须有相应的自定义属性且属性值相同才能匹配成功

 

匹配的顺序:

1) version 属性进行范围匹配,如果不止一个 bundle 匹配成功,则进入下一步

2) 如果有自定义属性,进行严格的自定义属性匹配,如果不止一个 bundle 匹配成功则进入下一步

3) 查找 Bundle-Version 更高的 Bundle ,以最高的版本为主

 

l  Fragment Bundle

一个模块就是一个 Bundle ,但有时候我们会需要将 Bundle 在物理上分成多个块,而且让细分出来的块属于同一个模块下,那么这个时候就有了 Fragment 的概念。

Fragment 没办法独立存在于 OSGI 中,需要依附于某个 Bundle 下,即有一个 Bundle 作为宿主。一个 Fragment 只能依附于一个 Bundle

Note Fragment 本身没有自己的 classloader ,使用的是 Bundle classloader

 

Class 的搜索顺序

在了解整个 class 的搜索路径前,我们需要先了解下面 2 个内容:

1 Bundle Classloader

OSGI 环境中,一个 Bundle 有一个 ClassLoader ,用于读取 bundle 内部的类文件

 

2 boot delegation

OSGI 的配置文件中我们可以配置一个选项用于将需要的包委派给 jvm classloader 进行搜索

例如 org.osgi.framework.bootdeletation=sun.*,com.sun.*

 

Bundle 请求一个 class 的顺序如下:

1) 如果请求的 class 来自于 java.* 下,那么 bundle classsloader 会通过父 classsloader (即 jvm classloader )进行搜索,找不到则抛错,如果不是 java 包下的,则直接进入 2

2) 如果请求的 class java 包下的,那么将搜索 boot delegation 的设置的包,同样也是通过 1 )的 classloader 进行搜索,搜索不到则进入下一步

3) 通过 Import-Package 对应的 Bundle classloader 进行搜索,搜索不到则进入下一步

4) 通过 Require-Bundle 对应的 Bundle classloader 进行搜索,搜索不到则进入下一步

5) 通过 Bundle 自身的 classpath 进行搜索,如果搜索不到则进入下一步

6) 通过 Fragment classpath 进行搜索,如果找不到则进入下一步

7) 如果设置了 DynamicImport-Package 的头信息,将通过扫描 DynamicImport-Package 中设置的包,然后去相应的 export Bundle 中查找,有则返回,没有则抛错,搜索完毕;如果没有设置该头信息,则直接抛错

 

l  Bundle 的生命周期



 如图,椭圆表示状态, bundle OSGI 中一共可以查看到 5 个状态,分别是 Installed Resolved starting Active stopping Uninstalled 则是一个虚的状态,即当 Bundle 不存在于 OSGI 中时的状态。

OSGI 里对 Bundle 的操作一共有 6 个,分别是 Install Update Refresh Start Stop Uninstall

通过这些操作就可以达到不同的状态

 

如何安装和启动 Bundle

可以通过 3 种方式:

1) OSGI 的配置文件

2) OSGI shell

3) OSGI API

 

关于 Resolved

上面提到解析就是从 Installed Resolved 间的动作, Resolved 表示解析成功,进入已解析状态,此状态下 Bundle 可以被其它 Bundle 使用或启动运行 Bundle 自身

只有在 Resolved 状态时当调用 start 命令后才能激活 starting 状态,并在调用结束后自动转入 Active 状态,如果 starting 时有异常则自动返回 Resolved 状态

 

Active 状态

只有当在 Active 状态时,调用 stop 命令才能激活 stopping 的状态,并在结束后(不管是否有异常)自动转入 Resolved 状态

 

Starting stopping

这两个状态可以交由 developer 控制,比如对资源进行载入和释放,做一些 bundle 初始化的事情。

Note

1) 如果 Bundle 是通过配置文件启动的话,那么 Bundle 将会通过 start level event dispatcher 的线程进行 starting 的操作,并且 bunlde 间的 start 是按顺序的,但是由于不是在 OSGI console 线程中启动,所以在 starting 过程中可以对 shell 进行操作。

2) 如果 Bunlde 是通过 shell 进行启动的, OSGI 将使用 shell 的当前线程,即 OSGI Console 线程进行启动,在该状态中 shell 将无法做任何操作。如果在 starting bundle 出现死锁或需要长时间的过程那么将会导致无法对 shell 进行操作,从而只能选择重启 OSGI 环境。

3) 由于 stopping 是不可能在 OSGI 启动时发生的,一般都是在 shell 下通过 stop 命令调用发生,这种情况与 2 )相同。

所以要慎重的考虑 starting stopping 的逻辑要如何处理。

starting stopping 的状态下调用任何相关的操作都是不合理的

 

关于 update refresh

Update 会对 bundle 内容进行更新, refresh 不会对 Bundle 内容进行更新

1) Bundle 处于 Installed 状态时,调用 update 保持状态不变,调用 refresh 将会自动进行解析,如果成功则进入 resolved ,否则还是处于 installed

2) Bundle 处于 Resolved 时,调用 update 命令将会回到 installed 状态,而调用 refresh 时会再次解析,成功则自定启动 bundle ,直到进入 active

3) Bundle 处于 Active 状态时,调用 update 后如果解析成功将返回 active 状态, refresh 一样

Note :使用 refresh 会造成依赖解析的发生

 

关于 Activator

Starting stopping 到底是在哪里进行定义呢? OSGI 定义了一个 Activator starting stopping 进行操作,这个类是 org.osgi.framework.BundleActivator 。只要继承该类,并在 MANIFEST.MF 中定义 Bundle-Activator 头信息即可实现

 

关于 BundleContext

通过 BundleContext 我们可以与 OSGI 框架进行交互,比如获取 OSGI 下当前的 bundle 列表。

一个 Bundle 都有一个对应的 BundleContext ,当 Bundle start 的时候创建且在 stop 的时候被销毁。每启动一次 Bundle 都将获得一个新的 BundleContext

如何获取 BundleContext

Activator 中, OSGI 将会在 start stop 方法中传入这个对象

 

 

关于 event

生命周期的控制总会有一些 event 可以处理,比如通过实现 OSGI 提供的 listener 接口即可,这里不详细熬述

 

l  OSGI 的缓存

OSGI 对于 Bundle install 操作是持久化的操作,即当 install 某一个 bundle 的时候, OSGI 将会把该 bundle 进行副本保存,而不再需要当前的 bundle jar 包。这是 OSGI 的默认行为,当 OSGI 停止再启动时,此 Bundle 将会自动载入保存的副本。

update 的时候 OSGI 将会保存一个新的 bundle 副本,但是老的副本也还在(不过只在当前的 OSGI 环境下存在了,重启则会找到最新的副本),因为可能有其它的 bundle 依赖于老副本存在,此时需要使用 refresh 来进行同步刷新。这种会引起依赖解析再次进行。

如果使用 uninstall bundle 的副本不再保存

Note :可以通过添加 -clean 的启动参数来清理缓存

 

l  Bundle persistent storage area

OSGI 会为 bundle 开辟一块持久化数据区域,用于给 bundle 进行资源的存取,这块区域通过 BundleContext 进行访问,并在 uninstall 的时候销毁

 

l  OSGI Service

什么是 OSGI Service

我们有了 export import ,已经可以很好的管理模块间的代码互访,但是,如果我们只希望曝露接口,而不是实现的话,如何让依赖的 Bundle 获得接口的实现呢?这个时候我们就需要一种类似服务查找的方式通过 OSGI 来获得实现了。

所以 Bundle 有两种角色:生产者和消费者

生产者 Bundle 通过注册服务将实现注入 OSGI 中,消费者 Bundle 则是通过查找服务的方式从 OSGI 中获得实现。

OSGI 目前提供了 2 种方式用于注册和查找服务:

1) 编程式

通过 BundleContext 进行注册或查找

 

2) 声明式

OSGI R4.0 中引入的新的方式,通过 XML 文件的配置进行服务的注册和查找

猜你喜欢

转载自marsvaadin.iteye.com/blog/1576389