《深入理解OSGi:Equinox原理、应用与最佳实践》一2.5 OSGi的类加载架构

简介: 本节书摘来自华章出版社《深入理解OSGi:Equinox原理、应用与最佳实践》一 书中的第2章,第2.5节,作者:周志明 谢小明,更多章节内容可以访问云栖社区“华章计算机”公众号查看。

2.5 OSGi的类加载架构

OSGi为Java平台提供了动态模块化的特性,但是它并没有对Java的底层实现如类库和Java虚拟机等进行修改,OSGi实现的模块间引用与隔离、模块的动态启用与停用的关键在于它扩展的类加载架构。
OSGi的类加载架构并未遵循Java所推荐的双亲委派模型(Parents Delegation Model),它的类加载器通过严谨定义的规则从Bundle的一个子集中加载类。除了Fragment Bundle外,每一个被正确解析的Bundle都有一个独立的类加载器支持,这些类加载器之间互相协作形成了一个类加载的代理网络架构,因此OSGi中采用的是网状的类加载架构,而不是Java传统的树状类加载架构,如图2-14所示。

screenshot

在OSGi中,类加载器可以划分为3类。
父类加载器:由Java平台直接提供,最典型的场景包括启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)。在一些特殊场景中(如将OSGi内嵌入一个Web中间件)还会有更多的加载器组成。它们用于加载以“java.*”开头的类以及在父类委派清单中声明为要委派给父类加载器加载的类。
Bundle类加载器:每个Bundle都有自己独立的类加载器,用于加载本Bundle中的类和资源。当一个Bundle去请求加载另一个Bundle导出的Package中的类时,要把加载请求委派给导出类的那个Bundle的加载器处理,而无法自己去加载其他Bundle的类。
其他加载器:譬如线程上下文类加载器、框架类加载器等。它们并非OSGi规范中专门定义的,但是为了实现方便,在许多OSGi框架中都会使用。例如框架类加载器,OSGi框架实现一般会将这个独立的框架类加载器用于加载框架实现的类和关键的服务接口类。
不同类加载器所能完成的(无论是自己完成加载,还是委派给其他类加载器来加载)加载请求的范围构成了该Bundle的类名称空间(Class Name Space)。在同一个类名称空间中,类必须是一致的,也就是说不会存在完全重名的两个类。但是在整个OSGi的模块层,允许多个相同名称的类同时存在,因为OSGi模块层是由多个Bundle的类名称空间组成的。单独一个Bundle的类名称空间由如下内容组成:
父类加载器提供的类(以java.*开头的类以及在委派名单中列明的类);
导入的Package(Import-Package);
导入的Bundle(Require-Bundle);
本Bundle的Classpath(私有Package,Bundle-Classpath);
附加的Fragment Bundle(fragment-attachment);
动态导入的Package(DynamicImport-Package)。
下面将介绍Bundle中各种类的加载过程,涉及类加载器,以及类加载的优先级次序。

2.5.1 父类加载器

