豫ICP备17040950号-2

深入理解OSGI的模块化

文章目录
  1. 1. OSGI框架
    1. 1.1. 模块和模块化
    2. 1.2. 构成
      1. 1.2.1. 模块层
        1. 1.2.1.1. 如何定义Bundle
        2. 1.2.1.2. MANIFEST.MF文件格式
        3. 1.2.1.3. OSGI类的加载和查找顺序
        4. 1.2.1.4. 依赖解析
      2. 1.2.2. 生命周期层
        1. 1.2.2.1. 作用
        2. 1.2.2.2. 重要接口
        3. 1.2.2.3. 启动级别
        4. 1.2.2.4. 系统Bundle
        5. 1.2.2.5. Bundle刷新流程
      3. 1.2.3. 服务层
        1. 1.2.3.1. 面向服务的设计
        2. 1.2.3.2. OSGI服务
      4. 1.2.4. 特点

OSGI(Open Service Gateway Initiative)技术是面向Java的动态模型系统。

OSGI框架实现了一个优雅、完整和动态地组件模型。应用程序(bundle)无 需重新引导可以被远程安装、启动、升级和卸载。
OSGi技术提供允许应用程序使用精炼、可重用和可协作的组件构建的标准化原语。 这些组件能够组装进一个应用和部署中。
OSGi服务平台提供在多种网络设备上无需重启的动态改变构造的功能。
为了最小化耦合度和促使这些耦合度可管理,OSGi技术提供一种面向服务的架构,它能使这些组件动态地发现对方。
OSGi联盟已经开发了例如像HTTP服务器、配置、日志、安全、用户管理、XML等很多公共功能标准组件接口。这些组件的兼容性插件实现可以从进行了不同优化和使用代价的不同计算机服务提供商得到。然而,服务接口能够基于专有权基础上开发。
OSGi的主要职责就是为了让开发者能够创建动态化、模块化的Java系统。

OSGI框架

image.png

模块和模块化

  • 模块(module):定义了一个逻辑边界,这种模块本身精确的控制了哪些类是完全被封装起来的,而哪些类需要暴出来作为外部使用。

OSGI是一个基于Java语言的动态模块化规范。OSGI中的模块称为bundle,以jar格式封装,和普通的jar包区别不大,但是bundle可以通过Import-package声明对其他bundle的依赖,也可用通过Export-Package声明对外提供的服(在MANIFEST.MF文件中配置),这样就可以精确的控制bundle内部服务的可见性。

OSGI的模块化和平级依赖,使得OSGI的程序基本上可以实现bundle的热插拔,一个bundle的停用,只会影响到依赖该bundle的bundle,如果一个bundle不依赖任何其他的bundle,那么停用,升级部署的影响基本可以忽略。

  • 模块化(modularity):将一个大型系统分解为多个较小的互相协作的逻辑单元,通过强制设置模块之间的逻辑边界来改善系统的维护性和封装性。

模块化远不止依赖问题那么简单。。。

1、替代传统的classpath。。。
2、解决冲突问题,如现有的方式两种不同版本的jar(假设api不兼容)很难共存,如spring 所依赖的cglib与hibernate依赖的cglib。在模块化环境中,每个模块都可以使用自己的classloader,不同版本的模块可以共存。
3、可以决定要暴露哪些 API,不是像现在classpath那样所有的jar中的api都可以访问。
4、完整的生命周期。

构成

OSGi框架从概念上可以分为三层:模块层、生命周期层和服务层。

Module Layer:模块层关注代码的打包和共享;
Lifecycle Layer:生命周期层提供运行时管理以及对OSGI框架的访问接口;
Service Layer:服务层关注模块之间的交互和通信。

image.png

模块层

模块层是OSGi框架中最基础的部分。

OSGi 的模块化,是通过为 Jar 包添加metadata 来定义哪些类该暴露,哪些类该隐藏,其控制单元叫做 Bundle(jar 包)。

首先,必须先了解一个基本概念——什么是Bundle?

bundle 是以 jar 包形式存在的一个模块化物理单元,里面包含了代码,资源文件和元数据(metadata),并且jar包的物理边界也同时是运行时逻辑模块的封装边界。

如何定义Bundle

Bundle 是 OSGi 中的基本组件,其表现形式仍然为 Java 概念中传统的 Jar 包。
通过 META-INF 目录下的 MANIFEST.MF 文件对其予以进一步的定义。
通常一个 MANIFEST.MF 文件的内容如下:

