《深入理解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

相关文章
|
10天前
|
设计模式 Java API
Java 可扩展 API 设计:打造灵活的应用架构
【4月更文挑战第27天】设计可扩展的 API 是构建灵活、易于维护的应用程序架构的关键。Java 提供了丰富的工具和技术来实现这一目标,使开发者能够构建具有高度可扩展性的应用程序。
31 4
|
1天前
|
消息中间件 数据管理 持续交付
构建高效微服务架构的最佳实践
【5月更文挑战第6天】在动态和快速演变的现代软件开发领域,微服务架构已经成为促进敏捷开发和部署的关键模式。本文将深入探讨构建和维护高效微服务架构的策略,包括服务划分准则、通信机制、数据管理及持续集成与持续交付(CI/CD)的实施。通过分析不同业务场景下的应用案例,本文旨在为开发者提供一套行之有效的指导原则和实践方法,以支持他们构建可扩展、灵活且高效的微服务系统。
18 2
|
1天前
|
监控 负载均衡 API
微服务架构在现代企业中的应用与挑战
微服务架构已成为现代企业构建灵活且可扩展软件系统的首选。然而,随着其应用的普及,企业也面临着一系列新的挑战。本篇文章将探讨微服务架构的优势、实施时遇到的问题以及解决这些问题的策略。
|
1天前
|
Kubernetes Cloud Native 持续交付
构建高效云原生应用:Kubernetes与微服务架构的融合
【5月更文挑战第6天】 在数字化转型的浪潮中,企业正迅速采纳云原生技术以实现敏捷性、可扩展性和弹性。本文深入探讨了如何利用Kubernetes这一领先的容器编排平台,结合微服务架构,构建和维护高效、可伸缩的云原生应用。通过分析现代软件设计原则和最佳实践,我们提出了一个综合指南,旨在帮助开发者和系统架构师优化云资源配置,提高部署流程的自动化水平,并确保系统的高可用性。
21 1
|
6天前
|
Cloud Native 安全 持续交付
构建未来:云原生架构在现代企业中的应用与挑战
【5月更文挑战第1天】 随着数字化转型的深入,云原生技术以其灵活性、可扩展性和敏捷性成为现代企业IT架构的核心。本文将探讨云原生架构的关键组件,包括容器化、微服务、持续集成/持续部署(CI/CD)以及DevOps实践,并分析它们如何共同塑造企业的运营模式。同时,文章还将讨论在采纳云原生过程中企业可能遇到的挑战,如安全性问题、技术复杂性以及组织文化的转变,并提出应对策略。
25 8
|
7天前
|
前端开发 JavaScript 安全
【TypeScript技术专栏】TypeScript在微前端架构中的应用
【4月更文挑战第30天】微前端架构通过拆分应用提升开发效率和降低维护成本,TypeScript作为静态类型语言,以其类型安全、代码智能提示和重构支持强化这一架构。在实践中,TypeScript定义公共接口确保跨微前端通信一致性,用于编写微前端以保证代码质量,且能无缝集成到构建流程中。在微前端架构中,TypeScript是保障正确性和可维护性的有力工具。
|
7天前
|
负载均衡 Java 开发者
Spring Cloud:一文读懂其原理与架构
Spring Cloud 是一套微服务解决方案,它整合了Netflix公司的多个开源框架,简化了分布式系统开发。Spring Cloud 提供了服务注册与发现、配置中心、消息总线、负载均衡、熔断机制等工具,让开发者可以快速地构建一些常见的微服务架构。
|
7天前
|
消息中间件 数据库 开发者
构建高效微服务架构:后端开发的最佳实践
【4月更文挑战第30天】在现代软件开发中,微服务架构已成为一种流行且有效的方法,它能够提高系统的可扩展性、弹性和维护性。本文将探讨后端开发中构建微服务架构的关键要素,包括服务划分、通信机制、数据一致性和容错处理。通过深入分析这些要素,我们将提供一系列最佳实践,帮助开发者构建一个高性能、可维护的微服务系统。
|
8天前
|
Cloud Native Devops 持续交付
构建未来应用:云原生架构在现代企业中的实践与挑战
【4月更文挑战第29天】 随着数字化转型的加速,企业正迅速转向云计算以支撑其业务敏捷性和创新。云原生技术,作为推动这一转型的关键因素,正在重新定义软件开发和运维模式。本文将深入探讨云原生架构的核心组件,包括容器化、微服务、持续集成/持续部署(CI/CD)以及DevOps文化,并分析这些技术如何帮助企业实现弹性、可扩展和高效的应用部署。同时,我们将讨论在采纳云原生实践中所面临的挑战,包括安全性、治理和人才缺口等问题。
|
8天前
|
消息中间件 PHP 数据库
【PHP开发专栏】PHP在微服务架构中的应用
【4月更文挑战第29天】微服务架构将大型应用拆分成独立小服务,PHP在其中可作为API网关、微服务提供者,参与服务发现、消息队列处理和事件驱动。最佳实践包括选择合适PHP框架、使用容器化技术、定义服务契约、采用分布式缓存、实现服务发现、监控和日志收集、优化数据库设计以及注重安全性。遵循这些实践,PHP开发者能构建高效、可扩展的微服务应用。