OSGi框架必须将以java.*开头的Package交给父类加载器代理,这一点是无须设置且不可改动的。除此之外,OSGi框架也允许用户通过系统参数“org.osgi.framework.bootdelegation”自行指定一些Package委派给父类加载器加载,这个参数被称为“父类委派清单”(Boot Delegation List)。它的值应为一系列的包名,用逗号分隔,支持通配符,例如:
org.osgi.framework.bootdelegation=sun.,com.sun.
如果org.osgi.framework.bootdelegation的参数值如以上代码中所示,那么以sun.和com.sun.开头的类也会委派给父类加载器去加载。这个设定在特定场景下很有用。
例如某个部署在Web中间件上的OSGi应用需要使用JDBC访问数据库,与大多数应用一样,访问数据库的Connection是由应用服务器的JNDI提供的,这时候就应当把JDBC驱动设置为由父类加载器加载,而不是由OSGi中的某个Bundle包提供。因为Web中间件通常会带有连接池实现,为了实现事务控制和连接监视等功能,从JNDI中查到的DataSource是被中间件服务器包装过的,并非直接由原生的JDBC驱动所提供。为了保证中间件服务器中一些需要把Connection、Statement、ResultSet等从接口转型为具体实现类的代码(大多数是操作大字段的代码)能正常执行,必须保证中间件服务器和OSGi应用所使用的JDBC驱动是同一个—不仅是同一个文件,还要是由同一个类加载器加载的,这样才能保证转型成功。
以java.开头的Package是默认被隐式导出的,在所有Bundle中无需导入便可以直接使用,并且OSGi规范明确禁止在Bundle中导入或导出以java.开头的Package。与前面提到的父类委派清单类似,OSGi也定义了添加隐式导出Package的参数“org.osgi.framework.system.packages”。这个参数使用标准的Export-Package语法描述,例如:
org.osgi.framework.system.packages=javax.crypto.interfaces
这里定义的Package将由系统Bundle(ID为0的Bundle)导出,由父类加载器加载。这样导出的Package与普通的导出方式没有太大区别,可以带有属性和版本号,也可以使用uses参数描述依赖。

2.5.2 Bundle类加载器

OSGi框架为每一个Bundle(不包括Fragment Bundle)生成了一个Bundle类加载器的实例,这些类加载器负责处理其他Bundle委派的加载请求,根据元数据信息确定这些加载请求的类是否与该Bundle的导出列表相符合,然后对合法的加载请求进行响应,返回该Bundle的类供其他Bundle使用。
Bundle-Classpath这个元数据标记与Bundle类加载器密切相关,它描述了Bundle加载器的Classpath范围,即Bundle加载器应该到哪里去查找类。
Bundle-Classpath标记有默认值“.”,它代表该Bundle的根目录,或者说代表该Bundle的JAR文件。如果不在元数据信息中显式定义这个标记,那么Bundle类加载器就在整个Bundle的范围内查找类。但是要注意,在这种默认配置下,如果Bundle存在其他JAR文件,类加载器只能把它当作一个普通资源来读取,而无法查找到这些JAR文件内部包含的类。例如,在Bundle中有如下路径:
Bundle:

   lib/log4j.jar
   org/fenixsoft/osgi/Example.class

log4j.jar

   org/apache/log4j/Logger.class

Bundle类加载器可以访问到Example.class,但是无法访问到Logger.class,最多只能把log4j.jar当作与图片、音频等类似的二进制资源整体提供出去。
要读取到Logger.class,必须设置Bundle-Classpath标记为:
Bundle-Classpath: lib/log4j.jar,.
注意不要遗漏了后面的“,.”,这里有两个Classpath路径,它们之间使用逗号分隔,如果没有了后面的“.”,那么Bundle类加载器就只能处理log4j.jar中的类而无法处理本Bundle的Example.class了。
如果Bundle-Classpath标记的值是多个Classpath路径,那么它们之间还有优先级关系,例如下面这个定义:
Bundle-Classpath: required.jar,optional.jar,default.jar
该定义中required.jar是必须出现在Bundle中的类和资源;optional.jar是某个可选的JAR包,其中存放着可选的类和资源;default.jar中存放着optional.jar不可用时这些类和资源的默认值,如果optional.jar中有可用的内容便会对其覆盖。
如果一个Bundle被另一个Fragment Bundle附加,那么Bundle-Classpath也会相应叠加,例如下面定义:
Bundle A:
Bundle-Classpath: required.jar,optional.jar,default.jar