1
2
3
4
5
6
7
8
9
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Util
Bundle-SymbolicName: com.ibm.director.la.util
Bundle-Version: 1.0.0
Bundle-RequiredExecutionEnvironment: J2SE-1.5
Import-Package: org.osgi.framework;version="1.3.0"
Export-Package: com.ibm.director.la.util;uses:="org.osgi.framework"
Bundle-ClassPath: lib/junit.jar,

MANIFEST.MF 文件存储的实际上是 Bundle 的元数据。
元数据的内容可以精确的定义 Bundle 的各种特征,同时能更好的对 Bundle 进行标识同时帮助用户对Bundle进行理解。

MANIFEST.MF文件格式
  1. 属性声明的一般格式:name:value
  2. 一行不超过72个字符,下一行继续则由单个空格字符开始
  3. 每个子句(clause) 进一步分解为一个目标(target)和一组由分号分隔的name-value对参数(parameter)

元素解释:

  • Bundle-SymbolicName
        唯一的bundle名称,相当于在系统中的id。singleton表示是否使用单启动方式 # 可选的

  • Bundle-Version
        主要的版本号

  • Bundle-Name
        bundel名称

  • Bundle-Vendor
        发布商

  • Bundle-RequiredExecutionEnvironment
        需要的执行环境

  • Build-Jdk
        jdk版本

  • Created-By
        创建者

  • Bundle-Activator
        Activator类路径

  • Bundle-ManifestVersion
        定义了bundle遵循规范的规则,1表示r3规范,2表示r4和以后的版本

    a) 唯一有效的值是2
    b) 没有Bundle-ManifestVersion的Bundle不要求指定Bundle- SymbolicName属性

  • Import-Package
        引用包的信息,包括包名称和版本号,只有引用了这些包,才能让classloader装载

    a) 导入一个包并没有导入它的子包
    b) Import-Package通过属性导入特定的包
    c) 除java.*
    d) 对于任意属性,OSGI只支持相等匹配
    e) 需要指定一个精确的版本范围,使用“[1.0.1,2.0.1]”这样的格式
    f) 当没有指定版本范围时,默认的值是“0.0.0”
    g) Version及其值的格式是OSGI规范所定义,支持更加灵活的匹配方法

导入包版本支持的格式image.png

  • Export-Package 对外暴露的Package

a) 标准Jar文件默认公开一切内容,而Bundle中默认不公开任何内容
b) 可导出多个包,用逗号分隔
c) 可以给导出包增加任意属性
d) 可以给导出包设置Version,默认为0.0.0

  • Require-Bundle
        直接引用整个bundle

  • Bundle-ClassPath
        Bundle 的 Classpath,内部类路径

  • Fragment-Host Fragment
        类型 Bundle 所属的 Bundle名

  • DynamicImport-Package
        Bundle动态引用的 package,如果您不想要捆绑包,则必须导入所需的所有包。要生成此列表,您可以使用maven-bundle-plugin(用于maven项目)。如果您不想生成所需导入的具体列表,那么您可以尝试DynamicImport-Package: * 哪个应该按需导入所需的包

OSGI类的加载和查找顺序

OSGi为每个bundle提供一个类加载器,OSGI的类加载顺序不同于双亲委派模式的向上委托,而是根据MANIFEST.MF文件的配置,去寻找类对应的加载器,类的加载可以在bundle间互相委托。所以OSGI的类加载器结构是平级的网状结构

1
2
3
4
5
6
7
8
9
#步骤#
1-检查是否java.*,或者在bootdelegation中定义。如果是,则bundle类加载器立即委托给父类加载器(通常是Application类加载器)
2-检查是否在Import-Package中声明。如果是,则找到导出包的bundle,将类加载请求委托给该bundle的类加载器。如此往复
3-检查是否在Require-Bundle中声明。如果是,则将类加载请求委托给required bundle的类加载器
4-搜索bundle的内嵌jar的类路径(Bundle Class Path)。如果找不到类或资源,继续下一步。
5-查找Bundle的Fragment Bundle中导入的包, 如果没找到继续下一步
6-如果类或资源位于自己导出的包中,则搜索结束并失败。
7-否则,如果类或资源位于DynamicImport-Package导入的包中,则尝试动态导入包。
8-如果动态导入包成功,则将请求委托给导出这个包的类加载器。如果请求被委托给导出类加载器并且找不到类或资源,则搜索终止且失败
依赖解析