Bundle B:
Bundle-Classpath: fragment.jar
Fragment-Host: BundleA
此时Bundle A的Bundle类加载器能搜索到的Classpath依次为:required.jar、optional.jar、default.jar、fragment.jar。
Bundle类加载器收到类加载请求时,会优先委托给导入包的其他Bundle类加载器处理,只有其他导入包的Bundle类加载器都无法处理时才会尝试自己处理。读者可以通俗地理解为“Import-Package”和“Require-Bundle”的优先级高于“Bundle-Classpath”,如果能在前者中找到所需的类,后者就不会起作用。这条规则读起来不复杂,但初接触OSGi的朋友在实际编码时候可能会对此有些不习惯,例如下面这个例子:
在Bundle A、B中都有Package p,两者的Package p中都存在有类ClassA。同时,Bundle B还导入了Bundle A中的Package p。在这个前提下,假设Bundle A中有下列代码:
ClassA anA = new ClassA();
这时候ClassA用的都是Bundle A中的类,符合一般思维习惯。但是如果Bundle B中有同样的代码,所使用的ClassA依然是Bundle A中的类,即使Bundle B自己的Classpath中也有这类ClassA,甚至与调用ClassA的代码文件存在于同一个目录下紧紧相邻的就是ClassA,都不会被使用,这就不符合一般的思维习惯了,如图2-15所示。

screenshot

这里假设Bundle A导出的p中存在ClassA这个类,这样Bundle B的ClassA就无法派上用场。如果情况更极端一些,Bundle A导出的p不存在ClassA这个类,那Bundle B的ClassA依然不会被使用,而会直接收到ClassNotFoundException异常,异常信息类似如下所示:
Caused by: java.lang.ClassNotFoundException: p.ClassA

at org.eclipse.osgi.internal.loader.BundleLoader.findClassInternal(BundleLoader.java:467)
at org.eclipse.osgi.internal.loader.BundleLoader.findClass(BundleLoader.java:429)
at org.eclipse.osgi.internal.loader.BundleLoader.findClass(BundleLoader.java:417)
at org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader.loadClass(DefaultClass-

Loader.java:107)

at java.lang.ClassLoader.loadClass(ClassLoader.java:248)

对于在Bundle中发生的加载请求而言,当前Bundle的Bundle类加载器是使用到的类的初始类加载器(Initiating Classloader,它表示加载请求最先发送到的类加载器),而哪个类加载器是定义类加载器(Defining Classloader,它表示加载请求被不断委派后,最终执行加载动作的类加载器)则要根据OSGi类加载顺序来判定。在类型强制转换和类型比较(譬如instanceOf操作)时理解类加载顺序很重要,因为即使是同一个类文件,由不同定义类加载器加载所形成的类在Java虚拟机中也是完全独立且不可互相转型的。

2.5.3 其他类加载器