含义

  1. 只有满足所有的依赖(Import-Package),bundle才可用
  2. OSGI框架的一个最重要任务之一就是:通过自依赖解析自动化地进行依赖管理

依赖解析规则

1、级联解析
2、Import-Package的属性约束和版本约束
3、多个Bund满足Import-Package依赖(多个Provider)时:
  - 3.1 已解析的(resolved)bundle优先级高,未解析的(installed)bundle优先级低
  - 3.2 相同优先级,有多个匹配时,版本高者优先,版本相同则选最先安装的bundle
4、一个bundle只能看到某个package的唯一一个实例
5、uses 子句
  - 5.1 用于限制Export-Package
  - 5.2 需要用到uses子句的场景
    - 5.2 .1 导出包中的类,其方法签名中包含了其Import-Package中的类
    - 5.2 .2 导出包中的类,继承了其Import-Package中的类
  - 5.3 users约束是可传递的
  - 5.4 谨慎使用uses,大大·限制解析的灵活性

生命周期层

作用

1 、在应用程序外部,生命周期层精确低定义了对bundle生命周期的相关操作
2 、对生命周期的操作,允许你动态地改变进行于框架中的bundle组成,并以此来管理和演化应用程序
3、 在应用程序内部,生命周期层定义了bundle访问其执行上下文的方式,为bundle提供了一种与OSGI框架交互的途径以及一些执行时的便利条件
4、 OSGI框架支持对bundle形式的JAR文件实现全生命周期管理,包括:安装、解析、启动、停止、更新和卸载
5、 运行时生命周期管理,“动态类路径”

下图为 Bundle 生命周期的状态转移图:

image.png

重要接口

生命周期层的API****主要是由以下三个核心接口来组成的:

BundleActivator****,BundleContext 和 Bundle****。

BundleActivator:让你能够捕捉bundle的start和stop事件,并对这两个事件作出自定义的反应。

其中:

1 调用start()方法的激活器实例与调用stop()的实例是同一个

2 当stop()方法被调用之后,激活器实例就被丢弃并不再不用

3 如果一个bundle被停止后,又重新启动,那么将创建一个新的激活器实例,同时它的start()方法和stop()方法也将被适时触发。

BundleContext:一个bundle在框架中的执行时上下文,这个上下文提供了和框架进行交互的方法。

其中:

1 在bundle属于active状态时,BundleContext才有意义,即start()方法被调用和stop()方法被调用之间的时间点

2 注册服务

方法如下:

1
public ServiceRegistration registerService(String clazz, Object service,Dictionary properties);

调用例子:

1
2
3
4
5
6
7
@Override
public void start(BundleContext context) throws Exception {
Dictionary<String, String> props = new Hashtable<String, String>();
props.put("ServiceName", "Calculation");
context.registerService(ICalculation.class.getName(), new Calculation(), props);
System.out.println("Service registered!");
}

3 获取服务

有几种方式:

1、ServiceReference ref = context.getServiceReference(LogService.class.getName());

优点:很难说有什么优点,硬要说几句的话,那就是逻辑够简单,调用最少,适合一次性操作。
缺点:需要判断返回值是否为null,需要手动申请和释放service,由于OSGi的动态性,请在获取ref后尽快使用,无法保证ref长期有效。每次访问都会有service获取和释放的开销。
用途:适合于不频繁的调用service,且在service不可用时也能继续执行后续操作的场景。

2、使用ServiceListener

优点:只在Service变更时产生一次service获取开销,动态感知service的注册和注销。
缺点:在ServiceListener注册之前已经存在的Service无法监听到。需要自己维护service的获取和释放。在需要监听多个Service实例时,使用并不方便。

3、使用ServiceTracker

ServiceTracker其实是对ServiceListener实现方式的封装,使得对service的获取更加简洁,同时也解决了不能监听到已经存在的Service的问题(其实就是在增加ServiceListener的同时调用BundleContext.getAllServiceReferences方法以获取现有的Service引用)。

有一点需要注意的是,tracker需要调用open方法才能监听到Service,另外,在bundle stop以后,bundle内open的ServiceTracker不会自动关闭,所以一定不要忘记在bundle结束之前,关闭所有在bundle中open的ServiceTracker。

4、使用OSGI Blueprint

如下:

image.png

Bundle:在逻辑上表示了一个bundle,OSGi环境中的一个物理bundle对应了一个bundle对象。该对象中包含了bundle的基本信息和bundle生命周期的控制接口。

image.png

启动级别

1、启动级别的数值越高,启动顺序越靠后

2、只有System Bundle(bundle ID为0)的启动级别可以为0,其他Bundle的启动级别都大于0,最大值为Integer.MAX_VALUE

3、动态启动级别

系统Bundle

启动过程:Bundle的start()方法为空操作,因为OSGI框架一启动。系统Bundle就已经启动了

停止过程:Bundle的stop()方法会立即返回并在另外一条线程中关闭OSGI框架

更新过程:Bundle的update()方法会立即返回并在另外一条线程中重启OSGI框架

卸载过程:系统Bundle无法卸载,如果执行了Bundle的uninstall()方法,那么框架会抛出一个BundleException异常

Bundle刷新流程

从某一bundle开始计算受影响的bundle有向图

处于Active状态的bundle被停止并被切换至Resolved状态

处于Resolved状态的bundle,切换至Installed状态,这些bundle的依赖关系不再被解析

处于uninstalled状态的bundle会被从图中移除,同时也会被彻底地从框架中移除(由GC回收)

其他bundle,如果框架重启之前处于Active状态,重启前框架会对这些bundle以及其所依赖的bundle进行解析

当所有的工作完成之后,框架会触发一个FrameworkEvent.PACKAGES_REFRESHED事件

服务层

面向服务的设计

1、降低服务提供者和使用者之间的耦合,这样更容易重用组件

2、更强调接口而不是实现类

3、清晰描述依赖关系,让你知道一切是如何结合在一起的(可以有附加的元数据描述)

4、支持多个相互竞争的服务实现,这样你可以互换这些实现(动态替换)

OSGI服务

OSGI框架拥有一个集中的服务注册中心,它遵循发布-查询-绑定模型

1、提供者bundle可以将POJOs发布为服务。

1.1、注册的时候可以设置这个 Service 的属性。而在获取 Service的时候可以根据属性进行过滤。

1.2、为了让别的bundle能发现这个服务,你必须在发布它之前对其进行特征描述。这些特征包括接口的名字(可以是名字的数组),接口的实现,和一个可选的java.util.Dictionary类型的元数据信息。

2、使用者bundle可以找到并绑定服务

3、服务注册、更新、注销

4、服务注册对象是私有的,不能被别的bundle共享,它们与发布服务的bundle的生命周期是绑定的

5、OSGI将会接受以具体类名注册的服务,但是不推荐这样做

6、当一个bundle停止时,任何没有被移除的服务都会被框架自动移除。当bundle停止时,不必明确地注销服务。

7、服务监听

8、服务追踪器 – Listener、ServiceTracker

9、服务工厂 – 为不同的bundle提供相同服务的不同实例

10、配置管理:可将配置文件放置/etc下,随着配置文件的更改,ManagedService接口的实现类的updated方法也会被调用。

特点

  • OSGi的入门门槛在Java众多技术中算是比较高的
  • OSGi本身就具有较高的复杂度
  • OSGi确实会增加系统不稳定的风险

bundle尽管可以为隔离的服务建立独立生命周期管理的热部署方式,以及明确的服务导出和导入依赖能力,但是其最终基于jvm,无法对bundle对应的服务实现计算资源的隔离,一个服务的故障依然会导致整个jvm crush,这使得在一个运行时的osgi上部署模块级服务只获得了模块部署和启停隔离,

服务明确依赖的好处,但是没办法实现计算节点的线性扩展,在当前分布式,微服务,网络计算的趋势下,使得osgi只适合构建单一服务节点的内部应用,但是其分离的bundle的部署负担对于微服务架构来说,有点用大炮打蚊子的臭味

目前我的理解就是,一个庞大单体应用,需要分团队开发,用户量不多,需要分布式部署,但是又不需要像微服务架构一样,按服务拆分得那么细,那么就可以使用OSGI架构。

相关文章:

https://blog.csdn.net/qq_32436781/article/details/117092788

https://blog.csdn.net/hpc_er/article/details/115705499

osgi框架实现:https://felix.apache.orghttps://karaf.apache.org