在OSGi中还可能使用到其他的类加载器,比如OSGi实现框架中一般都会有框架类加载器(Framework Classloader)。OSGi框架为每个Bundle创建Bundle类加载器的实例,而OSGi框架自身的代码——至少涉及OSGi框架启动的代码就没法使用Bundle类加载器来加载,因此需要一个专门的框架类加载器来完成这个任务。这个框架类加载器是各个OSGi实现框架自己定义的,有时候可能直接使用Java平台提供的应用程序类加载器(Application ClassLoader)。这个框架类加载器还可能同时充当父类加载器的角色,比如在Equinox框架中就可以选择是使用启动类加载器、扩展类加载器、应用程序类加载器还是使用框架类加载器来作为父类加载器。
另外一个在OSGi中比较常见的类加载器是线程上下文类加载器(Thread ContextClassLoaser),这个类加载器并不是在OSGi中才出现的,它在普通的Java应用中有广泛应用。这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时未设置,那么它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器就默认是应用程序类加载器。有了线程上下文类加载器,就可以做一些“舞弊”的事情,例如直接加载没有经过导入和导出的类,或者让由框架类加载器加载的OSGi框架代码在运行期得以访问一些系统Bundle中的类。
OSGi中其他的类加载器与具体实现密切相关,后面我们将会在确定具体OSGi实现框架和具体上下文的场景下再进行介绍,此处不再赘述。
2.5.4 类加载顺序
当一个Bundle类加载器遇到需要加载某个类或查找某个资源的请求时,搜索过程必须按以下指定步骤执行:
1)如果类或资源在以java.*开头的Package中,那么这个请求需要委派给父类加载器;否则,继续下一个步骤搜索。如果将这个请求委派给父类加载器后发现类或资源不存在,那么搜索终止并宣告这次类加载请求失败。
2)如果类或资源在父类委派清单(org.osgi.framework. bootdelegation)所列明的Package中,那么这个请求也将委派给父类加载器。如果将这个请求委派给父类加载器后,发现类或资源不存在,那么搜索将跳转到一个步骤。
3)如果类或资源在Import-Package标记描述的Package中,那么请求将委派给导出这个包的Bundle的类加载器,否则搜索过程将跳转到下一个步骤。如果将这个请求委派给Bundle类加载器后,发现类或资源不存在,那么搜索终止并宣告这次类加载请求失败。
4)如果类或资源在Require-Bundle导入的一个或多个Bundle的包中,这个请求将按照Require-Bundle指定的Bundle清单顺序逐一委派给对应Bundle的类加载器,由于被委派的加载器也会按照这里描述的搜索过程查找类,因此整个搜索过程就构成了深度优先的搜索策略。如果所有被委派的Bundle类加载器都没有找到类或资源,那么搜索将转到下一个步骤。
5)搜索Bundle内部的Classpath。如果类或资源没有找到,那么这个搜索将转到下一个步骤。
6)搜索每个附加的Fragment Bundle的Classpath。搜索顺序将按这些Fragment Bundle的ID升序搜索。如果这个类或资源没有找到,那么搜索转到下一个步骤。
7)如果类或资源在某个Bundle已声明导出的Package中,或者包含在已声明导入(Import-Package或Require-Bundle)的Package中,那么这次搜索过程将以没有找到指定的类或资源而终止。
8)如果类或资源在某个使用DynamicImport-Package声明导入的Package中,那么将尝试在运行时动态导入这个Package。如果在某个导出该Package的Bundle中找到需要加载的类,那么后面的类加载过程将按照步骤3)处理。
9)如果可以确定找到一个合适的完成动态导入的Bundle,那么这个请求将委派给该Bundle的类加载器。如果无法找到任何合适的Bundle来完成动态导入,那么搜索终止并宣告此次类加载请求失败。当将动态导入委派给另一个Bundle 类加载器时,类加载请求将按照步骤3)处理。
上述加载过程如图2-16所示。
screenshot
screenshot

相关文章
|
6天前
|
运维 持续交付 开发工具
深入浅出:GitOps在微服务架构中的应用
【10月更文挑战第26天】本文深入探讨了GitOps在微服务架构中的应用,介绍了其核心理念、自动化部署流程和增强的可观测性。通过实例展示了GitOps如何简化服务部署、配置管理和故障恢复,并推荐了一些实用工具和开发技巧。
|
4天前
|
机器学习/深度学习 人工智能 自然语言处理
医疗行业的语音识别技术解析:AI多模态能力平台的应用与架构
AI多模态能力平台通过语音识别技术,实现实时转录医患对话,自动生成结构化数据,提高医疗效率。平台具备强大的环境降噪、语音分离及自然语言处理能力,支持与医院系统无缝集成,广泛应用于门诊记录、多学科会诊和急诊场景,显著提升工作效率和数据准确性。
|
5天前
|
JavaScript 持续交付 Docker
解锁新技能:Docker容器化部署在微服务架构中的应用
【10月更文挑战第29天】在数字化转型中,微服务架构因灵活性和可扩展性成为企业首选。Docker容器化技术为微服务的部署和管理带来革命性变化。本文探讨Docker在微服务架构中的应用,包括隔离性、可移植性、扩展性、版本控制等方面,并提供代码示例。
29 1
|
8天前
|
开发者 容器
Flutter&鸿蒙next 布局架构原理详解
本文详细介绍了 Flutter 中的主要布局方式,包括 Row、Column、Stack、Container、ListView 和 GridView 等布局组件的架构原理及使用场景。通过了解这些布局 Widget 的基本概念、关键属性和布局原理,开发者可以更高效地构建复杂的用户界面。此外,文章还提供了布局优化技巧,帮助提升应用性能。
69 4
|
7天前
|
运维 Serverless 数据处理
Serverless架构通过提供更快的研发交付速度、降低成本、简化运维、优化资源利用、提供自动扩展能力、支持实时数据处理和快速原型开发等优势,为图像处理等计算密集型应用提供了一个高效、灵活且成本效益高的解决方案。
Serverless架构通过提供更快的研发交付速度、降低成本、简化运维、优化资源利用、提供自动扩展能力、支持实时数据处理和快速原型开发等优势,为图像处理等计算密集型应用提供了一个高效、灵活且成本效益高的解决方案。
31 1
|
8天前
|
存储 Dart 前端开发
flutter鸿蒙版本mvvm架构思想原理
在Flutter中实现MVVM架构,旨在将UI与业务逻辑分离,提升代码可维护性和可读性。本文介绍了MVVM的整体架构,包括Model、View和ViewModel的职责,以及各文件的详细实现。通过`main.dart`、`CounterViewModel.dart`、`MyHomePage.dart`和`Model.dart`的具体代码,展示了如何使用Provider进行状态管理,实现数据绑定和响应式设计。MVVM架构的分离关注点、数据绑定和可维护性特点,使得开发更加高效和整洁。
143 3
|
12天前
|
监控 Cloud Native 持续交付
云原生架构下微服务的最佳实践与挑战####
【10月更文挑战第20天】 本文深入探讨了云原生架构在现代软件开发中的应用,特别是针对微服务设计模式的最优实践与面临的主要挑战。通过分析容器化、持续集成/持续部署(CI/CD)、服务网格等关键技术,阐述了如何高效构建、部署及运维微服务系统。同时,文章也指出了在云原生转型过程中常见的难题,如服务间的复杂通信、安全性问题以及监控与可观测性的实现,为开发者和企业提供了宝贵的策略指导和解决方案建议。 ####
39 5
|
9天前
|
前端开发 API UED
深入理解微前端架构:构建灵活、高效的前端应用
【10月更文挑战第23天】微前端架构是一种将前端应用分解为多个小型、独立、可复用的服务的方法。每个服务独立开发和部署,但共同提供一致的用户体验。本文探讨了微前端架构的核心概念、优势及实施方法,包括定义服务边界、建立通信机制、共享UI组件库和版本控制等。通过实际案例和职业心得,帮助读者更好地理解和应用微前端架构。
|
12天前
|
Kubernetes Cloud Native 持续交付
云原生架构下的微服务设计原则与最佳实践##
在数字化转型的浪潮中,云原生技术以其高效、灵活和可扩展的特性成为企业IT架构转型的首选。本文深入探讨了云原生架构的核心理念,聚焦于微服务设计的关键原则与实施策略,旨在为开发者提供一套系统性的方法论,以应对复杂多变的业务需求和技术挑战。通过分析真实案例,揭示了如何有效利用容器化、持续集成/持续部署(CI/CD)、服务网格等关键技术,构建高性能、易维护的云原生应用。文章还强调了文化与组织变革在云原生转型过程中的重要性,为企业顺利过渡到云原生时代提供了宝贵的见解。 ##
|
11天前
|
监控 安全 Serverless
"揭秘D2终端大会热点技术:Serverless架构最佳实践全解析,让你的开发效率翻倍,迈向技术新高峰!"
【10月更文挑战第23天】D2终端大会汇聚了众多前沿技术,其中Serverless架构备受瞩目。它让开发者无需关注服务器管理,专注于业务逻辑,提高开发效率。本文介绍了选择合适平台、设计合理函数架构、优化性能及安全监控的最佳实践,助力开发者充分挖掘Serverless潜力,推动技术发展。
25 1

热门文章

最新文章